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 - name: Build Linux
if: matrix.os == 'ubuntu-latest' 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: env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -65,3 +68,4 @@ jobs:
dist/*.tar.gz dist/*.tar.gz
dist/*.yml dist/*.yml
dist/*.blockmap dist/*.blockmap
dist/*.pacman

View File

@ -37,7 +37,10 @@ jobs:
- name: Build Linux - name: Build Linux
if: matrix.os == 'ubuntu-latest' 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: env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -68,5 +71,6 @@ jobs:
dist/*.tar.gz dist/*.tar.gz
dist/*.yml dist/*.yml
dist/*.blockmap dist/*.blockmap
dist/*.pacman
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -20,6 +20,8 @@
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.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) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Змест ## Змест
- [Змест](#змест)
- [Апісанне](#апісанне) - [Апісанне](#апісанне)
- [Асаблівасці](#асаблівасці) - [Асаблівасці](#асаблівасці)
- [Усталёўка](#усталёўка) - [Усталёўка](#усталёўка)
- [Уклад](#contributing) - [Уклад](#-уклад)
- [Далучайцеся да нашага Telegram](#join-our-telegram) - [Далучайцеся да нашага Telegram](#-далучайцеся-да-нашага-telegram)
- [Форк і кланаванне рэпазітара](#форк-і-кланаванне-рэпазітара) - [Форк і кланаванне рэпазітара](#форк-і-кланаванне-рэпазітара)
- [Спосабы ўнесці свой уклад](#спосабы-ўнесці-свой-уклад) - [Спосабы ўнесці свой уклад](#спосабы-ўнесці-свой-уклад)
- [Структура праекту](#структура-праекту) - [Структура праекту](#структура-праекту)
@ -47,6 +50,7 @@
- [Зборка кліента BitTorrent](#зборка-кліента-bittorrent) - [Зборка кліента BitTorrent](#зборка-кліента-bittorrent)
- [Зборка прыкладання Electron](#зборка-прыкладання-electron) - [Зборка прыкладання 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

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![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) [![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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Tabla de Contenidos ## Tabla de Contenidos
- [Tabla de Contenidos](#tabla-de-contenidos)
- [Acerca de](#acerca-de) - [Acerca de](#acerca-de)
- [Características](#caracteristicas) - [Caracteristicas](#caracteristicas)
- [Instalación](#Instalacion) - [Instalacion](#instalacion)
- [Contribuir](#contribuir) - [Contribuir](#-contribuir)
- [Únete a nuestro Telegram](#unete-a-nuestro-telegram) - [Unete a nuestro Telegram](#-unete-a-nuestro-telegram)
- [Haz un fork y clona tu repositorio](#haz-un-fork-y-clona-tu-repositorio) - [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) - [Maneras en las que puedes contribuir](#maneras-en-las-que-puedes-contribuir)
- [Estructura del proyecto](#estructura-del-proyecto) - [Estructura del proyecto](#estructura-del-proyecto)
@ -40,13 +43,14 @@
- [Instalar Yarn](#instalar-yarn) - [Instalar Yarn](#instalar-yarn)
- [Instalar Dependencias de Node](#instalar-dependencias-de-node) - [Instalar Dependencias de Node](#instalar-dependencias-de-node)
- [Instalar Python 3.9](#instalar-python-39) - [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) - [Variables del Entorno](#variables-del-entorno)
- [Ejecución](#ejecucion) - [Ejecucion](#ejecucion)
- [Compilación](#compilacion) - [Compilacion](#compilacion)
- [Compilar el cliente de bittorrent](#compilar-el-cliente-de-bittorrent) - [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) - [Colaboradores](#colaboradores)
- [Licencia](#licencia)
## Acerca de ## Acerca de

View File

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![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) [![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) ![Catalogue Hydra](./docs/screenshot.png)
@ -27,6 +29,7 @@
## Table des Matières ## Table des Matières
- [Table des Matières](#table-des-matières)
- [À propos](#à-propos) - [À propos](#à-propos)
- [Fonctionnalités](#fonctionnalités) - [Fonctionnalités](#fonctionnalités)
- [Installation](#installation) - [Installation](#installation)
@ -47,6 +50,7 @@
- [Compiler le client bittorrent](#compiler-le-client-bittorrent) - [Compiler le client bittorrent](#compiler-le-client-bittorrent)
- [Compiler l'application Electron](#compiler-lapplication-electron) - [Compiler l'application Electron](#compiler-lapplication-electron)
- [Contributeurs](#contributeurs) - [Contributeurs](#contributeurs)
- [License](#license)
## À propos ## À 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

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![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) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Table of Contents ## Table of Contents
- [Table of Contents](#table-of-contents)
- [About](#about) - [About](#about)
- [Features](#features) - [Features](#features)
- [Installation](#installation) - [Installation](#installation)
- [Contributing](#contributing) - [Contributing](#-contributing)
- [Join our Telegram](#join-our-telegram) - [Join our Telegram](#-join-our-telegram)
- [Fork and clone your repository](#fork-and-clone-your-repository) - [Fork and clone your repository](#fork-and-clone-your-repository)
- [Ways you can contribute](#ways-you-can-contribute) - [Ways you can contribute](#ways-you-can-contribute)
- [Project Structure](#project-structure) - [Project Structure](#project-structure)
@ -47,6 +50,7 @@
- [Build the bittorrent client](#build-the-bittorrent-client) - [Build the bittorrent client](#build-the-bittorrent-client)
- [Build the Electron application](#build-the-electron-application) - [Build the Electron application](#build-the-electron-application)
- [Contributors](#contributors) - [Contributors](#contributors)
- [License](#license)
## About ## About

View File

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![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) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,26 +29,28 @@
## Zawartość. ## Zawartość.
- [Zawartość.](#zawartość)
- [O nas](#o-nas) - [O nas](#o-nas)
- [Cechy.](#cechy) - [Cechy](#cechy)
- [Instalacja](#instalacja) - [Instalacja](#instalacja)
- [Dokonaj wpłaty](#dokonaj-wpłaty) - [Dokonaj wpłaty](#-dokonaj-wpłaty)
- [Dołącz do naszego kanału Telegram](#dołącz-do-naszego-kanału-telegram) - [Dołącz do naszego kanału Telegram](#-dołącz-do-naszego-kanału-telegram)
- [Rozwidlenie i sklonowanie repozytorium](#rozwidlenie-i-sklonowanie-repozytorium) - [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) - [Struktura projektu](#struktura-projektu)
- [Utwórz kompilację z kodu źródłowego](#utwórz-kompilację-z-kodu-źródłowego) - [Utwórz kompilację z kodu źródłowego](#utwórz-kompilację-z-kodu-źródłowego)
- [Instalacja Node.js](#zainstaluj-nodejs) - [Zainstaluj Node.js](#zainstaluj-nodejs)
- [Instalacja Yarn](#zainstaluj-yarn) - [Zainstaluj Yarn](#zainstaluj-yarn)
- [Instalacja Node zależności](#zainstaluj-zależności-node) - [Zainstaluj zależności Node](#zainstaluj-zależności-node)
- [Instalacja Python 3.9](#zainstaluj-python-39) - [Zainstaluj Python 3.9](#zainstaluj-python-39)
- [Instalacja Python zależności](#zainstaluj-zależności-pythona) - [Zainstaluj zależności Pythona](#zainstaluj-zależności-pythona)
- [Zmienne środowiskowe](#zmienne-środowiskowe) - [Zmienne środowiskowe](#zmienne-środowiskowe)
- [Uruchomienie](#utwórz-kompilację-z-kodu-źródłowego) - [Run](#run)
- [Tworzenie kompilacji](#tworzenie-kompilacji) - [Tworzenie kompilacji](#tworzenie-kompilacji)
- [Tworzenie klienta bittorrent](#zbuduj-klienta-bittorrent) - [Zbuduj klienta bittorrent](#zbuduj-klienta-bittorrent)
- [Tworzenie kompilacji aplikacji Electron](#tworzenie-aplikacji-electron) - [Tworzenie aplikacji Electron](#tworzenie-aplikacji-electron)
- [Współtwórcy](#współtwórcy) - [Współtwórcy](#współtwórcy)
- [License](#license)
## O nas ## O nas

View File

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

View File

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Содержание ## Содержание
- [Содержание](#содержание)
- [Описание](#описание) - [Описание](#описание)
- [Особенности](#особенности) - [Особенности](#особенности)
- [Установка](#установка) - [Установка](#установка)
- [Вклад](#contributing) - [Вклад](#-вклад)
- [Присоединяйтесь к нашему Telegram](#join-our-telegram) - [Присоединяйтесь к нашему Telegram](#-присоединяйтесь-к-нашему-telegram)
- [Форк и клонирование репозитория](#форк-и-клонирование-репозитория) - [Форк и клонирование репозитория](#форк-и-клонирование-репозитория)
- [Способы внести свой вклад](#способы-внести-свой-вклад) - [Способы внести свой вклад](#способы-внести-свой-вклад)
- [Структура проекта](#структура-проекта) - [Структура проекта](#структура-проекта)
@ -47,6 +50,7 @@
- [Сборка клиента BitTorrent](#сборка-клиента-bittorrent) - [Сборка клиента BitTorrent](#сборка-клиента-bittorrent)
- [Сборка приложения Electron](#сборка-приложения-electron) - [Сборка приложения Electron](#сборка-приложения-electron)
- [Участники](#участники) - [Участники](#участники)
- [License](#license)
## Описание ## Описание

View File

@ -13,13 +13,15 @@
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![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) [![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) [![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) [![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) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.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) ![Hydra Catalogue](./docs/screenshot.png)
@ -27,11 +29,12 @@
## Зміст ## Зміст
- [Зміст](#зміст)
- [Про нас](#про-нас) - [Про нас](#про-нас)
- [Функції](#функції) - [Функції](#функції)
- [Встановлення](#встановлення) - [Встановлення](#встановлення)
- [Зробити свій внесок](#contributing) - [Зробити свій внесок](#-зробити-свій-внесок)
- [Приєднуйтесь до нашого Telegram](#join-our-telegram) - [Приєднуйтесь до нашого Telegram](#-приєднуйтесь-до-нашого-telegram)
- [Форк і клонування вашого репозиторію](#форк-і-клонування-вашого-репозиторію) - [Форк і клонування вашого репозиторію](#форк-і-клонування-вашого-репозиторію)
- [Як ви можете зробити свій внесок](#як-ви-можете-зробити-свій-внесок) - [Як ви можете зробити свій внесок](#як-ви-можете-зробити-свій-внесок)
- [Структура проекту](#структура-проекту) - [Структура проекту](#структура-проекту)
@ -47,6 +50,7 @@
- [Зробіть білд bittorrent client](#зробіть-білд-bittorrent-client) - [Зробіть білд bittorrent client](#зробіть-білд-bittorrent-client)
- [Зробіть білд Electron застосунку](#зробіть-білд-electron-застосунку) - [Зробіть білд Electron застосунку](#зробіть-білд-electron-застосунку)
- [Контриб'ютори](#контрибютори) - [Контриб'ютори](#контрибютори)
- [License](#license)
## Про нас ## Про нас

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{ {
"language_name": "Dansk",
"home": { "home": {
"featured": "Anbefalet", "featured": "Anbefalet",
"trending": "Trender", "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": { "app": {
"successfully_signed_in": "Successfully signed in" "successfully_signed_in": "Successfully signed in"
}, },
@ -20,7 +21,8 @@
"home": "Home", "home": "Home",
"queued": "{{title}} (Queued)", "queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected", "game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in" "sign_in": "Sign in",
"friends": "Friends"
}, },
"header": { "header": {
"search": "Search games", "search": "Search games",

View File

@ -1,4 +1,5 @@
{ {
"language_name": "Español",
"app": { "app": {
"successfully_signed_in": "Sesión iniciada correctamente" "successfully_signed_in": "Sesión iniciada correctamente"
}, },
@ -270,6 +271,7 @@
"pending": "Pendiente", "pending": "Pendiente",
"no_pending_invites": "No tienes invitaciones pendientes", "no_pending_invites": "No tienes invitaciones pendientes",
"no_blocked_users": "No has bloqueado a ningún usuario", "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": { "home": {
"featured": "پیشنهادی", "featured": "پیشنهادی",
"trending": "پرطرفدار", "trending": "پرطرفدار",

View File

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

View File

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

View File

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

View File

@ -1,21 +1,49 @@
export { default as en } from "./en/translation.json"; import en from "./en/translation.json";
export { default as pt } from "./pt/translation.json"; import ptPT from "./pt-PT/translation.json";
export { default as es } from "./es/translation.json"; import ptBR from "./pt-BR/translation.json";
export { default as nl } from "./nl/translation.json"; import es from "./es/translation.json";
export { default as fr } from "./fr/translation.json"; import nl from "./nl/translation.json";
export { default as hu } from "./hu/translation.json"; import fr from "./fr/translation.json";
export { default as it } from "./it/translation.json"; import hu from "./hu/translation.json";
export { default as pl } from "./pl/translation.json"; import it from "./it/translation.json";
export { default as ru } from "./ru/translation.json"; import de from "./de/translation.json";
export { default as tr } from "./tr/translation.json"; import pl from "./pl/translation.json";
export { default as be } from "./be/translation.json"; import ru from "./ru/translation.json";
export { default as uk } from "./uk/translation.json"; import tr from "./tr/translation.json";
export { default as zh } from "./zh/translation.json"; import be from "./be/translation.json";
export { default as id } from "./id/translation.json"; import uk from "./uk/translation.json";
export { default as ko } from "./ko/translation.json"; import zh from "./zh/translation.json";
export { default as da } from "./da/translation.json"; import id from "./id/translation.json";
export { default as ar } from "./ar/translation.json"; import ko from "./ko/translation.json";
export { default as fa } from "./fa/translation.json"; import da from "./da/translation.json";
export { default as ro } from "./ro/translation.json"; import ar from "./ar/translation.json";
export { default as ca } from "./ca/translation.json"; import fa from "./fa/translation.json";
export { default as kk } from "./kk/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": { "home": {
"featured": "In primo piano", "featured": "In primo piano",
"trending": "Di tendenza", "trending": "Di tendenza",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
{ {
"language_name": "Português (Brasil)",
"app": { "app": {
"successfully_signed_in": "Autenticado com sucesso" "successfully_signed_in": "Autenticado com sucesso"
}, },
"home": { "home": {
"featured": "Destaques", "featured": "Destaques",
"trending": "Populares", "trending": "Populares",
"hot": "🔥 Populares agora",
"weekly": "📅 Mais baixados da semana",
"surprise_me": "Surpreenda-me", "surprise_me": "Surpreenda-me",
"no_results": "Nenhum resultado encontrado" "no_results": "Nenhum resultado encontrado"
}, },
@ -20,7 +23,8 @@
"home": "Início", "home": "Início",
"queued": "{{title}} (Na fila)", "queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado", "game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login" "sign_in": "Login",
"friends": "Amigos"
}, },
"header": { "header": {
"search": "Buscar jogos", "search": "Buscar jogos",
@ -166,7 +170,7 @@
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada", "real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
"save_changes": "Salvar mudanças", "save_changes": "Salvar mudanças",
"changes_saved": "Ajustes salvos com sucesso", "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", "validate_download_source": "Validar",
"remove_download_source": "Remover", "remove_download_source": "Remover",
"add_download_source": "Adicionar fonte", "add_download_source": "Adicionar fonte",
@ -239,7 +243,7 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"successfully_signed_out": "Deslogado com sucesso", "successfully_signed_out": "Deslogado com sucesso",
"sign_out": "Sair da conta", "sign_out": "Sair da conta",
"sign_out_modal_title": "Tem certeza?", "sign_out_modal_title": "Deseja mesmo sair?",
"playing_for": "Jogando por {{amount}}", "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?", "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", "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": { "home": {
"featured": "Recomandate", "featured": "Recomandate",
"trending": "Populare", "trending": "Populare",

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{ {
"language_name": "中文",
"app": { "app": {
"successfully_signed_in": "已成功登录" "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 { registerEvent } from "../register-event";
import { RepacksManager, requestSteam250 } from "@main/services"; import { HydraApi, RepacksManager } from "@main/services";
import { formatName, steamUrlBuilder } from "@shared"; 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 response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
const trendingGames = await requestSteam250("/90day"); `/games/${category}?${params.toString()}`
const results: CatalogueEntry[] = []; );
for (let i = 0; i < resultSize; i++) { return Promise.all(
if (!trendingGames[i]) { response.map(async (game) => {
i++; const steamGame = await steamGamesWorker.run(Number(game.objectId), {
continue; name: "getById",
} });
const { title, objectID } = trendingGames[i]!; const repacks = RepacksManager.search({
const repacks = RepacksManager.search({ query: formatName(title) }); query: formatName(steamGame.name),
});
const catalogueEntry = { return {
objectID, title: steamGame.name,
title, shop: game.shop,
shop: "steam" as GameShop, repacks,
cover: steamUrlBuilder.library(objectID), cover: steamUrlBuilder.library(game.objectId),
}; objectID: game.objectId,
};
results.push({ ...catalogueEntry, repacks }); })
} );
return results;
}; };
registerEvent("getCatalogue", getCatalogue); 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/get-random-game";
import "./catalogue/search-games"; import "./catalogue/search-games";
import "./catalogue/search-game-repacks"; import "./catalogue/search-game-repacks";
import "./catalogue/get-game-stats";
import "./hardware/get-disk-free-space"; import "./hardware/get-disk-free-space";
import "./library/add-game-to-library"; import "./library/add-game-to-library";
import "./library/create-game-shortcut"; import "./library/create-game-shortcut";

View File

@ -1,10 +1,27 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services"; import { HydraApi, logger } from "@main/services";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import type { UserProfile } from "@types"; import type { UserProfile } from "@types";
import { getUserFriends } from "./get-user-friends"; import { getUserFriends } from "./get-user-friends";
import { steamUrlBuilder } from "@shared"; 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 ( const getUser = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
userId: string userId: string
@ -20,36 +37,51 @@ const getUser = async (
if (!profile) return null; if (!profile) return null;
const recentGames = await Promise.all( const recentGames = await Promise.all(
profile.recentGames.map(async (game) => { profile.recentGames
const steamGame = await steamGamesWorker.run(Number(game.objectId), { .map(async (game) => {
name: "getById", const steamGame = await getSteamGame(game.objectId);
});
return { return {
...game, ...game,
title: steamGame.name, ...steamGame,
iconUrl: steamUrlBuilder.icon(game.objectId, steamGame.clientIcon), };
}; })
}) .filter((game) => game)
); );
// const libraryGames = await Promise.all( const libraryGames = await Promise.all(
// profile.libraryGames.map(async (game) => { profile.libraryGames
// return getSteamUserGame(game); .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 { return {
...profile, ...profile,
// libraryGames, libraryGames,
recentGames, recentGames,
friends: friends.friends, friends: friends.friends,
totalFriends: friends.totalFriends, totalFriends: friends.totalFriends,
// currentGame,
}; };
} catch (err) { } catch (err) {
console.log("err", err);
return null; 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 { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, PythonInstance, WindowManager } from "@main/services"; import { logger, PythonInstance, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import * as resources from "@locales"; import resources from "@locales";
import { userPreferencesRepository } from "@main/repository"; import { userPreferencesRepository } from "@main/repository";
import { knexClient, migrationConfig } from "./knex-client"; import { knexClient, migrationConfig } from "./knex-client";

View File

@ -3,6 +3,15 @@ import { HydraApi } from "../hydra-api";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
export const createGame = async (game: Game) => { 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`, { HydraApi.post(`/profile/games`, {
objectId: game.objectID, objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),

View File

@ -12,6 +12,7 @@ import type {
FriendRequestAction, FriendRequestAction,
UpdateProfileRequest, UpdateProfileRequest,
} from "@types"; } from "@types";
import type { CatalogueCategory } from "@shared";
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {
/* Torrenting */ /* Torrenting */
@ -34,7 +35,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Catalogue */ /* Catalogue */
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query), 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) => getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language), ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"), getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
@ -44,6 +46,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getGames", take, prevCursor), ipcRenderer.invoke("getGames", take, prevCursor),
searchGameRepacks: (query: string) => searchGameRepacks: (query: string) =>
ipcRenderer.invoke("searchGameRepacks", query), ipcRenderer.invoke("searchGameRepacks", query),
getGameStats: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameStats", objectId, shop),
/* User preferences */ /* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), 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 {
import type { CatalogueEntry } from "@types"; DownloadIcon,
FileDirectoryIcon,
PeopleIcon,
} from "@primer/octicons-react";
import type { CatalogueEntry, GameStats } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import * as styles from "./game-card.css"; import * as styles from "./game-card.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge"; import { Badge } from "../badge/badge";
import { useCallback, useMemo, useState } from "react";
export interface GameCardProps export interface GameCardProps
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
@ -22,12 +27,35 @@ const shopIcon = {
export function GameCard({ game, ...props }: GameCardProps) { export function GameCard({ game, ...props }: GameCardProps) {
const { t } = useTranslation("game_card"); const { t } = useTranslation("game_card");
const [stats, setStats] = useState<GameStats | null>(null);
const { i18n } = useTranslation();
const uniqueRepackers = Array.from( const uniqueRepackers = Array.from(
new Set(game.repacks.map(({ repacker }) => repacker)) 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 ( return (
<button {...props} type="button" className={styles.card}> <button
{...props}
type="button"
className={styles.card}
onMouseEnter={handleHover}
>
<div className={styles.backdrop}> <div className={styles.backdrop}>
<img src={game.cover} alt={game.title} className={styles.cover} /> <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> <p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
)} )}
<div className={styles.specifics}> <div className={styles.specifics}>
<div className={styles.specificsItem}> <div className={styles.specificsItem}>
<DownloadIcon /> <DownloadIcon />
<span>{game.repacks.length}</span> <span>
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
</span>
</div> </div>
{game.repacks.length > 0 && ( <div className={styles.specificsItem}>
<div className={styles.specificsItem}> <PeopleIcon />
<FileDirectoryIcon /> <span>
<span>{game.repacks.at(0)?.fileSize}</span> {stats ? numberFormatter.format(stats?.playerCount) : "…"}
</div> </span>
)} </div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,3 +60,11 @@ export const noResults = style({
gap: "16px", gap: "16px",
gridColumn: "1 / -1", 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 { vars } from "@renderer/theme.css";
import Lottie from "lottie-react"; import Lottie from "lottie-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { CatalogueCategory } from "@shared";
export function Home() { export function Home() {
const { t } = useTranslation("home"); const { t } = useTranslation("home");
@ -21,15 +22,25 @@ export function Home() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null); 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); setIsLoading(true);
window.electron window.electron
.getCatalogue() .getCatalogue(category)
.then((catalogue) => { .then((catalogue) => {
setCatalogue(catalogue); setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
@ -58,11 +69,13 @@ export function Home() {
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
getCatalogue(); getCatalogue(CatalogueCategory.Hot);
getRandomGame(); getRandomGame();
}, [getCatalogue, getRandomGame]); }, [getCatalogue, getRandomGame]);
const categories = Object.values(CatalogueCategory);
return ( return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444"> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}> <section className={styles.content}>
@ -71,7 +84,22 @@ export function Home() {
<Hero /> <Hero />
<section className={styles.homeHeader}> <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 <Button
onClick={handleRandomizerClick} onClick={handleRandomizerClick}
@ -89,12 +117,14 @@ export function Home() {
</Button> </Button>
</section> </section>
<h2>{t(currentCatalogueCategory)}</h2>
<section className={styles.cards}> <section className={styles.cards}>
{isLoading {isLoading
? Array.from({ length: 12 }).map((_, index) => ( ? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} /> <Skeleton key={index} className={styles.cardSkeleton} />
)) ))
: catalogue.map((result) => ( : catalogue[currentCatalogueCategory].map((result) => (
<GameCard <GameCard
key={result.objectID} key={result.objectID}
game={result} 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"; import { globalStyle, style } from "@vanilla-extract/css";
export const gameCover = style({ export const gameCover = style({
transition: "all ease 0.2s", transition: "all ease 0.2s",
boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)", boxShadow: "0 8px 10px -2px rgba(0, 0, 0, 0.5)",
width: "100%",
":before": { ":before": {
content: "", content: "",
top: "0", top: "0",
@ -60,14 +61,66 @@ export const friend = style({
alignItems: "center", alignItems: "center",
}); });
export const friendAvatar = style({
width: "50px",
height: "50px",
borderRadius: "4px",
});
export const friendName = style({ export const friendName = style({
color: vars.color.muted, color: vars.color.muted,
fontWeight: "bold", fontWeight: "bold",
fontSize: vars.size.body, 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 { userProfileContext } from "@renderer/context";
import { useContext, useEffect, useMemo } from "react"; import { useCallback, useContext, useEffect, useMemo } from "react";
import { ProfileHero } from "./profile-hero/profile-hero"; import { ProfileHero } from "../profile-hero/profile-hero";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { steamUrlBuilder } from "@shared"; 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 * 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() { export function ProfileContent() {
const { userProfile } = useContext(userProfileContext); const { userProfile } = useContext(userProfileContext);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { i18n, t } = useTranslation("user_profile");
useEffect(() => { useEffect(() => {
if (userProfile) { if (userProfile) {
dispatch(setHeaderTitle(userProfile.displayName)); dispatch(setHeaderTitle(userProfile.displayName));
@ -21,8 +29,42 @@ export function ProfileContent() {
}, [userProfile, dispatch]); }, [userProfile, dispatch]);
const truncatedGamesList = useMemo(() => { const truncatedGamesList = useMemo(() => {
if (!userProfile) return [];
return userProfile?.libraryGames.slice(0, 12); 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 ( return (
<div> <div>
@ -35,23 +77,14 @@ export function ProfileContent() {
padding: `${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 3}px`,
}} }}
> >
<div style={{}}> <div style={{ flex: 1 }}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>Library</h2> <h2>{t("library")}</h2>
<h3>{userProfile?.libraryGames.length}</h3> <h3>{numberFormatter.format(userProfile.libraryGames.length)}</h3>
</div> </div>
<ul <ul className={styles.gamesGrid}>
style={{
listStyle: "none",
margin: 0,
padding: 0,
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
}}
>
{truncatedGamesList.map((game) => ( {truncatedGamesList.map((game) => (
<li <li
key={game.objectId} key={game.objectId}
@ -64,13 +97,20 @@ export function ProfileContent() {
> >
<button <button
type="button" type="button"
style={{ cursor: "pointer" }} style={{
cursor: "pointer",
}}
className={styles.gameCover} className={styles.gameCover}
onClick={() => navigate(buildUserGameDetailsPath(game))}
> >
<img <img
src={steamUrlBuilder.cover(game.objectId)} src={steamUrlBuilder.cover(game.objectId)}
alt={game.title} alt={game.title}
style={{ width: "100%" }} style={{
width: "100%",
objectFit: "cover",
borderRadius: 4,
}}
/> />
</button> </button>
</li> </li>
@ -78,14 +118,7 @@ export function ProfileContent() {
</ul> </ul>
</div> </div>
<div <div className={styles.rightContent}>
style={{
minWidth: 350,
display: "flex",
gap: SPACING_UNIT * 2,
flexDirection: "column",
}}
>
<div> <div>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>Played recently</h2> <h2>Played recently</h2>
@ -94,14 +127,10 @@ export function ProfileContent() {
<div className={styles.box}> <div className={styles.box}>
<ul className={styles.list}> <ul className={styles.list}>
{userProfile?.recentGames.map((game) => ( {userProfile?.recentGames.map((game) => (
<li> <li key={`${game.shop}-${game.objectId}`}>
<button <Link
type="button" to={buildUserGameDetailsPath(game)}
style={{ className={styles.listItem}
cursor: "pointer",
display: "flex",
gap: `${SPACING_UNIT}px`,
}}
> >
<img <img
src={game.iconUrl} 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> <span style={{ fontWeight: "bold" }}>{game.title}</span>
<div <div
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: `${SPACING_UNIT / 2}px`, gap: `${SPACING_UNIT}px`,
}} }}
> >
<ClockIcon /> <ClockIcon />
<span>{game.playTimeInSeconds}</span> <small>{formatPlayTime(game)}</small>
</div> </div>
</div> </div>
</button> </Link>
</li> </li>
))} ))}
</ul> </ul>
@ -136,30 +171,31 @@ export function ProfileContent() {
<div> <div>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h2>Friends</h2> <h2>{t("friends")}</h2>
<span>{userProfile?.totalFriends}</span> <span>{userProfile?.totalFriends}</span>
</div> </div>
<div className={styles.box}> <div className={styles.box}>
<ul className={styles.list}> <ul className={styles.list}>
{userProfile?.friends.map((friend) => ( {userProfile?.friends.map((friend) => (
<li> <li key={friend.id}>
<button <Link
type="button" to={`/profile/${friend.id}`}
style={{ cursor: "pointer" }} className={styles.listItem}
className={styles.friend}
> >
<img <img
src={friend.profileImageUrl} src={friend.profileImageUrl}
alt={friend.displayName} alt={friend.displayName}
style={{ width: "100%" }} style={{
className={styles.friendAvatar} width: "30px",
height: "30px",
borderRadius: "4px",
}}
/> />
<span className={styles.friendName}> <span className={styles.friendName}>
{friend.displayName} {friend.displayName}
</span> </span>
</button> </Link>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -6,11 +6,11 @@ export const profileContentBox = style({
flexDirection: "column", flexDirection: "column",
}); });
export const profileAvatarContainer = style({ export const profileAvatarButton = style({
width: "96px", width: "96px",
minWidth: "96px", minWidth: "96px",
height: "96px", height: "96px",
borderRadius: "50%", borderRadius: "4px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
@ -18,7 +18,11 @@ export const profileAvatarContainer = style({
overflow: "hidden", overflow: "hidden",
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", 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({ export const profileAvatar = style({
@ -44,3 +48,24 @@ export const profileDisplayName = style({
textOverflow: "ellipsis", textOverflow: "ellipsis",
width: "100%", 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 { SPACING_UNIT } from "@renderer/theme.css";
import * as styles from "./profile-hero.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 { userProfileContext } from "@renderer/context";
import { import {
CheckCircleFillIcon, CheckCircleFillIcon,
PencilIcon,
PersonIcon, PersonIcon,
SignOutIcon,
XCircleFillIcon, XCircleFillIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { Button, Link } from "@renderer/components"; import { Button, Link } from "@renderer/components";
import { useTranslation } from "react-i18next"; 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() { 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 { t } = useTranslation("user_profile");
const { formatDistance } = useDate(); 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(() => { const profileActions = useMemo(() => {
if (isMe) { if (isMe) {
return ( 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) { if (userProfile.relation == null) {
// return ( return (
// <> <>
// <Button <Button
// theme="outline" theme="outline"
// onClick={() => handleFriendAction(userProfile.id, "SEND")} onClick={() => handleFriendAction(userProfile.id, "SEND")}
// > >
// {t("add_friend")} {t("add_friend")}
// </Button> </Button>
// <Button theme="danger" onClick={() => setShowUserBlockModal(true)}> <Button
// {t("block_user")} theme="danger"
// </Button> onClick={() => handleFriendAction(userProfile.id, "BLOCK")}
// </> >
// ); {t("block_user")}
// } </Button>
</>
);
}
// if (userProfile.relation.status === "ACCEPTED") { if (userProfile.relation.status === "ACCEPTED") {
// return ( return (
// <> <Button
// <Button theme="outline"
// theme="outline" onClick={() => handleFriendAction(userProfile.id, "UNDO_FRIENDSHIP")}
// // className={styles.cancelRequestButton} >
// // onClick={() => setShowUndoFriendshipModal(true)} <XCircleFillIcon />
// > {t("undo_friendship")}
// <XCircleFillIcon size={28} /> {t("undo_friendship")} </Button>
// </Button> );
// </> }
// );
// }
// if (userProfile.relation.BId === userProfile.id) { if (userProfile.relation.BId === userProfile.id) {
// return ( return (
// <Button <Button
// theme="outline" theme="outline"
// // className={styles.cancelRequestButton} onClick={() =>
// // onClick={() => handleFriendAction(userProfile.relation!.BId, "CANCEL")
// // handleFriendAction(userProfile.relation!.BId, "CANCEL") }
// // } >
// > <XCircleFillIcon size={28} /> {t("cancel_request")}
// <XCircleFillIcon size={28} /> {t("cancel_request")} </Button>
// </Button> );
// ); }
// }
return ( return (
<> <>
<Button <Button
theme="outline" theme="outline"
// onClick={() => onClick={() =>
// handleFriendAction(userProfile.relation!.AId, "ACCEPTED") handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
// } }
> >
<CheckCircleFillIcon size={28} /> {t("accept_request")} <CheckCircleFillIcon size={28} /> {t("accept_request")}
</Button> </Button>
<Button <Button
theme="outline" theme="outline"
// onClick={() => onClick={() =>
// handleFriendAction(userProfile.relation!.AId, "REFUSED") handleFriendAction(userProfile.relation!.AId, "REFUSED")
// } }
> >
<XCircleFillIcon size={28} /> {t("ignore_request")} <XCircleFillIcon size={28} /> {t("ignore_request")}
</Button> </Button>
@ -105,19 +168,27 @@ export function ProfileHero() {
return ( 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 <section
className={styles.profileContentBox} className={styles.profileContentBox}
style={{ background: heroBackground }} style={{ background: heroBackground }}
> >
<div <div className={styles.userInformation}>
style={{ <button type="button" className={styles.profileAvatarButton}>
display: "flex",
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<div className={styles.profileAvatarContainer}>
{userProfile.profileImageUrl ? ( {userProfile.profileImageUrl ? (
<img <img
className={styles.profileAvatar} className={styles.profileAvatar}
@ -127,12 +198,13 @@ export function ProfileHero() {
) : ( ) : (
<PersonIcon size={72} /> <PersonIcon size={72} />
)} )}
</div> </button>
<div className={styles.profileInformation}> <div className={styles.profileInformation}>
<h2 className={styles.profileDisplayName}> <h2 className={styles.profileDisplayName}>
{userProfile.displayName} {userProfile.displayName}
</h2> </h2>
{currentGame && ( {currentGame && (
<div <div
style={{ style={{
@ -149,14 +221,23 @@ export function ProfileHero() {
alignItems: "center", alignItems: "center",
}} }}
> >
<Link to={buildGameDetailsPath(currentGame)}> <Link
to={buildGameDetailsPath({
...currentGame,
objectID: currentGame.objectId,
})}
>
{currentGame.title} {currentGame.title}
</Link> </Link>
</div> </div>
<small> <small>
{t("playing_for", { {t("playing_for", {
amount: formatDistance( amount: formatDistance(
currentGame.sessionDurationInSeconds, addSeconds(
new Date(),
-currentGame.sessionDurationInSeconds
),
new Date() new Date()
), ),
})} })}
@ -166,20 +247,7 @@ export function ProfileHero() {
</div> </div>
</div> </div>
<div <div className={styles.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)",
}}
>
<div></div> <div></div>
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}> <div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
{profileActions} {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"; import { style } from "@vanilla-extract/css";
export const wrapper = style({ export const wrapper = style({
@ -7,297 +7,3 @@ export const wrapper = style({
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT * 3}px`, 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 { useParams } from "react-router-dom";
import { ProfileSkeleton } from "./profile-skeleton"; import { ProfileSkeleton } from "./profile-skeleton";
import { ProfileContent } from "./profile-content"; import { ProfileContent } from "./profile-content/profile-content";
import { SkeletonTheme } from "react-loading-skeleton"; import { SkeletonTheme } from "react-loading-skeleton";
import { vars } from "@renderer/theme.css"; 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: [ filters: [
{ {
name: "Image", name: "Image",
extensions: ["jpg", "jpeg", "png", "webp"], extensions: ["jpg", "jpeg", "png"],
}, },
], ],
}); });

View File

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

View File

@ -1,106 +1,27 @@
import { style } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
export const container = style({ export const container = style({
padding: "24px", padding: "24px",
width: "100%", width: "100%",
display: "flex", display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "flex-start",
}); });
export const content = style({ export const content = style({
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
width: "100%", width: "100%",
height: "100%",
padding: `${SPACING_UNIT * 3}px`, padding: `${SPACING_UNIT * 3}px`,
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.5)", boxShadow: "0px 0px 15px 0px #000000",
borderRadius: "4px", borderRadius: "8px",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT * 2}px`,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
flex: "1",
}); });
export const sidebar = style({ export const settingsCategories = style({
width: "200px",
display: "flex", 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`, 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 * as styles from "./settings.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SettingsRealDebrid } from "./settings-real-debrid"; import { SettingsRealDebrid } from "./settings-real-debrid";
@ -13,10 +15,12 @@ import {
export function Settings() { export function Settings() {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const categories = { const categories = [
[t("account")]: [t("my_profile"), t("friends")], t("general"),
Hydra: [t("general"), t("behavior"), t("download_sources"), "Real-Debrid"], t("behavior"),
}; t("download_sources"),
"Real-Debrid",
];
return ( return (
<SettingsContextProvider> <SettingsContextProvider>
@ -40,34 +44,21 @@ export function Settings() {
return ( return (
<section className={styles.container}> <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}> <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> <h2>{categories[currentCategoryIndex]}</h2>
{renderCategory()} {renderCategory()}
</div> </div>

View File

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

View File

@ -309,7 +309,8 @@ export interface UserRelation {
updatedAt: string; updatedAt: string;
} }
export interface UserProfileCurrentGame extends GameRunning { export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
objectId: string;
sessionDurationInSeconds: number; sessionDurationInSeconds: number;
} }
@ -345,3 +346,8 @@ export interface DownloadSource {
createdAt: Date; createdAt: Date;
updatedAt: 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" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 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: iterator.prototype@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz" resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz"