Compare commits

..

27 Commits

Author SHA1 Message Date
Hachi-R
f22896303d lint 2024-11-07 20:35:33 -03:00
Hachi-R
a9085ec2ed feat: pass seeding list from downloader.py to download page 2024-11-07 20:35:17 -03:00
Hachi-R
452532e18b chore: removed remove_torrent() 2024-11-06 22:41:59 -03:00
Hachi-R
b55bc935f7 lint 2024-11-06 22:23:36 -03:00
Hachi-R
e8fc71b36b revert: partially revert changes in getStatus 2024-11-06 22:23:26 -03:00
Hachi-R
6295637b48 feat: add get-sedding endpoint 2024-11-06 21:58:55 -03:00
Hachi-R
e1a5a95ceb chore: removed resume torrent after download 2024-11-06 20:28:55 -03:00
Hachi-R
e5dff9babe lint 2024-11-06 19:30:06 -03:00
Hachi-R
5d59a15bbf feat: check if downloading data exists 2024-11-06 19:29:51 -03:00
Hachi-R
f555890b4c chore: remove possibility of null returning 2024-11-06 19:18:36 -03:00
Hachi-R
d4b1569892 lint 2024-11-06 19:11:38 -03:00
Hachi-R
e0f7f34d14 feat: use main-loop to watch seeding list 2024-11-06 19:10:13 -03:00
Zamitto
cd4715e00d feat: refactor get_download_status 2024-11-05 18:31:56 -03:00
Hachi-R
5daad057e7 feat: return seeding status in downloader 2024-11-05 17:10:25 -03:00
Hachi-R
3a3c0a2b35 Merge branch 'main' into feature/seed-completed-downloads 2024-11-05 15:20:03 -03:00
Hachi-R
64b1795ddd lint 2024-11-05 15:16:24 -03:00
Hachi-R
86d3f7ac81 chore: use resumeDownload() 2024-11-05 15:16:10 -03:00
Hachi-R
1458314df6 lint 2024-11-04 14:03:05 -03:00
Hachi-R
1ad501c64e chore: check if uri exists before adding to table 2024-11-04 14:02:48 -03:00
Hachi-R
92ec056ba8 lint 2024-11-04 03:13:50 -03:00
Hachi-R
bd8974c7cb feat: add initial seeding logic and separation between seeding from downloading 2024-11-04 03:13:17 -03:00
Hachi-R
83b7fb83ab feat: add seed-list table 2024-11-03 21:39:05 -03:00
Hachi-R
dc4dda7e17 Merge branch 'feature/seed-completed-downloads' of https://github.com/hydralauncher/hydra into feature/seed-completed-downloads 2024-10-31 23:06:50 -03:00
Hachi-R
1ebf8acb9b lint 2024-10-31 23:05:59 -03:00
Hachi-R
0955af1e69 feat: add option in user preferences to seed after download completes 2024-10-31 23:05:59 -03:00
Hachi-R
fd7f2403da lint 2024-10-31 23:02:44 -03:00
Hachi-R
e331b9b246 feat: add option in user preferences to seed after download completes 2024-10-31 23:02:23 -03:00
55 changed files with 1294 additions and 940 deletions

View File

@@ -1,4 +1,4 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID

View File

@@ -2,9 +2,6 @@ name: Build
on: pull_request
env:
AWS_REGION: us-east-1
jobs:
build:
strategy:
@@ -22,16 +19,6 @@ jobs:
with:
node-version: 20.18.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Push build to R2
run: aws s3 sync ./docs s3://${{ vars.BUILDS_BUCKET_NAME }}
- name: Install dependencies
run: yarn
@@ -57,7 +44,6 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
@@ -68,7 +54,6 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact

View File

