- Hydra is a game launcher with its own embedded bittorrent client and a self-managed repack scraper.
+ Hydra is a game launcher with its own embedded bittorrent client.
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
@@ -50,17 +50,15 @@
## About
-**Hydra** is a **Game Launcher** with its own embedded **BitTorrent Client** and a **self-managed repack scraper**.
+**Hydra** is a **Game Launcher** with its own embedded **BitTorrent Client**.
The launcher is written in TypeScript (Electron) and Python, which handles the torrenting system by using libtorrent.
## Features
-- Self-Managed repack scraper among all the most reliable websites on the [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
- Own embedded bittorrent client
- How Long To Beat (HLTB) integration on game page
- Downloads path customization
-- Repack list update notifications
- Windows and Linux support
- Constantly updated
- And more ...
@@ -134,9 +132,8 @@ pip install -r requirements.txt
## Environment variables
You'll need an SteamGridDB API Key in order to fetch the game icons on installation.
-If you want to have onlinefix as a repacker you'll need to add your credentials to the .env
-Once you have it, you can copy or rename the `.env.example` file to `.env` and put it on`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
+Once you have it, you can copy or rename the `.env.example` file to `.env` and put it on`STEAMGRIDDB_API_KEY`.
## Running
diff --git a/package.json b/package.json
index c99a9bac..1b99734d 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@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",
diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json
index c756745a..9124af79 100644
--- a/src/locales/ca/translation.json
+++ b/src/locales/ca/translation.json
@@ -1,4 +1,7 @@
{
+ "app": {
+ "successfully_signed_in": "Has entrat correctament"
+ },
"home": {
"featured": "Destacats",
"trending": "Populars",
@@ -14,7 +17,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 +35,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 +55,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 +91,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 +130,7 @@
"paused": "Pausada",
"verifying": "S'està verificant…",
"completed": "Completada",
+ "removed": "No descarregat",
"cancel": "Cancel·la",
"filter": "Filtra els jocs baixats",
"remove": "Elimina",
@@ -106,7 +139,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 +159,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 +217,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/en/translation.json b/src/locales/en/translation.json
index e44dc7ea..b24509d3 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -241,6 +241,26 @@
"successfully_signed_out": "Successfully signed out",
"sign_out": "Sign out",
"playing_for": "Playing for {{amount}}",
- "sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?"
+ "sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?",
+ "add_friends": "Add Friends",
+ "add": "Add",
+ "friend_code": "Friend code",
+ "see_profile": "See profile",
+ "sending": "Sending",
+ "friend_request_sent": "Friend request sent",
+ "friends": "Friends",
+ "friends_list": "Friends list",
+ "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}}"
}
}
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index 5e016d34..fcb2b099 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -241,6 +241,15 @@
"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"
}
}
diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json
index bb7f452b..198aa568 100644
--- a/src/locales/id/translation.json
+++ b/src/locales/id/translation.json
@@ -1,134 +1,255 @@
{
+ "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/pt/translation.json b/src/locales/pt/translation.json
index 02c0879f..ef94c31f 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt/translation.json
@@ -12,11 +12,11 @@
"catalogue": "Catálogo",
"downloads": "Downloads",
"settings": "Ajustes",
- "my_library": "Minha biblioteca",
+ "my_library": "Biblioteca",
"downloading_metadata": "{{title}} (Baixando metadados…)",
"paused": "{{title}} (Pausado)",
"downloading": "{{title}} ({{percentage}} - Baixando…)",
- "filter": "Filtrar biblioteca",
+ "filter": "Buscar",
"home": "Início",
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
@@ -54,7 +54,7 @@
"calculating_eta": "Calculando tempo restante…",
"downloading_metadata": "Baixando metadados…",
"filter": "Filtrar repacks",
- "requirements": "Requisitos do sistema",
+ "requirements": "Requisitos de sistema",
"minimum": "Mínimos",
"recommended": "Recomendados",
"paused": "Pausado",
@@ -68,16 +68,16 @@
"add_to_library": "Adicionar à biblioteca",
"remove_from_library": "Remover da biblioteca",
"no_downloads": "Nenhum download disponível",
- "play_time": "Jogado por {{amount}}",
+ "play_time": "Jogou por {{amount}}",
"next_suggestion": "Próxima sugestão",
"install": "Instalar",
- "last_time_played": "Jogou por último {{period}}",
+ "last_time_played": "Última sessão {{period}}",
"play": "Jogar",
"not_played_yet": "Você ainda não jogou {{title}}",
"close": "Fechar",
"deleting": "Excluindo instalador…",
"playing_now": "Jogando agora",
- "change": "Mudar",
+ "change": "Explorar",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"select_folder_hint": "Para trocar o diretório padrão, acesse a <0>Tela de Ajustes0>",
"download_now": "Iniciar download",
@@ -90,13 +90,13 @@
"open_screenshot": "Ver captura de tela {{number}}",
"download_settings": "Ajustes do download",
"downloader": "Downloader",
- "select_executable": "Selecionar",
+ "select_executable": "Explorar",
"no_executable_selected": "Nenhum executável selecionado",
"open_folder": "Abrir pasta",
"open_download_location": "Ver arquivos baixados",
"create_shortcut": "Criar atalho na área de trabalho",
"remove_files": "Remover arquivos",
- "options": "Opções",
+ "options": "Gerenciar",
"remove_from_library_description": "Isso irá remover {{game}} da sua biblioteca",
"remove_from_library_title": "Tem certeza?",
"executable_section_title": "Executável",
@@ -120,7 +120,7 @@
"loading": "Carregando…"
},
"downloads": {
- "resume": "Resumir",
+ "resume": "Retomar",
"pause": "Pausar",
"eta": "Conclusão {{eta}}",
"paused": "Pausado",
@@ -146,12 +146,12 @@
},
"settings": {
"downloads_path": "Diretório dos downloads",
- "change": "Mudar",
+ "change": "Explorar...",
"notifications": "Notificações",
"enable_download_notifications": "Quando um download for concluído",
"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 ao invés de minimizá-lo ao fechar",
+ "quit_app_instead_hiding": "Encerrar o Hydra em vez de apenas minimizá-lo ao fechar.",
"launch_with_system": "Iniciar o Hydra junto com o sistema",
"general": "Geral",
"behavior": "Comportamento",
@@ -208,7 +208,7 @@
},
"binary_not_found_modal": {
"title": "Programas não instalados",
- "description": "Não foram encontrados no seu sistema os executáveis do Wine ou Lutris",
+ "description": "Os executáveis do Wine ou Lutris não foram encontrados em seu sistema.",
"instructions": "Verifique a forma correta de instalar algum deles no seu distro Linux, garantindo assim a execução normal do jogo"
},
"catalogue": {
@@ -224,8 +224,8 @@
"user_profile": {
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
- "last_time_played": "Jogou {{period}}",
- "activity": "Atividade recente",
+ "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",
@@ -233,7 +233,7 @@
"display_name": "Nome de exibição",
"saving": "Salvando…",
"save": "Salvar",
- "edit_profile": "Editar Perfil",
+ "edit_profile": "Editar perfil",
"saved_successfully": "Salvo com sucesso",
"try_again": "Por favor, tente novamente",
"cancel": "Cancelar",
@@ -241,6 +241,26 @@
"sign_out": "Sair da conta",
"sign_out_modal_title": "Tem certeza?",
"playing_for": "Jogando por {{amount}}",
- "sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?"
+ "sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?",
+ "add_friends": "Adicionar Amigos",
+ "friend_code": "Código de amigo",
+ "see_profile": "Ver perfil",
+ "friend_request_sent": "Pedido de amizade enviado",
+ "friends": "Amigos",
+ "add": "Adicionar",
+ "sending": "Enviando",
+ "friends_list": "Lista de amigos",
+ "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}}"
}
}
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 1b500be9..57daf51c 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -43,8 +43,15 @@ import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
import "./user/get-user";
+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";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());
diff --git a/src/main/events/profile/get-friend-requests.ts b/src/main/events/profile/get-friend-requests.ts
new file mode 100644
index 00000000..11d8a884
--- /dev/null
+++ b/src/main/events/profile/get-friend-requests.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { FriendRequest } from "@types";
+
+const getFriendRequests = async (
+ _event: Electron.IpcMainInvokeEvent
+): Promise => {
+ return HydraApi.get(`/profile/friend-requests`).catch(() => []);
+};
+
+registerEvent("getFriendRequests", getFriendRequests);
diff --git a/src/main/events/profile/get-me.ts b/src/main/events/profile/get-me.ts
index 83463680..1626125b 100644
--- a/src/main/events/profile/get-me.ts
+++ b/src/main/events/profile/get-me.ts
@@ -9,9 +9,7 @@ const getMe = async (
_event: Electron.IpcMainInvokeEvent
): Promise => {
return HydraApi.get(`/profile/me`)
- .then((response) => {
- const me = response.data;
-
+ .then((me) => {
userAuthRepository.upsert(
{
id: 1,
@@ -26,12 +24,18 @@ const getMe = async (
return me;
})
- .catch((err) => {
+ .catch(async (err) => {
if (err instanceof UserNotLoggedInError) {
return null;
}
- return userAuthRepository.findOne({ where: { id: 1 } });
+ const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
+
+ if (loggedUser) {
+ return { ...loggedUser, id: loggedUser.userId };
+ }
+
+ return null;
});
};
diff --git a/src/main/events/profile/send-friend-request.ts b/src/main/events/profile/send-friend-request.ts
new file mode 100644
index 00000000..d696606f
--- /dev/null
+++ b/src/main/events/profile/send-friend-request.ts
@@ -0,0 +1,11 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+
+const sendFriendRequest = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string
+) => {
+ return HydraApi.post("/profile/friend-requests", { friendCode: userId });
+};
+
+registerEvent("sendFriendRequest", sendFriendRequest);
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-friend-request.ts b/src/main/events/profile/update-friend-request.ts
new file mode 100644
index 00000000..24929544
--- /dev/null
+++ b/src/main/events/profile/update-friend-request.ts
@@ -0,0 +1,19 @@
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { FriendRequestAction } from "@types";
+
+const updateFriendRequest = async (
+ _event: Electron.IpcMainInvokeEvent,
+ userId: string,
+ action: FriendRequestAction
+) => {
+ if (action == "CANCEL") {
+ return HydraApi.delete(`/profile/friend-requests/${userId}`);
+ }
+
+ return HydraApi.patch(`/profile/friend-requests/${userId}`, {
+ requestState: action,
+ });
+};
+
+registerEvent("updateFriendRequest", updateFriendRequest);
diff --git a/src/main/events/profile/update-profile.ts b/src/main/events/profile/update-profile.ts
index fe79d345..8620eaa1 100644
--- a/src/main/events/profile/update-profile.ts
+++ b/src/main/events/profile/update-profile.ts
@@ -26,11 +26,9 @@ const updateProfile = async (
_event: Electron.IpcMainInvokeEvent,
displayName: string,
newProfileImagePath: string | null
-) => {
+): Promise => {
if (!newProfileImagePath) {
- return patchUserProfile(displayName).then(
- (response) => response.data as UserProfile
- );
+ return patchUserProfile(displayName);
}
const stats = fs.statSync(newProfileImagePath);
@@ -42,7 +40,7 @@ const updateProfile = async (
imageLength: fileSizeInBytes,
})
.then(async (preSignedResponse) => {
- const { presignedUrl, profileImageUrl } = preSignedResponse.data;
+ const { presignedUrl, profileImageUrl } = preSignedResponse;
const mimeType = await fileTypeFromFile(newProfileImagePath);
@@ -51,13 +49,11 @@ const updateProfile = async (
"Content-Type": mimeType?.mime,
},
});
- return profileImageUrl;
+ return profileImageUrl as string;
})
.catch(() => undefined);
- return patchUserProfile(displayName, profileImageUrl).then(
- (response) => response.data as UserProfile
- );
+ return patchUserProfile(displayName, profileImageUrl);
};
registerEvent("updateProfile", updateProfile);
diff --git a/src/main/events/user/block-user.ts b/src/main/events/user/block-user.ts
new file mode 100644
index 00000000..8003f478
--- /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(`/user/${userId}/block`);
+};
+
+registerEvent("blockUser", blockUser);
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..28783459
--- /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(`/user/${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 596df084..eb4f0619 100644
--- a/src/main/events/user/get-user.ts
+++ b/src/main/events/user/get-user.ts
@@ -4,14 +4,19 @@ import { steamGamesWorker } from "@main/workers";
import { 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 response = await HydraApi.get(`/user/${userId}`);
- const profile = response.data;
+ const [profile, friends] = await Promise.all([
+ HydraApi.get(`/user/${userId}`),
+ getUserFriends(userId, 12, 0).catch(() => {
+ return { totalFriends: 0, friends: [] };
+ }),
+ ]);
const recentGames = await Promise.all(
profile.recentGames.map(async (game) => {
@@ -47,7 +52,13 @@ const getUser = async (
})
);
- return { ...profile, libraryGames, recentGames };
+ return {
+ ...profile,
+ libraryGames,
+ recentGames,
+ friends: friends.friends,
+ totalFriends: friends.totalFriends,
+ };
} catch (err) {
return null;
}
diff --git a/src/main/events/user/unblock-user.ts b/src/main/events/user/unblock-user.ts
new file mode 100644
index 00000000..ac678dbd
--- /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(`/user/${userId}/unblock`);
+};
+
+registerEvent("unblockUser", unblockUser);
diff --git a/src/main/index.ts b/src/main/index.ts
index e288302b..9ff74bf6 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -20,6 +20,8 @@ autoUpdater.setFeedURL({
autoUpdater.logger = logger;
+logger.log("Init Hydra");
+
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
@@ -121,6 +123,7 @@ 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/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 98b783f3..5365bd9e 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -10,7 +10,7 @@ import { UserNotLoggedInError } from "@shared";
export class HydraApi {
private static instance: AxiosInstance;
- private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
+ private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
@@ -45,6 +45,8 @@ export class HydraApi {
expirationTimestamp: tokenExpirationTimestamp,
};
+ logger.log("Sign in received", this.userAuth);
+
await userAuthRepository.upsert(
{
id: 1,
@@ -70,11 +72,11 @@ 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) => {
- logger.log("request error", error);
+ logger.error("request error", error);
return Promise.reject(error);
}
);
@@ -95,12 +97,18 @@ export class HydraApi {
const { config } = error;
- logger.error(config.method, config.baseURL, config.url, config.headers);
+ logger.error(
+ config.method,
+ config.baseURL,
+ config.url,
+ config.headers,
+ config.data
+ );
if (error.response) {
- logger.error(error.response.status, error.response.data);
+ logger.error("Response", error.response.status, error.response.data);
} else if (error.request) {
- logger.error(error.request);
+ logger.error("Request", error.request);
} else {
logger.error("Error", error.message);
}
@@ -146,6 +154,8 @@ export class HydraApi {
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
+ logger.log("Token refreshed", this.userAuth);
+
userAuthRepository.upsert(
{
id: 1,
@@ -170,6 +180,8 @@ export class HydraApi {
private static handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) {
+ logger.error("401 - Current credentials:", this.userAuth);
+
this.userAuth = {
authToken: "",
expirationTimestamp: 0,
@@ -184,48 +196,53 @@ 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 c0e8b1f8..b66a1897 100644
--- a/src/main/services/library-sync/create-game.ts
+++ b/src/main/services/library-sync/create-game.ts
@@ -10,11 +10,7 @@ export const createGame = async (game: Game) => {
lastTimePlayed: game.lastTimePlayed,
})
.then((response) => {
- const {
- id: remoteId,
- playTimeInMilliseconds,
- lastTimePlayed,
- } = response.data;
+ const { id: remoteId, playTimeInMilliseconds, lastTimePlayed } = response;
gameRepository.update(
{ objectID: game.objectID },
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 2162ea58..2a6b5bb5 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -6,7 +6,7 @@ import { getSteamAppAsset } from "@main/helpers";
export const mergeWithRemoteGames = async () => {
return HydraApi.get("/games")
.then(async (response) => {
- for (const game of response.data) {
+ for (const game of response) {
const localGame = await gameRepository.findOne({
where: {
objectID: game.objectId,
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 0cadbc03..3350a340 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -9,6 +9,7 @@ import type {
AppUpdaterEvent,
StartGameDownloadPayload,
GameRunning,
+ FriendRequestAction,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
@@ -134,11 +135,22 @@ contextBridge.exposeInMainWorld("electron", {
/* Profile */
getMe: () => ipcRenderer.invoke("getMe"),
+ undoFriendship: (userId: string) =>
+ ipcRenderer.invoke("undoFriendship", userId),
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
+ getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
+ updateFriendRequest: (userId: string, action: FriendRequestAction) =>
+ ipcRenderer.invoke("updateFriendRequest", userId, action),
+ sendFriendRequest: (userId: string) =>
+ ipcRenderer.invoke("sendFriendRequest", userId),
/* 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),
/* Auth */
signOut: () => ipcRenderer.invoke("signOut"),
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 543b85a9..d7abf3ad 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -6,7 +6,7 @@
Hydra
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index afce9622..8c6f7604 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -25,6 +25,7 @@ import {
setGameRunning,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
+import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
export interface AppProps {
children: React.ReactNode;
@@ -38,7 +39,15 @@ export function App() {
const { clearDownload, setLastPacket } = useDownload();
- const { fetchUserDetails, updateUserDetails, clearUserDetails } =
+ const {
+ isFriendsModalVisible,
+ friendRequetsModalTab,
+ friendModalUserId,
+ fetchFriendRequests,
+ hideFriendsModal,
+ } = useUserDetails();
+
+ const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
const dispatch = useAppDispatch();
@@ -94,7 +103,10 @@ export function App() {
}
fetchUserDetails().then((response) => {
- if (response) updateUserDetails(response);
+ if (response) {
+ updateUserDetails(response);
+ fetchFriendRequests();
+ }
});
}, [fetchUserDetails, updateUserDetails, dispatch]);
@@ -102,6 +114,7 @@ export function App() {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
+ fetchFriendRequests();
showSuccessToast(t("successfully_signed_in"));
}
});
@@ -206,6 +219,15 @@ export function App() {
onClose={handleToastClose}
/>
+ {userDetails && (
+
+ )}
+
diff --git a/src/renderer/src/components/sidebar/sidebar-profile.css.ts b/src/renderer/src/components/sidebar/sidebar-profile.css.ts
index d01b07f1..ba29c850 100644
--- a/src/renderer/src/components/sidebar/sidebar-profile.css.ts
+++ b/src/renderer/src/components/sidebar/sidebar-profile.css.ts
@@ -1,7 +1,18 @@
-import { style } from "@vanilla-extract/css";
+import { createVar, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
+export const profileContainerBackground = createVar();
+
+export const profileContainer = style({
+ background: profileContainerBackground,
+ position: "relative",
+ cursor: "pointer",
+ ":hover": {
+ backgroundColor: "rgba(255, 255, 255, 0.15)",
+ },
+});
+
export const profileButton = style({
display: "flex",
cursor: "pointer",
@@ -10,9 +21,8 @@ export const profileButton = style({
color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
- ":hover": {
- backgroundColor: "rgba(255, 255, 255, 0.15)",
- },
+ width: "100%",
+ zIndex: "10",
});
export const profileButtonContent = style({
@@ -64,3 +74,25 @@ export const profileButtonTitle = style({
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});
+
+export const friendRequestContainer = style({
+ position: "absolute",
+ padding: "8px",
+ right: `${SPACING_UNIT}px`,
+ display: "flex",
+ top: 0,
+ bottom: 0,
+ alignItems: "center",
+});
+
+export const friendRequestButton = style({
+ color: vars.color.success,
+ cursor: "pointer",
+ borderRadius: "50%",
+ overflow: "hidden",
+ width: "40px",
+ height: "40px",
+ ":hover": {
+ color: vars.color.muted,
+ },
+});
diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx
index 914481b0..81736e37 100644
--- a/src/renderer/src/components/sidebar/sidebar-profile.tsx
+++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx
@@ -1,17 +1,29 @@
import { useNavigate } from "react-router-dom";
-import { PersonIcon } from "@primer/octicons-react";
+import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css";
-
+import { assignInlineVars } from "@vanilla-extract/dynamic";
import { useAppSelector, useUserDetails } from "@renderer/hooks";
-import { useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
+import { profileContainerBackground } from "./sidebar-profile.css";
+import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
+import { FriendRequest } from "@types";
export function SidebarProfile() {
const navigate = useNavigate();
const { t } = useTranslation("sidebar");
- const { userDetails, profileBackground } = useUserDetails();
+ const { userDetails, profileBackground, friendRequests, showFriendsModal } =
+ useUserDetails();
+
+ const [receivedRequests, setReceivedRequests] = useState([]);
+
+ useEffect(() => {
+ setReceivedRequests(
+ friendRequests.filter((request) => request.type === "RECEIVED")
+ );
+ }, [friendRequests]);
const { gameRunning } = useAppSelector((state) => state.gameRunning);
@@ -30,46 +42,66 @@ export function SidebarProfile() {
}, [profileBackground]);
return (
-