diff --git a/.env.example b/.env.example
index c2ad43d9..47d1a1e3 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,4 @@
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
MAIN_VITE_API_URL=API_URL
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
+SENTRY_AUTH_TOKEN=
diff --git a/.eslintignore b/.eslintignore
index a6f34fea..a9960b13 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,3 +2,4 @@ node_modules
dist
out
.gitignore
+migration.stub
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 88467f6a..472ed853 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -27,7 +27,7 @@ body:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
- required: true
+ required: false
- type: textarea
id: screenshots
attributes:
@@ -56,3 +56,12 @@ body:
description: Please provide any additional information and context about your problem.
validations:
required: false
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Before opening this Issue
+ options:
+ - label: I have searched the issues of this repository and believe that this is not a duplicate.
+ required: true
+ - label: I am aware that Hydra team does not offer any support or help regarding the downloaded games.
+ required: true
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 83636af6..85a7fe1f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,8 +40,6 @@ jobs:
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
- MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
- MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
@@ -51,8 +49,6 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
- MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
- MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0a5296f0..e21acfcb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,8 +42,6 @@ jobs:
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
- MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
- MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
@@ -53,8 +51,6 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
- MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }}
- MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }}
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
diff --git a/.gitignore b/.gitignore
index 7a6496a5..fb4badd7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
.vscode
node_modules
hydra-download-manager/
-aria2/
fastlist.exe
__pycache__
dist
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..852e7cc3
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,68 @@
+# Security Policy
+
+## Purpose of the Policy
+
+The purpose of this Security Policy is to ensure the security of our project and maintain the trust of the community.
+
+## Who is Affected by the Policy
+
+This policy applies to all members of our project community, including developers, testers, repository administrators, and users.
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 2.0.x | :white_check_mark: |
+| < 1.2.0 | :x: |
+
+## Development Recommendations
+
+### Best Practices
+
+- Follow secure coding principles.
+- Use well-established libraries and frameworks.
+- Regularly update dependencies.
+- Conduct thorough testing, including security-related tests.
+
+### Unrecommended Practices
+
+- Do not use known vulnerabilities that have not been patched.
+- Do not publish sensitive information such as API keys or passwords.
+- Do not vote for changes that degrade the security of the project.
+
+### User-Generated Content
+
+- Ensure that user-generated content does not contain hidden threats.
+- Be cautious when handling user data.
+
+### Community Interaction
+
+- Treat each other with respect and politeness.
+- Do not spread spam or spam bots.
+- Follow community guidelines.
+
+### Vulnerability Discovery and Reporting
+
+- If you discover a vulnerability, report it as an issue on GitHub.
+- Your report should contain detailed information about the vulnerability, including steps to resolve it.
+
+### Reporting Method
+
+To report a vulnerability, create a new issue on GitHub and use branch isolation to provide details about the vulnerability.
+
+### Details to Provide
+
+Please provide the following information about the vulnerability:
+
+- Description of the vulnerability
+- Steps to resolve the vulnerability
+- Versions on which the vulnerability was found
+- Code examples illustrating the vulnerability (if it is safe to do so)
+
+### Expected Behavior
+
+- If we accept the reported vulnerability, we will release a patch and update the security information on GitHub.
+- If we reject the reported vulnerability, we will provide an explanation.
diff --git a/electron-builder.yml b/electron-builder.yml
index 65c847a2..8d94d3ed 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -3,7 +3,6 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- - aria2
- hydra-download-manager
- seeds
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
diff --git a/package.json b/package.json
index 1b99734d..1727e383 100644
--- a/package.json
+++ b/package.json
@@ -23,29 +23,27 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
- "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs",
+ "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux",
"prepare": "husky",
- "typeorm:migration-create": "yarn typeorm migration:create"
+ "knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
- "@fontsource/fira-mono": "^5.0.13",
- "@fontsource/fira-sans": "^5.0.20",
+ "@fontsource/noto-sans": "^5.0.22",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@sentry/electron": "^5.1.0",
"@vanilla-extract/css": "^1.14.2",
"@vanilla-extract/dynamic": "^2.1.1",
"@vanilla-extract/recipes": "^0.5.2",
- "aria2": "^4.1.2",
"auto-launch": "^5.0.6",
- "axios": "^1.6.8",
- "better-sqlite3": "^9.5.0",
+ "axios": "^1.7.7",
+ "better-sqlite3": "^11.2.1",
"check-disk-space": "^3.4.0",
"classnames": "^2.5.1",
"color": "^4.2.3",
@@ -60,9 +58,9 @@
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"icojs": "^0.19.3",
- "iso-639-1": "3.1.2",
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
+ "knex": "^3.1.0",
"lodash-es": "^4.17.21",
"lottie-react": "^2.4.0",
"parse-torrent": "^11.0.16",
@@ -97,7 +95,7 @@
"@types/user-agents": "^1.0.4",
"@vanilla-extract/vite-plugin": "^4.0.7",
"@vitejs/plugin-react": "^4.2.1",
- "electron": "^30.0.9",
+ "electron": "^30.3.0",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
@@ -108,6 +106,7 @@
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-svgr": "^4.2.0"
diff --git a/postinstall.cjs b/postinstall.cjs
deleted file mode 100644
index 547af988..00000000
--- a/postinstall.cjs
+++ /dev/null
@@ -1,50 +0,0 @@
-const { default: axios } = require("axios");
-const util = require("node:util");
-const fs = require("node:fs");
-
-const exec = util.promisify(require("node:child_process").exec);
-
-const downloadAria2 = async () => {
- if (fs.existsSync("aria2")) {
- console.log("Aria2 already exists, skipping download...");
- return;
- }
-
- const file =
- process.platform === "win32"
- ? "aria2-1.37.0-win-64bit-build1.zip"
- : "aria2-1.37.0-1-x86_64.pkg.tar.zst";
-
- const downloadUrl =
- process.platform === "win32"
- ? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
- : "https://archlinux.org/packages/extra/x86_64/aria2/download/";
-
- console.log(`Downloading ${file}...`);
-
- const response = await axios.get(downloadUrl, { responseType: "stream" });
-
- const stream = response.data.pipe(fs.createWriteStream(file));
-
- stream.on("finish", async () => {
- console.log(`Downloaded ${file}, extracting...`);
-
- if (process.platform === "win32") {
- await exec(`npx extract-zip ${file}`);
- console.log("Extracted. Renaming folder...");
-
- fs.renameSync(file.replace(".zip", ""), "aria2");
- } else {
- await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
- console.log("Extracted. Copying binary file...");
- fs.mkdirSync("aria2");
- fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
- fs.rmSync("usr", { recursive: true });
- }
-
- console.log(`Extracted ${file}, removing compressed downloaded file...`);
- fs.rmSync(file);
- });
-};
-
-downloadAria2();
diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json
index e95db2f1..26f0654d 100644
--- a/src/locales/ar/translation.json
+++ b/src/locales/ar/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "اَلْعَرَبِيَّةُ",
"home": {
"featured": "مميّز",
"trending": "شائع",
diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json
index 9e945f8d..b89946e8 100644
--- a/src/locales/be/translation.json
+++ b/src/locales/be/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "беларуская мова",
"home": {
"featured": "Рэкамэндаванае",
"trending": "Актуальнае",
diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json
index c756745a..393ea587 100644
--- a/src/locales/ca/translation.json
+++ b/src/locales/ca/translation.json
@@ -1,4 +1,8 @@
{
+ "language_name": "Català",
+ "app": {
+ "successfully_signed_in": "Has entrat correctament"
+ },
"home": {
"featured": "Destacats",
"trending": "Populars",
@@ -14,7 +18,10 @@
"paused": "{{title}} (Pausat)",
"downloading": "{{title}} ({{percentage}} - S'està baixant…)",
"filter": "Filtra la biblioteca",
- "home": "Inici"
+ "home": "Inici",
+ "queued": "{{title}} (En espera)",
+ "game_has_no_executable": "El joc encara no té un executable seleccionat",
+ "sign_in": "Entra"
},
"header": {
"search": "Cerca jocs",
@@ -29,7 +36,9 @@
"bottom_panel": {
"no_downloads_in_progress": "Cap baixada en curs",
"downloading_metadata": "S'estan baixant les metadades de: {{title}}…",
- "downloading": "S'està baixant: {{title}}… ({{percentage}} complet) - Finalització: {{eta}} - {{speed}}"
+ "downloading": "S'està baixant: {{title}}… ({{percentage}} complet) - Finalització: {{eta}} - {{speed}}",
+ "calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…",
+ "checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)"
},
"catalogue": {
"next_page": "Pàgina següent",
@@ -47,12 +56,14 @@
"cancel": "Cancel·la",
"remove": "Elimina",
"space_left_on_disk": "{{space}} lliures al disc",
- "eta": "Finalització: {{eta}}",
+ "eta": "Finalitza en: {{eta}}",
+ "calculating_eta": "Calculant temps estimat…",
"downloading_metadata": "S'estan baixant les metadades…",
"filter": "Filtra els reempaquetats",
"requirements": "Requisits del sistema",
"minimum": "Mínims",
"recommended": "Recomanats",
+ "paused": "Paused",
"release_date": "Publicat el {{date}}",
"publisher": "Publicat per {{publisher}}",
"hours": "hores",
@@ -81,7 +92,29 @@
"previous_screenshot": "Captura anterior",
"next_screenshot": "Captura següent",
"screenshot": "Captura {{number}}",
- "open_screenshot": "Obre la captura {{number}}"
+ "open_screenshot": "Obre la captura {{number}}",
+ "download_settings": "Configuració de descàrrega",
+ "downloader": "Descarregador",
+ "select_executable": "Selecciona",
+ "no_executable_selected": "No hi ha executable selccionat",
+ "open_folder": "Obre carpeta",
+ "open_download_location": "Visualitzar fitxers descarregats",
+ "create_shortcut": "Crear accés directe a l'escriptori",
+ "remove_files": "Elimina fitxers",
+ "remove_from_library_title": "Segur?",
+ "remove_from_library_description": "Això eliminarà el videojoc {{game}} del teu catàleg",
+ "options": "Opcions",
+ "executable_section_title": "Executable",
+ "executable_section_description": "Directori del fitxer des d'on s'executarà quan es cliqui a \"Executar\"",
+ "downloads_secion_title": "Descàrregues",
+ "downloads_section_description": "Comprova actualitzacions o altres versions del videojoc",
+ "danger_zone_section_title": "Zona de perill",
+ "danger_zone_section_description": "Elimina aquest videojoc del teu catàleg o els fitxers descarregats per Hydra",
+ "download_in_progress": "Descàrrega en progrés",
+ "download_paused": "Descàrrega en pausa",
+ "last_downloaded_option": "Opció de l'última descàrrega",
+ "create_shortcut_success": "Accés directe creat satisfactòriament",
+ "create_shortcut_error": "Error al crear l'accés directe"
},
"activation": {
"title": "Activa l'Hydra",
@@ -98,6 +131,7 @@
"paused": "Pausada",
"verifying": "S'està verificant…",
"completed": "Completada",
+ "removed": "No descarregat",
"cancel": "Cancel·la",
"filter": "Filtra els jocs baixats",
"remove": "Elimina",
@@ -106,7 +140,14 @@
"delete": "Elimina l'instal·lador",
"delete_modal_title": "N'estàs segur?",
"delete_modal_description": "S'eliminaran de l'ordinador tots els fitxers d'instal·lació",
- "install": "Instal·la"
+ "install": "Instal·la",
+ "download_in_progress": "En progrés",
+ "queued_downloads": "Descàrregues en espera",
+ "downloads_completed": "Completat",
+ "queued": "En espera",
+ "no_downloads_title": "Buit",
+ "no_downloads_description": "No has descarregat res amb Hydra encara, però mai és tard per començar a fer-ho.",
+ "checking_files": "Comprovant fitxers…"
},
"settings": {
"downloads_path": "Ruta de baixades",
@@ -119,16 +160,49 @@
"launch_with_system": "Inicia l'Hydra quan s'iniciï el sistema",
"general": "General",
"behavior": "Comportament",
+ "download_sources": "Fonts de descàrrega",
+ "language": "Idioma",
+ "real_debrid_api_token": "Testimoni API",
"enable_real_debrid": "Activa el Real Debrid",
+ "real_debrid_description": "Real-Debrid és un programa de descàrrega sense restriccions que us permet descarregar fitxers a l'instant i al màxim de la vostra velocitat d'Internet.",
+ "real_debrid_invalid_token": "Invalida el testimoni de l'API",
"real_debrid_api_token_hint": "Pots obtenir la teva clau de l'API <0>aquí0>.",
- "save_changes": "Desa els canvis"
+ "real_debrid_free_account_error": "L'usuari \"{{username}}\" és un compte gratuït. Si us plau subscriu-te a Real-Debrid",
+ "real_debrid_linked_message": "Compte \"{{username}}\" vinculat",
+ "save_changes": "Desa els canvis",
+ "changes_saved": "Els canvis s'han desat correctament",
+ "download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
+ "validate_download_source": "Valida",
+ "remove_download_source": "Elimina",
+ "add_download_source": "Afegeix font",
+ "download_count_zero": "No hi ha baixades a la llista",
+ "download_count_one": "{{countFormatted}} a la llista de baixades",
+ "download_count_other": "{{countFormatted}} baixades a la llista",
+ "download_options_zero": "No hi ha cap descàrrega disponible",
+ "download_options_one": "{{countFormatted}} descàrrega disponible",
+ "download_options_other": "{{countFormatted}} baixades disponibles",
+ "download_source_url": "Descarrega l'URL de la font",
+ "add_download_source_description": "Inseriu la URL que conté el fitxer .json",
+ "download_source_up_to_date": "Actualitzat",
+ "download_source_errored": "S'ha produït un error",
+ "sync_download_sources": "Sincronitza fonts",
+ "removed_download_source": "S'ha eliminat la font de descàrrega",
+ "added_download_source": "Added download source",
+ "download_sources_synced": "Totes les fonts de descàrrega estan sincronitzades",
+ "insert_valid_json_url": "Insereix una URL JSON vàlida",
+ "found_download_option_zero": "No s'ha trobat cap opció de descàrrega",
+ "found_download_option_one": "S'ha trobat l'opció de baixada de {{countFormatted}}",
+ "found_download_option_other": "S'han trobat {{countFormatted}} opcions de baixada",
+ "import": "Import"
},
"notifications": {
"download_complete": "La baixada ha finalitzat",
"game_ready_to_install": "{{title}} ja es pot instal·lar",
"repack_list_updated": "S'ha actualitzat la llista de reempaquetats",
"repack_count_one": "S'ha afegit {{count}} reempaquetat",
- "repack_count_other": "S'han afegit {{count}} reempaquetats"
+ "repack_count_other": "S'han afegit {{count}} reempaquetats",
+ "new_update_available": "Versió {{version}} disponible",
+ "restart_to_install_update": "Reinicieu Hydra per instal·lar l'actualització"
},
"system_tray": {
"open": "Obre l'Hydra",
@@ -144,5 +218,39 @@
},
"modal": {
"close": "Botó de tancar"
+ },
+ "forms": {
+ "toggle_password_visibility": "Commuta la visibilitat de la contrasenya"
+ },
+ "user_profile": {
+ "amount_hours": "{{amount}} hores",
+ "amount_minutes": "{{amount}} minuts",
+ "last_time_played": "Última partida {{period}}",
+ "activity": "Activitat recent",
+ "library": "Biblioteca",
+ "total_play_time": "Temps total de joc:{{amount}}",
+ "no_recent_activity_title": "Hmmm… encara no res",
+ "no_recent_activity_description": "No has jugat a cap joc recentment. És el moment de canviar-ho!",
+ "display_name": "Nom de visualització",
+ "saving": "Desant",
+ "save": "Desa",
+ "edit_profile": "Edita el Perfil",
+ "saved_successfully": "S'ha desat correctament",
+ "try_again": "Siusplau torna-ho a provar",
+ "sign_out_modal_title": "Segur?",
+ "cancel": "Cancel·la",
+ "successfully_signed_out": "S'ha tancat la sessió correctament",
+ "sign_out": "Tanca sessió",
+ "playing_for": "Jugant per {{amount}}",
+ "sign_out_modal_text": "La vostra biblioteca està enllaçada amb el vostre compte actual. Quan tanqueu la sessió, la vostra biblioteca ja no serà visible i cap progrés no es desarà. Voleu continuar amb tancar la sessió?",
+ "add_friends": "Afegeix amics",
+ "add": "Afegeix",
+ "friend_code": "Codi de l'amic",
+ "see_profile": "Veure Perfil",
+ "sending": "Enviant",
+ "friend_request_sent": "Sol·licitud d'amistat enviada",
+ "friends": "Amistats",
+ "friends_list": "Llista d'amistats",
+ "user_not_found": "Usuari no trobat"
}
}
diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json
index d5cac8db..20b2df34 100644
--- a/src/locales/da/translation.json
+++ b/src/locales/da/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Dansk",
"home": {
"featured": "Anbefalet",
"trending": "Trender",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 3f1bcdcd..ae9c2712 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "English",
"app": {
"successfully_signed_in": "Successfully signed in"
},
@@ -174,12 +175,9 @@
"validate_download_source": "Validate",
"remove_download_source": "Remove",
"add_download_source": "Add source",
- "download_count_zero": "No downloads in list",
- "download_count_one": "{{countFormatted}} download in list",
- "download_count_other": "{{countFormatted}} downloads in list",
- "download_options_zero": "No download available",
- "download_options_one": "{{countFormatted}} download available",
- "download_options_other": "{{countFormatted}} downloads available",
+ "download_count_zero": "No download options",
+ "download_count_one": "{{countFormatted}} download option",
+ "download_count_other": "{{countFormatted}} download options",
"download_source_url": "Download source URL",
"add_download_source_description": "Insert the URL containing the .json file",
"download_source_up_to_date": "Up-to-date",
@@ -250,6 +248,30 @@
"friend_request_sent": "Friend request sent",
"friends": "Friends",
"friends_list": "Friends list",
- "user_not_found": "User not found"
+ "user_not_found": "User not found",
+ "block_user": "Block user",
+ "add_friend": "Add friend",
+ "request_sent": "Request sent",
+ "request_received": "Request received",
+ "accept_request": "Accept request",
+ "ignore_request": "Ignore request",
+ "cancel_request": "Cancel request",
+ "undo_friendship": "Undo friendship",
+ "request_accepted": "Request accepted",
+ "user_blocked_successfully": "User blocked successfully",
+ "user_block_modal_text": "This will block {{displayName}}",
+ "settings": "Settings",
+ "public": "Public",
+ "private": "Private",
+ "friends_only": "Friends only",
+ "privacy": "Privacy",
+ "blocked_users": "Blocked users",
+ "unblock": "Unblock",
+ "no_friends_added": "You still don't have added friends",
+ "pending": "Pending",
+ "no_pending_invites": "You have no pending invites",
+ "no_blocked_users": "You have no blocked users",
+ "friend_code_copied": "Friend code copied",
+ "undo_friendship_modal_text": "This will undo your friendship with {{displayName}}"
}
}
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index 5e016d34..f8fa12e4 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Español",
"app": {
"successfully_signed_in": "Sesión iniciada correctamente"
},
@@ -177,9 +178,6 @@
"download_count_zero": "No hay descargas en la lista",
"download_count_one": "{{countFormatted}} descarga en la lista",
"download_count_other": "{{countFormatted}} descargas en la lista",
- "download_options_zero": "No hay descargas disponibles",
- "download_options_one": "{{countFormatted}} descarga disponible",
- "download_options_other": "{{countFormatted}} descargas disponibles",
"download_source_url": "Descargar URL de origen",
"add_download_source_description": "Introduce la URL con el archivo .json",
"download_source_up_to_date": "Al día",
@@ -241,6 +239,38 @@
"successfully_signed_out": "Sesión cerrada exitosamente",
"sign_out": "Cerrar sesión",
"playing_for": "Jugando por {{amount}}",
- "sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?"
+ "sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?",
+ "add_friends": "Añadir amigos",
+ "add": "Añadir",
+ "friend_code": "Código de amigo",
+ "see_profile": "Ver perfil",
+ "sending": "Enviando",
+ "friend_request_sent": "Solicitud de amistad enviada",
+ "friends": "Amigos",
+ "friends_list": "Lista de amigos",
+ "user_not_found": "Usuario no encontrado",
+ "block_user": "Bloquear usuario",
+ "add_friend": "Añadir amigo",
+ "request_sent": "Solicitud enviada",
+ "request_received": "Solicitud recibida",
+ "accept_request": "Aceptar solicitud",
+ "ignore_request": "Ignorar solicitud",
+ "cancel_request": "Cancelar solicitud",
+ "undo_friendship": "Eliminar amistad",
+ "request_accepted": "Solicitud aceptada",
+ "user_blocked_successfully": "Usuario bloqueado exitosamente",
+ "user_block_modal_text": "Esto va a bloquear a {{displayName}}",
+ "settings": "Ajustes",
+ "public": "Público",
+ "private": "Privado",
+ "friends_only": "Solo Amigos",
+ "privacy": "Privacidad",
+ "blocked_users": "Usuarios bloqueados",
+ "unblock": "Desbloquear",
+ "no_friends_added": "Todavía no tienes amigos añadidos",
+ "pending": "Pendiente",
+ "no_pending_invites": "No tienes invitaciones pendientes",
+ "no_blocked_users": "No has bloqueado a ningún usuario",
+ "friend_code_copied": "Código de amigo copiado"
}
}
diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json
index 8629332f..2b8cd3fb 100644
--- a/src/locales/fa/translation.json
+++ b/src/locales/fa/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "فارسی",
"home": {
"featured": "پیشنهادی",
"trending": "پرطرفدار",
diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json
index c732b22c..f635f1de 100644
--- a/src/locales/fr/translation.json
+++ b/src/locales/fr/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Français",
"home": {
"featured": "En vedette",
"trending": "Tendance",
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index 748ffe28..f68d71bd 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Magyar",
"home": {
"featured": "Featured",
"trending": "Népszerű",
diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json
index bb7f452b..3d0f1edf 100644
--- a/src/locales/id/translation.json
+++ b/src/locales/id/translation.json
@@ -1,134 +1,256 @@
{
+ "language_name": "Bahasa Indonesia",
+ "app": {
+ "successfully_signed_in": "Berhasil masuk"
+ },
"home": {
"featured": "Unggulan",
- "trending": "Trending",
- "surprise_me": "Kejutkan Saya",
- "no_results": "Tidak ada hasil"
+ "trending": "Sedang Tren",
+ "surprise_me": "Kejutkan saya",
+ "no_results": "Tidak ada hasil ditemukan"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "Unduhan",
"settings": "Pengaturan",
- "my_library": "Koleksi saya",
+ "my_library": "Perpustakaan saya",
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
- "paused": "{{title}} (Terhenti)",
+ "paused": "{{title}} (Dijeda)",
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
- "filter": "Filter koleksi",
- "home": "Beranda"
+ "filter": "Filter perpustakaan",
+ "home": "Beranda",
+ "queued": "{{title}} (Antrian)",
+ "game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
+ "sign_in": "Masuk"
},
"header": {
- "search": "Pencarian",
+ "search": "Cari game",
"home": "Beranda",
"catalogue": "Katalog",
"downloads": "Unduhan",
"search_results": "Hasil pencarian",
- "settings": "Pengaturan"
+ "settings": "Pengaturan",
+ "version_available_install": "Versi {{version}} tersedia. Klik di sini untuk restart dan instal.",
+ "version_available_download": "Versi {{version}} tersedia. Klik di sini untuk unduh."
},
"bottom_panel": {
- "no_downloads_in_progress": "Tidak ada unduhan berjalan",
- "downloading_metadata": "Mengunduh metadata {{title}}...",
- "downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}"
+ "no_downloads_in_progress": "Tidak ada unduhan yang sedang berjalan",
+ "downloading_metadata": "Mengunduh metadata {{title}}…",
+ "downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Estimasi selesai {{eta}} - {{speed}}",
+ "calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…",
+ "checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)"
},
"catalogue": {
- "next_page": "Halaman berikutnya",
- "previous_page": "Halaman sebelumnya"
+ "next_page": "Halaman Berikutnya",
+ "previous_page": "Halaman Sebelumnya"
},
"game_details": {
"open_download_options": "Buka opsi unduhan",
"download_options_zero": "Tidak ada opsi unduhan",
"download_options_one": "{{count}} opsi unduhan",
"download_options_other": "{{count}} opsi unduhan",
- "updated_at": "Diperbarui {{updated_at}}",
- "install": "Install",
+ "updated_at": "Diperbarui pada {{updated_at}}",
+ "install": "Instal",
"resume": "Lanjutkan",
- "pause": "Hentikan sementara",
- "cancel": "Batalkan",
+ "pause": "Jeda",
+ "cancel": "Batal",
"remove": "Hapus",
- "space_left_on_disk": "{{space}} tersisa pada disk",
- "eta": "Perkiraan {{eta}}",
+ "space_left_on_disk": "{{space}} tersisa di disk",
+ "eta": "Estimasi {{eta}}",
+ "calculating_eta": "Menghitung waktu yang tersisa…",
"downloading_metadata": "Mengunduh metadata…",
- "filter": "Saring repacks",
- "requirements": "Keperluan sistem",
+ "filter": "Filter repack",
+ "requirements": "Persyaratan sistem",
"minimum": "Minimum",
- "recommended": "Rekomendasi",
+ "recommended": "Dianjurkan",
+ "paused": "Dijeda",
"release_date": "Dirilis pada {{date}}",
- "publisher": "Dipublikasikan oleh {{publisher}}",
+ "publisher": "Diterbitkan oleh {{publisher}}",
"hours": "jam",
"minutes": "menit",
"amount_hours": "{{amount}} jam",
"amount_minutes": "{{amount}} menit",
"accuracy": "{{accuracy}}% akurasi",
- "add_to_library": "Tambahkan ke koleksi",
- "remove_from_library": "Hapus dari koleksi",
- "no_downloads": "Tidak ada unduhan tersedia",
+ "add_to_library": "Tambah ke perpustakaan",
+ "remove_from_library": "Hapus dari perpustakaan",
+ "no_downloads": "Tidak ada yang bisa diunduh",
"play_time": "Dimainkan selama {{amount}}",
"last_time_played": "Terakhir dimainkan {{period}}",
"not_played_yet": "Kamu belum memainkan {{title}}",
- "next_suggestion": "Rekomendasi berikutnya",
- "play": "Mainkan",
+ "next_suggestion": "Saran berikutnya",
+ "play": "Main",
"deleting": "Menghapus installer…",
"close": "Tutup",
- "playing_now": "Memainkan sekarang",
+ "playing_now": "Sedang dimainkan",
"change": "Ubah",
- "repacks_modal_description": "Pilih repack yang kamu ingin unduh",
- "select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
- "download_now": "Unduh sekarang"
+ "repacks_modal_description": "Pilih repack yang ingin kamu unduh",
+ "select_folder_hint": "Untuk ganti folder default, buka <0>Pengaturan0>",
+ "download_now": "Unduh sekarang",
+ "no_shop_details": "Gagal mendapatkan detail toko.",
+ "download_options": "Opsi unduhan",
+ "download_path": "Path unduhan",
+ "previous_screenshot": "Screenshot sebelumnya",
+ "next_screenshot": "Screenshot berikutnya",
+ "screenshot": "Screenshot {{number}}",
+ "open_screenshot": "Buka screenshot {{number}}",
+ "download_settings": "Pengaturan unduhan",
+ "downloader": "Pengunduh",
+ "select_executable": "Pilih",
+ "no_executable_selected": "Tidak ada file eksekusi yang dipilih",
+ "open_folder": "Buka folder",
+ "open_download_location": "Lihat file yang diunduh",
+ "create_shortcut": "Buat pintasan desktop",
+ "remove_files": "Hapus file",
+ "remove_from_library_title": "Apa kamu yakin?",
+ "remove_from_library_description": "Ini akan menghapus {{game}} dari perpustakaan kamu",
+ "options": "Opsi",
+ "executable_section_title": "Eksekusi",
+ "executable_section_description": "Path file eksekusi saat \"Main\" diklik",
+ "downloads_secion_title": "Unduhan",
+ "downloads_section_description": "Cek update atau versi lain dari game ini",
+ "danger_zone_section_title": "Zona Berbahaya",
+ "danger_zone_section_description": "Hapus game ini dari perpustakaan kamu atau file yang diunduh oleh Hydra",
+ "download_in_progress": "Sedang mengunduh",
+ "download_paused": "Unduhan dijeda",
+ "last_downloaded_option": "Opsi terakhir diunduh",
+ "create_shortcut_success": "Pintasan berhasil dibuat",
+ "create_shortcut_error": "Gagal membuat pintasan"
},
"activation": {
- "title": "Aktivasi Hydra",
- "installation_id": "ID instalasi:",
- "enter_activation_code": "Masukkan kode aktivasi",
- "message": "Jika kamu tidak tau dimana bertanya untuk ini, maka kamu tidak seharusnya memiliki ini.",
+ "title": "Aktifkan Hydra",
+ "installation_id": "ID Instalasi:",
+ "enter_activation_code": "Masukkan kode aktivasi kamu",
+ "message": "Kalau tidak tahu harus tanya ke siapa, berarti kamu tidak perlu ini.",
"activate": "Aktifkan",
"loading": "Memuat…"
},
"downloads": {
"resume": "Lanjutkan",
- "pause": "Hentikan sementara",
- "eta": "Perkiraan {{eta}}",
- "paused": "Terhenti sementara",
- "verifying": "Memeriksa…",
+ "pause": "Jeda",
+ "eta": "Estimasi {{eta}}",
+ "paused": "Dijeda",
+ "verifying": "Verifikasi…",
"completed": "Selesai",
- "cancel": "Batalkan",
- "filter": "Saring game yang diunduh",
+ "removed": "Tidak diunduh",
+ "cancel": "Batal",
+ "filter": "Filter game yang diunduh",
"remove": "Hapus",
"downloading_metadata": "Mengunduh metadata…",
- "deleting": "Menghapus file instalasi…",
- "delete": "Hapus file instalasi",
- "delete_modal_title": "Kamu yakin?",
- "delete_modal_description": "Proses ini akan menghapus semua file instalasi dari komputer kamu",
- "install": "Install"
+ "deleting": "Menghapus installer…",
+ "delete": "Hapus installer",
+ "delete_modal_title": "Apa kamu yakin?",
+ "delete_modal_description": "Ini akan menghapus semua file instalasi dari komputer kamu",
+ "install": "Instal",
+ "download_in_progress": "Sedang berlangsung",
+ "queued_downloads": "Unduhan dalam antrian",
+ "downloads_completed": "Selesai",
+ "queued": "Dalam antrian",
+ "no_downloads_title": "Kosong",
+ "no_downloads_description": "Kamu belum mengunduh apa pun dengan Hydra, tapi belum terlambat untuk mulai.",
+ "checking_files": "Memeriksa file…"
},
"settings": {
- "downloads_path": "Lokasi unduhan",
- "change": "Perbarui",
- "notifications": "Pengingat",
+ "downloads_path": "Path unduhan",
+ "change": "Ganti",
+ "notifications": "Notifikasi",
"enable_download_notifications": "Saat unduhan selesai",
- "enable_repack_list_notifications": "Saat repack terbaru ditambahkan",
+ "enable_repack_list_notifications": "Saat ada repack baru",
+ "real_debrid_api_token_label": "Token API Real-Debrid",
+ "quit_app_instead_hiding": "Jangan sembunyikan Hydra saat ditutup",
+ "launch_with_system": "Jalankan Hydra saat sistem dinyalakan",
+ "general": "Umum",
"behavior": "Perilaku",
- "quit_app_instead_hiding": "Tutup aplikasi alih-alih menyembunyikan aplikasi",
- "launch_with_system": "Jalankan saat memulai sistem"
+ "download_sources": "Sumber unduhan",
+ "language": "Bahasa",
+ "real_debrid_api_token": "Token API",
+ "enable_real_debrid": "Aktifkan Real-Debrid",
+ "real_debrid_description": "Real-Debrid adalah downloader tanpa batas yang memungkinkan kamu untuk mengunduh file dengan cepat dan pada kecepatan terbaik dari Internet kamu.",
+ "real_debrid_invalid_token": "Token API tidak valid",
+ "real_debrid_api_token_hint": "Kamu bisa dapatkan token API di <0>sini0>",
+ "real_debrid_free_account_error": "Akun \"{{username}}\" adalah akun gratis. Silakan berlangganan Real-Debrid",
+ "real_debrid_linked_message": "Akun \"{{username}}\" terhubung",
+ "save_changes": "Simpan perubahan",
+ "changes_saved": "Perubahan disimpan berhasil",
+ "download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
+ "validate_download_source": "Validasi",
+ "remove_download_source": "Hapus",
+ "add_download_source": "Tambahkan sumber",
+ "download_count_zero": "Tidak ada unduhan dalam daftar",
+ "download_count_one": "{{countFormatted}} unduhan dalam daftar",
+ "download_count_other": "{{countFormatted}} unduhan dalam daftar",
+ "download_options_zero": "Tidak ada unduhan tersedia",
+ "download_options_one": "{{countFormatted}} unduhan tersedia",
+ "download_options_other": "{{countFormatted}} unduhan tersedia",
+ "download_source_url": "URL sumber unduhan",
+ "add_download_source_description": "Masukkan URL yang berisi file .json",
+ "download_source_up_to_date": "Terkini",
+ "download_source_errored": "Terjadi kesalahan",
+ "sync_download_sources": "Sinkronkan sumber",
+ "removed_download_source": "Sumber unduhan dihapus",
+ "added_download_source": "Sumber unduhan ditambahkan",
+ "download_sources_synced": "Semua sumber unduhan disinkronkan",
+ "insert_valid_json_url": "Masukkan URL JSON yang valid",
+ "found_download_option_zero": "Tidak ada opsi unduhan ditemukan",
+ "found_download_option_one": "Ditemukan {{countFormatted}} opsi unduhan",
+ "found_download_option_other": "Ditemukan {{countFormatted}} opsi unduhan",
+ "import": "Impor"
},
"notifications": {
"download_complete": "Unduhan selesai",
- "game_ready_to_install": "{{title}} sudah siap untuk instalasi",
+ "game_ready_to_install": "{{title}} siap untuk diinstal",
"repack_list_updated": "Daftar repack diperbarui",
"repack_count_one": "{{count}} repack ditambahkan",
- "repack_count_other": "{{count}} repack ditambahkan"
+ "repack_count_other": "{{count}} repack ditambahkan",
+ "new_update_available": "Versi {{version}} tersedia",
+ "restart_to_install_update": "Restart Hydra untuk instal pembaruan"
},
"system_tray": {
"open": "Buka Hydra",
- "quit": "Tutup"
+ "quit": "Keluar"
},
"game_card": {
- "no_downloads": "Tidak ada unduhan tersedia"
+ "no_downloads": "Tidak ada unduhan yang tersedia"
},
"binary_not_found_modal": {
- "title": "Program tidak terinstal",
- "description": "Wine atau Lutris exe tidak ditemukan pada sistem kamu",
- "instructions": "Periksa cara instalasi yang benar pada Linux distro-mu agar game dapat dimainkan dengan benar"
+ "title": "Program tidak terpasang",
+ "description": "Executable Wine atau Lutris tidak ditemukan di sistem kamu",
+ "instructions": "Cek cara instalasi yang benar di distro Linux kamu agar game bisa jalan normal"
},
"modal": {
- "close": "Tombol tutup"
+ "close": "Tutup"
+ },
+ "forms": {
+ "toggle_password_visibility": "Tampilkan/Sembunyikan kata sandi"
+ },
+ "user_profile": {
+ "amount_hours": "{{amount}} jam",
+ "amount_minutes": "{{amount}} menit",
+ "last_time_played": "Terakhir dimainkan {{period}}",
+ "activity": "Aktivitas terbaru",
+ "library": "Perpustakaan",
+ "total_play_time": "Total waktu bermain: {{amount}}",
+ "no_recent_activity_title": "Hmm… kosong di sini",
+ "no_recent_activity_description": "Kamu belum main game baru-baru ini. Yuk, mulai main!",
+ "display_name": "Nama tampilan",
+ "saving": "Menyimpan",
+ "save": "Simpan",
+ "edit_profile": "Edit Profil",
+ "saved_successfully": "Berhasil disimpan",
+ "try_again": "Coba lagi yuk",
+ "sign_out_modal_title": "Apa kamu yakin?",
+ "cancel": "Batal",
+ "successfully_signed_out": "Berhasil keluar",
+ "sign_out": "Keluar",
+ "playing_for": "Bermain selama {{amount}}",
+ "sign_out_modal_text": "Perpustakaan kamu terhubung dengan akun saat ini. Saat keluar, perpustakaan kamu tidak akan terlihat lagi, dan progres tidak akan disimpan. Lanjutkan keluar?",
+ "add_friends": "Tambah Teman",
+ "add": "Tambah",
+ "friend_code": "Kode teman",
+ "see_profile": "Lihat profil",
+ "sending": "Mengirim",
+ "friend_request_sent": "Permintaan teman terkirim",
+ "friends": "Teman",
+ "friends_list": "Daftar teman",
+ "user_not_found": "Pengguna tidak ditemukan"
}
}
diff --git a/src/locales/index.ts b/src/locales/index.ts
index 10f128ae..ea0783c2 100644
--- a/src/locales/index.ts
+++ b/src/locales/index.ts
@@ -1,21 +1,47 @@
-export { default as en } from "./en/translation.json";
-export { default as pt } from "./pt/translation.json";
-export { default as es } from "./es/translation.json";
-export { default as nl } from "./nl/translation.json";
-export { default as fr } from "./fr/translation.json";
-export { default as hu } from "./hu/translation.json";
-export { default as it } from "./it/translation.json";
-export { default as pl } from "./pl/translation.json";
-export { default as ru } from "./ru/translation.json";
-export { default as tr } from "./tr/translation.json";
-export { default as be } from "./be/translation.json";
-export { default as uk } from "./uk/translation.json";
-export { default as zh } from "./zh/translation.json";
-export { default as id } from "./id/translation.json";
-export { default as ko } from "./ko/translation.json";
-export { default as da } from "./da/translation.json";
-export { default as ar } from "./ar/translation.json";
-export { default as fa } from "./fa/translation.json";
-export { default as ro } from "./ro/translation.json";
-export { default as ca } from "./ca/translation.json";
-export { default as kk } from "./kk/translation.json";
+import en from "./en/translation.json";
+import ptPT from "./pt-PT/translation.json";
+import ptBR from "./pt-BR/translation.json";
+import es from "./es/translation.json";
+import nl from "./nl/translation.json";
+import fr from "./fr/translation.json";
+import hu from "./hu/translation.json";
+import it from "./it/translation.json";
+import pl from "./pl/translation.json";
+import ru from "./ru/translation.json";
+import tr from "./tr/translation.json";
+import be from "./be/translation.json";
+import uk from "./uk/translation.json";
+import zh from "./zh/translation.json";
+import id from "./id/translation.json";
+import ko from "./ko/translation.json";
+import da from "./da/translation.json";
+import ar from "./ar/translation.json";
+import fa from "./fa/translation.json";
+import ro from "./ro/translation.json";
+import ca from "./ca/translation.json";
+import kk from "./kk/translation.json";
+
+export default {
+ "pt-BR": ptBR,
+ "pt-PT": ptPT,
+ en,
+ es,
+ nl,
+ fr,
+ hu,
+ it,
+ pl,
+ ru,
+ tr,
+ be,
+ uk,
+ zh,
+ id,
+ ko,
+ da,
+ ar,
+ fa,
+ ro,
+ ca,
+ kk,
+};
diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json
index 55f21310..1d5145f9 100644
--- a/src/locales/it/translation.json
+++ b/src/locales/it/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Italiano",
"home": {
"featured": "In primo piano",
"trending": "Di tendenza",
diff --git a/src/locales/kk/translation.json b/src/locales/kk/translation.json
index d565e3b7..15683eb2 100644
--- a/src/locales/kk/translation.json
+++ b/src/locales/kk/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "қазақ тілі",
"app": {
"successfully_signed_in": "Сәтті кіру"
},
diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json
index 3d45bb88..933c7dde 100644
--- a/src/locales/ko/translation.json
+++ b/src/locales/ko/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "한국어",
"home": {
"featured": "추천",
"trending": "인기",
diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json
index 59cf13e6..6f02c9a3 100644
--- a/src/locales/nl/translation.json
+++ b/src/locales/nl/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Nederlands",
"home": {
"featured": "Uitgelicht",
"trending": "Trending",
diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json
index a8e9bdc7..5eb2c242 100644
--- a/src/locales/pl/translation.json
+++ b/src/locales/pl/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Polski",
"home": {
"featured": "Wyróżnione",
"trending": "Trendujące",
diff --git a/src/locales/pt/translation.json b/src/locales/pt-BR/translation.json
similarity index 89%
rename from src/locales/pt/translation.json
rename to src/locales/pt-BR/translation.json
index 568116f8..1adac376 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Português (Brasil)",
"app": {
"successfully_signed_in": "Autenticado com sucesso"
},
@@ -166,7 +167,7 @@
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
"save_changes": "Salvar mudanças",
"changes_saved": "Ajustes salvos com sucesso",
- "download_sources_description": "Hydra vai buscar links de download em todas as fonte habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
+ "download_sources_description": "Hydra vai buscar links de download em todas as fontes habilitadas. A URL da fonte deve ser um link direto para um arquivo .json contendo uma lista de links.",
"validate_download_source": "Validar",
"remove_download_source": "Remover",
"add_download_source": "Adicionar fonte",
@@ -250,6 +251,30 @@
"add": "Adicionar",
"sending": "Enviando",
"friends_list": "Lista de amigos",
- "user_not_found": "Usuário não encontrado"
+ "user_not_found": "Usuário 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": "Usuário bloqueado com sucesso",
+ "user_block_modal_text": "Bloquear {{displayName}}",
+ "settings": "Configurações",
+ "privacy": "Privacidade",
+ "private": "Privado",
+ "friends_only": "Apenas amigos",
+ "public": "Público",
+ "blocked_users": "Usuários bloqueados",
+ "unblock": "Desbloquear",
+ "no_friends_added": "Você ainda não possui amigos adicionados",
+ "pending": "Pendentes",
+ "no_pending_invites": "Você não possui convites de amizade pendentes",
+ "no_blocked_users": "Você não tem nenhum usuário bloqueado",
+ "friend_code_copied": "Código de amigo copiado",
+ "undo_friendship_modal_text": "Isso irá remover sua amizade com {{displayName}}"
}
}
diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json
new file mode 100644
index 00000000..67f99921
--- /dev/null
+++ b/src/locales/pt-PT/translation.json
@@ -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ções0>",
+ "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>aqui0>",
+ "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"
+ }
+}
diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json
index 2aed7a7f..9fab3119 100644
--- a/src/locales/ro/translation.json
+++ b/src/locales/ro/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Română",
"home": {
"featured": "Recomandate",
"trending": "Populare",
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index f6f18d11..be3a000e 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Русский",
"app": {
"successfully_signed_in": "Успешный вход"
},
@@ -177,9 +178,6 @@
"download_count_zero": "В списке нет загрузок",
"download_count_one": "{{countFormatted}} загрузка в списке",
"download_count_other": "{{countFormatted}} загрузок в списке",
- "download_options_zero": "Нет доступных загрузок",
- "download_options_one": "{{countFormatted}} вариант загрузки доступен",
- "download_options_other": "{{countFormatted}} вариантов загрузки доступно",
"download_source_url": "Ссылка на источник",
"add_download_source_description": "Вставьте ссылку на .json-файл",
"download_source_up_to_date": "Обновлён",
@@ -241,6 +239,38 @@
"successfully_signed_out": "Успешный выход из аккаунта",
"sign_out": "Выйти",
"playing_for": "Сыграно {{amount}}",
- "sign_out_modal_text": "Ваша библиотека связана с текущей учетной записью. При выходе из системы ваша библиотека станет недоступна, и прогресс не будет сохранен. Выйти?"
+ "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}} будет заблокирован",
+ "settings": "Настройки",
+ "public": "Публичный",
+ "private": "Приватный",
+ "friends_only": "Только друзья",
+ "privacy": "Приватность",
+ "blocked_users": "Заблокированные пользователи",
+ "unblock": "Разблокировать",
+ "no_friends_added": "Вы ещё не добавили ни одного друга",
+ "pending": "Ожидание",
+ "no_pending_invites": "У вас нет запросов ожидающих ответа",
+ "no_blocked_users": "Вы не заблокировали ни одного пользователя",
+ "friend_code_copied": "Код друга скопирован"
}
}
diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json
index 150eda84..2da9c977 100644
--- a/src/locales/tr/translation.json
+++ b/src/locales/tr/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Türkçe",
"home": {
"featured": "Öne çıkan",
"trending": "Popüler",
diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json
index 48dec3e4..bb840bc2 100644
--- a/src/locales/uk/translation.json
+++ b/src/locales/uk/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "Українська",
"app": {
"successfully_signed_in": "Успішний вхід в систему"
},
diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json
index e0284b8d..0c793172 100644
--- a/src/locales/zh/translation.json
+++ b/src/locales/zh/translation.json
@@ -1,4 +1,5 @@
{
+ "language_name": "中文",
"app": {
"successfully_signed_in": "已成功登录"
},
diff --git a/src/main/data-source.ts b/src/main/data-source.ts
index b47ce2c0..29c72f8c 100644
--- a/src/main/data-source.ts
+++ b/src/main/data-source.ts
@@ -6,32 +6,22 @@ import {
GameShopCache,
Repack,
UserPreferences,
+ UserAuth,
} from "@main/entity";
-import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
import { databasePath } from "./constants";
-import migrations from "./migrations";
-import { UserAuth } from "./entity/user-auth";
-export const createDataSource = (
- options: Partial
-) =>
- new DataSource({
- type: "better-sqlite3",
- entities: [
- Game,
- Repack,
- UserPreferences,
- GameShopCache,
- DownloadSource,
- DownloadQueue,
- UserAuth,
- ],
- synchronize: true,
- database: databasePath,
- ...options,
- });
-
-export const dataSource = createDataSource({
- migrations,
+export const dataSource = new DataSource({
+ type: "better-sqlite3",
+ entities: [
+ Game,
+ Repack,
+ UserPreferences,
+ GameShopCache,
+ DownloadSource,
+ DownloadQueue,
+ UserAuth,
+ ],
+ synchronize: false,
+ database: databasePath,
});
diff --git a/src/main/declaration.d.ts b/src/main/declaration.d.ts
deleted file mode 100644
index ac2675a3..00000000
--- a/src/main/declaration.d.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-declare module "aria2" {
- export type Aria2Status =
- | "active"
- | "waiting"
- | "paused"
- | "error"
- | "complete"
- | "removed";
-
- export interface StatusResponse {
- gid: string;
- status: Aria2Status;
- totalLength: string;
- completedLength: string;
- uploadLength: string;
- bitfield: string;
- downloadSpeed: string;
- uploadSpeed: string;
- infoHash?: string;
- numSeeders?: string;
- seeder?: boolean;
- pieceLength: string;
- numPieces: string;
- connections: string;
- errorCode?: string;
- errorMessage?: string;
- followedBy?: string[];
- following: string;
- belongsTo: string;
- dir: string;
- files: {
- path: string;
- length: string;
- completedLength: string;
- selected: string;
- }[];
- bittorrent?: {
- announceList: string[][];
- comment: string;
- creationDate: string;
- mode: "single" | "multi";
- info: {
- name: string;
- verifiedLength: string;
- verifyIntegrityPending: string;
- };
- };
- }
-
- export default class Aria2 {
- constructor(options: any);
- open: () => Promise;
- call(
- method: "addUri",
- uris: string[],
- options: { dir: string }
- ): Promise;
- call(
- method: "tellStatus",
- gid: string,
- keys?: string[]
- ): Promise;
- call(method: "pause", gid: string): Promise;
- call(method: "forcePause", gid: string): Promise;
- call(method: "unpause", gid: string): Promise;
- call(method: "remove", gid: string): Promise;
- call(method: "forceRemove", gid: string): Promise;
- call(method: "pauseAll"): Promise;
- call(method: "forcePauseAll"): Promise;
- listNotifications: () => [
- "onDownloadStart",
- "onDownloadPause",
- "onDownloadStop",
- "onDownloadComplete",
- "onDownloadError",
- "onBtDownloadComplete",
- ];
- on: (event: string, callback: (params: any) => void) => void;
- }
-}
diff --git a/src/main/entity/repack.entity.ts b/src/main/entity/repack.entity.ts
index 1d5259fd..36de2a7c 100644
--- a/src/main/entity/repack.entity.ts
+++ b/src/main/entity/repack.entity.ts
@@ -16,15 +16,12 @@ export class Repack {
@Column("text", { unique: true })
title: string;
+ /**
+ * @deprecated Use uris instead
+ */
@Column("text", { unique: true })
magnet: string;
- /**
- * @deprecated
- */
- @Column("int", { nullable: true })
- page: number;
-
@Column("text")
repacker: string;
@@ -37,6 +34,9 @@ export class Repack {
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
downloadSource: DownloadSource;
+ @Column("text", { default: "[]" })
+ uris: string;
+
@CreateDateColumn()
createdAt: Date;
diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts
index fe640b9d..9998c733 100644
--- a/src/main/events/auth/sign-out.ts
+++ b/src/main/events/auth/sign-out.ts
@@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
/* Disconnects libtorrent */
PythonInstance.killTorrent();
+ HydraApi.handleSignOut();
+
await Promise.all([
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),
diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts
index 8f24caad..b8565645 100644
--- a/src/main/events/download-sources/get-download-sources.ts
+++ b/src/main/events/download-sources/get-download-sources.ts
@@ -1,16 +1,11 @@
import { downloadSourceRepository } from "@main/repository";
import { registerEvent } from "../register-event";
-const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
- return downloadSourceRepository
- .createQueryBuilder("downloadSource")
- .leftJoin("downloadSource.repacks", "repacks")
- .orderBy("downloadSource.createdAt", "DESC")
- .loadRelationCountAndMap(
- "downloadSource.repackCount",
- "downloadSource.repacks"
- )
- .getMany();
-};
+const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
+ downloadSourceRepository.find({
+ order: {
+ createdAt: "DESC",
+ },
+ });
registerEvent("getDownloadSources", getDownloadSources);
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index dd5e3263..3963e4b0 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -43,16 +43,19 @@ import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-user";
+import "./user/get-user-blocks";
+import "./user/block-user";
+import "./user/unblock-user";
+import "./user/get-user-friends";
import "./profile/get-friend-requests";
import "./profile/get-me";
+import "./profile/undo-friendship";
import "./profile/update-friend-request";
import "./profile/update-profile";
import "./profile/send-friend-request";
+import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());
-ipcMain.handle(
- "isPortableVersion",
- () => process.env.PORTABLE_EXECUTABLE_FILE != null
-);
+ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts
index 468f5b26..a8fc8b01 100644
--- a/src/main/events/library/remove-game-from-library.ts
+++ b/src/main/events/library/remove-game-from-library.ts
@@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (game?.remoteId) {
- HydraApi.delete(`/games/${game.remoteId}`).catch(() => {});
+ HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}
};
diff --git a/src/main/events/profile/undo-friendship.ts b/src/main/events/profile/undo-friendship.ts
new file mode 100644
index 00000000..371bc5cc
--- /dev/null
+++ b/src/main/events/profile/undo-friendship.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+
+const undoFriendship = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string
+) => {
+ await HydraApi.delete(`/profile/friends/${userId}`);
+};
+
+registerEvent("undoFriendship", undoFriendship);
diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts
index 8620eaa1..50d2ab66 100644
--- a/src/main/events/profile/update-profile.ts
+++ b/src/main/events/profile/update-profile.ts
@@ -4,33 +4,22 @@ import axios from "axios";
import fs from "node:fs";
import path from "node:path";
import { fileTypeFromFile } from "file-type";
-import { UserProfile } from "@types";
+import { UpdateProfileProps, UserProfile } from "@types";
-const patchUserProfile = async (
- displayName: string,
- profileImageUrl?: string
-) => {
- if (profileImageUrl) {
- return HydraApi.patch("/profile", {
- displayName,
- profileImageUrl,
- });
- } else {
- return HydraApi.patch("/profile", {
- displayName,
- });
- }
+const patchUserProfile = async (updateProfile: UpdateProfileProps) => {
+ return HydraApi.patch("/profile", updateProfile);
};
const updateProfile = async (
_event: Electron.IpcMainInvokeEvent,
- displayName: string,
- newProfileImagePath: string | null
+ updateProfile: UpdateProfileProps
): Promise => {
- if (!newProfileImagePath) {
- return patchUserProfile(displayName);
+ if (!updateProfile.profileImageUrl) {
+ return patchUserProfile(updateProfile);
}
+ const newProfileImagePath = updateProfile.profileImageUrl;
+
const stats = fs.statSync(newProfileImagePath);
const fileBuffer = fs.readFileSync(newProfileImagePath);
const fileSizeInBytes = stats.size;
@@ -53,7 +42,7 @@ const updateProfile = async (
})
.catch(() => undefined);
- return patchUserProfile(displayName, profileImageUrl);
+ return patchUserProfile({ ...updateProfile, profileImageUrl });
};
registerEvent("updateProfile", updateProfile);
diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts
index cea41596..f4db999f 100644
--- a/src/main/events/torrenting/start-game-download.ts
+++ b/src/main/events/torrenting/start-game-download.ts
@@ -18,7 +18,8 @@ const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload
) => {
- const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
+ const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
+ payload;
const [game, repack] = await Promise.all([
gameRepository.findOne({
@@ -54,7 +55,7 @@ const startGameDownload = async (
bytesDownloaded: 0,
downloadPath,
downloader,
- uri: repack.magnet,
+ uri,
isDeleted: false,
}
);
@@ -76,7 +77,7 @@ const startGameDownload = async (
shop,
status: "active",
downloadPath,
- uri: repack.magnet,
+ uri,
})
.then((result) => {
if (iconUrl) {
@@ -100,6 +101,7 @@ const startGameDownload = async (
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
+ await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!);
};
diff --git a/src/main/events/user/block-user.ts b/src/main/events/user/block-user.ts
new file mode 100644
index 00000000..c81231e5
--- /dev/null
+++ b/src/main/events/user/block-user.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+
+const blockUser = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string
+) => {
+ await HydraApi.post(`/users/${userId}/block`);
+};
+
+registerEvent("blockUser", blockUser);
diff --git a/src/main/events/user/get-user-blocks.ts b/src/main/events/user/get-user-blocks.ts
new file mode 100644
index 00000000..65bb3eb4
--- /dev/null
+++ b/src/main/events/user/get-user-blocks.ts
@@ -0,0 +1,13 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { UserBlocks } from "@types";
+
+export const getUserBlocks = async (
+ _event: Electron.IpcMainInvokeEvent,
+ take: number,
+ skip: number
+): Promise => {
+ return HydraApi.get(`/profile/blocks`, { take, skip });
+};
+
+registerEvent("getUserBlocks", getUserBlocks);
diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts
new file mode 100644
index 00000000..5ff4c8a4
--- /dev/null
+++ b/src/main/events/user/get-user-friends.ts
@@ -0,0 +1,29 @@
+import { userAuthRepository } from "@main/repository";
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { UserFriends } from "@types";
+
+export const getUserFriends = async (
+ userId: string,
+ take: number,
+ skip: number
+): Promise => {
+ const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
+
+ if (loggedUser?.userId === userId) {
+ return HydraApi.get(`/profile/friends`, { take, skip });
+ }
+
+ return HydraApi.get(`/users/${userId}/friends`, { take, skip });
+};
+
+const getUserFriendsEvent = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string,
+ take: number,
+ skip: number
+) => {
+ return getUserFriends(userId, take, skip);
+};
+
+registerEvent("getUserFriends", getUserFriendsEvent);
diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts
index 7b4c0aa8..68d69969 100644
--- a/src/main/events/user/get-user.ts
+++ b/src/main/events/user/get-user.ts
@@ -1,55 +1,76 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { steamGamesWorker } from "@main/workers";
-import { UserProfile } from "@types";
+import { GameRunning, UserGame, UserProfile } from "@types";
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
import { getSteamAppAsset } from "@main/helpers";
+import { getUserFriends } from "./get-user-friends";
const getUser = async (
_event: Electron.IpcMainInvokeEvent,
userId: string
): Promise => {
try {
- const profile = await HydraApi.get(`/user/${userId}`);
+ const [profile, friends] = await Promise.all([
+ HydraApi.get(`/users/${userId}`),
+ getUserFriends(userId, 12, 0).catch(() => {
+ return { totalFriends: 0, friends: [] };
+ }),
+ ]);
const recentGames = await Promise.all(
profile.recentGames.map(async (game) => {
- const steamGame = await steamGamesWorker.run(Number(game.objectId), {
- name: "getById",
- });
- const iconUrl = steamGame?.clientIcon
- ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
- : null;
-
- return {
- ...game,
- ...convertSteamGameToCatalogueEntry(steamGame),
- iconUrl,
- };
+ return getSteamUserGame(game);
})
);
const libraryGames = await Promise.all(
profile.libraryGames.map(async (game) => {
- const steamGame = await steamGamesWorker.run(Number(game.objectId), {
- name: "getById",
- });
- const iconUrl = steamGame?.clientIcon
- ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
- : null;
-
- return {
- ...game,
- ...convertSteamGameToCatalogueEntry(steamGame),
- iconUrl,
- };
+ return getSteamUserGame(game);
})
);
- return { ...profile, libraryGames, recentGames };
+ const currentGame = await getGameRunning(profile.currentGame);
+
+ return {
+ ...profile,
+ libraryGames,
+ recentGames,
+ friends: friends.friends,
+ totalFriends: friends.totalFriends,
+ currentGame,
+ };
} catch (err) {
return null;
}
};
+const getGameRunning = async (currentGame): Promise => {
+ if (!currentGame) {
+ return null;
+ }
+
+ const gameRunning = await getSteamUserGame(currentGame);
+
+ return {
+ ...gameRunning,
+ sessionDurationInMillis: currentGame.sessionDurationInSeconds * 1000,
+ };
+};
+
+const getSteamUserGame = async (game): Promise => {
+ const steamGame = await steamGamesWorker.run(Number(game.objectId), {
+ name: "getById",
+ });
+ const iconUrl = steamGame?.clientIcon
+ ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
+ : null;
+
+ return {
+ ...game,
+ ...convertSteamGameToCatalogueEntry(steamGame),
+ iconUrl,
+ };
+};
+
registerEvent("getUser", getUser);
diff --git a/src/main/events/user/unblock-user.ts b/src/main/events/user/unblock-user.ts
new file mode 100644
index 00000000..c604a0b5
--- /dev/null
+++ b/src/main/events/user/unblock-user.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+
+const unblockUser = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string
+) => {
+ await HydraApi.post(`/users/${userId}/unblock`);
+};
+
+registerEvent("unblockUser", unblockUser);
diff --git a/src/main/helpers/download-source.ts b/src/main/helpers/download-source.ts
index 012a4d24..c216212a 100644
--- a/src/main/helpers/download-source.ts
+++ b/src/main/helpers/download-source.ts
@@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
const repacks: QueryDeepPartialEntity[] = downloads.map(
(download) => ({
title: download.title,
- magnet: download.uris[0],
+ uris: JSON.stringify(download.uris),
+ magnet: download.uris[0]!,
fileSize: download.fileSize,
repacker: downloadSource.name,
uploadDate: download.uploadDate,
diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts
index 902b927d..b0ff391f 100644
--- a/src/main/helpers/index.ts
+++ b/src/main/helpers/index.ts
@@ -1,4 +1,5 @@
import axios from "axios";
+import { JSDOM } from "jsdom";
import UserAgent from "user-agents";
export const getSteamAppAsset = (
@@ -48,13 +49,19 @@ export const sleep = (ms: number) =>
export const requestWebPage = async (url: string) => {
const userAgent = new UserAgent();
- return axios
+ const data = await axios
.get(url, {
headers: {
"User-Agent": userAgent.toString(),
},
})
.then((response) => response.data);
+
+ const { window } = new JSDOM(data);
+ return window.document;
};
+export const isPortableVersion = () =>
+ process.env.PORTABLE_EXECUTABLE_FILE != null;
+
export * from "./download-source";
diff --git a/src/main/index.ts b/src/main/index.ts
index 9ff74bf6..3c5cc254 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -7,8 +7,9 @@ import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, PythonInstance, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source";
-import * as resources from "@locales";
+import resources from "@locales";
import { userPreferencesRepository } from "@main/repository";
+import { knexClient, migrationConfig } from "./knex-client";
const { autoUpdater } = updater;
@@ -20,8 +21,6 @@ autoUpdater.setFeedURL({
autoUpdater.logger = logger;
-logger.log("Init Hydra");
-
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -54,6 +53,18 @@ if (process.defaultApp) {
app.setAsDefaultProtocolClient(PROTOCOL);
}
+const runMigrations = async () => {
+ await knexClient.migrate.list(migrationConfig).then((result) => {
+ logger.log(
+ "Migrations to run:",
+ result[1].map((migration) => migration.name)
+ );
+ });
+
+ await knexClient.migrate.latest(migrationConfig);
+ await knexClient.destroy();
+};
+
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
@@ -65,8 +76,15 @@ app.whenReady().then(async () => {
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
});
+ await runMigrations()
+ .then(() => {
+ logger.log("Migrations executed successfully");
+ })
+ .catch((err) => {
+ logger.log("Migrations failed to run:", err);
+ });
+
await dataSource.initialize();
- await dataSource.runMigrations();
await import("./main");
@@ -88,10 +106,15 @@ app.on("browser-window-created", (_, window) => {
const handleDeepLinkPath = (uri?: string) => {
if (!uri) return;
- const url = new URL(uri);
- if (url.host === "install-source") {
- WindowManager.redirect(`settings${url.search}`);
+ try {
+ const url = new URL(uri);
+
+ if (url.host === "install-source") {
+ WindowManager.redirect(`settings${url.search}`);
+ }
+ } catch (error) {
+ logger.error("Error handling deep link", uri, error);
}
};
@@ -123,7 +146,6 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonInstance.kill();
- logger.log("Quit Hydra");
});
app.on("activate", () => {
diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts
new file mode 100644
index 00000000..031760f6
--- /dev/null
+++ b/src/main/knex-client.ts
@@ -0,0 +1,29 @@
+import knex, { Knex } from "knex";
+import { databasePath } from "./constants";
+import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
+import { RepackUris } from "./migrations/20240830143906_RepackUris";
+
+export type HydraMigration = Knex.Migration & { name: string };
+
+class MigrationSource implements Knex.MigrationSource {
+ getMigrations(): Promise {
+ return Promise.resolve([Hydra2_0_3, RepackUris]);
+ }
+ getMigrationName(migration: HydraMigration): string {
+ return migration.name;
+ }
+ getMigration(migration: HydraMigration): Promise {
+ return Promise.resolve(migration);
+ }
+}
+
+export const knexClient = knex({
+ client: "better-sqlite3",
+ connection: {
+ filename: databasePath,
+ },
+});
+
+export const migrationConfig: Knex.MigratorConfig = {
+ migrationSource: new MigrationSource(),
+};
diff --git a/src/main/knexfile.ts b/src/main/knexfile.ts
new file mode 100644
index 00000000..df7972a9
--- /dev/null
+++ b/src/main/knexfile.ts
@@ -0,0 +1,10 @@
+const config = {
+ development: {
+ migrations: {
+ extension: "ts",
+ stub: "migrations/migration.stub",
+ },
+ },
+};
+
+export default config;
diff --git a/src/main/main.ts b/src/main/main.ts
index fbabc56c..af594e20 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");
- if (userPreferences?.realDebridApiToken)
+ if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
+ }
HydraApi.setupApi().then(() => {
uploadGamesBatch();
diff --git a/src/main/migrations/1715900413313-fix_repack_uploadDate.ts b/src/main/migrations/1715900413313-fix_repack_uploadDate.ts
deleted file mode 100644
index e9d0a6c2..00000000
--- a/src/main/migrations/1715900413313-fix_repack_uploadDate.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { MigrationInterface, QueryRunner } from "typeorm";
-
-export class FixRepackUploadDate1715900413313 implements MigrationInterface {
- public async up(_: QueryRunner): Promise {
- return;
- }
-
- public async down(_: QueryRunner): Promise {
- return;
- }
-}
diff --git a/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts b/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts
deleted file mode 100644
index 6a562915..00000000
--- a/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Game } from "@main/entity";
-import { MigrationInterface, QueryRunner } from "typeorm";
-
-export class AlterLastTimePlayedToDatime1716776027208
- implements MigrationInterface
-{
- public async up(queryRunner: QueryRunner): Promise {
- // 2024-05-27 02:08:17
- // Mon, 27 May 2024 02:08:17 GMT
- const updateLastTimePlayedValues = `
- UPDATE game SET lastTimePlayed = (SELECT
- SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year
- CASE SUBSTR(lastTimePlayed, 9, 3)
- WHEN 'Jan' THEN '01'
- WHEN 'Feb' THEN '02'
- WHEN 'Mar' THEN '03'
- WHEN 'Apr' THEN '04'
- WHEN 'May' THEN '05'
- WHEN 'Jun' THEN '06'
- WHEN 'Jul' THEN '07'
- WHEN 'Aug' THEN '08'
- WHEN 'Sep' THEN '09'
- WHEN 'Oct' THEN '10'
- WHEN 'Nov' THEN '11'
- WHEN 'Dec' THEN '12'
- END || '-' || -- Month
- SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day
- SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss;
- FROM game)
- WHERE lastTimePlayed IS NOT NULL;
- `;
-
- await queryRunner.query(updateLastTimePlayedValues);
- }
-
- public async down(queryRunner: QueryRunner): Promise {
- const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game");
-
- const result = await queryBuilder.getMany();
-
- for (const game of result) {
- if (!game.lastTimePlayed) continue;
- await queryRunner.query(
- `UPDATE game set lastTimePlayed = ? WHERE id = ?;`,
- [game.lastTimePlayed.toUTCString(), game.id]
- );
- }
- }
-}
diff --git a/src/main/migrations/20240830143811_Hydra_2_0_3.ts b/src/main/migrations/20240830143811_Hydra_2_0_3.ts
new file mode 100644
index 00000000..6013f714
--- /dev/null
+++ b/src/main/migrations/20240830143811_Hydra_2_0_3.ts
@@ -0,0 +1,171 @@
+import type { HydraMigration } from "@main/knex-client";
+import type { Knex } from "knex";
+
+export const Hydra2_0_3: HydraMigration = {
+ name: "Hydra_2_0_3",
+ up: async (knex: Knex) => {
+ const timestamp = new Date().getTime();
+
+ await knex.schema.hasTable("migrations").then(async (exists) => {
+ if (exists) {
+ await knex.schema.dropTable("migrations");
+ }
+ });
+
+ await knex.schema.hasTable("download_source").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("download_source", (table) => {
+ table.increments("id").primary();
+ table
+ .text("url")
+ .unique({ indexName: "download_source_url_unique_" + timestamp });
+ table.text("name").notNullable();
+ table.text("etag");
+ table.integer("downloadCount").notNullable().defaultTo(0);
+ table.text("status").notNullable().defaultTo(0);
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ });
+ }
+ });
+
+ await knex.schema.hasTable("repack").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("repack", (table) => {
+ table.increments("id").primary();
+ table
+ .text("title")
+ .notNullable()
+ .unique({ indexName: "repack_title_unique_" + timestamp });
+ table
+ .text("magnet")
+ .notNullable()
+ .unique({ indexName: "repack_magnet_unique_" + timestamp });
+ table.integer("page");
+ table.text("repacker").notNullable();
+ table.text("fileSize").notNullable();
+ table.datetime("uploadDate").notNullable();
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ table
+ .integer("downloadSourceId")
+ .references("download_source.id")
+ .onDelete("CASCADE");
+ });
+ }
+ });
+
+ await knex.schema.hasTable("game").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("game", (table) => {
+ table.increments("id").primary();
+ table
+ .text("objectID")
+ .notNullable()
+ .unique({ indexName: "game_objectID_unique_" + timestamp });
+ table
+ .text("remoteId")
+ .unique({ indexName: "game_remoteId_unique_" + timestamp });
+ table.text("title").notNullable();
+ table.text("iconUrl");
+ table.text("folderName");
+ table.text("downloadPath");
+ table.text("executablePath");
+ table.integer("playTimeInMilliseconds").notNullable().defaultTo(0);
+ table.text("shop").notNullable();
+ table.text("status");
+ table.integer("downloader").notNullable().defaultTo(1);
+ table.float("progress").notNullable().defaultTo(0);
+ table.integer("bytesDownloaded").notNullable().defaultTo(0);
+ table.datetime("lastTimePlayed");
+ table.float("fileSize").notNullable().defaultTo(0);
+ table.text("uri");
+ table.boolean("isDeleted").notNullable().defaultTo(0);
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ table
+ .integer("repackId")
+ .references("repack.id")
+ .unique("repack_repackId_unique_" + timestamp);
+ });
+ }
+ });
+
+ await knex.schema.hasTable("user_preferences").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("user_preferences", (table) => {
+ table.increments("id").primary();
+ table.text("downloadsPath");
+ table.text("language").notNullable().defaultTo("en");
+ table.text("realDebridApiToken");
+ table
+ .boolean("downloadNotificationsEnabled")
+ .notNullable()
+ .defaultTo(0);
+ table
+ .boolean("repackUpdatesNotificationsEnabled")
+ .notNullable()
+ .defaultTo(0);
+ table.boolean("preferQuitInsteadOfHiding").notNullable().defaultTo(0);
+ table.boolean("runAtStartup").notNullable().defaultTo(0);
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ });
+ }
+ });
+
+ await knex.schema.hasTable("game_shop_cache").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("game_shop_cache", (table) => {
+ table.text("objectID").primary().notNullable();
+ table.text("shop").notNullable();
+ table.text("serializedData");
+ table.text("howLongToBeatSerializedData");
+ table.text("language");
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ });
+ }
+ });
+
+ await knex.schema.hasTable("download_queue").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("download_queue", (table) => {
+ table.increments("id").primary();
+ table
+ .integer("gameId")
+ .references("game.id")
+ .unique("download_queue_gameId_unique_" + timestamp);
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ });
+ }
+ });
+
+ await knex.schema.hasTable("user_auth").then(async (exists) => {
+ if (!exists) {
+ await knex.schema.createTable("user_auth", (table) => {
+ table.increments("id").primary();
+ table.text("userId").notNullable().defaultTo("");
+ table.text("displayName").notNullable().defaultTo("");
+ table.text("profileImageUrl");
+ table.text("accessToken").notNullable().defaultTo("");
+ table.text("refreshToken").notNullable().defaultTo("");
+ table.integer("tokenExpirationTimestamp").notNullable().defaultTo(0);
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ });
+ }
+ });
+ },
+
+ down: async (knex: Knex) => {
+ await knex.schema.dropTableIfExists("game");
+ await knex.schema.dropTableIfExists("repack");
+ await knex.schema.dropTableIfExists("download_queue");
+ await knex.schema.dropTableIfExists("user_auth");
+ await knex.schema.dropTableIfExists("game_shop_cache");
+ await knex.schema.dropTableIfExists("user_preferences");
+ await knex.schema.dropTableIfExists("download_source");
+ },
+};
diff --git a/src/main/migrations/20240830143906_RepackUris.ts b/src/main/migrations/20240830143906_RepackUris.ts
new file mode 100644
index 00000000..0785d50d
--- /dev/null
+++ b/src/main/migrations/20240830143906_RepackUris.ts
@@ -0,0 +1,58 @@
+import type { HydraMigration } from "@main/knex-client";
+import type { Knex } from "knex";
+
+export const RepackUris: HydraMigration = {
+ name: "RepackUris",
+ up: async (knex: Knex) => {
+ await knex.schema.createTable("temporary_repack", (table) => {
+ const timestamp = new Date().getTime();
+ table.increments("id").primary();
+ table
+ .text("title")
+ .notNullable()
+ .unique({ indexName: "repack_title_unique_" + timestamp });
+ table
+ .text("magnet")
+ .notNullable()
+ .unique({ indexName: "repack_magnet_unique_" + timestamp });
+ table.text("repacker").notNullable();
+ table.text("fileSize").notNullable();
+ table.datetime("uploadDate").notNullable();
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ table
+ .integer("downloadSourceId")
+ .references("download_source.id")
+ .onDelete("CASCADE");
+ table.text("uris").notNullable().defaultTo("[]");
+ });
+ await knex.raw(
+ `INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`
+ );
+ await knex.schema.dropTable("repack");
+ await knex.schema.renameTable("temporary_repack", "repack");
+ },
+
+ down: async (knex: Knex) => {
+ await knex.schema.renameTable("repack", "temporary_repack");
+ await knex.schema.createTable("repack", (table) => {
+ table.increments("id").primary();
+ table.text("title").notNullable().unique();
+ table.text("magnet").notNullable().unique();
+ table.integer("page");
+ table.text("repacker").notNullable();
+ table.text("fileSize").notNullable();
+ table.datetime("uploadDate").notNullable();
+ table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
+ table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
+ table
+ .integer("downloadSourceId")
+ .references("download_source.id")
+ .onDelete("CASCADE");
+ });
+ await knex.raw(
+ `INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`
+ );
+ await knex.schema.dropTable("temporary_repack");
+ },
+};
diff --git a/src/main/migrations/index.ts b/src/main/migrations/index.ts
deleted file mode 100644
index c0c96e45..00000000
--- a/src/main/migrations/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate";
-import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime";
-
-export default [
- FixRepackUploadDate1715900413313,
- AlterLastTimePlayedToDatime1716776027208,
-];
diff --git a/src/main/migrations/migration.stub b/src/main/migrations/migration.stub
new file mode 100644
index 00000000..9cb0cbab
--- /dev/null
+++ b/src/main/migrations/migration.stub
@@ -0,0 +1,11 @@
+import type { HydraMigration } from "@main/knex-client";
+import type { Knex } from "knex";
+
+export const MigrationName: HydraMigration = {
+ name: "MigrationName",
+ up: async (knex: Knex) => {
+ await knex.schema.createTable("table_name", (table) => {});
+ },
+
+ down: async (knex: Knex) => {},
+};
diff --git a/src/main/services/aria2c.ts b/src/main/services/aria2c.ts
deleted file mode 100644
index b1b1da76..00000000
--- a/src/main/services/aria2c.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import path from "node:path";
-import { spawn } from "node:child_process";
-import { app } from "electron";
-
-export const startAria2 = () => {
- const binaryPath = app.isPackaged
- ? path.join(process.resourcesPath, "aria2", "aria2c")
- : path.join(__dirname, "..", "..", "aria2", "aria2c");
-
- return spawn(
- binaryPath,
- [
- "--enable-rpc",
- "--rpc-listen-all",
- "--file-allocation=none",
- "--allow-overwrite=true",
- ],
- { stdio: "inherit", windowsHide: true }
- );
-};
diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts
index 31f28992..d4733a32 100644
--- a/src/main/services/download/download-manager.ts
+++ b/src/main/services/download/download-manager.ts
@@ -6,6 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types";
+import { GofileApi, QiwiApi } from "../hosters";
+import { GenericHttpDownloader } from "./generic-http-downloader";
export class DownloadManager {
private static currentDownloader: Downloader | null = null;
@@ -13,10 +15,12 @@ export class DownloadManager {
public static async watchDownloads() {
let status: DownloadProgress | null = null;
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ status = await PythonInstance.getStatus();
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus();
} else {
- status = await PythonInstance.getStatus();
+ status = await GenericHttpDownloader.getStatus();
}
if (status) {
@@ -62,10 +66,12 @@ export class DownloadManager {
}
static async pauseDownload() {
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ await PythonInstance.pauseDownload();
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload();
} else {
- await PythonInstance.pauseDownload();
+ await GenericHttpDownloader.pauseDownload();
}
WindowManager.mainWindow?.setProgressBar(-1);
@@ -73,20 +79,16 @@ export class DownloadManager {
}
static async resumeDownload(game: Game) {
- if (game.downloader === Downloader.RealDebrid) {
- RealDebridDownloader.startDownload(game);
- this.currentDownloader = Downloader.RealDebrid;
- } else {
- PythonInstance.startDownload(game);
- this.currentDownloader = Downloader.Torrent;
- }
+ return this.startDownload(game);
}
static async cancelDownload(gameId: number) {
- if (this.currentDownloader === Downloader.RealDebrid) {
+ if (this.currentDownloader === Downloader.Torrent) {
+ PythonInstance.cancelDownload(gameId);
+ } else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId);
} else {
- PythonInstance.cancelDownload(gameId);
+ GenericHttpDownloader.cancelDownload(gameId);
}
WindowManager.mainWindow?.setProgressBar(-1);
@@ -94,12 +96,40 @@ export class DownloadManager {
}
static async startDownload(game: Game) {
- if (game.downloader === Downloader.RealDebrid) {
- RealDebridDownloader.startDownload(game);
- this.currentDownloader = Downloader.RealDebrid;
- } else {
- PythonInstance.startDownload(game);
- this.currentDownloader = Downloader.Torrent;
+ switch (game.downloader) {
+ case Downloader.Gofile: {
+ const id = game!.uri!.split("/").pop();
+
+ const token = await GofileApi.authorize();
+ const downloadLink = await GofileApi.getDownloadLink(id!);
+
+ GenericHttpDownloader.startDownload(game, downloadLink, {
+ Cookie: `accountToken=${token}`,
+ });
+ break;
+ }
+ case Downloader.PixelDrain: {
+ const id = game!.uri!.split("/").pop();
+
+ await GenericHttpDownloader.startDownload(
+ game,
+ `https://pixeldrain.com/api/file/${id}?download`
+ );
+ break;
+ }
+ case Downloader.Qiwi: {
+ const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
+
+ await GenericHttpDownloader.startDownload(game, downloadUrl);
+ break;
+ }
+ case Downloader.Torrent:
+ PythonInstance.startDownload(game);
+ break;
+ case Downloader.RealDebrid:
+ RealDebridDownloader.startDownload(game);
}
+
+ this.currentDownloader = game.downloader;
}
}
diff --git a/src/main/services/download/generic-http-downloader.ts b/src/main/services/download/generic-http-downloader.ts
new file mode 100644
index 00000000..055c8561
--- /dev/null
+++ b/src/main/services/download/generic-http-downloader.ts
@@ -0,0 +1,109 @@
+import { Game } from "@main/entity";
+import { gameRepository } from "@main/repository";
+import { calculateETA } from "./helpers";
+import { DownloadProgress } from "@types";
+import { HttpDownload } from "./http-download";
+
+export class GenericHttpDownloader {
+ public static downloads = new Map();
+ public static downloadingGame: Game | null = null;
+
+ public static async getStatus() {
+ if (this.downloadingGame) {
+ const download = this.downloads.get(this.downloadingGame.id)!;
+ const status = download.getStatus();
+
+ if (status) {
+ const progress =
+ Number(status.completedLength) / Number(status.totalLength);
+
+ await gameRepository.update(
+ { id: this.downloadingGame!.id },
+ {
+ bytesDownloaded: Number(status.completedLength),
+ fileSize: Number(status.totalLength),
+ progress,
+ status: "active",
+ folderName: status.folderName,
+ }
+ );
+
+ const result = {
+ numPeers: 0,
+ numSeeds: 0,
+ downloadSpeed: status.downloadSpeed,
+ timeRemaining: calculateETA(
+ status.totalLength,
+ status.completedLength,
+ status.downloadSpeed
+ ),
+ isDownloadingMetadata: false,
+ isCheckingFiles: false,
+ progress,
+ gameId: this.downloadingGame!.id,
+ } as DownloadProgress;
+
+ if (progress === 1) {
+ this.downloads.delete(this.downloadingGame.id);
+ this.downloadingGame = null;
+ }
+
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ static async pauseDownload() {
+ if (this.downloadingGame) {
+ const httpDownload = this.downloads.get(this.downloadingGame!.id!);
+
+ if (httpDownload) {
+ await httpDownload.pauseDownload();
+ }
+
+ this.downloadingGame = null;
+ }
+ }
+
+ static async startDownload(
+ game: Game,
+ downloadUrl: string,
+ headers?: Record
+ ) {
+ this.downloadingGame = game;
+
+ if (this.downloads.has(game.id)) {
+ await this.resumeDownload(game.id!);
+ return;
+ }
+
+ const httpDownload = new HttpDownload(
+ game.downloadPath!,
+ downloadUrl,
+ headers
+ );
+
+ httpDownload.startDownload();
+
+ this.downloads.set(game.id!, httpDownload);
+ }
+
+ static async cancelDownload(gameId: number) {
+ const httpDownload = this.downloads.get(gameId);
+
+ if (httpDownload) {
+ await httpDownload.cancelDownload();
+ this.downloads.delete(gameId);
+ }
+ }
+
+ static async resumeDownload(gameId: number) {
+ const httpDownload = this.downloads.get(gameId);
+
+ if (httpDownload) {
+ await httpDownload.resumeDownload();
+ }
+ }
+}
diff --git a/src/main/services/download/http-download.ts b/src/main/services/download/http-download.ts
index 4553a6cb..4f6c31a9 100644
--- a/src/main/services/download/http-download.ts
+++ b/src/main/services/download/http-download.ts
@@ -1,68 +1,54 @@
-import type { ChildProcess } from "node:child_process";
-import { logger } from "../logger";
-import { sleep } from "@main/helpers";
-import { startAria2 } from "../aria2c";
-import Aria2 from "aria2";
+import { WindowManager } from "../window-manager";
+import path from "node:path";
export class HttpDownload {
- private static connected = false;
- private static aria2c: ChildProcess | null = null;
+ private downloadItem: Electron.DownloadItem;
- private static aria2 = new Aria2({});
+ constructor(
+ private downloadPath: string,
+ private downloadUrl: string,
+ private headers?: Record
+ ) {}
- private static async connect() {
- this.aria2c = startAria2();
-
- let retries = 0;
-
- while (retries < 4 && !this.connected) {
- try {
- await this.aria2.open();
- logger.log("Connected to aria2");
-
- this.connected = true;
- } catch (err) {
- await sleep(100);
- logger.log("Failed to connect to aria2, retrying...");
- retries++;
- }
- }
- }
-
- public static getStatus(gid: string) {
- if (this.connected) {
- return this.aria2.call("tellStatus", gid);
- }
-
- return null;
- }
-
- public static disconnect() {
- if (this.aria2c) {
- this.aria2c.kill();
- this.connected = false;
- }
- }
-
- static async cancelDownload(gid: string) {
- await this.aria2.call("forceRemove", gid);
- }
-
- static async pauseDownload(gid: string) {
- await this.aria2.call("forcePause", gid);
- }
-
- static async resumeDownload(gid: string) {
- await this.aria2.call("unpause", gid);
- }
-
- static async startDownload(downloadPath: string, downloadUrl: string) {
- if (!this.connected) await this.connect();
-
- const options = {
- dir: downloadPath,
+ public getStatus() {
+ return {
+ completedLength: this.downloadItem.getReceivedBytes(),
+ totalLength: this.downloadItem.getTotalBytes(),
+ downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
+ folderName: this.downloadItem.getFilename(),
};
+ }
- return this.aria2.call("addUri", [downloadUrl], options);
+ async cancelDownload() {
+ this.downloadItem.cancel();
+ }
+
+ async pauseDownload() {
+ this.downloadItem.pause();
+ }
+
+ async resumeDownload() {
+ this.downloadItem.resume();
+ }
+
+ async startDownload() {
+ return new Promise((resolve) => {
+ const options = this.headers ? { headers: this.headers } : {};
+ WindowManager.mainWindow?.webContents.downloadURL(
+ this.downloadUrl,
+ options
+ );
+
+ WindowManager.mainWindow?.webContents.session.once(
+ "will-download",
+ (_event, item, _webContents) => {
+ this.downloadItem = item;
+
+ item.setSavePath(path.join(this.downloadPath, item.getFilename()));
+
+ resolve(null);
+ }
+ );
+ });
}
}
diff --git a/src/main/services/download/python-instance.ts b/src/main/services/download/python-instance.ts
index c534e41d..37ec17db 100644
--- a/src/main/services/download/python-instance.ts
+++ b/src/main/services/download/python-instance.ts
@@ -19,6 +19,7 @@ import {
LibtorrentPayload,
ProcessPayload,
} from "./types";
+import { pythonInstanceLogger as logger } from "../logger";
export class PythonInstance {
private static pythonProcess: cp.ChildProcess | null = null;
@@ -32,11 +33,13 @@ export class PythonInstance {
});
public static spawn(args?: StartDownloadPayload) {
+ logger.log("spawning python process with args:", args);
this.pythonProcess = startRPCClient(args);
}
public static kill() {
if (this.pythonProcess) {
+ logger.log("killing python process");
this.pythonProcess.kill();
this.pythonProcess = null;
this.downloadingGameId = -1;
@@ -45,6 +48,7 @@ export class PythonInstance {
public static killTorrent() {
if (this.pythonProcess) {
+ logger.log("killing torrent in python process");
this.rpc.post("/action", { action: "kill-torrent" });
this.downloadingGameId = -1;
}
@@ -138,12 +142,14 @@ export class PythonInstance {
save_path: game.downloadPath!,
});
} else {
- await this.rpc.post("/action", {
- action: "start",
- game_id: game.id,
- magnet: game.uri,
- save_path: game.downloadPath,
- } as StartDownloadPayload);
+ await this.rpc
+ .post("/action", {
+ action: "start",
+ game_id: game.id,
+ magnet: game.uri,
+ save_path: game.downloadPath,
+ } as StartDownloadPayload)
+ .catch(this.handleRpcError);
}
this.downloadingGameId = game.id;
@@ -159,4 +165,14 @@ export class PythonInstance {
this.downloadingGameId = -1;
}
+
+ private static async handleRpcError(_error: unknown) {
+ await this.rpc.get("/healthcheck").catch(() => {
+ logger.error(
+ "RPC healthcheck failed. Killing process and starting again"
+ );
+ this.kill();
+ this.spawn();
+ });
+ }
}
diff --git a/src/main/services/download/real-debrid-downloader.ts b/src/main/services/download/real-debrid-downloader.ts
index 8ead0067..2818644a 100644
--- a/src/main/services/download/real-debrid-downloader.ts
+++ b/src/main/services/download/real-debrid-downloader.ts
@@ -1,162 +1,72 @@
import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid";
-import { gameRepository } from "@main/repository";
-import { calculateETA } from "./helpers";
-import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download";
+import { GenericHttpDownloader } from "./generic-http-downloader";
-export class RealDebridDownloader {
- private static downloads = new Map();
- private static downloadingGame: Game | null = null;
-
+export class RealDebridDownloader extends GenericHttpDownloader {
private static realDebridTorrentId: string | null = null;
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
- const torrentInfo = await RealDebridClient.getTorrentInfo(
+ let torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
- const { status, links } = torrentInfo;
-
- if (status === "waiting_files_selection") {
+ if (torrentInfo.status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
- return null;
+
+ torrentInfo = await RealDebridClient.getTorrentInfo(
+ this.realDebridTorrentId
+ );
}
+ const { links, status } = torrentInfo;
+
if (status === "downloaded") {
const [link] = links;
+
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
+
+ return null;
}
- return null;
- }
-
- public static async getStatus() {
- if (this.downloadingGame) {
- const gid = this.downloads.get(this.downloadingGame.id)!;
- const status = await HttpDownload.getStatus(gid);
-
- if (status) {
- const progress =
- Number(status.completedLength) / Number(status.totalLength);
-
- await gameRepository.update(
- { id: this.downloadingGame!.id },
- {
- bytesDownloaded: Number(status.completedLength),
- fileSize: Number(status.totalLength),
- progress,
- status: "active",
- }
- );
-
- const result = {
- numPeers: 0,
- numSeeds: 0,
- downloadSpeed: Number(status.downloadSpeed),
- timeRemaining: calculateETA(
- Number(status.totalLength),
- Number(status.completedLength),
- Number(status.downloadSpeed)
- ),
- isDownloadingMetadata: false,
- isCheckingFiles: false,
- progress,
- gameId: this.downloadingGame!.id,
- } as DownloadProgress;
-
- if (progress === 1) {
- this.downloads.delete(this.downloadingGame.id);
- this.realDebridTorrentId = null;
- this.downloadingGame = null;
- }
-
- return result;
- }
- }
-
- if (this.realDebridTorrentId && this.downloadingGame) {
- const torrentInfo = await RealDebridClient.getTorrentInfo(
- this.realDebridTorrentId
+ if (this.downloadingGame?.uri) {
+ const { download } = await RealDebridClient.unrestrictLink(
+ this.downloadingGame?.uri
);
- const { status } = torrentInfo;
-
- if (status === "downloaded") {
- this.startDownload(this.downloadingGame);
- }
-
- const progress = torrentInfo.progress / 100;
- const totalDownloaded = progress * torrentInfo.bytes;
-
- return {
- numPeers: 0,
- numSeeds: torrentInfo.seeders,
- downloadSpeed: torrentInfo.speed,
- timeRemaining: calculateETA(
- torrentInfo.bytes,
- totalDownloaded,
- torrentInfo.speed
- ),
- isDownloadingMetadata: status === "magnet_conversion",
- } as DownloadProgress;
+ return decodeURIComponent(download);
}
return null;
}
- static async pauseDownload() {
- const gid = this.downloads.get(this.downloadingGame!.id!);
- if (gid) {
- await HttpDownload.pauseDownload(gid);
- }
-
- this.realDebridTorrentId = null;
- this.downloadingGame = null;
- }
-
static async startDownload(game: Game) {
- this.downloadingGame = game;
-
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
-
+ this.downloadingGame = game;
return;
}
- this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
+ if (game.uri?.startsWith("magnet:")) {
+ this.realDebridTorrentId = await RealDebridClient.getTorrentId(
+ game!.uri!
+ );
+ }
+
+ this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl();
if (downloadUrl) {
this.realDebridTorrentId = null;
- const gid = await HttpDownload.startDownload(
- game.downloadPath!,
- downloadUrl
- );
+ const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
+ httpDownload.startDownload();
- this.downloads.set(game.id!, gid);
- }
- }
-
- static async cancelDownload(gameId: number) {
- const gid = this.downloads.get(gameId);
-
- if (gid) {
- await HttpDownload.cancelDownload(gid);
- this.downloads.delete(gameId);
- }
- }
-
- static async resumeDownload(gameId: number) {
- const gid = this.downloads.get(gameId);
-
- if (gid) {
- await HttpDownload.resumeDownload(gid);
+ this.downloads.set(game.id!, httpDownload);
}
}
}
diff --git a/src/main/services/download/torrent-client.ts b/src/main/services/download/torrent-client.ts
index 93d20b7f..2a16acad 100644
--- a/src/main/services/download/torrent-client.ts
+++ b/src/main/services/download/torrent-client.ts
@@ -4,6 +4,8 @@ import crypto from "node:crypto";
import fs from "node:fs";
import { app, dialog } from "electron";
import type { StartDownloadPayload } from "./types";
+import { Readable } from "node:stream";
+import { pythonInstanceLogger as logger } from "../logger";
const binaryNameByPlatform: Partial> = {
darwin: "hydra-download-manager",
@@ -15,6 +17,13 @@ export const BITTORRENT_PORT = "5881";
export const RPC_PORT = "8084";
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
+const logStderr = (readable: Readable | null) => {
+ if (!readable) return;
+
+ readable.setEncoding("utf-8");
+ readable.on("data", logger.log);
+};
+
export const startTorrentClient = (args?: StartDownloadPayload) => {
const commonArgs = [
BITTORRENT_PORT,
@@ -40,10 +49,14 @@ export const startTorrentClient = (args?: StartDownloadPayload) => {
app.quit();
}
- return cp.spawn(binaryPath, commonArgs, {
- stdio: "inherit",
+ const childProcess = cp.spawn(binaryPath, commonArgs, {
windowsHide: true,
+ stdio: ["inherit", "inherit"],
});
+
+ logStderr(childProcess.stderr);
+
+ return childProcess;
} else {
const scriptPath = path.join(
__dirname,
@@ -53,8 +66,12 @@ export const startTorrentClient = (args?: StartDownloadPayload) => {
"main.py"
);
- return cp.spawn("python3", [scriptPath, ...commonArgs], {
- stdio: "inherit",
+ const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
+ stdio: ["inherit", "inherit"],
});
+
+ logStderr(childProcess.stderr);
+
+ return childProcess;
}
};
diff --git a/src/main/services/hosters/gofile.ts b/src/main/services/hosters/gofile.ts
new file mode 100644
index 00000000..2c23556f
--- /dev/null
+++ b/src/main/services/hosters/gofile.ts
@@ -0,0 +1,63 @@
+import axios from "axios";
+
+export interface GofileAccountsReponse {
+ id: string;
+ token: string;
+}
+
+export interface GofileContentChild {
+ id: string;
+ link: string;
+}
+
+export interface GofileContentsResponse {
+ id: string;
+ type: string;
+ children: Record;
+}
+
+export const WT = "4fd6sg89d7s6";
+
+export class GofileApi {
+ private static token: string;
+
+ public static async authorize() {
+ const response = await axios.post<{
+ status: string;
+ data: GofileAccountsReponse;
+ }>("https://api.gofile.io/accounts");
+
+ if (response.data.status === "ok") {
+ this.token = response.data.data.token;
+ return this.token;
+ }
+
+ throw new Error("Failed to authorize");
+ }
+
+ public static async getDownloadLink(id: string) {
+ const searchParams = new URLSearchParams({
+ wt: WT,
+ });
+
+ const response = await axios.get<{
+ status: string;
+ data: GofileContentsResponse;
+ }>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
+ headers: {
+ Authorization: `Bearer ${this.token}`,
+ },
+ });
+
+ if (response.data.status === "ok") {
+ if (response.data.data.type !== "folder") {
+ throw new Error("Only folders are supported");
+ }
+
+ const [firstChild] = Object.values(response.data.data.children);
+ return firstChild.link;
+ }
+
+ throw new Error("Failed to get download link");
+ }
+}
diff --git a/src/main/services/hosters/index.ts b/src/main/services/hosters/index.ts
new file mode 100644
index 00000000..4c5b1803
--- /dev/null
+++ b/src/main/services/hosters/index.ts
@@ -0,0 +1,2 @@
+export * from "./gofile";
+export * from "./qiwi";
diff --git a/src/main/services/hosters/qiwi.ts b/src/main/services/hosters/qiwi.ts
new file mode 100644
index 00000000..e18b011c
--- /dev/null
+++ b/src/main/services/hosters/qiwi.ts
@@ -0,0 +1,15 @@
+import { requestWebPage } from "@main/helpers";
+
+export class QiwiApi {
+ public static async getDownloadUrl(url: string) {
+ const document = await requestWebPage(url);
+ const fileName = document.querySelector("h1")?.textContent;
+
+ const slug = url.split("/").pop();
+ const extension = fileName?.split(".").pop();
+
+ const downloadUrl = `https://spyderrock.com/${slug}.${extension}`;
+
+ return downloadUrl;
+ }
+}
diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts
index 39b938c5..c7164d09 100644
--- a/src/main/services/how-long-to-beat.ts
+++ b/src/main/services/how-long-to-beat.ts
@@ -1,8 +1,8 @@
import axios from "axios";
-import { JSDOM } from "jsdom";
import { requestWebPage } from "@main/helpers";
import { HowLongToBeatCategory } from "@types";
import { formatName } from "@shared";
+import { logger } from "./logger";
export interface HowLongToBeatResult {
game_id: number;
@@ -14,22 +14,27 @@ export interface HowLongToBeatSearchResponse {
}
export const searchHowLongToBeat = async (gameName: string) => {
- const response = await axios.post(
- "https://howlongtobeat.com/api/search",
- {
- searchType: "games",
- searchTerms: formatName(gameName).split(" "),
- searchPage: 1,
- size: 100,
- },
- {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
- Referer: "https://howlongtobeat.com/",
+ const response = await axios
+ .post(
+ "https://howlongtobeat.com/api/search",
+ {
+ searchType: "games",
+ searchTerms: formatName(gameName).split(" "),
+ searchPage: 1,
+ size: 100,
},
- }
- );
+ {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
+ Referer: "https://howlongtobeat.com/",
+ },
+ }
+ )
+ .catch((error) => {
+ logger.error("Error searching HowLongToBeat:", error?.response?.status);
+ return { data: { data: [] } };
+ });
return response.data as HowLongToBeatSearchResponse;
};
@@ -52,10 +57,7 @@ const parseListItems = ($lis: Element[]) => {
export const getHowLongToBeatGame = async (
id: string
): Promise => {
- const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
-
- const { window } = new JSDOM(response);
- const { document } = window;
+ const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return [];
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 97517b5b..120d27ac 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -64,6 +64,14 @@ export class HydraApi {
}
}
+ static handleSignOut() {
+ this.userAuth = {
+ authToken: "",
+ refreshToken: "",
+ expirationTimestamp: 0,
+ };
+ }
+
static async setupApi() {
this.instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_API_URL,
@@ -72,7 +80,7 @@ export class HydraApi {
this.instance.interceptors.request.use(
(request) => {
logger.log(" ---- REQUEST -----");
- logger.log(request.method, request.url, request.data);
+ logger.log(request.method, request.url, request.params, request.data);
return request;
},
(error) => {
@@ -196,52 +204,52 @@ export class HydraApi {
throw err;
};
- static async get(url: string) {
+ static async get(url: string, params?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
return this.instance
- .get(url, this.getAxiosConfig())
+ .get(url, { params, ...this.getAxiosConfig() })
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
- static async post(url: string, data?: any) {
+ static async post(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
return this.instance
- .post(url, data, this.getAxiosConfig())
+ .post(url, data, this.getAxiosConfig())
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
- static async put(url: string, data?: any) {
+ static async put(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
return this.instance
- .put(url, data, this.getAxiosConfig())
+ .put(url, data, this.getAxiosConfig())
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
- static async patch(url: string, data?: any) {
+ static async patch(url: string, data?: any) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
return this.instance
- .patch(url, data, this.getAxiosConfig())
+ .patch(url, data, this.getAxiosConfig())
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
- static async delete(url: string) {
+ static async delete(url: string) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
return this.instance
- .delete(url, this.getAxiosConfig())
+ .delete(url, this.getAxiosConfig())
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}
diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts
index b66a1897..6699788c 100644
--- a/src/main/services/library-sync/create-game.ts
+++ b/src/main/services/library-sync/create-game.ts
@@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api";
import { gameRepository } from "@main/repository";
export const createGame = async (game: Game) => {
- HydraApi.post(`/games`, {
+ HydraApi.post(`/profile/games`, {
objectId: game.objectID,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts
index 2a6b5bb5..2b3f51b3 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -4,7 +4,7 @@ import { steamGamesWorker } from "@main/workers";
import { getSteamAppAsset } from "@main/helpers";
export const mergeWithRemoteGames = async () => {
- return HydraApi.get("/games")
+ return HydraApi.get("/profile/games")
.then(async (response) => {
for (const game of response) {
const localGame = await gameRepository.findOne({
diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts
index 39206a12..5cfc4103 100644
--- a/src/main/services/library-sync/update-game-playtime.ts
+++ b/src/main/services/library-sync/update-game-playtime.ts
@@ -6,7 +6,7 @@ export const updateGamePlaytime = async (
deltaInMillis: number,
lastTimePlayed: Date
) => {
- HydraApi.put(`/games/${game.remoteId}`, {
+ HydraApi.put(`/profile/games/${game.remoteId}`, {
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
lastTimePlayed,
}).catch(() => {});
diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts
index 88f02375..22dc595e 100644
--- a/src/main/services/library-sync/upload-games-batch.ts
+++ b/src/main/services/library-sync/upload-games-batch.ts
@@ -14,7 +14,7 @@ export const uploadGamesBatch = async () => {
for (const chunk of gamesChunks) {
await HydraApi.post(
- "/games/batch",
+ "/profile/games/batch",
chunk.map((game) => {
return {
objectId: game.objectID,
diff --git a/src/main/services/logger.ts b/src/main/services/logger.ts
index 8da27a9e..1eb7060b 100644
--- a/src/main/services/logger.ts
+++ b/src/main/services/logger.ts
@@ -6,6 +6,10 @@ log.transports.file.resolvePathFn = (
_: log.PathVariables,
message?: log.LogMessage | undefined
) => {
+ if (message?.scope === "python-instance") {
+ return path.join(logsPath, "pythoninstance.txt");
+ }
+
if (message?.level === "error") {
return path.join(logsPath, "error.txt");
}
@@ -23,4 +27,5 @@ log.errorHandler.startCatching({
log.initialize();
+export const pythonInstanceLogger = log.scope("python-instance");
export const logger = log.scope("main");
diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts
index ca72707f..f2ec51ba 100644
--- a/src/main/services/main-loop.ts
+++ b/src/main/services/main-loop.ts
@@ -10,6 +10,6 @@ export const startMainLoop = async () => {
DownloadManager.watchDownloads(),
]);
- await sleep(500);
+ await sleep(1000);
}
};
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index 0f7efa62..080f1efc 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -4,12 +4,16 @@ import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import { GameRunning } from "@types";
import { PythonInstance } from "./download";
+import { Game } from "@main/entity";
export const gamesPlaytime = new Map<
number,
- { lastTick: number; firstTick: number }
+ { lastTick: number; firstTick: number; lastSyncTick: number }
>();
+const TICKS_TO_UPDATE_API = 120;
+let currentTick = 1;
+
export const watchProcesses = async () => {
const games = await gameRepository.find({
where: {
@@ -30,48 +34,17 @@ export const watchProcesses = async () => {
if (gameProcess) {
if (gamesPlaytime.has(game.id)) {
- const gamePlaytime = gamesPlaytime.get(game.id)!;
-
- const zero = gamePlaytime.lastTick;
- const delta = performance.now() - zero;
-
- await gameRepository.update(game.id, {
- playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
- lastTimePlayed: new Date(),
- });
-
- gamesPlaytime.set(game.id, {
- ...gamePlaytime,
- lastTick: performance.now(),
- });
+ onTickGame(game);
} else {
- if (game.remoteId) {
- updateGamePlaytime(game, 0, new Date());
- } else {
- createGame({ ...game, lastTimePlayed: new Date() });
- }
-
- gamesPlaytime.set(game.id, {
- lastTick: performance.now(),
- firstTick: performance.now(),
- });
+ onOpenGame(game);
}
} else if (gamesPlaytime.has(game.id)) {
- const gamePlaytime = gamesPlaytime.get(game.id)!;
- gamesPlaytime.delete(game.id);
-
- if (game.remoteId) {
- updateGamePlaytime(
- game,
- performance.now() - gamePlaytime.firstTick,
- game.lastTimePlayed!
- );
- } else {
- createGame(game);
- }
+ onCloseGame(game);
}
}
+ currentTick++;
+
if (WindowManager.mainWindow) {
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
return {
@@ -86,3 +59,68 @@ export const watchProcesses = async () => {
);
}
};
+
+function onOpenGame(game: Game) {
+ const now = performance.now();
+
+ gamesPlaytime.set(game.id, {
+ lastTick: now,
+ firstTick: now,
+ lastSyncTick: now,
+ });
+
+ if (game.remoteId) {
+ updateGamePlaytime(game, 0, new Date());
+ } else {
+ createGame({ ...game, lastTimePlayed: new Date() });
+ }
+}
+
+function onTickGame(game: Game) {
+ const now = performance.now();
+ const gamePlaytime = gamesPlaytime.get(game.id)!;
+
+ const delta = now - gamePlaytime.lastTick;
+
+ gameRepository.update(game.id, {
+ playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
+ lastTimePlayed: new Date(),
+ });
+
+ gamesPlaytime.set(game.id, {
+ ...gamePlaytime,
+ lastTick: now,
+ });
+
+ if (currentTick % TICKS_TO_UPDATE_API === 0) {
+ if (game.remoteId) {
+ updateGamePlaytime(
+ game,
+ now - gamePlaytime.lastSyncTick,
+ game.lastTimePlayed!
+ );
+ } else {
+ createGame(game);
+ }
+
+ gamesPlaytime.set(game.id, {
+ ...gamePlaytime,
+ lastSyncTick: now,
+ });
+ }
+}
+
+const onCloseGame = (game: Game) => {
+ const gamePlaytime = gamesPlaytime.get(game.id)!;
+ gamesPlaytime.delete(game.id);
+
+ if (game.remoteId) {
+ updateGamePlaytime(
+ game,
+ performance.now() - gamePlaytime.firstTick,
+ game.lastTimePlayed!
+ );
+ } else {
+ createGame(game);
+ }
+};
diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts
index 2e0debe6..26ba4c79 100644
--- a/src/main/services/real-debrid.ts
+++ b/src/main/services/real-debrid.ts
@@ -46,7 +46,7 @@ export class RealDebridClient {
static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" });
- await this.instance.post(
+ return this.instance.post(
`/torrents/selectFiles/${id}`,
searchParams.toString()
);
diff --git a/src/main/services/repacks-manager.ts b/src/main/services/repacks-manager.ts
index 02821127..93157d6c 100644
--- a/src/main/services/repacks-manager.ts
+++ b/src/main/services/repacks-manager.ts
@@ -8,11 +8,25 @@ export class RepacksManager {
private static repacksIndex = new flexSearch.Index();
public static async updateRepacks() {
- this.repacks = await repackRepository.find({
- order: {
- createdAt: "DESC",
- },
- });
+ this.repacks = await repackRepository
+ .find({
+ order: {
+ createdAt: "DESC",
+ },
+ })
+ .then((repacks) =>
+ repacks.map((repack) => {
+ const uris: string[] = [];
+ const magnet = repack?.magnet;
+
+ if (magnet) uris.push(magnet);
+
+ return {
+ ...repack,
+ uris: [...uris, ...JSON.parse(repack.uris)],
+ };
+ })
+ );
for (let i = 0; i < this.repacks.length; i++) {
this.repacksIndex.remove(i);
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 201b13ad..025e7219 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -158,7 +158,7 @@ export class WindowManager {
const recentlyPlayedGames: Array =
games.map(({ title, executablePath }) => ({
- label: title,
+ label: title.length > 15 ? `${title.slice(0, 15)}…` : title,
type: "normal",
click: async () => {
if (!executablePath) return;
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 91722606..087d573a 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -10,6 +10,7 @@ import type {
StartGameDownloadPayload,
GameRunning,
FriendRequestAction,
+ UpdateProfileProps,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@@ -135,8 +136,10 @@ contextBridge.exposeInMainWorld("electron", {
/* Profile */
getMe: () => ipcRenderer.invoke("getMe"),
- updateProfile: (displayName: string, newProfileImagePath: string | null) =>
- ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
+ undoFriendship: (userId: string) =>
+ ipcRenderer.invoke("undoFriendship", userId),
+ updateProfile: (updateProfile: UpdateProfileProps) =>
+ ipcRenderer.invoke("updateProfile", updateProfile),
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
ipcRenderer.invoke("updateFriendRequest", userId, action),
@@ -145,6 +148,12 @@ contextBridge.exposeInMainWorld("electron", {
/* User */
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
+ blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
+ unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
+ getUserFriends: (userId: string, take: number, skip: number) =>
+ ipcRenderer.invoke("getUserFriends", userId, take, skip),
+ getUserBlocks: (take: number, skip: number) =>
+ ipcRenderer.invoke("getUserBlocks", take, skip),
/* Auth */
signOut: () => ipcRenderer.invoke("signOut"),
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 52276268..d7abf3ad 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -6,7 +6,7 @@
Hydra
diff --git a/src/renderer/src/app.css.ts b/src/renderer/src/app.css.ts
index a5f9394b..c829021a 100644
--- a/src/renderer/src/app.css.ts
+++ b/src/renderer/src/app.css.ts
@@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", {
globalStyle("body", {
overflow: "hidden",
userSelect: "none",
- fontFamily: "'Fira Mono', monospace",
+ fontFamily: "Noto Sans, sans-serif",
fontSize: vars.size.body,
background: vars.color.background,
color: vars.color.body,
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index 24c7bed6..2b9ac187 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -42,11 +42,12 @@ export function App() {
const {
isFriendsModalVisible,
friendRequetsModalTab,
- updateFriendRequests,
+ friendModalUserId,
+ fetchFriendRequests,
hideFriendsModal,
} = useUserDetails();
- const { fetchUserDetails, updateUserDetails, clearUserDetails } =
+ const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
const dispatch = useAppDispatch();
@@ -104,20 +105,26 @@ export function App() {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
- updateFriendRequests();
+ fetchFriendRequests();
}
});
- }, [fetchUserDetails, updateUserDetails, dispatch]);
+ }, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
- updateFriendRequests();
+ fetchFriendRequests();
showSuccessToast(t("successfully_signed_in"));
}
});
- }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
+ }, [
+ fetchUserDetails,
+ fetchFriendRequests,
+ t,
+ showSuccessToast,
+ updateUserDetails,
+ ]);
useEffect(() => {
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
@@ -218,11 +225,14 @@ export function App() {
onClose={handleToastClose}
/>
-
+ {userDetails && (
+
+ )}
diff --git a/src/renderer/src/components/header/header.css.ts b/src/renderer/src/components/header/header.css.ts
index 0e82aaef..12855986 100644
--- a/src/renderer/src/components/header/header.css.ts
+++ b/src/renderer/src/components/header/header.css.ts
@@ -104,6 +104,7 @@ export const section = style({
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
height: "100%",
+ overflow: "hidden",
});
export const backButton = recipe({
@@ -136,11 +137,15 @@ export const backButton = recipe({
export const title = recipe({
base: {
transition: "all ease 0.2s",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ width: "100%",
},
variants: {
hasBackButton: {
true: {
transform: "translateX(28px)",
+ width: "calc(100% - 28px)",
},
},
},
diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx
index d3315098..c37e4b44 100644
--- a/src/renderer/src/components/header/header.tsx
+++ b/src/renderer/src/components/header/header.tsx
@@ -72,7 +72,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
isWindows: window.electron.platform === "win32",
})}
>
-
+
- {t("publisher", { publisher: shopDetails.publishers[0] })}
+
+ {Array.isArray(shopDetails.publishers) && (
+ {t("publisher", { publisher: shopDetails.publishers[0] })}
+ )}
);
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts
index f0bbfd2e..3dc0ec94 100644
--- a/src/renderer/src/pages/game-details/game-details.css.ts
+++ b/src/renderer/src/pages/game-details/game-details.css.ts
@@ -101,7 +101,6 @@ export const descriptionContent = style({
export const description = style({
userSelect: "text",
lineHeight: "22px",
- fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px",
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
"@media": {
diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx
index 5f32965a..5ac9673f 100644
--- a/src/renderer/src/pages/game-details/game-details.tsx
+++ b/src/renderer/src/pages/game-details/game-details.tsx
@@ -23,7 +23,7 @@ import {
} from "@renderer/context";
import { useDownload } from "@renderer/hooks";
import { GameOptionsModal, RepacksModal } from "./modals";
-import { Downloader } from "@shared";
+import { Downloader, getDownloadersForUri } from "@shared";
export function GameDetails() {
const [randomGame, setRandomGame] = useState(null);
@@ -70,6 +70,9 @@ export function GameDetails() {
}
};
+ const selectRepackUri = (repack: GameRepack, downloader: Downloader) =>
+ repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
+
return (
@@ -96,6 +99,7 @@ export function GameDetails() {
downloader,
shop: shop as GameShop,
downloadPath,
+ uri: selectRepackUri(repack, downloader),
});
await updateGame();
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
index e10d55a5..c379c1c3 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
@@ -9,6 +9,7 @@ export const panel = recipe({
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
+ backgroundColor: vars.color.darkBackground,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
index d5655d94..5450378c 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.css.ts
@@ -20,13 +20,16 @@ export const hintText = style({
});
export const downloaders = style({
- display: "flex",
+ display: "grid",
gap: `${SPACING_UNIT}px`,
+ gridTemplateColumns: "repeat(2, 1fr)",
});
export const downloaderOption = style({
- flex: "1",
position: "relative",
+ ":only-child": {
+ gridColumn: "1 / -1",
+ },
});
export const downloaderIcon = style({
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
index ef4ba040..3450af24 100644
--- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx
@@ -1,11 +1,11 @@
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
-import { Downloader, formatBytes } from "@shared";
+import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
@@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
repack: GameRepack | null;
}
-const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
-
export function DownloadSettingsModal({
visible,
onClose,
@@ -36,9 +34,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
- const [selectedDownloader, setSelectedDownloader] = useState(
- Downloader.Torrent
- );
+ const [selectedDownloader, setSelectedDownloader] =
+ useState(null);
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
@@ -50,6 +47,10 @@ export function DownloadSettingsModal({
}
}, [visible, selectedPath]);
+ const downloaders = useMemo(() => {
+ return getDownloadersForUris(repack?.uris ?? []);
+ }, [repack?.uris]);
+
useEffect(() => {
if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath);
@@ -59,9 +60,27 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
}
- if (userPreferences?.realDebridApiToken)
- setSelectedDownloader(Downloader.RealDebrid);
- }, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
+ const filteredDownloaders = downloaders.filter((downloader) => {
+ if (downloader === Downloader.RealDebrid)
+ return userPreferences?.realDebridApiToken;
+ return true;
+ });
+
+ /* Gives preference to Real Debrid */
+ const selectedDownloader = filteredDownloaders.includes(
+ Downloader.RealDebrid
+ )
+ ? Downloader.RealDebrid
+ : filteredDownloaders[0];
+
+ setSelectedDownloader(
+ selectedDownloader === undefined ? null : selectedDownloader
+ );
+ }, [
+ userPreferences?.downloadsPath,
+ downloaders,
+ userPreferences?.realDebridApiToken,
+ ]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
@@ -85,7 +104,7 @@ export function DownloadSettingsModal({
if (repack) {
setDownloadStarting(true);
- startDownload(repack, selectedDownloader, selectedPath).finally(() => {
+ startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
@@ -167,7 +186,10 @@ export function DownloadSettingsModal({
-
+
{t("download_now")}
diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts
index 8bf0ae7f..f844a686 100644
--- a/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts
+++ b/src/renderer/src/pages/game-details/modals/game-options-modal.css.ts
@@ -15,7 +15,6 @@ export const gameOptionHeader = style({
});
export const gameOptionHeaderDescription = style({
- fontFamily: "'Fira Sans', sans-serif",
fontWeight: "400",
});
diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
index d2d8f5d9..0d1b9c1d 100644
--- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
@@ -44,8 +44,10 @@ export function RepacksModal({
}, [repacks]);
const getInfoHash = useCallback(async () => {
- const torrent = await parseTorrent(game?.uri ?? "");
- if (torrent.infoHash) setInfoHash(torrent.infoHash);
+ if (game?.uri?.startsWith("magnet:")) {
+ const torrent = await parseTorrent(game?.uri ?? "");
+ if (torrent.infoHash) setInfoHash(torrent.infoHash);
+ }
}, [game]);
useEffect(() => {
@@ -74,6 +76,13 @@ export function RepacksModal({
);
};
+ const checkIfLastDownloadedOption = (repack: GameRepack) => {
+ if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
+ if (!game?.uri) return false;
+
+ return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
+ };
+
return (
<>
{filteredRepacks.map((repack) => {
- const isLastDownloadedOption =
- infoHash !== null &&
- repack.magnet.toLowerCase().includes(infoHash);
+ const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
return (
-
- {t("download_sources_description")}
-
+ {t("download_sources_description")}
-
-
-
-
- {t("download_options", {
- count: downloadSource.repackCount,
- countFormatted: downloadSource.repackCount.toLocaleString(),
- })}
-
diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx
index e09ebb11..10c17eca 100644
--- a/src/renderer/src/pages/settings/settings-general.tsx
+++ b/src/renderer/src/pages/settings/settings-general.tsx
@@ -1,6 +1,4 @@
import { useContext, useEffect, useState } from "react";
-import ISO6391 from "iso-639-1";
-
import {
TextField,
Button,
@@ -8,11 +6,9 @@ import {
SelectField,
} from "@renderer/components";
import { useTranslation } from "react-i18next";
-
import { useAppSelector } from "@renderer/hooks";
-
import { changeLanguage } from "i18next";
-import * as languageResources from "@locales";
+import languageResources from "@locales";
import { orderBy } from "lodash-es";
import { settingsContext } from "@renderer/context";
@@ -50,9 +46,9 @@ export function SettingsGeneral() {
setLanguageOptions(
orderBy(
- Object.keys(languageResources).map((language) => {
+ Object.entries(languageResources).map(([language, value]) => {
return {
- nativeName: ISO6391.getNativeName(language),
+ nativeName: value.language_name,
option: language,
};
}),
@@ -93,8 +89,6 @@ export function SettingsGeneral() {
function updateFormWithUserPreferences() {
if (userPreferences) {
- const parsedLanguage = userPreferences.language.split("-")[0];
-
setForm((prev) => ({
...prev,
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
@@ -102,7 +96,7 @@ export function SettingsGeneral() {
userPreferences.downloadNotificationsEnabled,
repackUpdatesNotificationsEnabled:
userPreferences.repackUpdatesNotificationsEnabled,
- language: parsedLanguage,
+ language: userPreferences.language,
}));
}
}
diff --git a/src/renderer/src/pages/settings/settings-real-debrid.css.ts b/src/renderer/src/pages/settings/settings-real-debrid.css.ts
index a9b545df..0dfc9d78 100644
--- a/src/renderer/src/pages/settings/settings-real-debrid.css.ts
+++ b/src/renderer/src/pages/settings/settings-real-debrid.css.ts
@@ -9,6 +9,5 @@ export const form = style({
});
export const description = style({
- fontFamily: "'Fira Sans', sans-serif",
marginBottom: `${SPACING_UNIT * 2}px`,
});
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx
new file mode 100644
index 00000000..3ca837fa
--- /dev/null
+++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx
@@ -0,0 +1,194 @@
+import {
+ CheckCircleIcon,
+ PersonIcon,
+ XCircleIcon,
+} from "@primer/octicons-react";
+import * as styles from "./user-friend-modal.css";
+import { SPACING_UNIT } from "@renderer/theme.css";
+import { useTranslation } from "react-i18next";
+
+export type UserFriendItemProps = {
+ userId: string;
+ profileImageUrl: string | null;
+ displayName: string;
+} & (
+ | {
+ type: "ACCEPTED";
+ onClickUndoFriendship: (userId: string) => void;
+ onClickItem: (userId: string) => void;
+ }
+ | { type: "BLOCKED"; onClickUnblock: (userId: string) => void }
+ | {
+ type: "SENT" | "RECEIVED";
+ onClickCancelRequest: (userId: string) => void;
+ onClickAcceptRequest: (userId: string) => void;
+ onClickRefuseRequest: (userId: string) => void;
+ onClickItem: (userId: string) => void;
+ }
+ | { type: null; onClickItem: (userId: string) => void }
+);
+
+export const UserFriendItem = (props: UserFriendItemProps) => {
+ const { t } = useTranslation("user_profile");
+ const { userId, profileImageUrl, displayName, type } = props;
+
+ const getRequestDescription = () => {
+ if (type === "ACCEPTED" || type === null) return null;
+
+ return (
+
+ {type == "SENT" ? t("request_sent") : t("request_received")}
+
+ );
+ };
+
+ const getRequestActions = () => {
+ if (type === null) return null;
+
+ if (type === "SENT") {
+ return (
+ props.onClickCancelRequest(userId)}
+ title={t("cancel_request")}
+ >
+
+
+ );
+ }
+
+ if (type === "RECEIVED") {
+ return (
+ <>
+ props.onClickAcceptRequest(userId)}
+ title={t("accept_request")}
+ >
+
+
+ props.onClickRefuseRequest(userId)}
+ title={t("ignore_request")}
+ >
+
+
+ >
+ );
+ }
+
+ if (type === "ACCEPTED") {
+ return (
+ props.onClickUndoFriendship(userId)}
+ title={t("undo_friendship")}
+ >
+
+
+ );
+ }
+
+ if (type === "BLOCKED") {
+ return (
+ props.onClickUnblock(userId)}
+ title={t("unblock")}
+ >
+
+
+ );
+ }
+
+ return null;
+ };
+
+ if (type === "BLOCKED") {
+ return (
+
+
+
+ {profileImageUrl ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {getRequestActions()}
+
+
+ );
+ }
+
+ return (
+
+
props.onClickItem(userId)}
+ >
+
+ {profileImageUrl ? (
+
+ ) : (
+
+ )}
+
+
+
{displayName}
+ {getRequestDescription()}
+
+
+
+
+ {getRequestActions()}
+
+
+ );
+};
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx
index bf4879b2..b6e6aaea 100644
--- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx
+++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx
@@ -4,7 +4,7 @@ import { SPACING_UNIT } from "@renderer/theme.css";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
-import { UserFriendRequest } from "./user-friend-request";
+import { UserFriendItem } from "./user-friend-item";
export interface UserFriendModalAddFriendProps {
closeModal: () => void;
@@ -23,7 +23,7 @@ export const UserFriendModalAddFriend = ({
const { sendFriendRequest, updateFriendRequestState, friendRequests } =
useUserDetails();
- const { showErrorToast } = useToast();
+ const { showSuccessToast, showErrorToast } = useToast();
const handleClickAddFriend = () => {
setIsAddingFriend(true);
@@ -40,37 +40,37 @@ export const UserFriendModalAddFriend = ({
});
};
- const resetAndClose = () => {
- setFriendCode("");
- closeModal();
- };
-
const handleClickRequest = (userId: string) => {
- resetAndClose();
+ closeModal();
navigate(`/user/${userId}`);
};
const handleClickSeeProfile = () => {
- resetAndClose();
- // TODO: add validation for this input?
- navigate(`/user/${friendCode}`);
+ closeModal();
+ if (friendCode.length === 8) {
+ navigate(`/user/${friendCode}`);
+ }
};
- const handleClickCancelFriendRequest = (userId: string) => {
+ const handleCancelFriendRequest = (userId: string) => {
updateFriendRequestState(userId, "CANCEL").catch(() => {
- showErrorToast("Falha ao cancelar convite");
+ showErrorToast(t("try_again"));
});
};
- const handleClickAcceptFriendRequest = (userId: string) => {
- updateFriendRequestState(userId, "ACCEPTED").catch(() => {
- showErrorToast("Falha ao aceitar convite");
- });
+ const handleAcceptFriendRequest = (userId: string) => {
+ updateFriendRequestState(userId, "ACCEPTED")
+ .then(() => {
+ showSuccessToast(t("request_accepted"));
+ })
+ .catch(() => {
+ showErrorToast(t("try_again"));
+ });
};
- const handleClickRefuseFriendRequest = (userId: string) => {
+ const handleRefuseFriendRequest = (userId: string) => {
updateFriendRequestState(userId, "REFUSED").catch(() => {
- showErrorToast("Falha ao recusar convite");
+ showErrorToast(t("try_again"));
});
};
@@ -118,19 +118,20 @@ export const UserFriendModalAddFriend = ({
gap: `${SPACING_UNIT * 2}px`,
}}
>
- Pendentes
+ {t("pending")}
+ {friendRequests.length === 0 && {t("no_pending_invites")}
}
{friendRequests.map((request) => {
return (
-
);
})}
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx
new file mode 100644
index 00000000..8ef96baf
--- /dev/null
+++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx
@@ -0,0 +1,135 @@
+import { SPACING_UNIT, vars } from "@renderer/theme.css";
+import { UserFriend } from "@types";
+import { useEffect, useRef, useState } from "react";
+import { UserFriendItem } from "./user-friend-item";
+import { useNavigate } from "react-router-dom";
+import { useToast, useUserDetails } from "@renderer/hooks";
+import { useTranslation } from "react-i18next";
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+
+export interface UserFriendModalListProps {
+ userId: string;
+ closeModal: () => void;
+}
+
+const pageSize = 12;
+
+export const UserFriendModalList = ({
+ userId,
+ closeModal,
+}: UserFriendModalListProps) => {
+ const { t } = useTranslation("user_profile");
+ const { showErrorToast } = useToast();
+ const navigate = useNavigate();
+
+ const [page, setPage] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+ const [maxPage, setMaxPage] = useState(0);
+ const [friends, setFriends] = useState([]);
+ const listContainer = useRef(null);
+
+ const { userDetails, undoFriendship } = useUserDetails();
+ const isMe = userDetails?.id == userId;
+
+ const loadNextPage = () => {
+ if (page > maxPage) return;
+ setIsLoading(true);
+ window.electron
+ .getUserFriends(userId, pageSize, page * pageSize)
+ .then((newPage) => {
+ if (page === 0) {
+ setMaxPage(newPage.totalFriends / pageSize);
+ }
+
+ setFriends([...friends, ...newPage.friends]);
+ setPage(page + 1);
+ })
+ .catch(() => {})
+ .finally(() => setIsLoading(false));
+ };
+
+ const handleScroll = () => {
+ const scrollTop = listContainer.current?.scrollTop || 0;
+ const scrollHeight = listContainer.current?.scrollHeight || 0;
+ const clientHeight = listContainer.current?.clientHeight || 0;
+ const maxScrollTop = scrollHeight - clientHeight;
+
+ if (scrollTop < maxScrollTop * 0.9 || isLoading) {
+ return;
+ }
+
+ loadNextPage();
+ };
+
+ useEffect(() => {
+ const container = listContainer.current;
+ container?.addEventListener("scroll", handleScroll);
+ return () => container?.removeEventListener("scroll", handleScroll);
+ }, [isLoading]);
+
+ const reloadList = () => {
+ setPage(0);
+ setMaxPage(0);
+ setFriends([]);
+ loadNextPage();
+ };
+
+ useEffect(() => {
+ reloadList();
+ }, [userId]);
+
+ const handleClickFriend = (userId: string) => {
+ closeModal();
+ navigate(`/user/${userId}`);
+ };
+
+ const handleUndoFriendship = (userId: string) => {
+ undoFriendship(userId)
+ .then(() => {
+ reloadList();
+ })
+ .catch(() => {
+ showErrorToast(t("try_again"));
+ });
+ };
+
+ return (
+
+
+ {!isLoading && friends.length === 0 &&
{t("no_friends_added")}
}
+ {friends.map((friend) => {
+ return (
+
+ );
+ })}
+ {isLoading && (
+
+ )}
+
+
+ );
+};
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.css.ts b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.css.ts
index 0d6e8643..41ab4156 100644
--- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.css.ts
+++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.css.ts
@@ -1,17 +1,6 @@
import { SPACING_UNIT, vars } from "../../../theme.css";
import { style } from "@vanilla-extract/css";
-export const profileContentBox = style({
- display: "flex",
- 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 friendAvatarContainer = style({
width: "35px",
minWidth: "35px",
@@ -42,8 +31,14 @@ export const profileAvatar = style({
});
export const friendListContainer = style({
+ display: "flex",
+ gap: `${SPACING_UNIT * 3}px`,
+ alignItems: "center",
+ borderRadius: "4px",
+ border: `solid 1px ${vars.color.border}`,
width: "100%",
height: "54px",
+ minHeight: "54px",
transition: "all ease 0.2s",
position: "relative",
":hover": {
@@ -90,3 +85,15 @@ export const cancelRequestButton = style({
color: vars.color.danger,
},
});
+
+export const friendCodeButton = style({
+ color: vars.color.body,
+ cursor: "pointer",
+ display: "flex",
+ gap: `${SPACING_UNIT / 2}px`,
+ alignItems: "center",
+ transition: "all ease 0.2s",
+ ":hover": {
+ color: vars.color.muted,
+ },
+});
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx
index 88cf4a6c..26c63abc 100644
--- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx
+++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx
@@ -1,25 +1,31 @@
import { Button, Modal } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
+import { useToast, useUserDetails } from "@renderer/hooks";
+import { UserFriendModalList } from "./user-friend-modal-list";
+import { CopyIcon } from "@primer/octicons-react";
+import * as styles from "./user-friend-modal.css";
export enum UserFriendModalTab {
FriendsList,
AddFriend,
}
-export interface UserAddFriendsModalProps {
+export interface UserFriendsModalProps {
visible: boolean;
onClose: () => void;
initialTab: UserFriendModalTab | null;
+ userId: string;
}
export const UserFriendModal = ({
visible,
onClose,
initialTab,
-}: UserAddFriendsModalProps) => {
+ userId,
+}: UserFriendsModalProps) => {
const { t } = useTranslation("user_profile");
const tabs = [t("friends_list"), t("add_friends")];
@@ -28,6 +34,11 @@ export const UserFriendModal = ({
initialTab || UserFriendModalTab.FriendsList
);
+ const { showSuccessToast } = useToast();
+
+ const { userDetails } = useUserDetails();
+ const isMe = userDetails?.id == userId;
+
useEffect(() => {
if (initialTab != null) {
setCurrentTab(initialTab);
@@ -36,7 +47,7 @@ export const UserFriendModal = ({
const renderTab = () => {
if (currentTab == UserFriendModalTab.FriendsList) {
- return <>>;
+ return ;
}
if (currentTab == UserFriendModalTab.AddFriend) {
@@ -46,6 +57,11 @@ export const UserFriendModal = ({
return <>>;
};
+ const copyToClipboard = useCallback(() => {
+ navigator.clipboard.writeText(userDetails!.id);
+ showSuccessToast(t("friend_code_copied"));
+ }, [userDetails, showSuccessToast, t]);
+
return (
-
- {tabs.map((tab, index) => {
- return (
- setCurrentTab(index)}
+ {isMe && (
+ <>
+
+
Seu código de amigo:
+
- {tab}
-
- );
- })}
-
-
{tabs[currentTab]}
+
{userDetails.id}
+
+
+
+
+ {tabs.map((tab, index) => {
+ return (
+ setCurrentTab(index)}
+ >
+ {tab}
+
+ );
+ })}
+
+ >
+ )}
{renderTab()}
diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-request.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-request.tsx
deleted file mode 100644
index 022807d5..00000000
--- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-request.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import {
- CheckCircleIcon,
- PersonIcon,
- XCircleIcon,
-} from "@primer/octicons-react";
-import * as styles from "./user-friend-modal.css";
-import cn from "classnames";
-import { SPACING_UNIT } from "@renderer/theme.css";
-
-export interface UserFriendRequestProps {
- userId: string;
- profileImageUrl: string | null;
- displayName: string;
- isRequestSent: boolean;
- onClickCancelRequest: (userId: string) => void;
- onClickAcceptRequest: (userId: string) => void;
- onClickRefuseRequest: (userId: string) => void;
- onClickRequest: (userId: string) => void;
-}
-
-export const UserFriendRequest = ({
- userId,
- profileImageUrl,
- displayName,
- isRequestSent,
- onClickCancelRequest,
- onClickAcceptRequest,
- onClickRefuseRequest,
- onClickRequest,
-}: UserFriendRequestProps) => {
- return (
-
-
onClickRequest(userId)}
- >
-
- {profileImageUrl ? (
-
- ) : (
-
- )}
-
-
-
{displayName}
-
{isRequestSent ? "Pedido enviado" : "Pedido recebido"}
-
-
-
-
- {isRequestSent ? (
- onClickCancelRequest(userId)}
- >
-
-
- ) : (
- <>
- onClickAcceptRequest(userId)}
- >
-
-
- onClickRefuseRequest(userId)}
- >
-
-
- >
- )}
-
-
- );
-};
diff --git a/src/renderer/src/pages/user/user-block-modal.tsx b/src/renderer/src/pages/user/user-block-modal.tsx
new file mode 100644
index 00000000..311eb060
--- /dev/null
+++ b/src/renderer/src/pages/user/user-block-modal.tsx
@@ -0,0 +1,42 @@
+import { Button, Modal } from "@renderer/components";
+import * as styles from "./user.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 (
+ <>
+
+
+
{t("user_block_modal_text", { displayName })}
+
+
+ {t("block_user")}
+
+
+
+ {t("cancel")}
+
+
+
+
+ >
+ );
+};
diff --git a/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx b/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx
new file mode 100644
index 00000000..cfdb5d06
--- /dev/null
+++ b/src/renderer/src/pages/user/user-confirm-undo-friendship-modal.tsx
@@ -0,0 +1,40 @@
+import { Button, Modal } from "@renderer/components";
+import * as styles from "./user.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 (
+
+
+
{t("undo_friendship_modal_text", { displayName })}
+
+
+ {t("undo_friendship")}
+
+
+
+ {t("cancel")}
+
+
+
+
+ );
+}
diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx
index e70335d8..f4a46ccd 100644
--- a/src/renderer/src/pages/user/user-content.tsx
+++ b/src/renderer/src/pages/user/user-content.tsx
@@ -1,4 +1,9 @@
-import { UserGame, UserProfile } from "@types";
+import {
+ FriendRequestAction,
+ GameRunning,
+ UserGame,
+ UserProfile,
+} from "@types";
import cn from "classnames";
import * as styles from "./user.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
@@ -12,12 +17,24 @@ import {
useUserDetails,
} from "@renderer/hooks";
import { useNavigate } from "react-router-dom";
-import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
-import { PersonIcon, PlusIcon, TelescopeIcon } from "@primer/octicons-react";
+import {
+ buildGameDetailsPath,
+ profileBackgroundFromProfileImage,
+ steamUrlBuilder,
+} from "@renderer/helpers";
+import {
+ CheckCircleIcon,
+ PersonIcon,
+ PlusIcon,
+ TelescopeIcon,
+ XCircleIcon,
+} from "@primer/octicons-react";
import { Button, Link } from "@renderer/components";
-import { UserEditProfileModal } from "./user-edit-modal";
-import { UserSignOutModal } from "./user-signout-modal";
+import { UserProfileSettingsModal } from "./user-profile-settings-modal";
+import { UserSignOutModal } from "./user-sign-out-modal";
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
+import { UserBlockModal } from "./user-block-modal";
+import { UserConfirmUndoFriendshipModal } from "./user-confirm-undo-friendship-modal";
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
@@ -26,23 +43,34 @@ export interface ProfileContentProps {
updateUserProfile: () => Promise;
}
+type FriendAction = FriendRequestAction | ("BLOCK" | "UNDO" | "SEND");
+
export function UserContent({
userProfile,
updateUserProfile,
}: ProfileContentProps) {
const { t, i18n } = useTranslation("user_profile");
-
const {
userDetails,
profileBackground,
signOut,
- updateFriendRequests,
+ sendFriendRequest,
+ fetchFriendRequests,
showFriendsModal,
+ updateFriendRequestState,
+ undoFriendship,
+ blockUser,
} = useUserDetails();
- const { showSuccessToast } = useToast();
+ const { showSuccessToast, showErrorToast } = useToast();
- const [showEditProfileModal, setShowEditProfileModal] = useState(false);
+ const [profileContentBoxBackground, setProfileContentBoxBackground] =
+ useState();
+ const [showProfileSettingsModal, setShowProfileSettingsModal] =
+ useState(false);
const [showSignOutModal, setShowSignOutModal] = useState(false);
+ const [showUserBlockModal, setShowUserBlockModal] = useState(false);
+ const [showUndoFriendshipModal, setShowUndoFriendshipModal] = useState(false);
+ const [currentGame, setCurrentGame] = useState(null);
const { gameRunning } = useAppSelector((state) => state.gameRunning);
@@ -75,7 +103,7 @@ export function UserContent({
};
const handleEditProfile = () => {
- setShowEditProfileModal(true);
+ setShowProfileSettingsModal(true);
};
const handleOnClickFriend = (userId: string) => {
@@ -93,20 +121,156 @@ export function UserContent({
const isMe = userDetails?.id == userProfile.id;
useEffect(() => {
- if (isMe) updateFriendRequests();
- }, [isMe]);
+ if (isMe && gameRunning) {
+ setCurrentGame(gameRunning);
+ return;
+ }
- const profileContentBoxBackground = useMemo(() => {
- if (profileBackground) return profileBackground;
- /* TODO: Render background colors for other users */
- return undefined;
- }, [profileBackground]);
+ setCurrentGame(userProfile.currentGame);
+ }, [gameRunning, isMe, userProfile.currentGame]);
+
+ useEffect(() => {
+ if (isMe) fetchFriendRequests();
+ }, [isMe, fetchFriendRequests]);
+
+ useEffect(() => {
+ if (isMe && profileBackground) {
+ setProfileContentBoxBackground(profileBackground);
+ }
+
+ if (userProfile.profileImageUrl) {
+ profileBackgroundFromProfileImage(userProfile.profileImageUrl).then(
+ (profileBackground) => {
+ setProfileContentBoxBackground(profileBackground);
+ }
+ );
+ }
+ }, [profileBackground, isMe, userProfile.profileImageUrl]);
+
+ const handleFriendAction = (userId: string, action: FriendAction) => {
+ try {
+ if (action === "UNDO") {
+ undoFriendship(userId).then(updateUserProfile);
+ return;
+ }
+
+ if (action === "BLOCK") {
+ blockUser(userId).then(() => {
+ setShowUserBlockModal(false);
+ showSuccessToast(t("user_blocked_successfully"));
+ navigate(-1);
+ });
+
+ return;
+ }
+
+ if (action === "SEND") {
+ sendFriendRequest(userProfile.id).then(updateUserProfile);
+ return;
+ }
+
+ updateFriendRequestState(userId, action).then(updateUserProfile);
+ } catch (err) {
+ showErrorToast(t("try_again"));
+ }
+ };
+
+ const showFriends = isMe || userProfile.totalFriends > 0;
+ const showProfileContent =
+ isMe ||
+ userProfile.profileVisibility === "PUBLIC" ||
+ (userProfile.relation?.status === "ACCEPTED" &&
+ userProfile.profileVisibility === "FRIENDS");
+
+ const getProfileActions = () => {
+ if (isMe) {
+ return (
+ <>
+
+ {t("settings")}
+
+
+ setShowSignOutModal(true)}>
+ {t("sign_out")}
+
+ >
+ );
+ }
+
+ if (userProfile.relation == null) {
+ return (
+ <>
+ handleFriendAction(userProfile.id, "SEND")}
+ >
+ {t("add_friend")}
+
+
+ setShowUserBlockModal(true)}>
+ {t("block_user")}
+
+ >
+ );
+ }
+
+ if (userProfile.relation.status === "ACCEPTED") {
+ return (
+ <>
+ setShowUndoFriendshipModal(true)}
+ >
+ {t("undo_friendship")}
+
+ >
+ );
+ }
+
+ if (userProfile.relation.BId === userProfile.id) {
+ return (
+
+ handleFriendAction(userProfile.relation!.BId, "CANCEL")
+ }
+ >
+ {t("cancel_request")}
+
+ );
+ }
+
+ return (
+ <>
+
+ handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
+ }
+ >
+ {t("accept_request")}
+
+
+ handleFriendAction(userProfile.relation!.AId, "REFUSED")
+ }
+ >
+ {t("ignore_request")}
+
+ >
+ );
+ };
return (
<>
- setShowEditProfileModal(false)}
+ setShowProfileSettingsModal(false)}
updateUserProfile={updateUserProfile}
userProfile={userProfile}
/>
@@ -117,6 +281,20 @@ export function UserContent({
onConfirm={handleConfirmSignout}
/>
+ setShowUserBlockModal(false)}
+ onConfirm={() => handleFriendAction(userProfile.id, "BLOCK")}
+ displayName={userProfile.displayName}
+ />
+
+ setShowUndoFriendshipModal(false)}
+ onConfirm={() => handleFriendAction(userProfile.id, "UNDO")}
+ displayName={userProfile.displayName}
+ />
+
- {gameRunning && isMe && (
+ {currentGame && (
)}
@@ -154,8 +332,10 @@ export function UserContent({
-
{userProfile.displayName}
- {isMe && gameRunning && (
+
+ {userProfile.displayName}
+
+ {currentGame && (
-
- {gameRunning.title}
+
+ {currentGame.title}
{t("playing_for", {
amount: formatDiffInMillis(
- gameRunning.sessionDurationInMillis,
+ currentGame.sessionDurationInMillis,
new Date()
),
})}
@@ -187,154 +367,89 @@ export function UserContent({
)}
- {isMe && (
+
-
- <>
-
- {t("edit_profile")}
-
-
- setShowSignOutModal(true)}
- >
- {t("sign_out")}
-
- >
-
+ {getProfileActions()}
- )}
+
-
-
-
{t("activity")}
-
- {!userProfile.recentGames.length ? (
-
-
-
-
-
{t("no_recent_activity_title")}
- {isMe && (
-
- {t("no_recent_activity_description")}
-
- )}
-
- ) : (
-
- {userProfile.recentGames.map((game) => (
-
handleGameClick(game)}
- >
-
-
-
{game.title}
-
- {t("last_time_played", {
- period: formatDistance(
- game.lastTimePlayed!,
- new Date(),
- {
- addSuffix: true,
- }
- ),
- })}
-
-
-
- ))}
-
- )}
-
-
-
+ {showProfileContent && (
+
-
-
{t("library")}
+
{t("activity")}
+ {!userProfile.recentGames.length ? (
+
+
+
+
+
{t("no_recent_activity_title")}
+ {isMe &&
{t("no_recent_activity_description")}
}
+
+ ) : (
-
- {userProfile.libraryGames.length}
-
-
-
{t("total_play_time", { amount: formatPlayTime() })}
-
- {userProfile.libraryGames.map((game) => (
-
handleGameClick(game)}
- title={game.title}
- >
- {game.iconUrl ? (
+ >
+ {userProfile.recentGames.map((game) => (
+ handleGameClick(game)}
+ >
- ) : (
-
- )}
-
- ))}
-
+
+
{game.title}
+
+ {t("last_time_played", {
+ period: formatDistance(
+ game.lastTimePlayed!,
+ new Date(),
+ {
+ addSuffix: true,
+ }
+ ),
+ })}
+
+
+
+ ))}
+
+ )}
- {(isMe ||
- (userProfile.friends && userProfile.friends.length > 0)) && (
-
-
showFriendsModal(UserFriendModalTab.FriendsList)}
+
+
+
-
{t("friends")}
+
{t("library")}
- {userProfile.friends.length}
+ {userProfile.libraryGames.length}
-
-
+
+
+ {t("total_play_time", { amount: formatPlayTime() })}
+
- {userProfile.friends.map((friend) => {
- return (
-
handleOnClickFriend(friend.id)}
- >
-
- {friend.profileImageUrl ? (
-
- ) : (
-
- )}
-
-
-
- {friend.displayName}
-
-
- );
- })}
-
- {isMe && (
-
- showFriendsModal(UserFriendModalTab.AddFriend)
- }
+ {userProfile.libraryGames.map((game) => (
+ handleGameClick(game)}
+ title={game.title}
>
- {t("add")}
-
- )}
+ {game.iconUrl ? (
+
+ ) : (
+
+ )}
+
+ ))}
- )}
+
+ {showFriends && (
+
+
+ showFriendsModal(
+ UserFriendModalTab.FriendsList,
+ userProfile.id
+ )
+ }
+ >
+ {t("friends")}
+
+
+
+ {userProfile.totalFriends}
+
+
+
+
+ {userProfile.friends.map((friend) => {
+ return (
+
handleOnClickFriend(friend.id)}
+ >
+
+ {friend.profileImageUrl ? (
+
+ ) : (
+
+ )}
+
+
+
+ {friend.displayName}
+
+
+ );
+ })}
+
+ {isMe && (
+
+ showFriendsModal(
+ UserFriendModalTab.AddFriend,
+ userProfile.id
+ )
+ }
+ >
+ {t("add")}
+
+ )}
+
+
+ )}
+
-
+ )}
>
);
}
diff --git a/src/renderer/src/pages/user/user-edit-modal.tsx b/src/renderer/src/pages/user/user-edit-modal.tsx
deleted file mode 100644
index a22650ee..00000000
--- a/src/renderer/src/pages/user/user-edit-modal.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { Button, Modal, TextField } from "@renderer/components";
-import { UserProfile } from "@types";
-import * as styles from "./user.css";
-import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
-import { SPACING_UNIT } from "@renderer/theme.css";
-import { useEffect, useMemo, useState } from "react";
-import { useToast, useUserDetails } from "@renderer/hooks";
-import { useTranslation } from "react-i18next";
-
-export interface UserEditProfileModalProps {
- userProfile: UserProfile;
- visible: boolean;
- onClose: () => void;
- updateUserProfile: () => Promise
;
-}
-
-export const UserEditProfileModal = ({
- userProfile,
- visible,
- onClose,
- updateUserProfile,
-}: UserEditProfileModalProps) => {
- const { t } = useTranslation("user_profile");
-
- const [displayName, setDisplayName] = useState("");
- const [newImagePath, setNewImagePath] = useState(null);
- const [isSaving, setIsSaving] = useState(false);
-
- const { patchUser } = useUserDetails();
-
- const { showSuccessToast, showErrorToast } = useToast();
-
- useEffect(() => {
- setDisplayName(userProfile.displayName);
- }, [userProfile.displayName]);
-
- const handleChangeProfileAvatar = async () => {
- const { filePaths } = await window.electron.showOpenDialog({
- properties: ["openFile"],
- filters: [
- {
- name: "Image",
- extensions: ["jpg", "jpeg", "png", "webp"],
- },
- ],
- });
-
- if (filePaths && filePaths.length > 0) {
- const path = filePaths[0];
-
- setNewImagePath(path);
- }
- };
-
- const handleSaveProfile: React.FormEventHandler = async (
- event
- ) => {
- event.preventDefault();
- setIsSaving(true);
-
- patchUser(displayName, newImagePath)
- .then(async () => {
- await updateUserProfile();
- showSuccessToast(t("saved_successfully"));
- cleanFormAndClose();
- })
- .catch(() => {
- showErrorToast(t("try_again"));
- })
- .finally(() => {
- setIsSaving(false);
- });
- };
-
- const resetModal = () => {
- setDisplayName(userProfile.displayName);
- setNewImagePath(null);
- };
-
- const cleanFormAndClose = () => {
- resetModal();
- onClose();
- };
-
- const avatarUrl = useMemo(() => {
- if (newImagePath) return `local:${newImagePath}`;
- if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
- return null;
- }, [newImagePath, userProfile.profileImageUrl]);
-
- return (
- <>
-
-
-
- >
- );
-};
diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx
new file mode 100644
index 00000000..896d3684
--- /dev/null
+++ b/src/renderer/src/pages/user/user-profile-settings-modal/index.tsx
@@ -0,0 +1 @@
+export * from "./user-profile-settings-modal";
diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx
new file mode 100644
index 00000000..0790b725
--- /dev/null
+++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx
@@ -0,0 +1,118 @@
+import { SPACING_UNIT, vars } from "@renderer/theme.css";
+import { UserFriend } from "@types";
+import { useEffect, useRef, useState } from "react";
+import { useToast, useUserDetails } from "@renderer/hooks";
+import { useTranslation } from "react-i18next";
+import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item";
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+
+const pageSize = 12;
+
+export const UserEditProfileBlockList = () => {
+ const { t } = useTranslation("user_profile");
+ const { showErrorToast } = useToast();
+
+ const [page, setPage] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+ const [maxPage, setMaxPage] = useState(0);
+ const [blocks, setBlocks] = useState([]);
+ const listContainer = useRef(null);
+
+ const { unblockUser } = useUserDetails();
+
+ const loadNextPage = () => {
+ if (page > maxPage) return;
+ setIsLoading(true);
+ window.electron
+ .getUserBlocks(pageSize, page * pageSize)
+ .then((newPage) => {
+ if (page === 0) {
+ setMaxPage(newPage.totalBlocks / pageSize);
+ }
+
+ setBlocks([...blocks, ...newPage.blocks]);
+ setPage(page + 1);
+ })
+ .catch(() => {})
+ .finally(() => setIsLoading(false));
+ };
+
+ const handleScroll = () => {
+ const scrollTop = listContainer.current?.scrollTop || 0;
+ const scrollHeight = listContainer.current?.scrollHeight || 0;
+ const clientHeight = listContainer.current?.clientHeight || 0;
+ const maxScrollTop = scrollHeight - clientHeight;
+
+ if (scrollTop < maxScrollTop * 0.9 || isLoading) {
+ return;
+ }
+
+ loadNextPage();
+ };
+
+ useEffect(() => {
+ const container = listContainer.current;
+ container?.addEventListener("scroll", handleScroll);
+ return () => container?.removeEventListener("scroll", handleScroll);
+ }, [isLoading]);
+
+ const reloadList = () => {
+ setPage(0);
+ setMaxPage(0);
+ setBlocks([]);
+ loadNextPage();
+ };
+
+ useEffect(() => {
+ reloadList();
+ }, []);
+
+ const handleUnblock = (userId: string) => {
+ unblockUser(userId)
+ .then(() => {
+ reloadList();
+ })
+ .catch(() => {
+ showErrorToast(t("try_again"));
+ });
+ };
+
+ return (
+
+
+ {!isLoading && blocks.length === 0 &&
{t("no_blocked_users")}
}
+ {blocks.map((friend) => {
+ return (
+
+ );
+ })}
+ {isLoading && (
+
+ )}
+
+
+ );
+};
diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx
new file mode 100644
index 00000000..6ecdb8a1
--- /dev/null
+++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-edit-profile.tsx
@@ -0,0 +1,150 @@
+import { DeviceCameraIcon, PersonIcon } from "@primer/octicons-react";
+import { Button, SelectField, TextField } from "@renderer/components";
+import { useToast, useUserDetails } from "@renderer/hooks";
+import { UserProfile } from "@types";
+import { useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import * as styles from "../user.css";
+import { SPACING_UNIT } from "@renderer/theme.css";
+
+export interface UserEditProfileProps {
+ userProfile: UserProfile;
+ updateUserProfile: () => Promise;
+}
+
+export const UserEditProfile = ({
+ userProfile,
+ updateUserProfile,
+}: UserEditProfileProps) => {
+ const { t } = useTranslation("user_profile");
+
+ const [form, setForm] = useState({
+ displayName: userProfile.displayName,
+ profileVisibility: userProfile.profileVisibility,
+ imageProfileUrl: null as string | null,
+ });
+ const [isSaving, setIsSaving] = useState(false);
+
+ const { patchUser } = useUserDetails();
+
+ const { showSuccessToast, showErrorToast } = useToast();
+
+ const [profileVisibilityOptions, setProfileVisibilityOptions] = useState<
+ { value: string; label: string }[]
+ >([]);
+
+ useEffect(() => {
+ setProfileVisibilityOptions([
+ { value: "PUBLIC", label: t("public") },
+ { value: "FRIENDS", label: t("friends_only") },
+ { value: "PRIVATE", label: t("private") },
+ ]);
+ }, [t]);
+
+ const handleChangeProfileAvatar = async () => {
+ const { filePaths } = await window.electron.showOpenDialog({
+ properties: ["openFile"],
+ filters: [
+ {
+ name: "Image",
+ extensions: ["jpg", "jpeg", "png", "webp"],
+ },
+ ],
+ });
+
+ if (filePaths && filePaths.length > 0) {
+ const path = filePaths[0];
+
+ setForm({ ...form, imageProfileUrl: path });
+ }
+ };
+
+ const handleProfileVisibilityChange = (event) => {
+ setForm({
+ ...form,
+ profileVisibility: event.target.value,
+ });
+ };
+
+ const handleSaveProfile: React.FormEventHandler = async (
+ event
+ ) => {
+ event.preventDefault();
+ setIsSaving(true);
+
+ patchUser(form)
+ .then(async () => {
+ await updateUserProfile();
+ showSuccessToast(t("saved_successfully"));
+ })
+ .catch(() => {
+ showErrorToast(t("try_again"));
+ })
+ .finally(() => {
+ setIsSaving(false);
+ });
+ };
+
+ const avatarUrl = useMemo(() => {
+ if (form.imageProfileUrl) return `local:${form.imageProfileUrl}`;
+ if (userProfile.profileImageUrl) return userProfile.profileImageUrl;
+ return null;
+ }, [form, userProfile]);
+
+ return (
+
+ );
+};
diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx
new file mode 100644
index 00000000..d71b1bd7
--- /dev/null
+++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-profile-settings-modal.tsx
@@ -0,0 +1,73 @@
+import { Button, Modal } from "@renderer/components";
+import { UserProfile } from "@types";
+import { SPACING_UNIT } from "@renderer/theme.css";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { UserEditProfile } from "./user-edit-profile";
+import { UserEditProfileBlockList } from "./user-block-list";
+
+export interface UserProfileSettingsModalProps {
+ userProfile: UserProfile;
+ visible: boolean;
+ onClose: () => void;
+ updateUserProfile: () => Promise;
+}
+
+export const UserProfileSettingsModal = ({
+ userProfile,
+ visible,
+ onClose,
+ updateUserProfile,
+}: UserProfileSettingsModalProps) => {
+ const { t } = useTranslation("user_profile");
+
+ const tabs = [t("edit_profile"), t("blocked_users")];
+
+ const [currentTabIndex, setCurrentTabIndex] = useState(0);
+
+ const renderTab = () => {
+ if (currentTabIndex == 0) {
+ return (
+
+ );
+ }
+
+ if (currentTabIndex == 1) {
+ return ;
+ }
+
+ return <>>;
+ };
+
+ return (
+ <>
+
+
+
+ {tabs.map((tab, index) => {
+ return (
+ setCurrentTabIndex(index)}
+ >
+ {tab}
+
+ );
+ })}
+
+ {renderTab()}
+
+
+ >
+ );
+};
diff --git a/src/renderer/src/pages/user/user-signout-modal.tsx b/src/renderer/src/pages/user/user-sign-out-modal.tsx
similarity index 84%
rename from src/renderer/src/pages/user/user-signout-modal.tsx
rename to src/renderer/src/pages/user/user-sign-out-modal.tsx
index a91e8c9d..afc5561b 100644
--- a/src/renderer/src/pages/user/user-signout-modal.tsx
+++ b/src/renderer/src/pages/user/user-sign-out-modal.tsx
@@ -2,7 +2,7 @@ import { Button, Modal } from "@renderer/components";
import * as styles from "./user.css";
import { useTranslation } from "react-i18next";
-export interface UserEditProfileModalProps {
+export interface UserSignOutModalProps {
visible: boolean;
onConfirm: () => void;
onClose: () => void;
@@ -12,7 +12,7 @@ export const UserSignOutModal = ({
visible,
onConfirm,
onClose,
-}: UserEditProfileModalProps) => {
+}: UserSignOutModalProps) => {
const { t } = useTranslation("user_profile");
return (
@@ -23,7 +23,7 @@ export const UserSignOutModal = ({
onClose={onClose}
>
-
{t("sign_out_modal_text")}
+
{t("sign_out_modal_text")}
{t("sign_out")}
diff --git a/src/renderer/src/pages/user/user.css.ts b/src/renderer/src/pages/user/user.css.ts
index fb0aee21..6bcb30b0 100644
--- a/src/renderer/src/pages/user/user.css.ts
+++ b/src/renderer/src/pages/user/user.css.ts
@@ -23,6 +23,7 @@ export const profileContentBox = style({
export const profileAvatarContainer = style({
width: "96px",
+ minWidth: "96px",
height: "96px",
borderRadius: "50%",
display: "flex",
@@ -60,6 +61,7 @@ export const friendListDisplayName = style({
});
export const profileAvatarEditContainer = style({
+ alignSelf: "center",
width: "128px",
height: "128px",
display: "flex",
@@ -78,6 +80,8 @@ export const profileAvatar = style({
height: "100%",
width: "100%",
objectFit: "cover",
+ borderRadius: "50%",
+ overflow: "hidden",
});
export const profileAvatarEditOverlay = style({
@@ -97,6 +101,14 @@ export const profileInformation = style({
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({
@@ -277,3 +289,16 @@ export const profileBackground = style({
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,
+});
diff --git a/src/renderer/src/pages/user/user.tsx b/src/renderer/src/pages/user/user.tsx
index 4c45f789..565d412a 100644
--- a/src/renderer/src/pages/user/user.tsx
+++ b/src/renderer/src/pages/user/user.tsx
@@ -31,7 +31,7 @@ export const User = () => {
navigate(-1);
}
});
- }, [dispatch, userId, t]);
+ }, [dispatch, navigate, showErrorToast, userId, t]);
useEffect(() => {
getUserProfile();
diff --git a/src/shared/index.ts b/src/shared/index.ts
index 409bdbc9..28e7315b 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -1,6 +1,9 @@
export enum Downloader {
RealDebrid,
Torrent,
+ Gofile,
+ PixelDrain,
+ Qiwi,
}
export enum DownloadSourceStatus {
@@ -51,6 +54,8 @@ export const removeSpecialEditionFromName = (name: string) =>
export const removeDuplicateSpaces = (name: string) =>
name.replace(/\s{2,}/g, " ");
+export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
+
export const replaceUnderscoreWithSpace = (name: string) =>
name.replace(/_/g, " ");
@@ -58,8 +63,38 @@ export const formatName = pipe(
removeReleaseYearFromName,
removeSpecialEditionFromName,
replaceUnderscoreWithSpace,
+ replaceDotsWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
removeSymbolsFromName,
removeDuplicateSpaces,
(str) => str.trim()
);
+
+const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
+
+export const getDownloadersForUri = (uri: string) => {
+ if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
+
+ if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
+ if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
+
+ if (realDebridHosts.some((host) => uri.startsWith(host)))
+ return [Downloader.RealDebrid];
+
+ if (uri.startsWith("magnet:")) {
+ return [Downloader.Torrent, Downloader.RealDebrid];
+ }
+
+ return [];
+};
+
+export const getDownloadersForUris = (uris: string[]) => {
+ const downloadersSet = uris.reduce>((prev, next) => {
+ const downloaders = getDownloadersForUri(next);
+ downloaders.forEach((downloader) => prev.add(downloader));
+
+ return prev;
+ }, new Set());
+
+ return Array.from(downloadersSet);
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index 891c75f9..3260d274 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -67,7 +67,11 @@ export interface SteamAppDetails {
export interface GameRepack {
id: number;
title: string;
+ /**
+ * @deprecated Use uris instead
+ */
magnet: string;
+ uris: string[];
repacker: string;
fileSize: string | null;
uploadDate: Date | string | null;
@@ -137,9 +141,9 @@ export interface Game {
export type LibraryGame = Omit;
export interface GameRunning {
- id: number;
+ id?: number;
title: string;
- iconUrl: string;
+ iconUrl: string | null;
objectID: string;
shop: GameShop;
sessionDurationInMillis: number;
@@ -194,6 +198,7 @@ export interface StartGameDownloadPayload {
objectID: string;
title: string;
shop: GameShop;
+ uri: string;
downloadPath: string;
downloader: Downloader;
}
@@ -277,6 +282,16 @@ export interface UserFriend {
profileImageUrl: string | null;
}
+export interface UserFriends {
+ totalFriends: number;
+ friends: UserFriend[];
+}
+
+export interface UserBlocks {
+ totalBlocks: number;
+ blocks: UserFriend[];
+}
+
export interface FriendRequest {
id: string;
displayName: string;
@@ -284,14 +299,33 @@ export interface FriendRequest {
type: "SENT" | "RECEIVED";
}
+export interface UserRelation {
+ AId: string;
+ BId: string;
+ status: "ACCEPTED" | "PENDING";
+ createdAt: string;
+ updatedAt: string;
+}
+
export interface UserProfile {
id: string;
displayName: string;
profileImageUrl: string | null;
+ profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS";
totalPlayTimeInSeconds: number;
libraryGames: UserGame[];
recentGames: UserGame[];
friends: UserFriend[];
+ totalFriends: number;
+ relation: UserRelation | null;
+ currentGame: GameRunning | null;
+}
+
+export interface UpdateProfileProps {
+ displayName?: string;
+ profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
+ profileImageUrl?: string | null;
+ bio?: string;
}
export interface DownloadSource {
diff --git a/torrent-client/downloader.py b/torrent-client/downloader.py
deleted file mode 100644
index 142da020..00000000
--- a/torrent-client/downloader.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import libtorrent as lt
-
-class Downloader:
- def __init__(self, port: str):
- self.torrent_handles = {}
- self.downloading_game_id = -1
- self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
-
- def start_download(self, game_id: int, magnet: str, save_path: str):
- params = {'url': magnet, 'save_path': save_path}
- 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.resume()
-
- self.downloading_game_id = game_id
-
- def pause_download(self, game_id: int):
- torrent_handle = self.torrent_handles.get(game_id)
- if torrent_handle:
- torrent_handle.pause()
- torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
- self.downloading_game_id = -1
-
- def cancel_download(self, game_id: int):
- torrent_handle = self.torrent_handles.get(game_id)
- if torrent_handle:
- torrent_handle.pause()
- self.session.remove_torrent(torrent_handle)
- self.torrent_handles[game_id] = None
- self.downloading_game_id = -1
-
- def abort_session(self):
- for game_id in self.torrent_handles:
- torrent_handle = self.torrent_handles[game_id]
- torrent_handle.pause()
- self.session.remove_torrent(torrent_handle)
-
- self.session.abort()
- self.torrent_handles = {}
- self.downloading_game_id = -1
-
- def get_download_status(self):
- if self.downloading_game_id == -1:
- return None
-
- torrent_handle = self.torrent_handles.get(self.downloading_game_id)
-
- status = torrent_handle.status()
- info = torrent_handle.get_torrent_info()
-
- return {
- 'folderName': info.name() if info else "",
- 'fileSize': info.total_size() if info else 0,
- 'gameId': self.downloading_game_id,
- 'progress': status.progress,
- 'downloadSpeed': status.download_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,
- }
diff --git a/torrent-client/main.py b/torrent-client/main.py
index 004cd108..a2ea190b 100644
--- a/torrent-client/main.py
+++ b/torrent-client/main.py
@@ -3,23 +3,40 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import urllib.parse
import psutil
-from downloader import Downloader
+from torrent_downloader import TorrentDownloader
torrent_port = sys.argv[1]
http_port = sys.argv[2]
rpc_password = sys.argv[3]
start_download_payload = sys.argv[4]
-downloader = None
+torrent_downloader = None
if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
- downloader = Downloader(torrent_port)
- downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
+ torrent_downloader = TorrentDownloader(torrent_port)
+ torrent_downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
class Handler(BaseHTTPRequestHandler):
rpc_password_header = 'x-hydra-rpc-password'
+ skip_log_routes = [
+ "process-list",
+ "status"
+ ]
+
+ def log_error(self, format, *args):
+ sys.stderr.write("%s - - [%s] %s\n" %
+ (self.address_string(),
+ self.log_date_time_string(),
+ format%args))
+
+ def log_message(self, format, *args):
+ for route in self.skip_log_routes:
+ if route in args[0]: return
+
+ super().log_message(format, *args)
+
def do_GET(self):
if self.path == "/status":
if self.headers.get(self.rpc_password_header) != rpc_password:
@@ -31,7 +48,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-type", "application/json")
self.end_headers()
- status = downloader.get_download_status()
+ status = torrent_downloader.get_download_status()
self.wfile.write(json.dumps(status).encode('utf-8'))
@@ -54,7 +71,7 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(json.dumps(process_list).encode('utf-8'))
def do_POST(self):
- global downloader
+ global torrent_downloader
if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password:
@@ -66,18 +83,18 @@ class Handler(BaseHTTPRequestHandler):
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
- if downloader is None:
- downloader = Downloader(torrent_port)
+ if torrent_downloader is None:
+ torrent_downloader = TorrentDownloader(torrent_port)
if data['action'] == 'start':
- downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
+ torrent_downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
elif data['action'] == 'pause':
- downloader.pause_download(data['game_id'])
+ torrent_downloader.pause_download(data['game_id'])
elif data['action'] == 'cancel':
- downloader.cancel_download(data['game_id'])
+ torrent_downloader.cancel_download(data['game_id'])
elif data['action'] == 'kill-torrent':
- downloader.abort_session()
- downloader = None
+ torrent_downloader.abort_session()
+ torrent_downloader = None
self.send_response(200)
self.end_headers()
diff --git a/torrent-client/torrent_downloader.py b/torrent-client/torrent_downloader.py
new file mode 100644
index 00000000..d59cd28b
--- /dev/null
+++ b/torrent-client/torrent_downloader.py
@@ -0,0 +1,158 @@
+import libtorrent as lt
+
+class TorrentDownloader:
+ def __init__(self, port: str):
+ self.torrent_handles = {}
+ self.downloading_game_id = -1
+ self.session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=port)})
+ self.trackers = [
+ "udp://tracker.opentrackr.org:1337/announce",
+ "http://tracker.opentrackr.org:1337/announce",
+ "udp://open.tracker.cl:1337/announce",
+ "udp://open.demonii.com:1337/announce",
+ "udp://open.stealth.si:80/announce",
+ "udp://tracker.torrent.eu.org:451/announce",
+ "udp://exodus.desync.com:6969/announce",
+ "udp://tracker.theoks.net:6969/announce",
+ "udp://tracker-udp.gbitt.info:80/announce",
+ "udp://explodie.org:6969/announce",
+ "https://tracker.tamersunion.org:443/announce",
+ "udp://tracker2.dler.org:80/announce",
+ "udp://tracker1.myporn.club:9337/announce",
+ "udp://tracker.tiny-vps.com:6969/announce",
+ "udp://tracker.dler.org:6969/announce",
+ "udp://tracker.bittor.pw:1337/announce",
+ "udp://tracker.0x7c0.com:6969/announce",
+ "udp://retracker01-msk-virt.corbina.net:80/announce",
+ "udp://opentracker.io:6969/announce",
+ "udp://open.free-tracker.ga:6969/announce",
+ "udp://new-line.net:6969/announce",
+ "udp://moonburrow.club:6969/announce",
+ "udp://leet-tracker.moe:1337/announce",
+ "udp://bt2.archive.org:6969/announce",
+ "udp://bt1.archive.org:6969/announce",
+ "http://tracker2.dler.org:80/announce",
+ "http://tracker1.bt.moack.co.kr:80/announce",
+ "http://tracker.dler.org:6969/announce",
+ "http://tr.kxmp.cf:80/announce",
+ "udp://u.peer-exchange.download:6969/announce",
+ "udp://ttk2.nbaonlineservice.com:6969/announce",
+ "udp://tracker.tryhackx.org:6969/announce",
+ "udp://tracker.srv00.com:6969/announce",
+ "udp://tracker.skynetcloud.site:6969/announce",
+ "udp://tracker.jamesthebard.net:6969/announce",
+ "udp://tracker.fnix.net:6969/announce",
+ "udp://tracker.filemail.com:6969/announce",
+ "udp://tracker.farted.net:6969/announce",
+ "udp://tracker.edkj.club:6969/announce",
+ "udp://tracker.dump.cl:6969/announce",
+ "udp://tracker.deadorbit.nl:6969/announce",
+ "udp://tracker.darkness.services:6969/announce",
+ "udp://tracker.ccp.ovh:6969/announce",
+ "udp://tamas3.ynh.fr:6969/announce",
+ "udp://ryjer.com:6969/announce",
+ "udp://run.publictracker.xyz:6969/announce",
+ "udp://public.tracker.vraphim.com:6969/announce",
+ "udp://p4p.arenabg.com:1337/announce",
+ "udp://p2p.publictracker.xyz:6969/announce",
+ "udp://open.u-p.pw:6969/announce",
+ "udp://open.publictracker.xyz:6969/announce",
+ "udp://open.dstud.io:6969/announce",
+ "udp://open.demonoid.ch:6969/announce",
+ "udp://odd-hd.fr:6969/announce",
+ "udp://martin-gebhardt.eu:25/announce",
+ "udp://jutone.com:6969/announce",
+ "udp://isk.richardsw.club:6969/announce",
+ "udp://evan.im:6969/announce",
+ "udp://epider.me:6969/announce",
+ "udp://d40969.acod.regrucolo.ru:6969/announce",
+ "udp://bt.rer.lol:6969/announce",
+ "udp://amigacity.xyz:6969/announce",
+ "udp://1c.premierzal.ru:6969/announce",
+ "https://trackers.run:443/announce",
+ "https://tracker.yemekyedim.com:443/announce",
+ "https://tracker.renfei.net:443/announce",
+ "https://tracker.pmman.tech:443/announce",
+ "https://tracker.lilithraws.org:443/announce",
+ "https://tracker.imgoingto.icu:443/announce",
+ "https://tracker.cloudit.top:443/announce",
+ "https://tracker-zhuqiy.dgj055.icu:443/announce",
+ "http://tracker.renfei.net:8080/announce",
+ "http://tracker.mywaifu.best:6969/announce",
+ "http://tracker.ipv6tracker.org:80/announce",
+ "http://tracker.files.fm:6969/announce",
+ "http://tracker.edkj.club:6969/announce",
+ "http://tracker.bt4g.com:2095/announce",
+ "http://tracker-zhuqiy.dgj055.icu:80/announce",
+ "http://t1.aag.moe:17715/announce",
+ "http://t.overflow.biz:6969/announce",
+ "http://bittorrent-tracker.e-n-c-r-y-p-t.net:1337/announce",
+ "udp://torrents.artixlinux.org:6969/announce",
+ "udp://mail.artixlinux.org:6969/announce",
+ "udp://ipv4.rer.lol:2710/announce",
+ "udp://concen.org:6969/announce",
+ "udp://bt.rer.lol:2710/announce",
+ "udp://aegir.sexy:6969/announce",
+ "https://www.peckservers.com:9443/announce",
+ "https://tracker.ipfsscan.io:443/announce",
+ "https://tracker.gcrenwp.top:443/announce",
+ "http://www.peckservers.com:9000/announce",
+ "http://tracker1.itzmx.com:8080/announce",
+ "http://ch3oh.ru:6969/announce",
+ "http://bvarf.tracker.sh:2086/announce",
+ ]
+
+ def start_download(self, game_id: int, magnet: str, save_path: str):
+ 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.resume()
+
+ self.downloading_game_id = game_id
+
+ def pause_download(self, game_id: int):
+ torrent_handle = self.torrent_handles.get(game_id)
+ if torrent_handle:
+ torrent_handle.pause()
+ torrent_handle.unset_flags(lt.torrent_flags.auto_managed)
+ self.downloading_game_id = -1
+
+ def cancel_download(self, game_id: int):
+ torrent_handle = self.torrent_handles.get(game_id)
+ if torrent_handle:
+ torrent_handle.pause()
+ self.session.remove_torrent(torrent_handle)
+ self.torrent_handles[game_id] = None
+ self.downloading_game_id = -1
+
+ def abort_session(self):
+ for game_id in self.torrent_handles:
+ torrent_handle = self.torrent_handles[game_id]
+ torrent_handle.pause()
+ self.session.remove_torrent(torrent_handle)
+
+ self.session.abort()
+ self.torrent_handles = {}
+ self.downloading_game_id = -1
+
+ def get_download_status(self):
+ if self.downloading_game_id == -1:
+ return None
+
+ torrent_handle = self.torrent_handles.get(self.downloading_game_id)
+
+ status = torrent_handle.status()
+ info = torrent_handle.get_torrent_info()
+
+ return {
+ 'folderName': info.name() if info else "",
+ 'fileSize': info.total_size() if info else 0,
+ 'gameId': self.downloading_game_id,
+ 'progress': status.progress,
+ 'downloadSpeed': status.download_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,
+ }
diff --git a/yarn.lock b/yarn.lock
index e6b91b9e..91d9977f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -579,6 +579,13 @@
"@types/conventional-commits-parser" "^5.0.0"
chalk "^5.3.0"
+"@cspotcode/source-map-support@^0.8.0":
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+ integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+ dependencies:
+ "@jridgewell/trace-mapping" "0.3.9"
+
"@develar/schema-utils@~2.6.5":
version "2.6.5"
resolved "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz"
@@ -943,15 +950,10 @@
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
-"@fontsource/fira-mono@^5.0.13":
- version "5.0.13"
- resolved "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.0.13.tgz"
- integrity sha512-fZDjR2BdAqmauEbTjcIT62zYzbOgDa5+IQH34D2k8Pxmy1T815mAqQkZciWZVQ9dc/BgdTtTUV9HJ2ulBNwchg==
-
-"@fontsource/fira-sans@^5.0.20":
- version "5.0.20"
- resolved "https://registry.npmjs.org/@fontsource/fira-sans/-/fira-sans-5.0.20.tgz"
- integrity sha512-inmUjoKPrgnO4uUaZTAgP0b6YdhDfA52axHXvdTwgLvwd2kn3ZgK52UZoxD0VnrvTOjLA/iE4oC0tNtz4nyb5g==
+"@fontsource/noto-sans@^5.0.22":
+ version "5.0.22"
+ resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.0.22.tgz#2c5249347ba84fef16e71a58e0ec01b460174093"
+ integrity sha512-PwjvKPGFbgpwfKjWZj1zeUvd7ExUW2AqHE9PF9ysAJ2gOuzIHWE6mEVIlchYif7WC2pQhn+g0w6xooCObVi+4A==
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
@@ -1008,7 +1010,7 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.24"
-"@jridgewell/resolve-uri@^3.1.0":
+"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
@@ -1023,6 +1025,14 @@
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+"@jridgewell/trace-mapping@0.3.9":
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+ integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.0.3"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz"
@@ -1881,6 +1891,26 @@
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
+"@tsconfig/node10@^1.0.7":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
+ integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==
+
+"@tsconfig/node12@^1.0.7":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
+ integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
+
+"@tsconfig/node14@^1.0.0":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
+ integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
+
+"@tsconfig/node16@^1.0.2":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
+ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
+
"@types/accepts@*":
version "1.3.7"
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265"
@@ -2526,6 +2556,18 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+acorn-walk@^8.1.1:
+ version "8.3.3"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e"
+ integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==
+ dependencies:
+ acorn "^8.11.0"
+
+acorn@^8.11.0, acorn@^8.4.1:
+ version "8.12.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
+ integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+
acorn@^8.11.3, acorn@^8.9.0:
version "8.11.3"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
@@ -2665,6 +2707,11 @@ applescript@^1.0.0:
resolved "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz"
integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
@@ -2677,14 +2724,6 @@ aria-query@^5.3.0:
dependencies:
dequal "^2.0.3"
-aria2@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/aria2/-/aria2-4.1.2.tgz#0ecbc50beea82856c88b4de71dac336154f67362"
- integrity sha512-qTBr2RY8RZQmiUmbj2KXFvkErNxU4aTHZszszzwhE8svy2PEVX+IYR/c4Rp9Tuw4QkeU8cylGy6McV6Yl8i7Qw==
- dependencies:
- node-fetch "^2.6.1"
- ws "^7.4.0"
-
array-buffer-byte-length@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
@@ -2883,10 +2922,10 @@ bep53-range@^2.0.0:
resolved "https://registry.npmjs.org/bep53-range/-/bep53-range-2.0.0.tgz"
integrity sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==
-better-sqlite3@^9.5.0:
- version "9.6.0"
- resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz"
- integrity sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==
+better-sqlite3@^11.2.1:
+ version "11.2.1"
+ resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.2.1.tgz#3c6b8a8e2e12444d380e811796b59c8aba012e03"
+ integrity sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==
dependencies:
bindings "^1.5.0"
prebuild-install "^7.1.1"
@@ -3258,6 +3297,11 @@ color@^4.2.3:
color-convert "^2.0.1"
color-string "^1.9.0"
+colorette@2.0.19:
+ version "2.0.19"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
+ integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
@@ -3265,6 +3309,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
+commander@^10.0.0:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
commander@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz"
@@ -3376,6 +3425,11 @@ create-desktop-shortcuts@^1.11.0:
dependencies:
which "2.0.2"
+create-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
+ integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+
cross-fetch-ponyfill@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz"
@@ -3475,7 +3529,7 @@ dayjs@^1.11.9:
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
-debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -3579,6 +3633,11 @@ detect-node@^2.0.4:
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+diff@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+ integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
dir-compare@^3.0.0:
version "3.3.0"
resolved "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz"
@@ -3751,10 +3810,10 @@ electron-vite@^2.0.0:
magic-string "^0.30.5"
picocolors "^1.0.0"
-electron@^30.0.9:
- version "30.0.9"
- resolved "https://registry.npmjs.org/electron/-/electron-30.0.9.tgz"
- integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==
+electron@^30.3.0:
+ version "30.3.1"
+ resolved "https://registry.yarnpkg.com/electron/-/electron-30.3.1.tgz#fe27ca2a4739bec832b2edd6f46140ab46bf53a0"
+ integrity sha512-Ai/OZ7VlbFAVYMn9J5lyvtr+ZWyEbXDVd5wBLb5EVrp4352SRmMAmN5chcIe3n9mjzcgehV9n4Hwy15CJW+YbA==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@@ -4114,6 +4173,11 @@ eslint@^8.56.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
+esm@^3.2.25:
+ version "3.2.25"
+ resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
+ integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
+
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz"
@@ -4476,6 +4540,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
+get-package-type@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+ integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
get-stdin@^9.0.0:
version "9.0.0"
resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz"
@@ -4502,6 +4571,11 @@ get-symbol-description@^1.0.2:
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
+getopts@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
+ integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==
+
git-raw-commits@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz"
@@ -4922,6 +4996,11 @@ internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
+interpret@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+ integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
is-array-buffer@^3.0.4:
version "3.0.4"
resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz"
@@ -5163,11 +5242,6 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-iso-639-1@3.1.2:
- version "3.1.2"
- resolved "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.2.tgz"
- integrity sha512-Le7BRl3Jt9URvaiEHJCDEdvPZCfhiQoXnFgLAWNRhzFMwRFdWO7/5tLRQbiPzE394I9xd7KdRCM7S6qdOhwG5A==
-
iterator.prototype@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz"
@@ -5363,6 +5437,26 @@ keyv@^4.0.0, keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
+knex@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c"
+ integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==
+ dependencies:
+ colorette "2.0.19"
+ commander "^10.0.0"
+ debug "4.3.4"
+ escalade "^3.1.1"
+ esm "^3.2.25"
+ get-package-type "^0.1.0"
+ getopts "2.3.0"
+ interpret "^2.2.0"
+ lodash "^4.17.21"
+ pg-connection-string "2.6.2"
+ rechoir "^0.8.0"
+ resolve-from "^5.0.0"
+ tarn "^3.0.2"
+ tildify "2.0.0"
+
language-subtag-registry@^0.3.20:
version "0.3.22"
resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz"
@@ -5502,7 +5596,7 @@ lodash.upperfirst@^4.3.1:
resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz"
integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==
-lodash@^4.17.15:
+lodash@^4.17.15, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5580,6 +5674,11 @@ magnet-uri@^7.0.5:
bep53-range "^2.0.0"
uint8-util "^2.1.9"
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
matcher@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz"
@@ -5833,7 +5932,7 @@ node-domexception@^1.0.0:
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
-node-fetch@^2.6.1, node-fetch@^2.6.7:
+node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@@ -6127,6 +6226,11 @@ pend@~1.2.0:
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
+pg-connection-string@2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475"
+ integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==
+
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
@@ -6470,6 +6574,13 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+rechoir@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
+ integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
+ dependencies:
+ resolve "^1.20.0"
+
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz"
@@ -6567,7 +6678,7 @@ resolve-from@^5.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-resolve@^1.22.1:
+resolve@^1.20.0, resolve@^1.22.1:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
@@ -7096,6 +7207,11 @@ tar@^6.1.12:
mkdirp "^1.0.3"
yallist "^4.0.0"
+tarn@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693"
+ integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==
+
temp-file@^3.4.0:
version "3.4.0"
resolved "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz"
@@ -7133,6 +7249,11 @@ thenify-all@^1.0.0:
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+tildify@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
+ integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==
+
tiny-typed-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz"
@@ -7214,6 +7335,25 @@ ts-api-utils@^1.0.1:
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz"
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
+ts-node@^10.9.2:
+ version "10.9.2"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
+ integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
+ dependencies:
+ "@cspotcode/source-map-support" "^0.8.0"
+ "@tsconfig/node10" "^1.0.7"
+ "@tsconfig/node12" "^1.0.7"
+ "@tsconfig/node14" "^1.0.0"
+ "@tsconfig/node16" "^1.0.2"
+ acorn "^8.4.1"
+ acorn-walk "^8.1.1"
+ arg "^4.1.0"
+ create-require "^1.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ v8-compile-cache-lib "^3.0.1"
+ yn "3.1.1"
+
tslib@^2.0.3, tslib@^2.5.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
@@ -7425,6 +7565,11 @@ uuid@^9.0.0:
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
+v8-compile-cache-lib@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+ integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
verror@^1.10.0:
version "1.10.1"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb"
@@ -7629,11 +7774,6 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-ws@^7.4.0:
- version "7.5.10"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
- integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
-
ws@^8.16.0:
version "8.17.0"
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"
@@ -7723,6 +7863,11 @@ yauzl@^2.10.0:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"