@@ -46,8 +46,8 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
if: matrix.os == 'windows-latest'
run: yarn build:win
@@ -56,8 +56,8 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -1,183 +0,0 @@
<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)
[![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)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Table of Contents
- [A proposito](#a-proposito)
- [Caratteristiche](#caratteristiche)
- [Installazione](#installazione)
- [Contribuire](#contribuire)
- [Uscisciti su Telegram](#unisciti-su-telegram)
- [Forka e Clona la tua repository](#forka-e-clona-la-tua-repository)
- [Modi in cui contribuire](#modi-in-cui-contribuire)
- [Struttura del Progetto](#struttura-del-progetto)
- [Compila il codice sorgente](#compila-il-codice-sorgente)
- [Installa Node.js](#installa-nodejs)
- [Installa Yarn](#installa-yarn)
- [Installa le Node Dependencies](#installa-node-dependencies)
- [Installa Python 3.9](#installa-python-39)
- [Installa le Python Dependencies](#installa-python-dependencies)
- [Variabili d'Ambiente](#variabili-di-ambiente)
- [Esecuzione](#esecuzione)
- [Compilazione](#compilazione)
- [Compilare il client bittorrent](#compilazione-client-bittorrent)
- [Compilare l'applicazione Electron](#compilazione-applicazione-electron)
- [Collaboratori](#collaboratori)
## 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

@@ -36,7 +36,6 @@
"@electron-toolkit/utils": "^3.0.0",
"@fontsource/noto-sans": "^5.0.22",
"@hookform/resolvers": "^3.9.0",
"@intercom/messenger-js-sdk": "^0.0.14",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@vanilla-extract/css": "^1.14.2",

View File

@@ -1,381 +0,0 @@
{
"language_name": "Български",
"app": {
"successfully_signed_in": "Успешно вписване"
},
"home": {
"featured": "Препоръчани",
"surprise_me": "Изненадай ме",
"no_results": "Не са намерени резултати",
"start_typing": "Търсене...",
"hot": "Актуално сега",
"weekly": "📅 Най-доброто от седмицата",
"achievements": "🏆 Игри, които да победите"
},
"sidebar": {
"catalogue": "Каталог",
"downloads": "Изтегляния",
"settings": "Настройки",
"my_library": "Моята библиотека",
"downloading_metadata": "{{title}} (Сваляне на метаданни…)",
"paused": "{{title}} (Пауза)",
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
"filter": "Търсене по име",
"home": "Начало",
"queued": "{{title}} (Опашка)",
"game_has_no_executable": "Играта няма избран изпълним файл",
"sign_in": "Вписване",
"friends": "Приятели",
"need_help": "Имате нужда от помощ??"
},
"header": {
"search": "Търси игри",
"home": "Начало",
"catalogue": "Каталог",
"downloads": "Изтегляния",
"search_results": "Резултати от търсене",
"settings": "Настройки",
"version_available_install": "Версия {{version}} е налична. Кликни тук, за да рестартирате и инсталирате.",
"version_available_download": "Версия {{version}} е налична. Кликни тук за изтегляне."
},
"bottom_panel": {
"no_downloads_in_progress": "Няма изтегляния в ход",
"downloading_metadata": "Сваляне на {{title}} метадата…",
"downloading": "Изтегляне на {{title}}… ({{percentage}} готово) - Остават {{eta}} - {{speed}}",
"calculating_eta": "Изтегляне на {{title}}… ({{percentage}} готово) - Изчисляване на оставащо време…",
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
},
"catalogue": {
"next_page": "Следваща страница",
"previous_page": "Предишна страница"
},
"game_details": {
"open_download_options": "Варианти за изтегляне",
"download_options_zero": "Няма варианти за изтегляне",
"download_options_one": "{{count}} варианти за изтегляне",
"download_options_other": "{{count}} варианти за изтегляне",
"updated_at": "Обновено на {{updated_at}}",
"install": "Инсталирай",
"resume": "Продължи",
"pause": "Пауза",
"cancel": "Отказ",
"remove": "Премахни",
"space_left_on_disk": "{{space}} място на диска",
"eta": "Заклчение {{eta}}",
"calculating_eta": "Калкулиране на оставащо време…",
"downloading_metadata": "Изтегляне на метадата…",
"filter": "Филтрирай repacks",
"requirements": "Състемни изисквания",
"minimum": "Минимални",
"recommended": "Препоръчителни",
"paused": "Паузирано",
"release_date": "Издадено на {{date}}",
"publisher": "Публикувано от {{publisher}}",
"hours": "часове",
"minutes": "минути",
"amount_hours": "{{amount}} часа",
"amount_minutes": "{{amount}} минути",
"accuracy": "{{accuracy}}% точност",
"add_to_library": "Добави в библиотеката",
"remove_from_library": "Премахни от библиотеката",
"no_downloads": "Няма налични изтегляния",
"play_time": "Играно {{amount}}",
"last_time_played": "Последно играно {{period}}",
"not_played_yet": "Не сте играли {{title}} все още",
"next_suggestion": "Следващо предложение",
"play": "Пускане",
"deleting": "Изтриване на инсталация…",
"close": "Затвори",
"playing_now": "Играй сега",
"change": "Промяна",
"repacks_modal_description": "Избери repack който искаш да изтеглиш",
"select_folder_hint": "За да промените стандартната папка отидете в <0>Настройки</0>",
"download_now": "Изтегли сега",
"no_shop_details": "Не може да се извлекат данни за магазина.",
"download_options": "Опции за сваляне",
"download_path": "Път за сваляне",
"previous_screenshot": "Предишна снимка",
"next_screenshot": "Следваща снимка",
"screenshot": "Снимка {{number}}",
"open_screenshot": "Отвори снимки {{number}}",
"download_settings": "Настройки за сваляне",
"downloader": "Downloader",
"select_executable": "Избери",
"no_executable_selected": "Няма избран стартиращ файл",
"open_folder": "Отвори папка",
"open_download_location": "Виж свалените файлове",
"create_shortcut": "Пряк път на Десктопа",
"remove_files": "Премахни файловете",
"remove_from_library_title": "Сигурен ли си?",
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
"options": "Опции",
"executable_section_title": "Стартиращ файл",
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Играй\"",
"downloads_secion_title": "Свалени",
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
"danger_zone_section_title": "Опасна зона",
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
"download_in_progress": "Изтегляне в ход",
"download_paused": "Изтеглянето е паузирано",
"last_downloaded_option": "Опция от последно изтегляне",
"create_shortcut_success": "Прекият път е създаден успешно",
"create_shortcut_error": "Грешка при създаването на пряк път",
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
"allow_nsfw_content": "Продължи",
"refuse_nsfw_content": "Назад",
"stats": "Статистики",
"download_count": "Сваляния",
"player_count": "Активни играчи",
"download_error": "Тази опция за изтегляне не е налична",
"download": "Свали",
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
"warning": "Внимание:",
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена, когато е завършено. Ако Hydra се затвори преди завършването, ще загубите напредъка си..",
"achievements": "Постижения",
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Запазване в облака",
"cloud_save_description": "Запазете напредъка си в облака и продължете да играете на всяко устройство",
"backups": "Резервни копия",
"install_backup": "Инсталирай",
"delete_backup": "Изтрий",
"create_backup": "Ново копие",
"last_backup_date": "Последно копие от {{date}}",
"no_backup_preview": "Не бяха намерени запазени игри за това заглавие",
"restoring_backup": "Възстановяване на резервно копие ({{progress}} готово)…",
"uploading_backup": "Качване на резервно копие…",
"no_backups": "Все още не сте създали резервни копия за тази игра",
"backup_uploaded": "Качено резервно копие",
"backup_deleted": "Изтрито резервно копие",
"backup_restored": "Възстановен бекъп",
"see_all_achievements": "Вижте всички постижения",
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
"mapping_method_automatic": "Автоматично",
"mapping_method_manual": "Ръчно",
"mapping_method_label": "Метод на картографиране",
"files_automatically_mapped": "Автоматично картографиране на файлове",
"no_backups_created": "Не са създадени резервни копия за тази игра",
"manage_files": "Управление на файлове",
"loading_save_preview": "Търсене на запазени игри…",
"wine_prefix": "Wine Префикс",
"wine_prefix_description": "Wine prefix използван за тази игра",
"no_download_option_info": "Няма налични данни",
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
"achievements_not_sync": "Постиженията ви не са синхронизирани",
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
"select_folder": "Избери папка",
"backup_from": "Резервно копие от {{date}}",
"custom_backup_location_set": "Задаване на персонализирано местоположение за архивиране"
},
"activation": {
"title": "Активирай Hydra",
"installation_id": "Идентификатор на инсталацията:",
"enter_activation_code": "Въведете кода за активиране",
"message": "Ако не знаете къде да попитате за това, значи не трябва да го имате..",
"activate": "Активирай",
"loading": "Зареждане…"
},
"downloads": {
"resume": "Продължи",
"pause": "Пауза",
"eta": "Conclusion {{eta}}",
"paused": "Паузирано",
"verifying": "Проверка…",
"completed": "Готово",
"removed": "Не е изтеглен",
"cancel": "Отказ",
"filter": "Филтриране на изтеглени игри",
"remove": "Премахни",
"downloading_metadata": "Изтегляне на метаданни…",
"deleting": "Изтриване на инсталатора…",
"delete": "Премахване на инсталатора",
"delete_modal_title": "Сигурени ли сте?",
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви.",
"install": "Инсталирай",
"download_in_progress": "В процес на изпълнение",
"queued_downloads": "Изтеглени файлове в опашката",
"downloads_completed": "Приключени",
"queued": "В опашка",
"no_downloads_title": "Толкова е празно",
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете..",
"checking_files": "Проверка на файлове…"
},
"settings": {
"downloads_path": "Инсталационен път",
"change": "Актуализиране",
"notifications": "Известия",
"enable_download_notifications": "Когато изтеглянето е завършено",
"enable_repack_list_notifications": "Когато се добави нов repack",
"real_debrid_api_token_label": "Real-Debrid API токен",
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
"general": "Общ",
"behavior": "Поведение",
"download_sources": "Източници за изтегляне",
"language": "Език",
"real_debrid_api_token": "API Токен",
"enable_real_debrid": "Включи Real-Debrid",
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
"real_debrid_invalid_token": "Невалиден API токен",
"real_debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
"real_debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
"save_changes": "Запази промените",
"changes_saved": "Промените са успешно запазни",
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
"validate_download_source": "Валидиране",
"remove_download_source": "Премахни",
"add_download_source": "Добави източник",
"download_count_zero": "Няма опции за сваляне",
"download_count_one": "{{countFormatted}} опции за сваляне",
"download_count_other": "{{countFormatted}} опции за сваляне",
"download_source_url": "URL адрес на източника за изтегляне",
"add_download_source_description": "Вмъкнете URL адреса на файла .json",
"download_source_up_to_date": "Актуален",
"download_source_errored": "Сгрешен",
"sync_download_sources": "Синхронизирай източниците",
"removed_download_source": "Източника за сваляне е премахнат",
"added_download_source": "Добавен източник за сваляне",
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
"insert_valid_json_url": "Добавете ваиден JSON линк",
"found_download_option_zero": "Няма намерени опции за сваляне",
"found_download_option_one": "Намерени {{countFormatted}} опции за сваляне",
"found_download_option_other": "Намерени {{countFormatted}} опции за сваляне",
"import": "Внеси",
"public": "Публичен",
"private": "Личен",
"friends_only": "Само за приятели",
"privacy": "Поверителност",
"profile_visibility": "Видимост на профила",
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
"required_field": "Това поле е задължително",
"source_already_exists": "Този източник вече е добавен",
"must_be_valid_url": "Източникът трябва да е валиден URL адрес.",
"blocked_users": "Блокирани потребители",
"user_unblocked": "Потребителят е бил деблокиран",
"enable_achievement_notifications": "Когато е отключено постижение",
"launch_minimized": "Стартиране на Hydra минимизирано",
"disable_nsfw_alert": "Деактивиране на предупреждението NSFW"
},
"notifications": {
"download_complete": "Изтеглянето е завършено",
"game_ready_to_install": "{{title}} е готово за инсталиране",
"repack_list_updated": "Repack лист е обновен",
"repack_count_one": "{{count}} repack е добавен",
"repack_count_other": "{{count}} repacks добавени",
"new_update_available": "Версия {{version}} е налична",
"restart_to_install_update": "Рестартирайте Hydra, за да инсталирате актуализацията",
"notification_achievement_unlocked_title": "Отключено постижение за {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} и други {{count}} са отклщчени"
},
"system_tray": {
"open": "Отвори Hydra",
"quit": "Изход"
},
"game_card": {
"no_downloads": "Няма налични изтегляния"
},
"binary_not_found_modal": {
"title": "Не инсталирани програми",
"description": "Wine или Lutris изпълними файлове не бяха открити на вашата система",
"instructions": "Проверете правилния начин за инсталиране на някоя от тях на вашата дистрибуция на Linux, за да може играта да работи нормално"
},
"modal": {
"close": "Бутон за затваряне"
},
"forms": {
"toggle_password_visibility": "Превключване на видимостта на паролата"
},
"user_profile": {
"amount_hours": "{{amount}} часове",
"amount_minutes": "{{amount}} минути",
"last_time_played": "Последно играно {{period}}",
"activity": "Скорошна активност",
"library": "Библиотека",
"total_play_time": "Общо време за игра: {{amount}}",
"no_recent_activity_title": "Хмм… няма нищо тук",
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
"display_name": "Показване на името",
"saving": "Запазване",
"save": "Запис",
"edit_profile": "Редактиране на профила",
"saved_successfully": "Запазено успешно",
"try_again": "Моля, опитайте пак",
"sign_out_modal_title": "Сигурни ли сте?",
"cancel": "Отказ",
"successfully_signed_out": "Успешно се отписахте",
"sign_out": "Отписване",
"playing_for": "В игра от {{amount}}",
"sign_out_modal_text": "Вашата библиотека е свързана с текущата ви сметка. Когато се отпишете, библиотеката ви вече няма да е видима и напредъкът няма да бъде запазен. Продължете с отписването?",
"add_friends": "Добави приятели",
"add": "Добави",
"friend_code": "Приятелски код",
"see_profile": "Виж профила",
"sending": "Изпращане",
"friend_request_sent": "Изпратена покана за приятелство",
"friends": "Приятели",
"friends_list": "Списък с приятели",
"user_not_found": "Не е намерен потребител",
"block_user": "Блокирай потребител",
"add_friend": "Добави приятел",
"request_sent": "Изпратена покана",
"request_received": "Получена покана",
"accept_request": "Приеми поканата",
"ignore_request": "Игнирирай поканата",
"cancel_request": "Откажи поканата",
"undo_friendship": "Отмяна на приятелството",
"request_accepted": "Поканата е приета",
"user_blocked_successfully": "Потребителят е блокиран успешно",
"user_block_modal_text": "Това ще блокира {{displayName}}",
"blocked_users": "Блокирани потребители",
"unblock": "Отблокирай",
"no_friends_added": "Не сте добавили приятели",
"pending": "Чакащо",
"no_pending_invites": "Нямате чакащи покани",
"no_blocked_users": "Нямате блокирани потребители",
"friend_code_copied": "Приятелския код е копиран",
"undo_friendship_modal_text": "Това ще отмени приятелството ви с {{displayName}}",
"privacy_hint": "За да настроите кой може да вижда това, отидете в <0>Настройки</0>",
"locked_profile": "Този профил е личен",
"image_process_failure": "Грешка при обработката на изображението",
"required_field": "Това поле е задължително",
"displayname_min_length": "Името трябва да е дълго поне 3 символа",
"displayname_max_length": "Името трябва да е с дължина не повече от 50 символа.",
"report_profile": "Докладвай този профил",
"report_reason": "Защо докладвате този профил?",
"report_description": "Допълнителна информация",
"report_description_placeholder": "Допълнителна информация",
"report": "Докладвай",
"report_reason_hate": "Омразна реч",
"report_reason_sexual_content": "Сексуално съдържание",
"report_reason_violence": "Насилия",
"report_reason_spam": "Спам",
"report_reason_other": "Друго",
"profile_reported": "Профилът е докладван",
"your_friend_code": "Вашия приятелски код:",
"upload_banner": "Качи банер",
"uploading_banner": "Качване на банер…",
"background_image_updated": "Обновено фоново изображение"
},
"achievement": {
"achievement_unlocked": "Постижението е отключено",
"user_achievements": "Постиженията на {{displayName}} ",
"your_achievements": "Вашите Постижения",
"unlocked_at": "Отключено на:",
"subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание",
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Hydra Cloud Абонамент",
"subscribe_now": "Абонирай се сега",
"cloud_saving": "Запазване в облака",
"cloud_achievements": "Запазете постиженията си в облака",
"animated_profile_picture": "Анимирана профилна снимка",
"premium_support": "Премиум поддръжка",
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с тези на други потребители",
"animated_profile_banner": "Анимиран профилен банер"
}
}

View File

@@ -25,8 +25,7 @@
"queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in",
"friends": "Friends",
"need_help": "Need help?"
"friends": "Friends"
},
"header": {
"search": "Search games",
@@ -199,7 +198,10 @@
"queued": "Queued",
"no_downloads_title": "Such empty",
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
"checking_files": "Checking files…"
"checking_files": "Checking files…",
"seeding": "Seeding",
"stop_seed": "Stop seed",
"resume_seed": "Resume seed"
},
"settings": {
"downloads_path": "Downloads path",
@@ -254,9 +256,9 @@
"must_be_valid_url": "The source must be a valid URL",
"blocked_users": "Blocked users",
"user_unblocked": "User has been unblocked",
"enable_achievement_notifications": "When an achievement is unlocked",
"enable_achievement_notifications": "When an achievement in unlocked",
"launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert"
"seed_after_download_completes": "Seed after download completes"
},
"notifications": {
"download_complete": "Download complete",

View File

@@ -25,8 +25,7 @@
"queued": "{{title}} (En cola)",
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar sesión",
"friends": "Amigos",
"need_help": "¿Necesitas ayuda?"
"friends": "Amigos"
},
"header": {
"search": "Buscar juegos",

View File

@@ -24,7 +24,6 @@ import kk from "./kk/translation.json";
import cs from "./cs/translation.json";
import nb from "./nb/translation.json";
import et from "./et/translation.json";
import bg from "./bg/translation.json";
export default {
"pt-BR": ptBR,
@@ -49,7 +48,6 @@ export default {
fa,
ro,
ca,
bg,
kk,
cs,
nb,

View File

@@ -25,8 +25,7 @@
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos",
"need_help": "Precisa de ajuda?"
"friends": "Amigos"
},
"header": {
"search": "Buscar jogos",
@@ -195,7 +194,10 @@
"queued": "Na fila",
"no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
"checking_files": "Verificando arquivos…"
"checking_files": "Verificando arquivos…",
"seeding": "Semeando",
"stop_seed": "Parar seed",
"resume_seed": "Retomar seed"
},
"settings": {
"downloads_path": "Diretório dos downloads",
@@ -251,8 +253,7 @@
"blocked_users": "Usuários bloqueados",
"user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado"
"launch_minimized": "Iniciar o Hydra minimizado"
},
"notifications": {
"download_complete": "Download concluído",

View File

@@ -24,8 +24,7 @@
"queued": "{{title}} (В очереди)",
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья",
"need_help": "Нужна помощь?"
"friends": "Друзья"
},
"header": {
"search": "Поиск",

View File

@@ -5,12 +5,12 @@ export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
export const defaultDownloadsPath = app.getPath("downloads");
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
export const databasePath = path.join(
databaseDirectory,
isStaging ? "hydra_test.db" : "hydra.db"
import.meta.env.MAIN_VITE_API_URL.includes("staging")
? "hydra_test.db"
: "hydra.db"
);
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
@@ -25,4 +25,4 @@ export const achievementSoundPath = app.isPackaged
export const backupsPath = path.join(app.getPath("userData"), "Backups");
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const appVersion = app.getVersion();

View File

@@ -9,6 +9,7 @@ import {
UserAuth,
GameAchievement,
UserSubscription,
SeedList,
} from "@main/entity";
import { databasePath } from "./constants";
@@ -25,6 +26,7 @@ export const dataSource = new DataSource({
DownloadSource,
DownloadQueue,
GameAchievement,
SeedList,
],
synchronize: false,
database: databasePath,

View File

@@ -54,6 +54,9 @@ export class Game {
@Column("int", { default: Downloader.Torrent })
downloader: Downloader;
@Column("boolean", { default: false })
shouldSeed: boolean;
/**
* Progress is a float between 0 and 1
*/

View File

@@ -8,3 +8,4 @@ export * from "./game.entity";
export * from "./game-achievements.entity";
export * from "./download-source.entity";
export * from "./download-queue.entity";
export * from "./seed-list.entity";

View File

@@ -0,0 +1,25 @@
import {
Entity,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
Column,
} from "typeorm";
@Entity("seed_list")
export class SeedList {
@PrimaryGeneratedColumn()
id: number;
@Column("text")
downloadUri: string;
@Column("boolean", { default: false })
shouldSeed: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -38,8 +38,8 @@ export class UserPreferences {
@Column("boolean", { default: false })
startMinimized: boolean;
@Column("boolean", { default: false })
disableNsfwAlert: boolean;
@Column("boolean", { default: true })
seedAfterDownloadCompletes: boolean;
@CreateDateColumn()
createdAt: Date;

View File

@@ -1,16 +1,10 @@
import { shell } from "electron";
import { registerEvent } from "../register-event";
import {
userAuthRepository,
userPreferencesRepository,
} from "@main/repository";
import { userAuthRepository } from "@main/repository";
import { HydraApi } from "@main/services";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const [userAuth, userPreferences] = await Promise.all([
userAuthRepository.findOne({ where: { id: 1 } }),
userPreferencesRepository.findOne({ where: { id: 1 } }),
]);
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
if (!userAuth) {
return;
@@ -22,7 +16,6 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const params = new URLSearchParams({
token: paymentToken,
lng: userPreferences?.language || "en",
});
shell.openExternal(

View File

@@ -91,15 +91,9 @@ const startGameDownload = async (
logger.error("Failed to create game download", err);
});
if (uri.startsWith("magnet:")) {
try {
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
} catch (err) {
logger.error("Failed to parse torrent", err);
}
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
await DownloadManager.cancelDownload(updatedGame!.id);

View File

@@ -12,7 +12,9 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
import { AddSeedAfterDownloadCompletesColumn } from "./migrations/20241101012727_add_seed_after_download_completes_column";
import { AddSeedListTable } from "./migrations/20241103231555_add_seed_list_table";
import { AddShouldSeedColumn } from "./migrations/20241107211345_add_should_seed_colum";
export type HydraMigration = Knex.Migration & { name: string };
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
@@ -29,7 +31,9 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
AddBackgroundImageUrl,
AddWinePrefixToGame,
AddStartMinimizedColumn,
AddDisableNsfwAlertColumn,
AddSeedAfterDownloadCompletesColumn,
AddSeedListTable,
AddShouldSeedColumn,
]);
}
getMigrationName(migration: HydraMigration): string {

View File

@@ -1,17 +1,20 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddDisableNsfwAlertColumn: HydraMigration = {
name: "AddDisableNsfwAlertColumn",
export const AddSeedAfterDownloadCompletesColumn: HydraMigration = {
name: "AddSeedAfterDownloadCompletesColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.boolean("disableNsfwAlert").notNullable().defaultTo(0);
return table
.boolean("seedAfterDownloadCompletes")
.notNullable()
.defaultTo(1);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.dropColumn("disableNsfwAlert");
return table.dropColumn("seedAfterDownloadCompletes");
});
},
};

View File

@@ -0,0 +1,19 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddSeedListTable: HydraMigration = {
name: "AddSeedListTable",
up: (knex: Knex) => {
return knex.schema.createTable("seed_list", async (table) => {
table.increments("id").primary();
table.text("downloadUri").notNullable();
table.boolean("shouldSeed").defaultTo(false);
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
});
},
down: async (knex: Knex) => {
return knex.schema.dropTable("seed_list");
},
};

View File

@@ -0,0 +1,17 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddShouldSeedColumn: HydraMigration = {
name: "AddShouldSeedColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.boolean("shouldSeed").notNullable().defaultTo(false);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("game", (table) => {
return table.dropColumn("shouldSeed");
});
},
};

View File

@@ -9,6 +9,7 @@ import {
UserAuth,
GameAchievement,
UserSubscription,
SeedList,
} from "@main/entity";
export const gameRepository = dataSource.getRepository(Game);
@@ -27,6 +28,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
export const userAuthRepository = dataSource.getRepository(UserAuth);
export const seedListRepository = dataSource.getRepository(SeedList);
export const userSubscriptionRepository =
dataSource.getRepository(UserSubscription);

View File

@@ -102,7 +102,7 @@ export const mergeAchievements = async (
);
});
})
.filter((achievement) => Boolean(achievement))
.filter((achievement) => achievement)
.map((achievement) => {
return {
displayName: achievement!.displayName,

View File

@@ -2,7 +2,11 @@ import { Game } from "@main/entity";
import { Downloader } from "@shared";
import { PythonInstance } from "./python-instance";
import { WindowManager } from "../window-manager";
import { downloadQueueRepository, gameRepository } from "@main/repository";
import {
downloadQueueRepository,
gameRepository,
userPreferencesRepository,
} from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types";
@@ -50,6 +54,19 @@ export class DownloadManager {
await downloadQueueRepository.delete({ game });
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
if (
userPreferences?.seedAfterDownloadCompletes &&
this.currentDownloader === Downloader.Torrent
) {
if (!game.shouldSeed) {
await gameRepository.update(game.id, { shouldSeed: true });
}
}
const [nextQueueItem] = await downloadQueueRepository.find({
order: {
id: "DESC",
@@ -66,6 +83,21 @@ export class DownloadManager {
}
}
public static async watchSeedingList() {
const shouldSeedGames = await gameRepository.findOne({
where: { shouldSeed: true },
});
if (shouldSeedGames) {
const seedingList = await PythonInstance.getSeedingList();
WindowManager.mainWindow?.webContents.send(
"on-seeding-list",
JSON.parse(JSON.stringify(seedingList))
);
}
}
static async pauseDownload() {
if (this.currentDownloader === Downloader.Torrent) {
await PythonInstance.pauseDownload();

View File

@@ -16,8 +16,9 @@ import {
StartDownloadPayload,
PauseDownloadPayload,
LibtorrentStatus,
LibtorrentPayload,
ProcessPayload,
LibtorrentSeedingPayload,
LibtorrentDownloadingPayload,
} from "./types";
import { pythonInstanceLogger as logger } from "../logger";
@@ -60,67 +61,87 @@ export class PythonInstance {
);
}
public static async getSeedingList() {
const response = await this.rpc.get<LibtorrentSeedingPayload[] | null>(
"/seed-list"
);
if (response.data && response.data.length > 0) {
for (const seed of response.data) {
await gameRepository.update({ id: seed.gameId }, { status: "seeding" });
}
}
return response.data;
}
public static async getStatus() {
if (this.downloadingGameId === -1) return null;
const response = await this.rpc.get<LibtorrentDownloadingPayload | null>(
"/status"
);
const response = await this.rpc.get<LibtorrentPayload | null>("/status");
if (response.data === null) return null;
try {
const {
progress,
numPeers,
numSeeds,
downloadSpeed,
bytesDownloaded,
fileSize,
folderName,
status,
gameId,
} = response.data;
this.downloadingGameId = gameId;
const isDownloadingMetadata =
status === LibtorrentStatus.DownloadingMetadata;
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
if (!isDownloadingMetadata && !isCheckingFiles) {
const update: QueryDeepPartialEntity<Game> = {
if (response.data) {
try {
const {
progress,
numPeers,
numSeeds,
downloadSpeed,
bytesDownloaded,
fileSize,
folderName,
status,
gameId,
} = response.data;
this.downloadingGameId = gameId;
const isDownloadingMetadata =
status === LibtorrentStatus.DownloadingMetadata;
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
if (!isDownloadingMetadata && !isCheckingFiles) {
const update: QueryDeepPartialEntity<Game> = {
bytesDownloaded,
fileSize,
progress,
status: "active",
};
await gameRepository.update(
{ id: gameId },
{
...update,
folderName,
}
);
}
if (
progress === 1 &&
!isCheckingFiles &&
status !== LibtorrentStatus.Seeding
) {
this.downloadingGameId = -1;
}
return {
numPeers,
numSeeds,
downloadSpeed,
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
isDownloadingMetadata,
isCheckingFiles,
progress,
status: "active",
};
await gameRepository.update(
{ id: gameId },
{
...update,
folderName,
}
);
gameId,
} as DownloadProgress;
} catch (err) {
return null;
}
if (progress === 1 && !isCheckingFiles) {
this.downloadingGameId = -1;
}
return {
numPeers,
numSeeds,
downloadSpeed,
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
isDownloadingMetadata,
isCheckingFiles,
progress,
gameId,
} as DownloadProgress;
} catch (err) {
return null;
}
return null;
}
static async pauseDownload() {

View File

@@ -20,7 +20,7 @@ export enum LibtorrentStatus {
Seeding = 5,
}
export interface LibtorrentPayload {
export interface LibtorrentDownloadingPayload {
progress: number;
numPeers: number;
numSeeds: number;
@@ -32,7 +32,22 @@ export interface LibtorrentPayload {
gameId: number;
}
export interface LibtorrentSeedingPayload {
progress: number;
numPeers: number;
numSeeds: number;
uploadSpeed: number;
fileSize: number;
folderName: string;
status: LibtorrentStatus;
gameId: number;
}
export interface ProcessPayload {
exe: string;
pid: number;
}
export interface SeedPayload {
should_seed: boolean;
}

View File

@@ -112,8 +112,6 @@ export class HydraApi {
expirationTimestamp: 0,
subscription: null,
};
this.post("/auth/logout", {}, { needsAuth: false }).catch(() => {});
}
static async setupApi() {

View File

@@ -10,6 +10,7 @@ export const startMainLoop = async () => {
watchProcesses(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.watchSeedingList(),
]);
await sleep(1500);

View File

@@ -56,7 +56,6 @@ export const getUserData = () => {
id: loggedUser.userId,
username: "",
bio: "",
email: null,
profileVisibility: "PUBLIC" as ProfileVisibility,
subscription: loggedUser.subscription
? {

View File

@@ -85,10 +85,6 @@ export class WindowManager {
return callback(details);
}
if (details.url.includes("intercom.io")) {
return callback(details);
}
const headers = {
"access-control-allow-origin": ["*"],
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],

View File

@@ -15,6 +15,7 @@ import type {
import type { CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios";
import { GameAchievement } from "@main/entity";
import { LibtorrentSeedingPayload } from "@main/services/download/types";
contextBridge.exposeInMainWorld("electron", {
/* Torrenting */
@@ -26,6 +27,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("pauseGameDownload", gameId),
resumeGameDownload: (gameId: number) =>
ipcRenderer.invoke("resumeGameDownload", gameId),
startSeeding: (gameId: number, magnet: string, savePath: string) =>
ipcRenderer.invoke("startSeeding", gameId, magnet, savePath),
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
@@ -34,6 +37,14 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-download-progress", listener);
return () => ipcRenderer.removeListener("on-download-progress", listener);
},
onSeedingList: (cb: (value: LibtorrentSeedingPayload[]) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: LibtorrentSeedingPayload[]
) => cb(value);
ipcRenderer.on("on-seeding-list", listener);
return () => ipcRenderer.removeListener("on-seeding-list", listener);
},
/* Catalogue */
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),

View File

@@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
/>
</head>
<body>

View File

@@ -126,9 +126,3 @@ export const titleBar = style({
zIndex: "4",
borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule);
export const cloudText = style({
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
backgroundClip: "text",
color: "transparent",
});

View File

@@ -2,8 +2,6 @@ import { useCallback, useContext, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import Intercom from "@intercom/messenger-js-sdk";
import {
useAppDispatch,
useAppSelector,
@@ -36,10 +34,6 @@ export interface AppProps {
children: React.ReactNode;
}
Intercom({
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
});
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary, library } = useLibrary();
@@ -60,13 +54,8 @@ export function App() {
hideFriendsModal,
} = useUserDetails();
const {
userDetails,
hasActiveSubscription,
fetchUserDetails,
updateUserDetails,
clearUserDetails,
} = useUserDetails();
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
const dispatch = useAppDispatch();
@@ -215,9 +204,7 @@ export function App() {
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector(
"[role=dialog]:not([data-intercom-frame='true'])"
);
const modal = document.body.querySelector("[role=dialog]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {
@@ -283,12 +270,7 @@ export function App() {
<>
{window.electron.platform === "win32" && (
<div className={styles.titleBar}>
<h4>
Hydra
{hasActiveSubscription && (
<span className={styles.cloudText}> Cloud</span>
)}
</h4>
<h4>Hydra</h4>
</div>
)}

View File

@@ -0,0 +1,843 @@
{
"v": "4.8.0",
"meta": { "g": "LottieFiles AE 3.5.6", "a": "", "k": "", "d": "", "tc": "" },
"fr": 60,
"ip": 0,
"op": 120,
"w": 714,
"h": 678,
"nm": "Pre-comp 1",
"ddd": 0,
"assets": [
{
"id": "comp_0",
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "centro",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.214, "y": 1 },
"o": { "x": 0.462, "y": 0 },
"t": 0,
"s": [450, 907, 0],
"to": [0, 0, 0],
"ti": [0, 0, 0]
},
{ "t": 30, "s": [450, 1513, 0] }
],
"ix": 2
},
"a": { "a": 0, "k": [-348, -169, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-348, -420],
[-348, -30]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [-348, -164], "ix": 2 },
"a": { "a": 0, "k": [-348, -156], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "esquerdo",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.298], "y": [1] },
"o": { "x": [0.448], "y": [0] },
"t": 6,
"s": [43.5]
},
{ "t": 36, "s": [-1] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "direito",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.265], "y": [1] },
"o": { "x": [0.53], "y": [0] },
"t": 6,
"s": [-43.5]
},
{ "t": 36, "s": [1] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [-100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
]
},
{
"id": "comp_1",
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "centro",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.569, "y": 1 },
"o": { "x": 0.809, "y": 0 },
"t": 0,
"s": [450, 391, 0],
"to": [0, 0, 0],
"ti": [0, 0, 0]
},
{ "t": 30, "s": [450, 997, 0] }
],
"ix": 2
},
"a": { "a": 0, "k": [-348, -169, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-348, -420],
[-348, -30]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [-348, -164], "ix": 2 },
"a": { "a": 0, "k": [-348, -156], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "esquerdo",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.552], "y": [1] },
"o": { "x": [0.702], "y": [0] },
"t": 0,
"s": [-1]
},
{ "t": 30, "s": [43.5] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "direito",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.47], "y": [1] },
"o": { "x": [0.735], "y": [0] },
"t": 0,
"s": [1]
},
{ "t": 30, "s": [-43.5] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [-100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
]
}
],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 0,
"nm": "seta 2",
"refId": "comp_0",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, -247, 0], "ix": 2 },
"a": { "a": 0, "k": [450, 960, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"w": 900,
"h": 1920,
"ip": 30,
"op": 120,
"st": 30,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 0,
"nm": "seta",
"refId": "comp_1",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, 258, 0], "ix": 2 },
"a": { "a": 0, "k": [450, 345, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"w": 900,
"h": 690,
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "base Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, 548.713, 0], "ix": 2 },
"a": { "a": 0, "k": [357.81, 129.934, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 50.043],
[0, 0],
[-21.158, 0],
[0, -21.447],
[0, 0],
[-7.049, 0],
[0, 0],
[0, 7.149],
[0, 0],
[-21.158, 0],
[0, -21.447],
[0, 0],
[49.368, 0]
],
"o": [
[-49.369, 0],
[0, 0],
[0, -21.447],
[21.158, 0],
[0, 0],
[0, 7.145],
[0, 0],
[7.053, 0],
[0, 0],
[0, -21.447],
[21.158, 0],
[0, 0],
[0, 50.043],
[0, 0]
],
"v": [
[-268.169, 129.445],
[-357.559, 38.834],
[-357.559, -90.61],
[-319.249, -129.445],
[-280.939, -90.61],
[-280.939, 38.834],
[-268.169, 51.778],
[268.169, 51.778],
[280.939, 38.834],
[280.939, -90.61],
[319.249, -129.445],
[357.559, -90.61],
[357.559, 38.834],
[268.169, 129.445]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.865977448108, 0.86824388691, 0.890449075138, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [357.809, 129.695], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
],
"markers": []
}

View File

@@ -4,7 +4,7 @@
color: globals.$muted-color;
font-size: 10px;
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
border: solid 1px globals.$muted-color;
border: solid 1px globals.$border-color;
border-radius: 4px;
display: flex;
align-items: center;

View File

@@ -1,7 +1,7 @@
@use "../../scss/globals.scss";
.bottom-panel {
width: 100%;
width: "100%";
border-top: solid 1px globals.$border-color;
background-color: globals.$background-color;
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2);

View File

@@ -13,7 +13,6 @@ export const sidebar = recipe({
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
justifyContent: "space-between",
},
variants: {
resizing: {
@@ -125,28 +124,3 @@ export const section = style({
flexDirection: "column",
paddingBottom: `${SPACING_UNIT}px`,
});
export const helpButton = style({
color: vars.color.muted,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
gap: "9px",
display: "flex",
alignItems: "center",
cursor: "pointer",
borderTop: `solid 1px ${vars.color.border}`,
transition: "background-color ease 0.1s",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const helpButtonIcon = style({
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#fff",
borderRadius: "50%",
});

View File

@@ -5,12 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import type { LibraryGame } from "@types";
import { TextField } from "@renderer/components";
import {
useDownload,
useLibrary,
useToast,
useUserDetails,
} from "@renderer/hooks";
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
import { routes } from "./routes";
@@ -20,9 +15,6 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react";
import { show, update } from "@intercom/messenger-js-sdk";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
@@ -50,21 +42,6 @@ export function Sidebar() {
return sortBy(library, (game) => game.title);
}, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails();
useEffect(() => {
if (userDetails) {
update({
name: userDetails.displayName,
Username: userDetails.username,
email: userDetails.email ?? undefined,
Email: userDetails.email,
"Subscription expiration date": userDetails?.subscription?.expiresAt,
"Payment status": userDetails?.subscription?.status,
});
}
}, [userDetails, hasActiveSubscription]);
const { lastPacket, progress } = useDownload();
const { showWarningToast } = useToast();
@@ -189,91 +166,77 @@ export function Sidebar() {
maxWidth: sidebarWidth,
}}
>
<div
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
>
<SidebarProfile />
<SidebarProfile />
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname ===
`/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname === `/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
>
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
</div>
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
</div>
{hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}>
<div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} />
</div>
<span>{t("need_help")}</span>
</button>
)}
<button
type="button"
className={styles.handle}

View File

@@ -147,8 +147,7 @@ export function GameDetailsContextProvider({
if (
result?.content_descriptors.ids.includes(
SteamContentDescriptor.AdultOnlySexualContent
) &&
!userPreferences?.disableNsfwAlert
)
) {
setHasNSFWContentBlocked(true);
}

View File

@@ -49,6 +49,9 @@ declare global {
onDownloadProgress: (
cb: (value: DownloadProgress) => void
) => () => Electron.IpcRenderer;
onSeedingList: (
cb: (value: LibtorrentSeedingPayload[]) => void
) => () => Electron.IpcRenderer;
/* Catalogue */
searchGames: (query: string) => Promise<CatalogueEntry[]>;

View File

@@ -10,22 +10,12 @@ export interface HowLongToBeatEntry {
updatedAt: Date;
}
export interface CatalogueCache {
id?: number;
category: string;
games: { objectId: string; shop: GameShop }[];
createdAt: Date;
updatedAt: Date;
expiresAt: Date;
}
export const db = new Dexie("Hydra");
db.version(5).stores({
db.version(4).stores({
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
});
export const downloadSourcesTable = db.table("downloadSources");
@@ -34,6 +24,4 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
"howLongToBeatEntries"
);
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
db.open();

View File

View File

@@ -1,6 +1,7 @@
import { useNavigate } from "react-router-dom";
import { useMemo } from "react";
import type { LibraryGame } from "@types";
import type { LibraryGame, SeedingList } from "@types";
import { Badge, Button } from "@renderer/components";
import {
@@ -21,6 +22,7 @@ export interface DownloadGroupProps {
title: string;
openDeleteGameModal: (gameId: number) => void;
openGameInstaller: (gameId: number) => void;
seedingList: SeedingList[];
}
export function DownloadGroup({
@@ -28,6 +30,7 @@ export function DownloadGroup({
title,
openDeleteGameModal,
openGameInstaller,
seedingList = [],
}: DownloadGroupProps) {
const navigate = useNavigate();
@@ -46,6 +49,17 @@ export function DownloadGroup({
isGameDeleting,
} = useDownload();
const seedingMap = useMemo(() => {
if (!Array.isArray(seedingList) || seedingList.length === 0) {
return new Map<number, SeedingList>();
}
const map = new Map<number, SeedingList>();
seedingList.forEach((seed) => {
map.set(seed.gameId, seed);
});
return map;
}, [seedingList]);
const getFinalDownloadSize = (game: LibraryGame) => {
const isGameDownloading = lastPacket?.game.id === game.id;
@@ -60,6 +74,7 @@ export function DownloadGroup({
const getGameInfo = (game: LibraryGame) => {
const isGameDownloading = lastPacket?.game.id === game.id;
const finalDownloadSize = getFinalDownloadSize(game);
const seed = seedingMap.get(game.id);
if (isGameDeleting(game.id)) {
return <p>{t("deleting")}</p>;
@@ -98,7 +113,18 @@ export function DownloadGroup({
}
if (game.progress === 1) {
return <p>{t("completed")}</p>;
return (
<>
{seed ? (
<>
<p>{t("seeding")}</p>
<p>{formatBytes(seed.uploadSpeed ?? 0)}/s</p>
</>
) : (
<p>{t("completed")}</p>
)}
</>
);
}
if (game.status === "paused") {
@@ -127,8 +153,8 @@ export function DownloadGroup({
const getGameActions = (game: LibraryGame) => {
const isGameDownloading = lastPacket?.game.id === game.id;
const deleting = isGameDeleting(game.id);
const seed = seedingMap.get(game.id);
if (game.progress === 1) {
return (
@@ -144,6 +170,14 @@ export function DownloadGroup({
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
{t("delete")}
</Button>
{seed && game.shouldSeed && (
<Button theme="outline">{t("stop_seed")}</Button>
)}
{seed && !game.shouldSeed && (
<Button theme="outline">{t("resume_seed")}</Button>
)}
</>
);
}

View File

@@ -2,12 +2,12 @@ import { useTranslation } from "react-i18next";
import { useDownload, useLibrary } from "@renderer/hooks";
import { useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
import * as styles from "./downloads.css";
import { DeleteGameModal } from "./delete-game-modal";
import { DownloadGroup } from "./download-group";
import type { LibraryGame } from "@types";
import type { LibraryGame, SeedingList } from "@types";
import { orderBy } from "lodash-es";
import { ArrowDownIcon } from "@primer/octicons-react";
@@ -30,6 +30,12 @@ export default function Downloads() {
const { lastPacket } = useDownload();
const [seedingList, setSeedingList] = useState<SeedingList[]>([]);
useEffect(() => {
window.electron.onSeedingList((value) => setSeedingList(value));
}, []);
const handleOpenGameInstaller = (gameId: number) =>
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
@@ -122,6 +128,7 @@ export default function Downloads() {
library={group.library}
openDeleteGameModal={handleOpenDeleteGameModal}
openGameInstaller={handleOpenGameInstaller}
seedingList={seedingList}
/>
))}
</div>

View File

@@ -15,14 +15,6 @@ import * as styles from "./home.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { buildGameDetailsPath } from "@renderer/helpers";
import { CatalogueCategory } from "@shared";
import { catalogueCacheTable, db } from "@renderer/dexie";
import { add } from "date-fns";
const categoryCacheDurationInSeconds = {
[CatalogueCategory.Hot]: 60 * 60 * 2,
[CatalogueCategory.Weekly]: 60 * 60 * 24,
[CatalogueCategory.Achievements]: 60 * 60 * 24,
};
export default function Home() {
const { t } = useTranslation("home");
@@ -44,43 +36,19 @@ export default function Home() {
[CatalogueCategory.Achievements]: [],
});
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
try {
const catalogueCache = await catalogueCacheTable
.where("expiresAt")
.above(new Date())
.and((cache) => cache.category === category)
.first();
const getCatalogue = useCallback((category: CatalogueCategory) => {
setCurrentCatalogueCategory(category);
setIsLoading(true);
setCurrentCatalogueCategory(category);
setIsLoading(true);
if (catalogueCache)
return setCatalogue((prev) => ({
...prev,
[category]: catalogueCache.games,
}));
const catalogue = await window.electron.getCatalogue(category);
db.transaction("rw", catalogueCacheTable, async () => {
await catalogueCacheTable.where("category").equals(category).delete();
await catalogueCacheTable.add({
category,
games: catalogue,
createdAt: new Date(),
updatedAt: new Date(),
expiresAt: add(new Date(), {
seconds: categoryCacheDurationInSeconds[category],
}),
});
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
});
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
} finally {
setIsLoading(false);
}
}, []);
const getRandomGame = useCallback(() => {

View File

@@ -18,7 +18,7 @@ export function SettingsBehavior() {
preferQuitInsteadOfHiding: false,
runAtStartup: false,
startMinimized: false,
disableNsfwAlert: false,
seedAfterDownloadCompletes: true,
});
const { t } = useTranslation("settings");
@@ -29,7 +29,7 @@ export function SettingsBehavior() {
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
runAtStartup: userPreferences.runAtStartup,
startMinimized: userPreferences.startMinimized,
disableNsfwAlert: userPreferences.disableNsfwAlert,
seedAfterDownloadCompletes: userPreferences.seedAfterDownloadCompletes,
});
}
}, [userPreferences]);
@@ -90,10 +90,12 @@ export function SettingsBehavior() {
)}
<CheckboxField
label={t("disable_nsfw_alert")}
checked={form.disableNsfwAlert}
label={t("seed_after_download_completes")}
checked={form.seedAfterDownloadCompletes}
onChange={() =>
handleChange({ disableNsfwAlert: !form.disableNsfwAlert })
handleChange({
seedAfterDownloadCompletes: !form.seedAfterDownloadCompletes,
})
}
/>
</>

View File

@@ -1,10 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -7,7 +7,8 @@ export type GameStatus =
| "paused"
| "error"
| "complete"
| "removed";
| "removed"
| "seeding";
export type GameShop = "steam" | "epic";
@@ -124,6 +125,7 @@ export interface Game {
objectID: string;
shop: GameShop;
downloadQueue: DownloadQueue | null;
shouldSeed: boolean;
createdAt: Date;
updatedAt: Date;
}
@@ -151,6 +153,16 @@ export interface DownloadProgress {
game: LibraryGame;
}
export interface SeedingList {
progress: number;
numPeers: number;
numSeeds: number;
uploadSpeed: number;
gameId: number;
folderName: string;
fileSize: number;
}
export interface UserPreferences {
downloadsPath: string | null;
language: string;
@@ -161,7 +173,7 @@ export interface UserPreferences {
preferQuitInsteadOfHiding: boolean;
runAtStartup: boolean;
startMinimized: boolean;
disableNsfwAlert: boolean;
seedAfterDownloadCompletes: boolean;
}
export interface Steam250Game {
@@ -246,7 +258,6 @@ export interface Subscription {
export interface UserDetails {
id: string;
username: string;
email: string | null;
displayName: string;
profileImageUrl: string | null;
backgroundImageUrl: string | null;
@@ -259,7 +270,6 @@ export interface UserProfile {
id: string;
displayName: string;
profileImageUrl: string | null;
email: string | null;
backgroundImageUrl: string | null;
profileVisibility: ProfileVisibility;
libraryGames: UserGame[];
@@ -376,4 +386,4 @@ export interface ComparedAchievements {
export * from "./steam.types";
export * from "./real-debrid.types";
export * from "./ludusavi.types";
export * from "./how-long-to-beat.types";
export * from "./howlongtobeat.types";

View File

@@ -50,6 +50,21 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(json.dumps(status).encode('utf-8'))
elif self.path == "/seed-list":
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
seed_list = torrent_downloader.get_seed_list()
self.wfile.write(json.dumps(seed_list).encode('utf-8'))
elif self.path == "/healthcheck":
self.send_response(200)
self.end_headers()
@@ -107,6 +122,8 @@ class Handler(BaseHTTPRequestHandler):
elif data['action'] == 'kill-torrent':
torrent_downloader.abort_session()
torrent_downloader = None
elif data['action'] == 'start-seeding':
torrent_downloader.start_seeding(data['game_id'], data['magnet'], data['save_path'])
self.send_response(200)
self.end_headers()

View File

@@ -106,7 +106,7 @@ class TorrentDownloader:
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
torrent_handle = self.session.add_torrent(params)
self.torrent_handles[game_id] = torrent_handle
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
torrent_handle.set_flags(lt.torrent_flags.auto_managed, lt.torrent_flags.seed_mode)
torrent_handle.resume()
self.downloading_game_id = game_id
@@ -151,6 +151,7 @@ class TorrentDownloader:
'gameId': self.downloading_game_id,
'progress': status.progress,
'downloadSpeed': status.download_rate,
'uploadSpeed': status.upload_rate,
'numPeers': status.num_peers,
'numSeeds': status.num_seeds,
'status': status.state,
@@ -158,8 +159,34 @@ class TorrentDownloader:
}
if status.progress == 1:
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.downloading_game_id = -1
return response
def get_seed_list(self):
response = []
for game_id, torrent_handle in self.torrent_handles.items():
if game_id == self.downloading_game_id:
continue
status = torrent_handle.status()
info = torrent_handle.torrent_file()
torrent_info = {
'folderName': info.name() if info else "",
'fileSize': info.total_size() if info else 0,
'gameId': game_id,
'progress': status.progress,
'downloadSpeed': status.download_rate,
'uploadSpeed': status.upload_rate,
'numPeers': status.num_peers,
'numSeeds': status.num_seeds,
'status': status.state,
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
}
if status.state == 5:
response.append(torrent_info)
return response

View File

@@ -1066,11 +1066,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@intercom/messenger-js-sdk@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.14.tgz#a27999370cc0a82a2a57a779426df25a57891863"
integrity sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"