feat: returning with edit profile modal

This commit is contained in:
Chubby Granny Chaser 2024-09-13 00:03:58 +01:00
commit 2304e19558
No known key found for this signature in database
78 changed files with 1810 additions and 906 deletions

View File

@ -35,7 +35,10 @@ jobs:
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -65,3 +68,4 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
dist/*.pacman

View File

@ -37,7 +37,10 @@ jobs:
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -68,5 +71,6 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
dist/*.pacman
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra - гэта гульнявы лаўнчар з уласным убудаваным кліентам BitTorrent і самастойным scraper`ам для рэпакаў.</strong>
</p>
@ -20,6 +20,8 @@
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Змест
- [Змест](#змест)
- [Апісанне](#апісанне)
- [Асаблівасці](#асаблівасці)
- [Усталёўка](#усталёўка)
- [Уклад](#contributing)
- [Далучайцеся да нашага Telegram](#join-our-telegram)
- [Уклад](#-уклад)
- [Далучайцеся да нашага Telegram](#-далучайцеся-да-нашага-telegram)
- [Форк і кланаванне рэпазітара](#форк-і-кланаванне-рэпазітара)
- [Спосабы ўнесці свой уклад](#спосабы-ўнесці-свой-уклад)
- [Структура праекту](#структура-праекту)
@ -47,6 +50,7 @@
- [Зборка кліента BitTorrent](#зборка-кліента-bittorrent)
- [Зборка прыкладання Electron](#зборка-прыкладання-electron)
- [Удзельнікі](#удзельнікі)
- [Ліцэнзія](#ліцэнзія)
## Апісанне

182
README.de.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 ist ein Launcher für Spiele mit einem eigenen eingebetteten BitTorrent-Client.</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)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![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)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
![Hydra Katalog](./docs/screenshot.png)
</div>
## Inhaltsverzeichnis
- [Über Hydra](#über-hydra)
- [Eigenschaften](#eigenschaften)
- [Installation](#installation)
- [Mitwirken](#mitwirken)
- [Tritt uns auf Telegram bei](#tritt-uns-auf-telegram-bei)
- [Forke und klone dein Repo](#forke-und-klone-dein-repo)
- [Wie du mitwirken kannst](#wie-du-mitwirken-kannst)
- [Projektstruktur](#projektstruktur)
- [Den Quellcode kompilieren](#den-quellcode-kompilieren)
- [Installiere Node.js](#installiere-nodejs)
- [Installiere Yarn](#installiere-yarn)
- [Installiere Node-Abhängigkeiten](#installiere-node-abhängigkeiten)
- [Installiere Python 3.9](#installiere-python-39)
- [Installiere Python-Abhängigkeiten](#installiere-python-abhängigkeiten)
- [Umgebungsvariablen](#umgebungsvariablen)
- [Ausführung](#ausführung)
- [Kompilation](#kompilation)
- [Kompiliere den BitTorrent-Client](#kompiliere-den-bittorrent-client)
- [Kompiliere die Electron-Applikation](#kompiliere-die-electron-applikation)
- [Mitwirkende](#mitwirkende)
## Über Hydra
**Hydra** ist ein **Launcher für Spiele** mit einem eigenen eingebetteten **BitTorrent-Client**.
<br>
Der Launcher ist in TypeScript (Electron) und Python, womit das Torrentingsystem durch Einsatz von libtorrent geregelt ist, geschrieben.
## Eigenschaften
- Eigener eingebetteter BitTorrent-Client
- How Long to Beat (HLTB) Integration auf der Spielseite
- Anpassbarkeit des Downloadverzeichnisses
- Unterstützung von Windows und Linux
- Regelmäßig aktualisiert
- Und mehr ...
## Installation
Die folgenden Schritte beschreiben den Installationsprozess:
1. Lade die neueste Version von Hydra von der [Releases](https://github.com/hydralauncher/hydra/releases/latest) Seite herunter.
- Für die Installation von Hydra auf Windows, wähle die .exe Datei.
- Für die Installation von Hydra auf Linux, wähle die .deb, .rpm oder .zip Datei. (Abhängig von deiner Linux-Distribution)
2. Führe die heruntergeladene Datei aus.
3. Genieße Hydra!
## Mitwirken
### Tritt uns auf Telegram bei
Wir konzentrieren unsere Diskussionen in unserem [Telegram](https://t.me/hydralauncher) Kanal.
### Forke und klone dein Repo
1. Forke das Repo [(Klicke hier, um direkt zu forken)](https://github.com/hydralauncher/hydra/fork)
2. Klone deinen geforketen Code `git clone https://github.com/dein_nutzername/hydra`
3. Erstelle einen neuen Branch
4. Pushe deine Commits
5. Stelle eine neue Pull-Anfrage
### Wie du mitwirken kannst
- Übersetzung: Wir wollen Hydra so vielen Menschen wie möglich zugänglich machen. Gerne kannst du uns helfen neue Sprachen zu übersetzen oder für Hydra bereits verfügbare Sprachen zu aktualisieren und verbessern.
- Code: Hydra ist mit TypeScript, Electron und etwas Python gebaut. Wenn du mitwirken möchtest, tritt unserem [Telegram](https://t.me/hydralauncher) bei!
### Projektstruktur
- torrent-client: Wir verwenden die Python-Bibliothek libtorrent zur Verwaltung von Torrent-Downloads.
- src/renderer: die UI der Applikation.
- src/main: sämtliche Logik liegt hier.
## Den Quellcode kompilieren
### Installiere Node.js
Stelle sicher, dass du Node.js auf deinem System installiert hast. Falls nicht, installiere es von [nodejs.org](https://nodejs.org/).
### Installiere Yarn
Yarn ist ein Packetmanager für Node.js. Sollte er dir fehlen, installiere ihn mithilfe der Anleitung auf [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Installiere Node-Abhängigkeiten
Navigiere zum Projektverzeichnis und installiere die Node-Abhängigkeiten mit Yarn:
```bash
cd hydra
yarn
```
### Installiere Python 3.9
Stelle sicher, dass du Python 3.9 auf deinem System installiert hast. Ansonsten kannst du es von [python.org](https://www.python.org/downloads/release/python-3913/) herunterladen und installieren.
### Installiere Python-Abhängigkeiten
Installiere die benötigten Python-Abhängigkeiten mit pip:
```bash
pip install -r requirements.txt
```
## Umgebungsvariablen
Du wirst einen SteamGridDB API Schlüssel benötigen, um die Spielicons bei Installation abzurufen.
Sobald du einen hast, kannst du die .env.example Datei zu .env kopieren oder umbenennen und den Schlüssel bei STEAMGRIDDB_API_KEY einfügen.
## Ausführung
Sobald du alles eingerichtet hast, kannst du den folgenden Befehl nutzen, um sowohl den Electron-Prozess als auch den BitTorrent-Client zu starten:
```bash
yarn dev
```
## Kompilation
### Kompiliere den BitTorrent-Client
Kompiliere den BitTorrent-Client mit folgendem Befehl:
```bash
python torrent-client/setup.py build
```
### Kompiliere die Electron-Applikation
Kompiliere die Electron-Applikation mit folgendem Befehl:
Auf Windows:
```bash
yarn build:win
```
Auf Linux:
```bash
yarn build:linux
```
## Mitwirkende
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Lizenz
Hydra ist unter der [MIT Lizenz](LICENSE) lizensiert.

View File

@ -5,7 +5,7 @@
[<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>
@ -13,13 +13,15 @@
[![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)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Tabla de Contenidos
- [Tabla de Contenidos](#tabla-de-contenidos)
- [Acerca de](#acerca-de)
- [Características](#caracteristicas)
- [Instalación](#Instalacion)
- [Contribuir](#contribuir)
- [Únete a nuestro Telegram](#unete-a-nuestro-telegram)
- [Caracteristicas](#caracteristicas)
- [Instalacion](#instalacion)
- [Contribuir](#-contribuir)
- [Unete 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)
@ -40,13 +43,14 @@
- [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)
- [Instalar Dependencias de Python](#instalar-dependencias-de-python)
- [Variables del Entorno](#variables-del-entorno)
- [Ejecución](#ejecucion)
- [Compilación](#compilacion)
- [Ejecucion](#ejecucion)
- [Compilacion](#compilacion)
- [Compilar el cliente de bittorrent](#compilar-el-cliente-de-bittorrent)
- [Compilar la aplicación Electron](#compilar-la-aplicacion-electron)
- [Compilar la aplicacion Electron](#compilar-la-aplicacion-electron)
- [Colaboradores](#colaboradores)
- [Licencia](#licencia)
## Acerca de

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra est un lanceur de jeux avec son propre client bittorrent intégré et un scraper de repack auto-géré.</strong>
</p>
@ -13,13 +13,15 @@
[![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)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Catalogue Hydra](./docs/screenshot.png)
@ -27,6 +29,7 @@
## Table des Matières
- [Table des Matières](#table-des-matières)
- [À propos](#à-propos)
- [Fonctionnalités](#fonctionnalités)
- [Installation](#installation)
@ -47,6 +50,7 @@
- [Compiler le client bittorrent](#compiler-le-client-bittorrent)
- [Compiler l'application Electron](#compiler-lapplication-electron)
- [Contributeurs](#contributeurs)
- [License](#license)
## À propos

187
README.it.md Normal file
View File

@ -0,0 +1,187 @@
<br>
<div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra è un game launcher con il proprio client bittorrent e autogestore di 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)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Table of Contents
- [Table of Contents](#table-of-contents)
- [A proposito](#a-proposito)
- [Caratteristiche](#caratteristiche)
- [Installazione](#installazione)
- [Contribuire](#-contribuire)
- [Unisciti su Telegram](#-unisciti-su-telegram)
- [Forka e Clona la repository](#forka-e-clona-la-repository)
- [Modi in cui contribuire](#modi-in-cui-contribuire)
- [Struttura del Progetto](#struttura-del-progetto)
- [Compilazione](#compilazione)
- [Installa Node.js](#installa-nodejs)
- [Installa Yarn](#installa-yarn)
- [Installa le dipendenze Node](#installa-le-dipendenze-node)
- [Installa Python 3.9](#installa-python-39)
- [Installa le Dipendenze Python](#installa-le-dipendenze-python)
- [Variabili d'ambiente](#variabili-dambiente)
- [Esecuzione](#esecuzione)
- [Compilazione](#compilazione-1)
- [Compila il bittorrent](#compila-il-bittorrent)
- [Compila l'applicazione Electron](#compila-lapplicazione-electron)
- [Collaboratori](#collaboratori)
- [Licenza](#licenza)
## A proposito
**Hydra** è un **Game Launcher** con il proprio **Client BitTorrent** e **autogestore di repack**.
<br>
Il launcher è scritto in TypeScript (Electron) and Python, che gestisce il sistema di torrenting appoggiandosi a libtorrent.
## Caratteristiche
- Motore di ricerca automatizzato sulle fonti di repack dal [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
- Client Bittorrent integrato
- Integrazione How Long To Beat (HLTB) nella pagina del gioco
- Percorso del download Personalizzato
- Notifiche di aggiornamenti sulla list dei repacks
- Supporto Windows e Linux
- Costantemente Aggiornato
- E molto altro ...
## Installazione
Segui i seguenti passi:
1. Scarica l'ultima versione di Hydra dalla pagina [Releases](https://github.com/hydralauncher/hydra/releases/latest).
- Scarica solo il file .exe per installare Hydra su Windows.
- Scarica il file .deb o .rpm o .zip per Linux. (Dipende dalla tua distro Linux)
2. Esegui il file scaricato.
3. Goditi Hydra!
## <a name="contribuire"> Contribuire
### <a name="unisciti-su-telegram"></a> Unisciti su Telegram
Puoi unirti alle nostre conversazioni sul canale [Telegram](https://t.me/hydralauncher).
### Forka e Clona la repository
1. Forka la repository [(clicca qui per forkare)](https://github.com/hydralauncher/hydra/fork)
2. Clona il tuo codice forkato `git clone https://github.com/your_username/hydra`
3. Crea un nuovo branch
4. Aggiungi le modifiche (push)
5. Invia la richiesta di pull
### Modi in cui contribuire
- Traduzione: Vogliamo rendere Hydra disponibile a più persone possibile. Sentiti libero di tradurre in altre lingue o aggiornare e migliorare quelle già disponibili su Hydra.
- Programmazione: Hydra è programmato in TypeScript, Electron e un po' di Python. Se intendi contribuire unisciti al nostro [Telegram](https://t.me/hydralauncher)!
### Struttura del Progetto
- client-torrent: Usiamo libtorrent, una libreria Python, per gestire i download dei torrent
- src/renderer: l'UI dell'applicazione
- src/main: tutta la logica qui.
## Compilazione
### Installa Node.js
Assicurati di avere Node.js installato sulla tua macchina. Scaricalo e installalo da [nodejs.org](https://nodejs.org/).
### Installa Yarn
Yarn è un gestore di pacchetti per Node.js. Se non hai ancora installato Yarn segui le istruzioni su [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Installa le dipendenze Node
Naviga alla cartella del progetto e installa le dipendenze Node con Yarn:
```bash
cd hydra
yarn
```
### Installa Python 3.9
Assicurati di avere Python 3.9 installato. Puoi scaricarlo da [python.org](https://www.python.org/downloads/release/python-3913/).
### Installa le Dipendenze Python
Installa le dipendenze con pip:
```bash
pip install -r requirements.txt
```
## Variabili d'ambiente
Avrai bisogno di una chiave API SteamGridDB per poter caricare le icone di gioco.
Se intendi avere onlinefix come repacker dovrai aggiungere le tue credenziali al file .env
Una volta ottenuta, puoi copiare e rinominare il file `.env.example` a `.env` e metterlo in `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
## Esecuzione
Una volta impostato tutto, puoi eseguire il seguente comando per avviare il processo Electron e il client bittorrent:
```bash
yarn dev
```
## Compilazione
### Compila il bittorrent
Usa il comando:
```bash
python torrent-client/setup.py build
```
### Compila l'applicazione Electron
Usa il comando:
Per Windows:
```bash
yarn build:win
```
Per Linux:
```bash
yarn build:linux
```
## Collaboratori
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licenza
Hydra è concesso in licenza secondo la [MIT License](LICENSE).

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra is a game launcher with its own embedded bittorrent client.</strong>
</p>
@ -13,13 +13,15 @@
[![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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Table of Contents
- [Table of Contents](#table-of-contents)
- [About](#about)
- [Features](#features)
- [Installation](#installation)
- [Contributing](#contributing)
- [Join our Telegram](#join-our-telegram)
- [Contributing](#-contributing)
- [Join our Telegram](#-join-our-telegram)
- [Fork and clone your repository](#fork-and-clone-your-repository)
- [Ways you can contribute](#ways-you-can-contribute)
- [Project Structure](#project-structure)
@ -47,6 +50,7 @@
- [Build the bittorrent client](#build-the-bittorrent-client)
- [Build the Electron application](#build-the-electron-application)
- [Contributors](#contributors)
- [License](#license)
## About

View File

@ -5,7 +5,7 @@
[<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>
@ -13,13 +13,15 @@
[![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)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,26 +29,28 @@
## Zawartość.
- [Zawartość.](#zawartość)
- [O nas](#o-nas)
- [Cechy.](#cechy)
- [Cechy](#cechy)
- [Instalacja](#instalacja)
- [Dokonaj wpłaty](#dokonaj-wpłaty)
- [Dołącz do naszego kanału Telegram](#dołącz-do-naszego-kanału-telegram)
- [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)
- [Jak możesz pomóc](#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)
- [Zainstaluj Node.js](#zainstaluj-nodejs)
- [Zainstaluj Yarn](#zainstaluj-yarn)
- [Zainstaluj zależności Node](#zainstaluj-zależności-node)
- [Zainstaluj Python 3.9](#zainstaluj-python-39)
- [Zainstaluj zależności Pythona](#zainstaluj-zależności-pythona)
- [Zmienne środowiskowe](#zmienne-środowiskowe)
- [Uruchomienie](#utwórz-kompilację-z-kodu-źródłowego)
- [Run](#run)
- [Tworzenie kompilacji](#tworzenie-kompilacji)
- [Tworzenie klienta bittorrent](#zbuduj-klienta-bittorrent)
- [Tworzenie kompilacji aplikacji Electron](#tworzenie-aplikacji-electron)
- [Zbuduj klienta bittorrent](#zbuduj-klienta-bittorrent)
- [Tworzenie aplikacji Electron](#tworzenie-aplikacji-electron)
- [Współtwórcy](#współtwórcy)
- [License](#license)
## O nas

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra é um Launcher de Jogos com seu próprio cliente de bittorrent integrado e um wrapper autogerenciado para busca de repacks.</strong>
</p>
@ -13,13 +13,15 @@
[![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)
[![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)
[![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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,26 +29,28 @@
## Índice
- [Sobre](#about)
- [Recursos](#features)
- [Instalação](#installation)
- [Contribuindo](#contributing)
- [Junte-se ao nosso Telegram](#join-our-telegram)
- [Fork e clone seu repositorio](#fork-and-clone-your-repository)
- [Como contribuir](#ways-you-can-contribute)
- [Estrutura do projeto](#project-structure)
- [Compile a partir do código-fonte](#build-from-source)
- [Instale Node.js](#install-nodejs)
- [Instale Yarn](#install-yarn)
- [Instale Node Dependencies](#install-node-dependencies)
- [Instale Python 3.9](#install-python-39)
- [Instale Python Dependencies](#install-python-dependencies)
- [variaveis de ambiente](#environment-variables)
- [Rodando o programa](#running)
- [Compilando](#build)
- [Compile o client bittorrent](#build-the-bittorrent-client)
- [Compile a aplicação Electron](#build-the-electron-application)
- [Contribuidores](#contributors)
- [Índice](#índice)
- [Sobre](#-sobre)
- [Recursos](#-recursos)
- [Instalação](#-instalação)
- [Contribuindo](#-contribuindo)
- [Junte-se ao nosso Telegram](#-junte-se-ao-nosso-telegram)
- [Fork e clone o seu repositório](#-fork-e-clone-o-seu-repositório)
- [Formas de contribuir](#-formas-de-contribuir)
- [Estrutura do Projeto](#-estrutura-do-projeto)
- [Compile a partir do código-fonte](#-compile-a-partir-do-código-fonte)
- [Instale Node.js](#-instale-nodejs)
- [Instale Yarn](#-instale-yarn)
- [Instale Dependencias do Node](#-instale-dependencias-do-node)
- [Instale Python 3.9](#-instale-python-39)
- [Instale Python Dependencies](#-instale-python-dependencies)
- [Environment variables](#-environment-variables)
- [Running](#-running)
- [Build](#-build)
- [Build the bittorrent client](#-build-the-bittorrent-client)
- [Build the Electron application](#-build-the-electron-application)
- [Contributors](#-contributors)
- [Licença](#-licença)
## <a name="about"> Sobre

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra - это игровой лаунчер с собственным встроенным клиентом BitTorrent и самостоятельным scraper`ом для репаков.</strong>
</p>
@ -13,13 +13,15 @@
[![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)
[![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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Содержание
- [Содержание](#содержание)
- [Описание](#описание)
- [Особенности](#особенности)
- [Установка](#установка)
- [Вклад](#contributing)
- [Присоединяйтесь к нашему Telegram](#join-our-telegram)
- [Вклад](#-вклад)
- [Присоединяйтесь к нашему Telegram](#-присоединяйтесь-к-нашему-telegram)
- [Форк и клонирование репозитория](#форк-и-клонирование-репозитория)
- [Способы внести свой вклад](#способы-внести-свой-вклад)
- [Структура проекта](#структура-проекта)
@ -47,6 +50,7 @@
- [Сборка клиента BitTorrent](#сборка-клиента-bittorrent)
- [Сборка приложения Electron](#сборка-приложения-electron)
- [Участники](#участники)
- [License](#license)
## Описание

View File

@ -5,7 +5,7 @@
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra - це ігровий лаунчер з власним вбудованим bittorrent-клієнтом і самокерованим збирачем репаків.</strong>
</p>
@ -13,13 +13,15 @@
[![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)
[![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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Зміст
- [Зміст](#зміст)
- [Про нас](#про-нас)
- [Функції](#функції)
- [Встановлення](#встановлення)
- [Зробити свій внесок](#contributing)
- [Приєднуйтесь до нашого Telegram](#join-our-telegram)
- [Зробити свій внесок](#-зробити-свій-внесок)
- [Приєднуйтесь до нашого Telegram](#-приєднуйтесь-до-нашого-telegram)
- [Форк і клонування вашого репозиторію](#форк-і-клонування-вашого-репозиторію)
- [Як ви можете зробити свій внесок](#як-ви-можете-зробити-свій-внесок)
- [Структура проекту](#структура-проекту)
@ -47,6 +50,7 @@
- [Зробіть білд bittorrent client](#зробіть-білд-bittorrent-client)
- [Зробіть білд Electron застосунку](#зробіть-білд-electron-застосунку)
- [Контриб'ютори](#контрибютори)
- [License](#license)
## Про нас

View File

@ -44,6 +44,7 @@ linux:
- AppImage
- snap
- deb
- pacman
- rpm
maintainer: electronjs.org
category: Game

View File

@ -42,7 +42,7 @@
"@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/recipes": "^0.5.2",
"auto-launch": "^5.0.6",
"axios": "^1.6.8",
"axios": "^1.7.7",
"better-sqlite3": "^11.2.1",
"check-disk-space": "^3.4.0",
"classnames": "^2.5.1",
@ -58,7 +58,6 @@
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"icojs": "^0.19.3",
"iso-639-1": "3.1.2",
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",

View File

@ -1,4 +1,5 @@
{
"language_name": "اَلْعَرَبِيَّةُ",
"home": {
"featured": "مميّز",
"trending": "شائع",

View File

@ -1,4 +1,5 @@
{
"language_name": "беларуская мова",
"home": {
"featured": "Рэкамэндаванае",
"trending": "Актуальнае",

View File

@ -1,4 +1,5 @@
{
"language_name": "Català",
"app": {
"successfully_signed_in": "Has entrat correctament"
},

View File

@ -1,4 +1,5 @@
{
"language_name": "Dansk",
"home": {
"featured": "Anbefalet",
"trending": "Trender",

View File

@ -0,0 +1,277 @@
{
"language_name": "Deutsch",
"app": {
"successfully_signed_in": "Erfolgreich angemeldet"
},
"home": {
"featured": "Empfohlen",
"trending": "Beliebt",
"surprise_me": "Überrasche mich",
"no_results": "Keine Ergebnisse gefunden"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "Downloads",
"settings": "Einstellungen",
"my_library": "Meine Bibliothek",
"downloading_metadata": "{{title}} (Metadaten werden heruntergeladen…)",
"paused": "{{title}} (Pausiert)",
"downloading": "{{title}} ({{percentage}} - Wird heruntergeladen…)",
"filter": "Bibliothek filtern",
"home": "Home",
"queued": "{{title}} (In Warteschlange)",
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
"sign_in": "Anmelden"
},
"header": {
"search": "Spiele suchen",
"home": "Home",
"catalogue": "Katalog",
"downloads": "Downloads",
"search_results": "Suchergebnisse",
"settings": "Einstellungen",
"version_available_install": "Version {{version}} verfügbar. Klicke hier, um neuzustarten und sie zu installieren.",
"version_available_download": "Version {{version}} verfügbar. Klicke hier, um sie herunterzuladen."
},
"bottom_panel": {
"no_downloads_in_progress": "Keine aktive Downloads",
"downloading_metadata": "Metadaten von {{title}} werden heruntergeladen…",
"downloading": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Abschluss {{eta}} - {{speed}}",
"calculating_eta": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Verbleibende Zeit wird berechnet…",
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)"
},
"catalogue": {
"next_page": "Nächste Seite",
"previous_page": "Vorherige Seite"
},
"game_details": {
"open_download_options": "Download-Optionen öffnen",
"download_options_zero": "Keine Download-Optionen",
"download_options_one": "{{count}} Download-Option",
"download_options_other": "{{count}} Download-Optionen",
"updated_at": "Aktualisiert {{updated_at}}",
"install": "Installieren",
"resume": "Fortfahren",
"pause": "Pausieren",
"cancel": "Abbrechen",
"remove": "Entfernen",
"space_left_on_disk": "{{space}} auf Festplatte verfügbar",
"eta": "Abschluss {{eta}}",
"calculating_eta": "Verbleibende Zeit wird berechnet…",
"downloading_metadata": "Metadaten werden heruntergeladen…",
"filter": "Repacks filtern",
"requirements": "Systemanforderungen",
"minimum": "Minimum",
"recommended": "Empfohlen",
"paused": "Pausiert",
"release_date": "Veröffentlicht am {{date}}",
"publisher": "Veröffentlicht von {{publisher}}",
"hours": "Stunden",
"minutes": "Minuten",
"amount_hours": "{{amount}} Stunden",
"amount_minutes": "{{amount}} Minuten",
"accuracy": "{{accuracy}}% Genauigkeit",
"add_to_library": "Zu Bibliothek hinzufügen",
"remove_from_library": "Von Bibliothek entfernen",
"no_downloads": "Keine Downloads verfügbar",
"play_time": "{{amount}} lang gespielt",
"last_time_played": "Zuletzt gespielt {{period}}",
"not_played_yet": "{{title}} wurde noch nicht gespielt",
"next_suggestion": "Nächste Empfehlung",
"play": "Spielen",
"deleting": "Installer wird gelöscht…",
"close": "Schließen",
"playing_now": "Spielt jetzt",
"change": "Ändern",
"repacks_modal_description": "Wähle das Repack, das du herunterladen möchtest",
"select_folder_hint": "Um das Standardverzeichnis zu ändern, gehe zu den <0>Einstellungen</0>",
"download_now": "Jetzt herunterladen",
"no_shop_details": "Shop-Details konnten nicht abgerufen werden.",
"download_options": "Download-Optionen",
"download_path": "Download-Verzeichnis",
"previous_screenshot": "Vorheriger Screenshot",
"next_screenshot": "Nächster Screenshot",
"screenshot": "Screenshot {{number}}",
"open_screenshot": "Screenshot {{number}} öffnen",
"download_settings": "Download-Einstellungen",
"downloader": "Downloader",
"select_executable": "Auswählen",
"no_executable_selected": "Keine ausführbare Datei gewählt",
"open_folder": "Verzeichnis öffnen",
"open_download_location": "Heruntergeladene Dateien anzeigen",
"create_shortcut": "Desktop-Verknüpfung erstellen",
"remove_files": "Dateien entfernen",
"remove_from_library_title": "Bist du dir sicher?",
"remove_from_library_description": "Dies wird {{game}} aus deiner Bibliothek entfernen",
"options": "Optionen",
"executable_section_title": "Ausführbare Datei",
"executable_section_description": "Pfad der Datei, die bei Klick auf \"Play\" ausgeführt wird",
"downloads_secion_title": "Downloads",
"downloads_section_description": "Sieh dir Updates oder andere Versionen dieses Spiels an",
"danger_zone_section_title": "Gefahrenzone",
"danger_zone_section_description": "Entferne dieses Spiel aus deiner Bibliothek oder die von Hydra heruntergeladenen Dateien",
"download_in_progress": "Download erfolgt",
"download_paused": "Download ist pausiert",
"last_downloaded_option": "Letzte Download-Option",
"create_shortcut_success": "Verknüpfung erfolgreich erstellt",
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung"
},
"activation": {
"title": "Hydra aktivieren",
"installation_id": "Installations ID:",
"enter_activation_code": "Aktivierungscode eingeben",
"message": "Wenn du nicht weißt wo du fragen musst, solltest du dies nicht haben.",
"activate": "Aktivieren",
"loading": "Lädt…"
},
"downloads": {
"resume": "Fortfahren",
"pause": "Pause",
"eta": "Abschluss {{eta}}",
"paused": "Pausiert",
"verifying": "Verifiziere…",
"completed": "Abgeschlossen",
"removed": "Nicht heruntergeladen",
"cancel": "Abbrechen",
"filter": "Heruntergeladene Spiele filtern",
"remove": "Entfernen",
"downloading_metadata": "Metadaten werden heruntergeladen…",
"deleting": "Installer wird entfernt…",
"delete": "Installer entfernen",
"delete_modal_title": "Bist du dir sicher?",
"delete_modal_description": "Dies wird alle Installationsdateien von deinem Computer entfernen",
"install": "Installieren",
"download_in_progress": "Läuft",
"queued_downloads": "Downloads in Warteschlange",
"downloads_completed": "Abgeschlossen",
"queued": "In Warteschlange",
"no_downloads_title": "Welch Leere",
"no_downloads_description": "Du hast mit Hydra noch nichts heruntergeladen, aber es ist nie zu spät anzufangen.",
"checking_files": "Dateien werden überprüft…"
},
"settings": {
"downloads_path": "Download-Pfad",
"change": "Aktualisieren",
"notifications": "Benachrichtigungen",
"enable_download_notifications": "Wenn ein Download abgeschlossen wird",
"enable_repack_list_notifications": "Wenn ein neues Repack hinzugefügt wird",
"real_debrid_api_token_label": "Real-Debrid API Token",
"quit_app_instead_hiding": "Hydra verlassen statt minimieren beim Schließen",
"launch_with_system": "Hydra bei Systemstart starten",
"general": "Allgemein",
"behavior": "Verhalten",
"download_sources": "Download-Quellen",
"language": "Sprache",
"real_debrid_api_token": "API Token",
"enable_real_debrid": "Real-Debrid aktivieren",
"real_debrid_description": "Real-Debrid ist ein unrestriktiver Downloader, der es dir ermöglicht Dateien sofort und mit deiner maximalen Internetgeschwindigkeit herunterzuladen.",
"real_debrid_invalid_token": "API token nicht gültig",
"real_debrid_api_token_hint": "<0>Hier</0> kannst du dir deinen API Token holen",
"real_debrid_free_account_error": "Das Konto \"{{username}}\" ist ein gratis account. Bitte abonniere Real-Debrid",
"real_debrid_linked_message": "Konto \"{{username}}\" verknüpft",
"save_changes": "Änderungen speichern",
"changes_saved": "Änderungen erfolgreich gespeichert",
"download_sources_description": "Hydra wird die Download-Links von diesen Quellen abrufen. Die Quell-URL muss ein direkter Link zu einer .json Datei, welche die Download-Links enthält, sein.",
"validate_download_source": "Validieren",
"remove_download_source": "Entfernen",
"add_download_source": "Quelle hinzufügen",
"download_count_zero": "Keine Download-Option",
"download_count_one": "{{countFormatted}} Download-Option",
"download_count_other": "{{countFormatted}} Download-Optionen",
"download_source_url": "Download Quell-URL",
"add_download_source_description": "Füge die URL, welche die .json Datei enthält, ein",
"download_source_up_to_date": "Auf aktuellem Stand",
"download_source_errored": "Fehlgeschlagen",
"sync_download_sources": "Quellen synchronisieren",
"removed_download_source": "Download-Quelle entfernt",
"added_download_source": "Download-Quelle hinzugefügt",
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
"insert_valid_json_url": "Füge eine gültige JSON URL ein",
"found_download_option_zero": "Keine Download-Option gefunden",
"found_download_option_one": "{{countFormatted}} Download-Option gefunden",
"found_download_option_other": "{{countFormatted}} Download-Optionen gefunden",
"import": "Importieren"
},
"notifications": {
"download_complete": "Download abgeschlossen",
"game_ready_to_install": "{{title}} ist bereit zur Installation",
"repack_list_updated": "Repack-Liste aktualisiert",
"repack_count_one": "{{count}} Repack hinzugefügt",
"repack_count_other": "{{count}} Repacks hinzugefügt",
"new_update_available": "Version {{version}} verfügbar",
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu"
},
"system_tray": {
"open": "Hydra öffnen",
"quit": "Schließen"
},
"game_card": {
"no_downloads": "Keine Downloads verfügbar"
},
"binary_not_found_modal": {
"title": "Programme nicht installiert",
"description": "Ausführbare Dateien für Wine oder Lutris wurden auf deinem System nicht gefunden",
"instructions": "Überprüfe die korrekte Installation dieser für deine Linux-Distro, damit das Spiel normal laufen kann"
},
"modal": {
"close": "Knopf schließen"
},
"forms": {
"toggle_password_visibility": "Sichtbarkeit des Passworts umschalten"
},
"user_profile": {
"amount_hours": "{{amount}} Stunden",
"amount_minutes": "{{amount}} Minuten",
"last_time_played": "Zuletzt gespielt {{period}}",
"activity": "Letzte Aktivität",
"library": "Bibliothek",
"total_play_time": "Gesamtspielzeit: {{amount}}",
"no_recent_activity_title": "Hmmm… hier ist nichts",
"no_recent_activity_description": "Du hast in letzter Zeit keine Spiele gespielt. Es wird Zeit das zu ändern!",
"display_name": "Anzeigename",
"saving": "Speichert",
"save": "Speichern",
"edit_profile": "Profil Bearbeiten",
"saved_successfully": "Erfolgreich gespeichert",
"try_again": "Bitte versuche es erneut",
"sign_out_modal_title": "Bist du dir sicher?",
"cancel": "Abbrechen",
"successfully_signed_out": "Erfolgreich abgemeldet",
"sign_out": "Abmelden",
"playing_for": "Spielt {{amount}} lang",
"sign_out_modal_text": "Deine Bibliothek ist mit deinem aktuellen Konto verknüpft. Wenn du dich abmeldest, wird deine Bibliothek nicht mehr sichtbar sein und jeglicher Fortschritt wird nicht gespeichert. Abmelden fortführen?",
"add_friends": "Freunde hinzufügen",
"add": "Hinzufügen",
"friend_code": "Freundescode",
"see_profile": "Profil anzeigen",
"sending": "Sendet",
"friend_request_sent": "Freundschaftsanfrage versendet",
"friends": "Freunde",
"friends_list": "Freundesliste",
"user_not_found": "Nutzer nicht gefunden",
"block_user": "Nutzer blockieren",
"add_friend": "Freund hinzufügen",
"request_sent": "Anfrage versendet",
"request_received": "Anfrage erhalten",
"accept_request": "Anfrage annehmen",
"ignore_request": "Anfrage ignorieren",
"cancel_request": "Anfrage zurückziehen",
"undo_friendship": "Freundschaft kündigen",
"request_accepted": "Anfrage akzeptiert",
"user_blocked_successfully": "Nutzer erfolgreich blockiert",
"user_block_modal_text": "{{displayName}} wird dadurch blockiert",
"settings": "Einstellungen",
"public": "Öffentlich",
"private": "Privat",
"friends_only": "Nur Freunde",
"privacy": "Privatsphäre",
"blocked_users": "Blockierte Nutzer",
"unblock": "Freigeben",
"no_friends_added": "Du hast noch keine Freunde hinzugefügt",
"pending": "Ausstehend",
"no_pending_invites": "Du hast keine ausstehenden Einladungen",
"no_blocked_users": "Du hast keine blockierten Nutzer",
"friend_code_copied": "Freundescode kopiert",
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt"
}
}

View File

@ -1,4 +1,5 @@
{
"language_name": "English",
"app": {
"successfully_signed_in": "Successfully signed in"
},
@ -20,7 +21,8 @@
"home": "Home",
"queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in"
"sign_in": "Sign in",
"friends": "Friends"
},
"header": {
"search": "Search games",

View File

@ -1,4 +1,5 @@
{
"language_name": "Español",
"app": {
"successfully_signed_in": "Sesión iniciada correctamente"
},
@ -270,6 +271,7 @@
"pending": "Pendiente",
"no_pending_invites": "No tienes invitaciones pendientes",
"no_blocked_users": "No has bloqueado a ningún usuario",
"friend_code_copied": "Código de amigo copiado"
"friend_code_copied": "Código de amigo copiado",
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}"
}
}

View File

@ -1,4 +1,5 @@
{
"language_name": "فارسی",
"home": {
"featured": "پیشنهادی",
"trending": "پرطرفدار",

View File

@ -1,4 +1,5 @@
{
"language_name": "Français",
"home": {
"featured": "En vedette",
"trending": "Tendance",

View File

@ -1,4 +1,5 @@
{
"language_name": "Magyar",
"home": {
"featured": "Featured",
"trending": "Népszerű",

View File

@ -1,4 +1,5 @@
{
"language_name": "Bahasa Indonesia",
"app": {
"successfully_signed_in": "Berhasil masuk"
},

View File

@ -1,21 +1,49 @@
export { default as en } from "./en/translation.json";
export { default as pt } from "./pt/translation.json";
export { default as es } from "./es/translation.json";
export { default as nl } from "./nl/translation.json";
export { default as fr } from "./fr/translation.json";
export { default as hu } from "./hu/translation.json";
export { default as it } from "./it/translation.json";
export { default as pl } from "./pl/translation.json";
export { default as ru } from "./ru/translation.json";
export { default as tr } from "./tr/translation.json";
export { default as be } from "./be/translation.json";
export { default as uk } from "./uk/translation.json";
export { default as zh } from "./zh/translation.json";
export { default as id } from "./id/translation.json";
export { default as ko } from "./ko/translation.json";
export { default as da } from "./da/translation.json";
export { default as ar } from "./ar/translation.json";
export { default as fa } from "./fa/translation.json";
export { default as ro } from "./ro/translation.json";
export { default as ca } from "./ca/translation.json";
export { default as kk } from "./kk/translation.json";
import en from "./en/translation.json";
import ptPT from "./pt-PT/translation.json";
import ptBR from "./pt-BR/translation.json";
import es from "./es/translation.json";
import nl from "./nl/translation.json";
import fr from "./fr/translation.json";
import hu from "./hu/translation.json";
import it from "./it/translation.json";
import de from "./de/translation.json";
import pl from "./pl/translation.json";
import ru from "./ru/translation.json";
import tr from "./tr/translation.json";
import be from "./be/translation.json";
import uk from "./uk/translation.json";
import zh from "./zh/translation.json";
import id from "./id/translation.json";
import ko from "./ko/translation.json";
import da from "./da/translation.json";
import ar from "./ar/translation.json";
import fa from "./fa/translation.json";
import ro from "./ro/translation.json";
import ca from "./ca/translation.json";
import kk from "./kk/translation.json";
export default {
"pt-BR": ptBR,
"pt-PT": ptPT,
en,
de,
es,
nl,
fr,
hu,
it,
pl,
ru,
tr,
be,
uk,
zh,
id,
ko,
da,
ar,
fa,
ro,
ca,
kk,
};

View File

@ -1,4 +1,5 @@
{
"language_name": "Italiano",
"home": {
"featured": "In primo piano",
"trending": "Di tendenza",

View File

@ -1,4 +1,5 @@
{
"language_name": "қазақ тілі",
"app": {
"successfully_signed_in": "Сәтті кіру"
},

View File

@ -1,4 +1,5 @@
{
"language_name": "한국어",
"home": {
"featured": "추천",
"trending": "인기",

View File

@ -1,4 +1,5 @@
{
"language_name": "Nederlands",
"home": {
"featured": "Uitgelicht",
"trending": "Trending",

View File

@ -1,4 +1,5 @@
{
"language_name": "Polski",
"home": {
"featured": "Wyróżnione",
"trending": "Trendujące",

View File

@ -1,10 +1,13 @@
{
"language_name": "Português (Brasil)",
"app": {
"successfully_signed_in": "Autenticado com sucesso"
},
"home": {
"featured": "Destaques",
"trending": "Populares",
"hot": "🔥 Populares agora",
"weekly": "📅 Mais baixados da semana",
"surprise_me": "Surpreenda-me",
"no_results": "Nenhum resultado encontrado"
},
@ -20,7 +23,8 @@
"home": "Início",
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login"
"sign_in": "Login",
"friends": "Amigos"
},
"header": {
"search": "Buscar jogos",
@ -166,7 +170,7 @@
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
"save_changes": "Salvar mudanças",
"changes_saved": "Ajustes salvos com sucesso",
"download_sources_description": "Hydra vai buscar links de download em todas as fonte habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
"download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
"validate_download_source": "Validar",
"remove_download_source": "Remover",
"add_download_source": "Adicionar fonte",
@ -239,7 +243,7 @@
"cancel": "Cancelar",
"successfully_signed_out": "Deslogado com sucesso",
"sign_out": "Sair da conta",
"sign_out_modal_title": "Tem certeza?",
"sign_out_modal_title": "Deseja mesmo sair?",
"playing_for": "Jogando por {{amount}}",
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?",
"add_friends": "Adicionar Amigos",

View File

@ -0,0 +1,281 @@
{
"language_name": "Português (Portugal)",
"app": {
"successfully_signed_in": "Sessão iniciada com sucesso"
},
"home": {
"featured": "Destaques",
"trending": "Populares",
"surprise_me": "Surpreende-me",
"no_results": "Nenhum resultado encontrado"
},
"sidebar": {
"catalogue": "Catálogo",
"downloads": "Transferências",
"settings": "Definições",
"my_library": "Biblioteca",
"downloading_metadata": "{{title}} (A transferir metadados…)",
"paused": "{{title}} (Pausado)",
"downloading": "{{title}} ({{percentage}} - A transferir…)",
"filter": "Procurar",
"home": "Início",
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não tem executável selecionado",
"sign_in": "Iniciar sessão"
},
"header": {
"search": "Procurar jogos",
"catalogue": "Catálogo",
"downloads": "Transferências",
"search_results": "Resultados da pesquisa",
"settings": "Definições",
"home": "Início",
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
},
"bottom_panel": {
"no_downloads_in_progress": "Sem transferências em andamento",
"downloading_metadata": "A transferir metadados de {{title}}…",
"downloading": "A transferir {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "A transferir {{title}}… ({{percentage}} concluído) - A calcular tempo restante…",
"checking_files": "A verificar ficheiros de {{title}}…"
},
"game_details": {
"open_download_options": "Ver opções de transferência",
"download_options_zero": "Sem opções de transferência",
"download_options_one": "{{count}} opção de transferência",
"download_options_other": "{{count}} opções de transferência",
"updated_at": "Atualizado a {{updated_at}}",
"resume": "Retomar",
"pause": "Pausar",
"cancel": "Cancelar",
"remove": "Remover",
"space_left_on_disk": "{{space}} livres no disco",
"eta": "Conclusão {{eta}}",
"calculating_eta": "A calcular tempo restante…",
"downloading_metadata": "A transferir metadados…",
"filter": "Filtrar repacks",
"requirements": "Requisitos do sistema",
"minimum": "Mínimos",
"recommended": "Recomendados",
"paused": "Pausado",
"release_date": "Lançado em {{date}}",
"publisher": "Publicado por {{publisher}}",
"hours": "horas",
"minutes": "minutos",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"accuracy": "{{accuracy}}% de precisão",
"add_to_library": "Adicionar à biblioteca",
"remove_from_library": "Remover da biblioteca",
"no_downloads": "Nenhuma transferência disponível",
"play_time": "Jogou por {{amount}}",
"next_suggestion": "Próxima sugestão",
"install": "Instalar",
"last_time_played": "Última sessão {{period}}",
"play": "Jogar",
"not_played_yet": "Ainda não jogou {{title}}",
"close": "Fechar",
"deleting": "A eliminar instalador…",
"playing_now": "A jogar agora",
"change": "Explorar",
"repacks_modal_description": "Escolha o repack do jogo que deseja transferir",
"select_folder_hint": "Para trocar o diretório padrão, aceda à <0>Tela de Definições</0>",
"download_now": "Iniciar transferência",
"no_shop_details": "Não foi possível obter os detalhes da loja.",
"download_options": "Opções de transferência",
"download_path": "Diretório de transferência",
"previous_screenshot": "Captura de ecrã anterior",
"next_screenshot": "Próxima captura de ecrã",
"screenshot": "Captura de ecrã {{number}}",
"open_screenshot": "Ver captura de ecrã {{number}}",
"download_settings": "Definições de transferência",
"downloader": "Downloader",
"select_executable": "Explorar",
"no_executable_selected": "Nenhum executável selecionado",
"open_folder": "Abrir pasta",
"open_download_location": "Ver ficheiros transferidos",
"create_shortcut": "Criar atalho no ambiente de trabalho",
"remove_files": "Remover ficheiros",
"options": "Gerir",
"remove_from_library_description": "Isto irá remover {{game}} da sua biblioteca",
"remove_from_library_title": "Tem a certeza?",
"executable_section_title": "Executável",
"executable_section_description": "O caminho do ficheiro que será executado ao clicar em \"Jogar\"",
"downloads_secion_title": "Transferências",
"downloads_section_description": "Confira atualizações ou versões diferentes para este mesmo título",
"danger_zone_section_title": "Zona de perigo",
"danger_zone_section_description": "Remova o jogo da sua biblioteca ou os ficheiros que foram transferidos pelo Hydra",
"download_in_progress": "Transferência em andamento",
"download_paused": "Transferência pausada",
"last_downloaded_option": "Última opção transferida",
"create_shortcut_success": "Atalho criado com sucesso",
"create_shortcut_error": "Erro ao criar atalho"
},
"activation": {
"title": "Ativação",
"installation_id": "ID da instalação:",
"enter_activation_code": "Insira o seu código de ativação",
"message": "Se não sabe onde conseguir o código, talvez não devesse estar aqui.",
"activate": "Ativar",
"loading": "A carregar…"
},
"downloads": {
"resume": "Retomar",
"pause": "Pausar",
"eta": "Conclusão {{eta}}",
"paused": "Pausado",
"verifying": "A verificar…",
"completed": "Concluído",
"removed": "Cancelado",
"cancel": "Cancelar",
"filter": "Filtrar jogos transferidos",
"remove": "Remover",
"downloading_metadata": "A transferir metadados…",
"delete": "Remover instalador",
"delete_modal_description": "Isto removerá todos os ficheiros de instalação do seu computador",
"delete_modal_title": "Tem a certeza?",
"deleting": "A eliminar instalador…",
"install": "Instalar",
"download_in_progress": "A transferir agora",
"queued_downloads": "Na fila",
"downloads_completed": "Concluído",
"queued": "Na fila",
"no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Ainda não transferiu nada pelo Hydra, mas nunca é tarde para começar.",
"checking_files": "A verificar ficheiros…"
},
"settings": {
"downloads_path": "Diretório das transferências",
"change": "Explorar...",
"notifications": "Notificações",
"enable_download_notifications": "Quando uma transferência for concluída",
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"real_debrid_api_token_label": "Token de API do Real-Debrid",
"quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
"launch_with_system": "Iniciar o Hydra com o sistema",
"general": "Geral",
"behavior": "Comportamento",
"download_sources": "Fontes de transferência",
"language": "Idioma",
"real_debrid_api_token": "Token de API",
"enable_real_debrid": "Ativar Real-Debrid",
"real_debrid_api_token_hint": "Pode obter o seu token de API <0>aqui</0>",
"real_debrid_description": "O Real-Debrid é um downloader sem restrições que permite transferir ficheiros instantaneamente e com a melhor velocidade da sua Internet.",
"real_debrid_invalid_token": "Token de API inválido",
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, subscreva o Real-Debrid",
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
"save_changes": "Guardar alterações",
"changes_saved": "Definições guardadas com sucesso",
"download_sources_description": "O Hydra vai procurar links de transferência em todas as fontes ativadas. A URL da página de detalhes da loja não é guardada no seu dispositivo. Utilizamos um sistema de metadados criado pela comunidade para fornecer suporte a mais fontes de transferência de jogos.",
"enable_source": "Ativar",
"disable_source": "Desativar",
"validate_download_source": "Validar",
"remove_download_source": "Remover",
"add_download_source": "Adicionar fonte",
"download_count_zero": "Sem transferências na lista",
"download_count_one": "{{countFormatted}} transferência na lista",
"download_count_other": "{{countFormatted}} transferências na lista",
"download_options_zero": "Sem transferências disponíveis",
"download_options_one": "{{countFormatted}} transferência disponível",
"download_options_other": "{{countFormatted}} transferências disponíveis",
"download_source_url": "URL da fonte",
"add_download_source_description": "Insira o URL contendo o arquivo .json",
"download_source_up_to_date": "Sincronizada",
"download_source_errored": "Falhou",
"sync_download_sources": "Sincronizar",
"removed_download_source": "Fonte removida",
"added_download_source": "Fonte adicionada",
"download_sources_synced": "As fontes foram sincronizadas",
"insert_valid_json_url": "Insira o URL de um JSON válido",
"found_download_option_zero": "Nenhuma opção de transferência encontrada",
"found_download_option_one": "{{countFormatted}} opção de transferência encontrada",
"found_download_option_other": "{{countFormatted}} opções de transferências encontradas",
"import": "Importar"
},
"notifications": {
"download_complete": "Transferência concluída",
"game_ready_to_install": "{{title}} está pronto para ser descarregado",
"repack_list_updated": "Lista de repacks atualizada",
"repack_count_one": "{{count}} novo repack",
"repack_count_other": "{{count}} novos repacks",
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
},
"system_tray": {
"open": "Abrir Hydra",
"quit": "Fechar"
},
"game_card": {
"no_downloads": "Sem transferências disponíveis"
},
"binary_not_found_modal": {
"title": "Programas não instalados",
"description": "Os executáveis do Wine ou Lutris não foram encontrados em seu sistema.",
"instructions": "Verifique a forma correta de instalar algum deles na sua distro Linux, garantindo assim a execução normal do jogo."
},
"catalogue": {
"next_page": "Próxima página",
"previous_page": "Página anterior"
},
"modal": {
"close": "Botão de fechar"
},
"forms": {
"toggle_password_visibility": "Alternar visibilidade da palavra-passe"
},
"user_profile": {
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"last_time_played": "Última sessão {{period}}",
"activity": "Atividades recentes",
"library": "Biblioteca",
"total_play_time": "Tempo total de jogo: {{amount}}",
"no_recent_activity_title": "Hmmm… nada por aqui",
"no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?",
"display_name": "Nome de exibição",
"saving": "a guardar…",
"save": "Guardar",
"edit_profile": "Editar perfil",
"saved_successfully": "Guardado com sucesso",
"try_again": "Por favor, tenta novamente",
"cancel": "Cancelar",
"successfully_signed_out": "Terminado com sucesso",
"sign_out": "Terminar sessão",
"sign_out_modal_title": "Tens a certeza?",
"playing_for": "A jogar há {{amount}}",
"sign_out_modal_text": "A tua biblioteca de jogos está associada com a tua conta atual. Ao sair, a tua biblioteca não aparecerá mais no Hydra e qualquer progresso não será guardado. Desejas continuar?",
"add_friends": "Adicionar Amigos",
"friend_code": "Código de amigo",
"see_profile": "Ver perfil",
"friend_request_sent": "Pedido de amizade enviado",
"friends": "Amigos",
"add": "Adicionar",
"sending": "A enviar",
"friends_list": "Lista de amigos",
"user_not_found": "Utilizador não encontrado",
"block_user": "Bloquear",
"add_friend": "Adicionar amigo",
"request_sent": "Pedido enviado",
"request_received": "Pedido recebido",
"accept_request": "Aceitar pedido",
"ignore_request": "Ignorar pedido",
"cancel_request": "Cancelar pedido",
"undo_friendship": "Desfazer amizade",
"request_accepted": "Pedido de amizade aceito",
"user_blocked_successfully": "Utilizador bloqueado com sucesso",
"user_block_modal_text": "Bloquear {{displayName}}",
"settings": "Definições",
"privacy": "Privacidade",
"private": "Privado",
"friends_only": "Apenas amigos",
"public": "Público",
"blocked_users": "Utilizadores bloqueados",
"unblock": "Desbloquear",
"no_friends_added": "Ainda não adicionaste amigos",
"pending": "Pendentes",
"no_pending_invites": "Não tens convites de amizade pendentes",
"no_blocked_users": "Não tens nenhum utilizador bloqueado",
"friend_code_copied": "Código de amigo copiado"
}
}

View File

@ -1,4 +1,5 @@
{
"language_name": "Română",
"home": {
"featured": "Recomandate",
"trending": "Populare",

View File

@ -1,4 +1,5 @@
{
"language_name": "Русский",
"app": {
"successfully_signed_in": "Успешный вход"
},

View File

@ -1,4 +1,5 @@
{
"language_name": "Türkçe",
"home": {
"featured": "Öne çıkan",
"trending": "Popüler",

View File

@ -1,4 +1,5 @@
{
"language_name": "Українська",
"app": {
"successfully_signed_in": "Успішний вхід в систему"
},

View File

@ -1,4 +1,5 @@
{
"language_name": "中文",
"app": {
"successfully_signed_in": "已成功登录"
},

View File

@ -1,35 +1,42 @@
import type { CatalogueEntry, GameShop } from "@types";
import type { GameShop } from "@types";
import { registerEvent } from "../register-event";
import { RepacksManager, requestSteam250 } from "@main/services";
import { formatName, steamUrlBuilder } from "@shared";
import { HydraApi, RepacksManager } from "@main/services";
import { CatalogueCategory, formatName, steamUrlBuilder } from "@shared";
import { steamGamesWorker } from "@main/workers";
const resultSize = 12;
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
category: CatalogueCategory
) => {
const params = new URLSearchParams({
take: "12",
skip: "0",
});
const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
const trendingGames = await requestSteam250("/90day");
const results: CatalogueEntry[] = [];
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
`/games/${category}?${params.toString()}`
);
for (let i = 0; i < resultSize; i++) {
if (!trendingGames[i]) {
i++;
continue;
}
return Promise.all(
response.map(async (game) => {
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
name: "getById",
});
const { title, objectID } = trendingGames[i]!;
const repacks = RepacksManager.search({ query: formatName(title) });
const repacks = RepacksManager.search({
query: formatName(steamGame.name),
});
const catalogueEntry = {
objectID,
title,
shop: "steam" as GameShop,
cover: steamUrlBuilder.library(objectID),
};
results.push({ ...catalogueEntry, repacks });
}
return results;
return {
title: steamGame.name,
shop: game.shop,
repacks,
cover: steamUrlBuilder.library(game.objectId),
objectID: game.objectId,
};
})
);
};
registerEvent("getCatalogue", getCatalogue);

View File

@ -0,0 +1,23 @@
import type { GameShop } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import type { GameStats } from "@types";
const getGameStats = async (
_event: Electron.IpcMainInvokeEvent,
objectId: string,
shop: GameShop
) => {
const params = new URLSearchParams({
objectId,
shop,
});
const response = await HydraApi.get<GameStats>(
`/games/stats?${params.toString()}`
);
return response;
};
registerEvent("getGameStats", getGameStats);

View File

@ -8,6 +8,7 @@ import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-random-game";
import "./catalogue/search-games";
import "./catalogue/search-game-repacks";
import "./catalogue/get-game-stats";
import "./hardware/get-disk-free-space";
import "./library/add-game-to-library";
import "./library/create-game-shortcut";

View File

@ -1,10 +1,27 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { HydraApi, logger } from "@main/services";
import { steamGamesWorker } from "@main/workers";
import type { UserProfile } from "@types";
import { getUserFriends } from "./get-user-friends";
import { steamUrlBuilder } from "@shared";
const getSteamGame = async (objectId: string) => {
try {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
return {
title: steamGame.name,
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
};
} catch (err) {
logger.error("Failed to get Steam game", err);
return null;
}
};
const getUser = async (
_event: Electron.IpcMainInvokeEvent,
userId: string
@ -20,36 +37,51 @@ const getUser = async (
if (!profile) return null;
const recentGames = await Promise.all(
profile.recentGames.map(async (game) => {
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
name: "getById",
});
profile.recentGames
.map(async (game) => {
const steamGame = await getSteamGame(game.objectId);
return {
...game,
title: steamGame.name,
iconUrl: steamUrlBuilder.icon(game.objectId, steamGame.clientIcon),
};
})
return {
...game,
...steamGame,
};
})
.filter((game) => game)
);
// const libraryGames = await Promise.all(
// profile.libraryGames.map(async (game) => {
// return getSteamUserGame(game);
// })
// );
const libraryGames = await Promise.all(
profile.libraryGames
.map(async (game) => {
const steamGame = await getSteamGame(game.objectId);
// const currentGame = await getGameRunning(profile.currentGame);
return {
...game,
...steamGame,
};
})
.filter((game) => game)
);
if (profile.currentGame) {
const steamGame = await getSteamGame(profile.currentGame.objectId);
if (steamGame) {
profile.currentGame = {
...profile.currentGame,
...steamGame,
};
}
}
return {
...profile,
// libraryGames,
libraryGames,
recentGames,
friends: friends.friends,
totalFriends: friends.totalFriends,
// currentGame,
};
} catch (err) {
console.log("err", err);
return null;
}
};

Binary file not shown.

View File

@ -7,7 +7,7 @@ import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, PythonInstance, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
import * as resources from "@locales";
import resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
import { knexClient, migrationConfig } from "./knex-client";

View File

@ -3,6 +3,15 @@ import { HydraApi } from "../hydra-api";
import { gameRepository } from "@main/repository";
export const createGame = async (game: Game) => {
console.log({ objectId: game.objectID, shop: game.shop });
HydraApi.post("/games/download", {
objectId: game.objectID,
shop: game.shop,
}).catch((err) => {
console.log(err);
});
HydraApi.post(`/profile/games`, {
objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),

View File

@ -12,6 +12,7 @@ import type {
FriendRequestAction,
UpdateProfileRequest,
} from "@types";
import type { CatalogueCategory } from "@shared";
contextBridge.exposeInMainWorld("electron", {
/* Torrenting */
@ -34,7 +35,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Catalogue */
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
getCatalogue: () => ipcRenderer.invoke("getCatalogue"),
getCatalogue: (category: CatalogueCategory) =>
ipcRenderer.invoke("getCatalogue", category),
getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
@ -44,6 +46,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getGames", take, prevCursor),
searchGameRepacks: (query: string) =>
ipcRenderer.invoke("searchGameRepacks", query),
getGameStats: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameStats", objectId, shop),
/* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),

View File

@ -0,0 +1,13 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const actions = style({
display: "flex",
alignSelf: "flex-end",
gap: `${SPACING_UNIT * 2}px`,
});
export const descriptionText = style({
fontSize: "16px",
lineHeight: "24px",
});

View File

@ -0,0 +1,30 @@
import { Button } from "../button/button";
import { Modal, type ModalProps } from "../modal/modal";
import * as styles from "./confirmation-modal.css";
export interface ConfirmationModalProps extends ModalProps {
confirmButtonLabel: string;
cancelButtonLabel: string;
descriptionText: string;
}
export function ConfirmationModal({
confirmButtonLabel,
cancelButtonLabel,
descriptionText,
...props
}: ConfirmationModalProps) {
return (
<Modal {...props}>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<p className={styles.descriptionText}>{descriptionText}</p>
<div className={styles.actions}>
<Button theme="danger">{cancelButtonLabel}</Button>
<Button>{confirmButtonLabel}</Button>
</div>
</div>
</Modal>
);
}

View File

@ -1,11 +1,16 @@
import { DownloadIcon, FileDirectoryIcon } from "@primer/octicons-react";
import type { CatalogueEntry } from "@types";
import {
DownloadIcon,
FileDirectoryIcon,
PeopleIcon,
} from "@primer/octicons-react";
import type { CatalogueEntry, GameStats } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import * as styles from "./game-card.css";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { useCallback, useMemo, useState } from "react";
export interface GameCardProps
extends React.DetailedHTMLProps<
@ -22,12 +27,35 @@ const shopIcon = {
export function GameCard({ game, ...props }: GameCardProps) {
const { t } = useTranslation("game_card");
const [stats, setStats] = useState<GameStats | null>(null);
const { i18n } = useTranslation();
const uniqueRepackers = Array.from(
new Set(game.repacks.map(({ repacker }) => repacker))
);
const handleHover = useCallback(() => {
if (!stats) {
window.electron.getGameStats(game.objectID, game.shop).then((stats) => {
setStats(stats);
});
}
}, [game, stats]);
const numberFormatter = useMemo(() => {
return new Intl.NumberFormat(i18n.language, {
maximumFractionDigits: 0,
});
}, [i18n.language]);
return (
<button {...props} type="button" className={styles.card}>
<button
{...props}
type="button"
className={styles.card}
onMouseEnter={handleHover}
>
<div className={styles.backdrop}>
<img src={game.cover} alt={game.title} className={styles.cover} />
@ -48,19 +76,20 @@ export function GameCard({ game, ...props }: GameCardProps) {
) : (
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
)}
<div className={styles.specifics}>
<div className={styles.specificsItem}>
<DownloadIcon />
<span>{game.repacks.length}</span>
<span>
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
</span>
</div>
{game.repacks.length > 0 && (
<div className={styles.specificsItem}>
<FileDirectoryIcon />
<span>{game.repacks.at(0)?.fileSize}</span>
</div>
)}
<div className={styles.specificsItem}>
<PeopleIcon />
<span>
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
</span>
</div>
</div>
</div>
</div>

View File

@ -11,3 +11,4 @@ export * from "./link/link";
export * from "./select-field/select-field";
export * from "./toast/toast";
export * from "./badge/badge";
export * from "./confirmation-modal/confirmation-modal";

View File

@ -34,7 +34,7 @@ export const profileButtonContent = style({
export const profileAvatar = style({
width: "35px",
height: "35px",
borderRadius: "50%",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
alignItems: "center",
@ -52,17 +52,6 @@ export const profileButtonInformation = style({
minWidth: 0,
});
export const statusBadge = style({
width: "9px",
height: "9px",
borderRadius: "50%",
backgroundColor: vars.color.danger,
position: "absolute",
bottom: "-2px",
right: "-3px",
zIndex: "1",
});
export const profileButtonTitle = style({
fontWeight: "bold",
fontSize: vars.size.body,
@ -85,11 +74,11 @@ export const friendsButton = style({
position: "relative",
transition: "all ease 0.3s",
":hover": {
backgroundColor: "#DADBE1",
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const friendsButtonLabel = style({
export const friendsButtonBadge = style({
backgroundColor: vars.color.success,
display: "flex",
justifyContent: "center",

View File

@ -2,10 +2,9 @@ import { useNavigate } from "react-router-dom";
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css";
import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useEffect, useState } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { FriendRequest } from "@types";
export function SidebarProfile() {
const navigate = useNavigate();
@ -14,17 +13,13 @@ export function SidebarProfile() {
const { userDetails, friendRequests, showFriendsModal } = useUserDetails();
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
useEffect(() => {
setReceivedRequests(
friendRequests.filter((request) => request.type === "RECEIVED")
);
const receivedRequests = useMemo(() => {
return friendRequests.filter((request) => request.type === "RECEIVED");
}, [friendRequests]);
const { gameRunning } = useAppSelector((state) => state.gameRunning);
const handleButtonClick = () => {
const handleProfileClick = () => {
if (userDetails === null) {
window.electron.openAuthWindow();
return;
@ -33,12 +28,35 @@ export function SidebarProfile() {
navigate(`/profile/${userDetails!.id}`);
};
const friendsButton = useMemo(() => {
if (!userDetails) return null;
return (
<button
type="button"
className={styles.friendsButton}
onClick={() =>
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
}
title={t("friends")}
>
{receivedRequests.length > 0 && (
<small className={styles.friendsButtonBadge}>
{receivedRequests.length > 99 ? "99+" : receivedRequests.length}
</small>
)}
<PeopleIcon size={16} />
</button>
);
}, [userDetails, t, receivedRequests, showFriendsModal]);
return (
<div className={styles.profileContainer}>
<button
type="button"
className={styles.profileButton}
onClick={handleButtonClick}
onClick={handleProfileClick}
>
<div className={styles.profileButtonContent}>
<div className={styles.profileAvatar}>
@ -76,17 +94,7 @@ export function SidebarProfile() {
</div>
</button>
<button
type="button"
className={styles.friendsButton}
onClick={() =>
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
}
>
<small className={styles.friendsButtonLabel}>10</small>
<PeopleIcon size={16} />
</button>
{friendsButton}
</div>
);
}

View File

@ -21,6 +21,11 @@ export const sidebar = recipe({
pointerEvents: "none",
},
},
darwin: {
true: {
paddingTop: `${SPACING_UNIT * 6}px`,
},
},
},
});

View File

@ -153,12 +153,14 @@ export function Sidebar() {
<>
<aside
ref={sidebarRef}
className={styles.sidebar({ resizing: isResizing })}
className={styles.sidebar({
resizing: isResizing,
darwin: window.electron.platform === "darwin",
})}
style={{
width: sidebarWidth,
minWidth: sidebarWidth,
maxWidth: sidebarWidth,
paddingTop: 8 * 6,
}}
>
<SidebarProfile />
@ -180,8 +182,6 @@ export function Sidebar() {
>
{render(isDownloading)}
<span>{t(nameKey)}</span>
<ChevronDownIcon />
</button>
</li>
))}

View File

@ -9,3 +9,5 @@ export const DOWNLOADER_NAME = {
[Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi",
};
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;

View File

@ -12,6 +12,8 @@ export interface UserProfileContext {
heroBackground: string;
/* Indicates if the current user is viewing their own profile */
isMe: boolean;
getUserProfile: () => Promise<void>;
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@ -20,6 +22,7 @@ export const userProfileContext = createContext<UserProfileContext>({
userProfile: null,
heroBackground: DEFAULT_USER_PROFILE_BACKGROUND,
isMe: false,
getUserProfile: async () => {},
});
const { Provider } = userProfileContext;
@ -47,7 +50,7 @@ export function UserProfileContextProvider({
format: "hex",
});
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`;
};
const { t } = useTranslation("user_profile");
@ -73,6 +76,9 @@ export function UserProfileContextProvider({
}, [navigate, showErrorToast, userId, t]);
useEffect(() => {
setUserProfile(null);
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
getUserProfile();
}, [getUserProfile]);
@ -82,6 +88,7 @@ export function UserProfileContextProvider({
userProfile,
heroBackground,
isMe: userDetails?.id === userProfile?.id,
getUserProfile,
}}
>
{children}

View File

@ -1,3 +1,4 @@
import type { CatalogueCategory } from "@shared";
import type {
AppUpdaterEvent,
CatalogueEntry,
@ -19,6 +20,7 @@ import type {
UserFriends,
UserBlocks,
UpdateProfileRequest,
GameStats,
} from "@types";
import type { DiskSpace } from "check-disk-space";
@ -40,7 +42,7 @@ declare global {
/* Catalogue */
searchGames: (query: string) => Promise<CatalogueEntry[]>;
getCatalogue: () => Promise<CatalogueEntry[]>;
getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
getGameShopDetails: (
objectID: string,
shop: GameShop,
@ -57,6 +59,7 @@ declare global {
prevCursor?: number
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
/* Library */
addGameToLibrary: (

View File

@ -27,7 +27,7 @@ import {
import { store } from "./store";
import * as resources from "@locales";
import resources from "@locales";
Sentry.init({});

View File

@ -6,8 +6,7 @@ import { useDate, useDownload } from "@renderer/hooks";
import { Link } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context";
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
export function HeroPanelPlaytime() {
const [lastTimePlayed, setLastTimePlayed] = useState("");
@ -36,7 +35,7 @@ export function HeroPanelPlaytime() {
});
}, [i18n.language]);
const formatPlayTime = () => {
const formattedPlayTime = useMemo(() => {
const milliseconds = game?.playTimeInMilliseconds || 0;
const seconds = milliseconds / 1000;
const minutes = seconds / 60;
@ -49,7 +48,7 @@ export function HeroPanelPlaytime() {
const hours = minutes / 60;
return t("amount_hours", { amount: numberFormatter.format(hours) });
};
}, [game?.playTimeInMilliseconds, numberFormatter, t]);
if (!game) return null;
@ -96,7 +95,7 @@ export function HeroPanelPlaytime() {
<>
<p>
{t("play_time", {
amount: formatPlayTime(),
amount: formattedPlayTime,
})}
</p>

View File

@ -60,3 +60,11 @@ export const noResults = style({
gap: "16px",
gridColumn: "1 / -1",
});
export const buttonsList = style({
display: "flex",
listStyle: "none",
margin: "0",
padding: "0",
gap: `${SPACING_UNIT}px`,
});

View File

@ -13,6 +13,7 @@ import * as styles from "./home.css";
import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
import { buildGameDetailsPath } from "@renderer/helpers";
import { CatalogueCategory } from "@shared";
export function Home() {
const { t } = useTranslation("home");
@ -21,15 +22,25 @@ export function Home() {
const [isLoading, setIsLoading] = useState(false);
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
const [catalogue, setCatalogue] = useState<CatalogueEntry[]>([]);
const [currentCatalogueCategory, setCurrentCatalogueCategory] = useState(
CatalogueCategory.Hot
);
const getCatalogue = useCallback(() => {
const [catalogue, setCatalogue] = useState<
Record<CatalogueCategory, CatalogueEntry[]>
>({
[CatalogueCategory.Hot]: [],
[CatalogueCategory.Weekly]: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setCurrentCatalogueCategory(category);
setIsLoading(true);
window.electron
.getCatalogue()
.getCatalogue(category)
.then((catalogue) => {
setCatalogue(catalogue);
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
@ -58,11 +69,13 @@ export function Home() {
useEffect(() => {
setIsLoading(true);
getCatalogue();
getCatalogue(CatalogueCategory.Hot);
getRandomGame();
}, [getCatalogue, getRandomGame]);
const categories = Object.values(CatalogueCategory);
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
@ -71,7 +84,22 @@ export function Home() {
<Hero />
<section className={styles.homeHeader}>
<h2>{t("trending")}</h2>
<ul className={styles.buttonsList}>
{categories.map((category) => (
<li key={category}>
<Button
theme={
category === currentCatalogueCategory
? "primary"
: "outline"
}
onClick={() => getCatalogue(category)}
>
{t(category)}
</Button>
</li>
))}
</ul>
<Button
onClick={handleRandomizerClick}
@ -89,12 +117,14 @@ export function Home() {
</Button>
</section>
<h2>{t(currentCatalogueCategory)}</h2>
<section className={styles.cards}>
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))
: catalogue.map((result) => (
: catalogue[currentCatalogueCategory].map((result) => (
<GameCard
key={result.objectID}
game={result}

View File

@ -1,9 +1,10 @@
import { vars, SPACING_UNIT } from "../../theme.css";
import { vars, SPACING_UNIT } from "../../../theme.css";
import { globalStyle, style } from "@vanilla-extract/css";
export const gameCover = style({
transition: "all ease 0.2s",
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
width: "100%",
":before": {
content: "",
top: "0",
@ -60,14 +61,66 @@ export const friend = style({
alignItems: "center",
});
export const friendAvatar = style({
width: "50px",
height: "50px",
borderRadius: "4px",
});
export const friendName = style({
color: vars.color.muted,
fontWeight: "bold",
fontSize: vars.size.body,
});
export const rightContent = style({
width: "100%",
height: "100%",
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
flexDirection: "column",
"@media": {
"(min-width: 768px)": {
width: "100%",
maxWidth: "200px",
},
"(min-width: 1024px)": {
maxWidth: "300px",
width: "100%",
},
"(min-width: 1280px)": {
width: "100%",
maxWidth: "400px",
},
},
});
export const listItem = style({
display: "flex",
cursor: "pointer",
transition: "all ease 0.1s",
color: vars.color.muted,
width: "100%",
overflow: "hidden",
borderRadius: "4px",
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
textDecoration: "none",
},
});
export const gamesGrid = style({
listStyle: "none",
margin: 0,
padding: 0,
display: "grid",
gap: `${SPACING_UNIT * 2}px`,
"@media": {
"(min-width: 768px)": {
gridTemplateColumns: "repeat(2, 1fr)",
},
"(min-width: 1250px)": {
gridTemplateColumns: "repeat(3, 1fr)",
},
"(min-width: 1600px)": {
gridTemplateColumns: "repeat(8, 1fr)",
},
},
});

View File

@ -1,19 +1,27 @@
import { userProfileContext } from "@renderer/context";
import { useContext, useEffect, useMemo } from "react";
import { ProfileHero } from "./profile-hero/profile-hero";
import { useCallback, useContext, useEffect, useMemo } from "react";
import { ProfileHero } from "../profile-hero/profile-hero";
import { useAppDispatch } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { steamUrlBuilder } from "@shared";
import { SPACING_UNIT } from "@renderer/theme.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import * as styles from "./profile-content.css";
import { ClockIcon, PeopleIcon } from "@primer/octicons-react";
import { ClockIcon } from "@primer/octicons-react";
import { Link } from "@renderer/components";
import { useTranslation } from "react-i18next";
import { UserGame } from "@types";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { buildGameDetailsPath } from "@renderer/helpers";
import { useNavigate } from "react-router-dom";
export function ProfileContent() {
const { userProfile } = useContext(userProfileContext);
const dispatch = useAppDispatch();
const { i18n, t } = useTranslation("user_profile");
useEffect(() => {
if (userProfile) {
dispatch(setHeaderTitle(userProfile.displayName));
@ -21,8 +29,42 @@ export function ProfileContent() {
}, [userProfile, dispatch]);
const truncatedGamesList = useMemo(() => {
if (!userProfile) return [];
return userProfile?.libraryGames.slice(0, 12);
}, [userProfile?.libraryGames]);
}, [userProfile]);
const numberFormatter = useMemo(() => {
return new Intl.NumberFormat(i18n.language, {
maximumFractionDigits: 0,
});
}, [i18n.language]);
const navigate = useNavigate();
const formatPlayTime = useCallback(
(game: UserGame) => {
const seconds = game?.playTimeInSeconds || 0;
const minutes = seconds / 60;
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
return t("amount_minutes", {
amount: minutes.toFixed(0),
});
}
const hours = minutes / 60;
return t("amount_hours", { amount: numberFormatter.format(hours) });
},
[numberFormatter, t]
);
const buildUserGameDetailsPath = (game: UserGame) =>
buildGameDetailsPath({
...game,
objectID: game.objectId,
});
if (!userProfile) return null;
return (
<div>
@ -35,23 +77,14 @@ export function ProfileContent() {
padding: `${SPACING_UNIT * 3}px`,
}}
>
<div style={{}}>
<div style={{ flex: 1 }}>
<div className={styles.sectionHeader}>
<h2>Library</h2>
<h2>{t("library")}</h2>
<h3>{userProfile?.libraryGames.length}</h3>
<h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3>
</div>
<ul
style={{
listStyle: "none",
margin: 0,
padding: 0,
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<ul className={styles.gamesGrid}>
{truncatedGamesList.map((game) => (
<li
key={game.objectId}
@ -64,13 +97,20 @@ export function ProfileContent() {
>
<button
type="button"
style={{ cursor: "pointer" }}
style={{
cursor: "pointer",
}}
className={styles.gameCover}
onClick={() => navigate(buildUserGameDetailsPath(game))}
>
<img
src={steamUrlBuilder.cover(game.objectId)}
alt={game.title}
style={{ width: "100%" }}
style={{
width: "100%",
objectFit: "cover",
borderRadius: 4,
}}
/>
</button>
</li>
@ -78,14 +118,7 @@ export function ProfileContent() {
</ul>
</div>
<div
style={{
minWidth: 350,
display: "flex",
gap: SPACING_UNIT * 2,
flexDirection: "column",
}}
>
<div className={styles.rightContent}>
<div>
<div className={styles.sectionHeader}>
<h2>Played recently</h2>
@ -94,14 +127,10 @@ export function ProfileContent() {
<div className={styles.box}>
<ul className={styles.list}>
{userProfile?.recentGames.map((game) => (
<li>
<button
type="button"
style={{
cursor: "pointer",
display: "flex",
gap: `${SPACING_UNIT}px`,
}}
<li key={`${game.shop}-${game.objectId}`}>
<Link
to={buildUserGameDetailsPath(game)}
className={styles.listItem}
>
<img
src={game.iconUrl}
@ -113,21 +142,27 @@ export function ProfileContent() {
}}
/>
<div style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT / 2}px`,
}}
>
<span style={{ fontWeight: "bold" }}>{game.title}</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT / 2}px`,
gap: `${SPACING_UNIT}px`,
}}
>
<ClockIcon />
<span>{game.playTimeInSeconds}</span>
<small>{formatPlayTime(game)}</small>
</div>
</div>
</button>
</Link>
</li>
))}
</ul>
@ -136,30 +171,31 @@ export function ProfileContent() {
<div>
<div className={styles.sectionHeader}>
<h2>Friends</h2>
<h2>{t("friends")}</h2>
<span>{userProfile?.totalFriends}</span>
</div>
<div className={styles.box}>
<ul className={styles.list}>
{userProfile?.friends.map((friend) => (
<li>
<button
type="button"
style={{ cursor: "pointer" }}
className={styles.friend}
<li key={friend.id}>
<Link
to={`/profile/${friend.id}`}
className={styles.listItem}
>
<img
src={friend.profileImageUrl}
alt={friend.displayName}
style={{ width: "100%" }}
className={styles.friendAvatar}
style={{
width: "30px",
height: "30px",
borderRadius: "4px",
}}
/>
<span className={styles.friendName}>
{friend.displayName}
</span>
</button>
</Link>
</li>
))}
</ul>

View File

@ -6,11 +6,11 @@ export const profileContentBox = style({
flexDirection: "column",
});
export const profileAvatarContainer = style({
export const profileAvatarButton = style({
width: "96px",
minWidth: "96px",
height: "96px",
borderRadius: "50%",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
alignItems: "center",
@ -18,7 +18,11 @@ export const profileAvatarContainer = style({
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
zIndex: 1,
cursor: "pointer",
transition: "all ease 0.3s",
":hover": {
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.7)",
},
});
export const profileAvatar = style({
@ -44,3 +48,24 @@ export const profileDisplayName = style({
textOverflow: "ellipsis",
width: "100%",
});
export const heroPanel = style({
width: "100%",
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
display: "flex",
gap: `${SPACING_UNIT}px`,
justifyContent: "space-between",
backdropFilter: `blur(10px)`,
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
backgroundColor: "rgba(0, 0, 0, 0.3)",
});
export const userInformation = style({
display: "flex",
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
});

View File

@ -1,101 +1,164 @@
import { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./profile-hero.css";
import { useContext, useMemo } from "react";
import { useContext, useMemo, useState } from "react";
import { userProfileContext } from "@renderer/context";
import {
CheckCircleFillIcon,
PencilIcon,
PersonIcon,
SignOutIcon,
XCircleFillIcon,
} from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers";
import { Button, Link } from "@renderer/components";
import { useTranslation } from "react-i18next";
import { useDate } from "@renderer/hooks";
import { useDate, useToast, useUserDetails } from "@renderer/hooks";
import { addSeconds } from "date-fns";
import { useNavigate } from "react-router-dom";
import type { FriendRequestAction } from "@types";
import { UserProfileSettingsModal } from "../user-profile-settings-modal";
type FriendAction =
| FriendRequestAction
| ("BLOCK" | "UNDO_FRIENDSHIP" | "SEND");
export function ProfileHero() {
const { userProfile, heroBackground, isMe } = useContext(userProfileContext);
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const context = useContext(userProfileContext);
const {
signOut,
updateFriendRequestState,
sendFriendRequest,
undoFriendship,
blockUser,
} = useUserDetails();
const { isMe, heroBackground, getUserProfile } = context;
const userProfile = context.userProfile!;
const { currentGame } = userProfile;
const { t } = useTranslation("user_profile");
const { formatDistance } = useDate();
if (!userProfile) return null;
const { showSuccessToast, showErrorToast } = useToast();
const { currentGame } = userProfile;
const navigate = useNavigate();
console.log(userProfile);
const handleSignOut = async () => {
await signOut();
showSuccessToast(t("successfully_signed_out"));
navigate("/");
};
const handleFriendAction = (userId: string, action: FriendAction) => {
try {
if (action === "UNDO_FRIENDSHIP") {
undoFriendship(userId).then(getUserProfile);
return;
}
if (action === "BLOCK") {
blockUser(userId).then(() => {
showSuccessToast(t("user_blocked_successfully"));
navigate(-1);
});
return;
}
if (action === "SEND") {
sendFriendRequest(userProfile.id).then(getUserProfile);
return;
}
updateFriendRequestState(userId, action).then(getUserProfile);
} catch (err) {
showErrorToast(t("try_again"));
}
};
const profileActions = useMemo(() => {
if (isMe) {
return (
<>
<Button theme="outline">{t("settings")}</Button>
<Button theme="outline" onClick={() => setShowEditProfileModal(true)}>
<PencilIcon />
{t("edit_profile")}
</Button>
<Button theme="danger">{t("sign_out")}</Button>
<Button theme="danger" onClick={handleSignOut}>
<SignOutIcon />
{t("sign_out")}
</Button>
</>
);
}
// if (userProfile.relation == null) {
// return (
// <>
// <Button
// theme="outline"
// onClick={() => handleFriendAction(userProfile.id, "SEND")}
// >
// {t("add_friend")}
// </Button>
if (userProfile.relation == null) {
return (
<>
<Button
theme="outline"
onClick={() => handleFriendAction(userProfile.id, "SEND")}
>
{t("add_friend")}
</Button>
// <Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
// {t("block_user")}
// </Button>
// </>
// );
// }
<Button
theme="danger"
onClick={() => handleFriendAction(userProfile.id, "BLOCK")}
>
{t("block_user")}
</Button>
</>
);
}
// if (userProfile.relation.status === "ACCEPTED") {
// return (
// <>
// <Button
// theme="outline"
// // className={styles.cancelRequestButton}
// // onClick={() => setShowUndoFriendshipModal(true)}
// >
// <XCircleFillIcon size={28} /> {t("undo_friendship")}
// </Button>
// </>
// );
// }
if (userProfile.relation.status === "ACCEPTED") {
return (
<Button
theme="outline"
onClick={() => handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")}
>
<XCircleFillIcon />
{t("undo_friendship")}
</Button>
);
}
// if (userProfile.relation.BId === userProfile.id) {
// return (
// <Button
// theme="outline"
// // className={styles.cancelRequestButton}
// // onClick={() =>
// // handleFriendAction(userProfile.relation!.BId, "CANCEL")
// // }
// >
// <XCircleFillIcon size={28} /> {t("cancel_request")}
// </Button>
// );
// }
if (userProfile.relation.BId === userProfile.id) {
return (
<Button
theme="outline"
onClick={() =>
handleFriendAction(userProfile.relation!.BId, "CANCEL")
}
>
<XCircleFillIcon size={28} /> {t("cancel_request")}
</Button>
);
}
return (
<>
<Button
theme="outline"
// onClick={() =>
// handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
// }
onClick={() =>
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
}
>
<CheckCircleFillIcon size={28} /> {t("accept_request")}
</Button>
<Button
theme="outline"
// onClick={() =>
// handleFriendAction(userProfile.relation!.AId, "REFUSED")
// }
onClick={() =>
handleFriendAction(userProfile.relation!.AId, "REFUSED")
}
>
<XCircleFillIcon size={28} /> {t("ignore_request")}
</Button>
@ -105,19 +168,27 @@ export function ProfileHero() {
return (
<>
{/* <ConfirmationModal
visible
title={t("sign_out_modal_title")}
descriptionText={t("sign_out_modal_text")}
confirmButtonLabel={t("sign_out")}
cancelButtonLabel={t("cancel")}
/> */}
<UserProfileSettingsModal
visible={showEditProfileModal}
userProfile={userProfile}
updateUserProfile={getUserProfile}
onClose={() => setShowEditProfileModal(false)}
/>
<section
className={styles.profileContentBox}
style={{ background: heroBackground }}
>
<div
style={{
display: "flex",
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<div className={styles.profileAvatarContainer}>
<div className={styles.userInformation}>
<button type="button" className={styles.profileAvatarButton}>
{userProfile.profileImageUrl ? (
<img
className={styles.profileAvatar}
@ -127,12 +198,13 @@ export function ProfileHero() {
) : (
<PersonIcon size={72} />
)}
</div>
</button>
<div className={styles.profileInformation}>
<h2 className={styles.profileDisplayName}>
{userProfile.displayName}
</h2>
{currentGame && (
<div
style={{
@ -149,14 +221,23 @@ export function ProfileHero() {
alignItems: "center",
}}
>
<Link to={buildGameDetailsPath(currentGame)}>
<Link
to={buildGameDetailsPath({
...currentGame,
objectID: currentGame.objectId,
})}
>
{currentGame.title}
</Link>
</div>
<small>
{t("playing_for", {
amount: formatDistance(
currentGame.sessionDurationInSeconds,
addSeconds(
new Date(),
-currentGame.sessionDurationInSeconds
),
new Date()
),
})}
@ -166,20 +247,7 @@ export function ProfileHero() {
</div>
</div>
<div
style={{
width: "100%",
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
display: "flex",
gap: `${SPACING_UNIT}px`,
justifyContent: "space-between",
backdropFilter: `blur(10px)`,
borderTop: `solid 1px rgba(255, 255, 255, 0.1)`,
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
}}
>
<div className={styles.heroPanel}>
<div></div>
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
{profileActions}

View File

@ -1,4 +1,4 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const wrapper = style({
@ -7,297 +7,3 @@ export const wrapper = style({
flexDirection: "column",
gap: `${SPACING_UNIT * 3}px`,
});
export const profileContentBox = style({
display: "flex",
cursor: "pointer",
gap: `${SPACING_UNIT * 3}px`,
alignItems: "center",
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
width: "100%",
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
transition: "all ease 0.3s",
});
export const profileAvatarContainer = style({
width: "96px",
minWidth: "96px",
height: "96px",
borderRadius: "50%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
position: "relative",
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
zIndex: 1,
});
export const friendAvatarContainer = style({
width: "35px",
minWidth: "35px",
height: "35px",
borderRadius: "50%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
});
export const friendListDisplayName = style({
fontWeight: "bold",
fontSize: vars.size.body,
textAlign: "left",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});
export const profileAvatarEditContainer = style({
alignSelf: "center",
width: "128px",
height: "128px",
display: "flex",
borderRadius: "50%",
color: vars.color.body,
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
position: "relative",
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
cursor: "pointer",
});
export const profileAvatar = style({
height: "100%",
width: "100%",
objectFit: "cover",
borderRadius: "50%",
overflow: "hidden",
});
export const profileAvatarEditOverlay = style({
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "#00000055",
color: vars.color.muted,
zIndex: 1,
cursor: "pointer",
});
export const profileInformation = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
alignItems: "flex-start",
color: "#c0c1c7",
zIndex: 1,
overflow: "hidden",
});
export const profileDisplayName = style({
fontWeight: "bold",
overflow: "hidden",
textOverflow: "ellipsis",
width: "100%",
});
export const profileContent = style({
display: "flex",
height: "100%",
flexDirection: "row",
gap: `${SPACING_UNIT * 4}px`,
});
export const profileGameSection = style({
width: "100%",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
});
export const friendsSection = style({
width: "100%",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
});
export const friendsSectionHeader = style({
fontSize: vars.size.body,
color: vars.color.body,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: `${SPACING_UNIT * 2}px`,
":hover": {
color: vars.color.muted,
},
});
export const contentSidebar = style({
width: "100%",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 3}px`,
"@media": {
"(min-width: 768px)": {
width: "100%",
maxWidth: "150px",
},
"(min-width: 1024px)": {
maxWidth: "250px",
width: "100%",
},
},
});
export const feedGameIcon = style({
height: "100%",
});
export const libraryGameIcon = style({
width: "100%",
height: "100%",
borderRadius: "4px",
});
export const friendProfileIcon = style({
height: "100%",
});
export const feedItem = style({
color: vars.color.body,
display: "flex",
flexDirection: "row",
gap: `${SPACING_UNIT * 2}px`,
width: "100%",
overflow: "hidden",
height: "72px",
transition: "all ease 0.2s",
cursor: "pointer",
zIndex: "1",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const gameListItem = style({
color: vars.color.body,
transition: "all ease 0.2s",
cursor: "pointer",
zIndex: "1",
overflow: "hidden",
padding: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const friendListContainer = style({
color: vars.color.body,
width: "100%",
height: "54px",
padding: `0 ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
transition: "all ease 0.2s",
position: "relative",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const gameInformation = style({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: `${SPACING_UNIT / 2}px`,
});
export const profileHeaderSkeleton = style({
height: "144px",
});
export const editProfileImageBadge = style({
width: "28px",
height: "28px",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: vars.color.background,
backgroundColor: vars.color.muted,
position: "absolute",
bottom: "0px",
right: "0px",
zIndex: "1",
});
export const telescopeIcon = style({
width: "60px",
height: "60px",
borderRadius: "50%",
backgroundColor: "rgba(255, 255, 255, 0.06)",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: `${SPACING_UNIT * 2}px`,
});
export const noDownloads = style({
display: "flex",
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});
export const signOutModalContent = style({
display: "flex",
width: "100%",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});
export const signOutModalButtonsContainer = style({
display: "flex",
width: "100%",
justifyContent: "end",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
paddingTop: `${SPACING_UNIT}px`,
});
export const profileBackground = style({
width: "100%",
height: "100%",
position: "absolute",
objectFit: "cover",
left: "0",
top: "0",
borderRadius: "4px",
});
export const cancelRequestButton = style({
cursor: "pointer",
color: vars.color.body,
":hover": {
color: vars.color.danger,
},
});
export const acceptRequestButton = style({
cursor: "pointer",
color: vars.color.success,
});

View File

@ -1,6 +1,6 @@
import { useParams } from "react-router-dom";
import { ProfileSkeleton } from "./profile-skeleton";
import { ProfileContent } from "./profile-content";
import { ProfileContent } from "./profile-content/profile-content";
import { SkeletonTheme } from "react-loading-skeleton";
import { vars } from "@renderer/theme.css";

View File

@ -1,42 +0,0 @@
import { Button, Modal } from "@renderer/components";
import * as styles from "./profile.css";
import { useTranslation } from "react-i18next";
export interface UserBlockModalProps {
visible: boolean;
displayName: string;
onConfirm: () => void;
onClose: () => void;
}
export const UserBlockModal = ({
visible,
displayName,
onConfirm,
onClose,
}: UserBlockModalProps) => {
const { t } = useTranslation("user_profile");
return (
<>
<Modal
visible={visible}
title={t("sign_out_modal_title")}
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p>{t("user_block_modal_text", { displayName })}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("block_user")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</div>
</Modal>
</>
);
};

View File

@ -1,40 +0,0 @@
import { Button, Modal } from "@renderer/components";
import * as styles from "./profile.css";
import { useTranslation } from "react-i18next";
export interface UserConfirmUndoFriendshipModalProps {
visible: boolean;
displayName: string;
onConfirm: () => void;
onClose: () => void;
}
export function UserConfirmUndoFriendshipModal({
visible,
displayName,
onConfirm,
onClose,
}: UserConfirmUndoFriendshipModalProps) {
const { t } = useTranslation("user_profile");
return (
<Modal
visible={visible}
title={t("sign_out_modal_title")}
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p>{t("undo_friendship_modal_text", { displayName })}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("undo_friendship")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</div>
</Modal>
);
}

View File

@ -47,7 +47,7 @@ export const UserEditProfile = ({
filters: [
{
name: "Image",
extensions: ["jpg", "jpeg", "png", "webp"],
extensions: ["jpg", "jpeg", "png"],
},
],
});

View File

@ -44,7 +44,12 @@ export const UserProfileSettingsModal = ({
return (
<>
<Modal visible={visible} title={t("settings")} onClose={onClose}>
<Modal
visible={visible}
title={t("settings")}
onClose={onClose}
clickOutsideToClose={false}
>
<div
style={{
display: "flex",

View File

@ -1,40 +0,0 @@
import { Button, Modal } from "@renderer/components";
import * as styles from "./profile.css";
import { useTranslation } from "react-i18next";
export interface UserSignOutModalProps {
visible: boolean;
onConfirm: () => void;
onClose: () => void;
}
export const UserSignOutModal = ({
visible,
onConfirm,
onClose,
}: UserSignOutModalProps) => {
const { t } = useTranslation("user_profile");
return (
<>
<Modal
visible={visible}
title={t("sign_out_modal_title")}
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p>{t("sign_out_modal_text")}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("sign_out")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</div>
</Modal>
</>
);
};

View File

@ -1,6 +1,4 @@
import { useContext, useEffect, useState } from "react";
import ISO6391 from "iso-639-1";
import {
TextField,
Button,
@ -8,11 +6,9 @@ import {
SelectField,
} from "@renderer/components";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "@renderer/hooks";
import { changeLanguage } from "i18next";
import * as languageResources from "@locales";
import languageResources from "@locales";
import { orderBy } from "lodash-es";
import { settingsContext } from "@renderer/context";
@ -50,9 +46,9 @@ export function SettingsGeneral() {
setLanguageOptions(
orderBy(
Object.keys(languageResources).map((language) => {
Object.entries(languageResources).map(([language, value]) => {
return {
nativeName: ISO6391.getNativeName(language),
nativeName: value.language_name,
option: language,
};
}),
@ -93,8 +89,6 @@ export function SettingsGeneral() {
function updateFormWithUserPreferences() {
if (userPreferences) {
const parsedLanguage = userPreferences.language.split("-")[0];
setForm((prev) => ({
...prev,
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
@ -102,7 +96,7 @@ export function SettingsGeneral() {
userPreferences.downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled:
userPreferences.repackUpdatesNotificationsEnabled,
language: parsedLanguage,
language: userPreferences.language,
}));
}
}

View File

@ -1,106 +1,27 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
export const container = style({
padding: "24px",
width: "100%",
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "flex-start",
});
export const content = style({
backgroundColor: vars.color.background,
width: "100%",
height: "100%",
padding: `${SPACING_UNIT * 3}px`,
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
borderRadius: "4px",
boxShadow: "0px 0px 15px 0px #000000",
borderRadius: "8px",
gap: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
flex: "1",
});
export const sidebar = style({
width: "200px",
export const settingsCategories = style({
display: "flex",
border: `solid 1px ${vars.color.border}`,
borderRadius: "4px",
backgroundColor: vars.color.background,
minHeight: "500px",
flexDirection: "column",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT * 2}px`,
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)",
});
export const menuGroup = style({
gap: `${SPACING_UNIT}px`,
display: "flex",
flexDirection: "column",
});
export const menu = style({
listStyle: "none",
margin: "0",
padding: "0",
gap: `${SPACING_UNIT / 2}px`,
display: "flex",
flexDirection: "column",
overflow: "hidden",
});
export const menuItem = recipe({
base: {
transition: "all ease 0.1s",
cursor: "pointer",
textWrap: "nowrap",
display: "flex",
color: vars.color.muted,
borderRadius: "4px",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
},
variants: {
active: {
true: {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
},
muted: {
true: {
opacity: vars.opacity.disabled,
":hover": {
opacity: "1",
},
},
},
},
});
export const menuItemButton = style({
color: "inherit",
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
cursor: "pointer",
overflow: "hidden",
width: "100%",
padding: `9px ${SPACING_UNIT}px`,
});
export const menuItemButtonLabel = style({
textOverflow: "ellipsis",
overflow: "hidden",
});
export const categoryTitle = style({
color: "#ff",
fontWeight: "bold",
fontSize: "18px",
paddingBottom: `${SPACING_UNIT}px`,
});

View File

@ -1,3 +1,5 @@
import { Button } from "@renderer/components";
import * as styles from "./settings.css";
import { useTranslation } from "react-i18next";
import { SettingsRealDebrid } from "./settings-real-debrid";
@ -13,10 +15,12 @@ import {
export function Settings() {
const { t } = useTranslation("settings");
const categories = {
[t("account")]: [t("my_profile"), t("friends")],
Hydra: [t("general"), t("behavior"), t("download_sources"), "Real-Debrid"],
};
const categories = [
t("general"),
t("behavior"),
t("download_sources"),
"Real-Debrid",
];
return (
<SettingsContextProvider>
@ -40,34 +44,21 @@ export function Settings() {
return (
<section className={styles.container}>
<aside className={styles.sidebar}>
{Object.entries(categories).map(([category, items]) => (
<div key={category} className={styles.menuGroup}>
<span className={styles.categoryTitle}>{category}</span>
<ul className={styles.menu}>
{items.map((item, index) => (
<li
key={`item-${index}`}
className={styles.menuItem({
active: currentCategoryIndex === index,
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={() => setCurrentCategoryIndex(index)}
>
{item}
</button>
</li>
))}
</ul>
</div>
))}
</aside>
<div className={styles.content}>
<section className={styles.settingsCategories}>
{categories.map((category, index) => (
<Button
key={category}
theme={
currentCategoryIndex === index ? "primary" : "outline"
}
onClick={() => setCurrentCategoryIndex(index)}
>
{category}
</Button>
))}
</section>
<h2>{categories[currentCategoryIndex]}</h2>
{renderCategory()}
</div>

View File

@ -11,6 +11,11 @@ export enum DownloadSourceStatus {
Errored,
}
export enum CatalogueCategory {
Hot = "hot",
Weekly = "weekly",
}
export class UserNotLoggedInError extends Error {
constructor() {
super("user not logged in");

View File

@ -309,7 +309,8 @@ export interface UserRelation {
updatedAt: string;
}
export interface UserProfileCurrentGame extends GameRunning {
export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
objectId: string;
sessionDurationInSeconds: number;
}
@ -345,3 +346,8 @@ export interface DownloadSource {
createdAt: Date;
updatedAt: Date;
}
export interface GameStats {
downloadCount: number;
playerCount: number;
}

View File

@ -5242,11 +5242,6 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
iso-639-1@3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.2.tgz"
integrity sha512-Le7BRl3Jt9URvaiEHJCDEdvPZCfhiQoXnFgLAWNRhzFMwRFdWO7/5tLRQbiPzE394I9xd7KdRCM7S6qdOhwG5A==
iterator.prototype@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz"