mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' into feature/adding-generic-http-downloads
This commit is contained in:
commit
070340b34f
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"app": {
|
||||||
|
"successfully_signed_in": "Has entrat correctament"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destacats",
|
"featured": "Destacats",
|
||||||
"trending": "Populars",
|
"trending": "Populars",
|
||||||
@ -14,7 +17,10 @@
|
|||||||
"paused": "{{title}} (Pausat)",
|
"paused": "{{title}} (Pausat)",
|
||||||
"downloading": "{{title}} ({{percentage}} - S'està baixant…)",
|
"downloading": "{{title}} ({{percentage}} - S'està baixant…)",
|
||||||
"filter": "Filtra la biblioteca",
|
"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": {
|
"header": {
|
||||||
"search": "Cerca jocs",
|
"search": "Cerca jocs",
|
||||||
@ -29,7 +35,9 @@
|
|||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Cap baixada en curs",
|
"no_downloads_in_progress": "Cap baixada en curs",
|
||||||
"downloading_metadata": "S'estan baixant les metadades de: {{title}}…",
|
"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": {
|
"catalogue": {
|
||||||
"next_page": "Pàgina següent",
|
"next_page": "Pàgina següent",
|
||||||
@ -47,12 +55,14 @@
|
|||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"remove": "Elimina",
|
"remove": "Elimina",
|
||||||
"space_left_on_disk": "{{space}} lliures al disc",
|
"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…",
|
"downloading_metadata": "S'estan baixant les metadades…",
|
||||||
"filter": "Filtra els reempaquetats",
|
"filter": "Filtra els reempaquetats",
|
||||||
"requirements": "Requisits del sistema",
|
"requirements": "Requisits del sistema",
|
||||||
"minimum": "Mínims",
|
"minimum": "Mínims",
|
||||||
"recommended": "Recomanats",
|
"recommended": "Recomanats",
|
||||||
|
"paused": "Paused",
|
||||||
"release_date": "Publicat el {{date}}",
|
"release_date": "Publicat el {{date}}",
|
||||||
"publisher": "Publicat per {{publisher}}",
|
"publisher": "Publicat per {{publisher}}",
|
||||||
"hours": "hores",
|
"hours": "hores",
|
||||||
@ -81,7 +91,29 @@
|
|||||||
"previous_screenshot": "Captura anterior",
|
"previous_screenshot": "Captura anterior",
|
||||||
"next_screenshot": "Captura següent",
|
"next_screenshot": "Captura següent",
|
||||||
"screenshot": "Captura {{number}}",
|
"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": {
|
"activation": {
|
||||||
"title": "Activa l'Hydra",
|
"title": "Activa l'Hydra",
|
||||||
@ -98,6 +130,7 @@
|
|||||||
"paused": "Pausada",
|
"paused": "Pausada",
|
||||||
"verifying": "S'està verificant…",
|
"verifying": "S'està verificant…",
|
||||||
"completed": "Completada",
|
"completed": "Completada",
|
||||||
|
"removed": "No descarregat",
|
||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"filter": "Filtra els jocs baixats",
|
"filter": "Filtra els jocs baixats",
|
||||||
"remove": "Elimina",
|
"remove": "Elimina",
|
||||||
@ -106,7 +139,14 @@
|
|||||||
"delete": "Elimina l'instal·lador",
|
"delete": "Elimina l'instal·lador",
|
||||||
"delete_modal_title": "N'estàs segur?",
|
"delete_modal_title": "N'estàs segur?",
|
||||||
"delete_modal_description": "S'eliminaran de l'ordinador tots els fitxers d'instal·lació",
|
"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": {
|
"settings": {
|
||||||
"downloads_path": "Ruta de baixades",
|
"downloads_path": "Ruta de baixades",
|
||||||
@ -119,16 +159,49 @@
|
|||||||
"launch_with_system": "Inicia l'Hydra quan s'iniciï el sistema",
|
"launch_with_system": "Inicia l'Hydra quan s'iniciï el sistema",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Comportament",
|
"behavior": "Comportament",
|
||||||
|
"download_sources": "Fonts de descàrrega",
|
||||||
|
"language": "Idioma",
|
||||||
|
"real_debrid_api_token": "Testimoni API",
|
||||||
"enable_real_debrid": "Activa el Real Debrid",
|
"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>.",
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "La baixada ha finalitzat",
|
"download_complete": "La baixada ha finalitzat",
|
||||||
"game_ready_to_install": "{{title}} ja es pot instal·lar",
|
"game_ready_to_install": "{{title}} ja es pot instal·lar",
|
||||||
"repack_list_updated": "S'ha actualitzat la llista de reempaquetats",
|
"repack_list_updated": "S'ha actualitzat la llista de reempaquetats",
|
||||||
"repack_count_one": "S'ha afegit {{count}} reempaquetat",
|
"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": {
|
"system_tray": {
|
||||||
"open": "Obre l'Hydra",
|
"open": "Obre l'Hydra",
|
||||||
@ -144,5 +217,39 @@
|
|||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Botó de tancar"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +250,17 @@
|
|||||||
"friend_request_sent": "Friend request sent",
|
"friend_request_sent": "Friend request sent",
|
||||||
"friends": "Friends",
|
"friends": "Friends",
|
||||||
"friends_list": "Friends list",
|
"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}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,6 +241,15 @@
|
|||||||
"successfully_signed_out": "Sesión cerrada exitosamente",
|
"successfully_signed_out": "Sesión cerrada exitosamente",
|
||||||
"sign_out": "Cerrar sesión",
|
"sign_out": "Cerrar sesión",
|
||||||
"playing_for": "Jugando por {{amount}}",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,134 +1,255 @@
|
|||||||
{
|
{
|
||||||
|
"app": {
|
||||||
|
"successfully_signed_in": "Berhasil masuk"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Unggulan",
|
"featured": "Unggulan",
|
||||||
"trending": "Trending",
|
"trending": "Sedang Tren",
|
||||||
"surprise_me": "Kejutkan Saya",
|
"surprise_me": "Kejutkan saya",
|
||||||
"no_results": "Tidak ada hasil"
|
"no_results": "Tidak ada hasil ditemukan"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Unduhan",
|
"downloads": "Unduhan",
|
||||||
"settings": "Pengaturan",
|
"settings": "Pengaturan",
|
||||||
"my_library": "Koleksi saya",
|
"my_library": "Perpustakaan saya",
|
||||||
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
|
"downloading_metadata": "{{title}} (Mengunduh metadata…)",
|
||||||
"paused": "{{title}} (Terhenti)",
|
"paused": "{{title}} (Dijeda)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
"downloading": "{{title}} ({{percentage}} - Mengunduh…)",
|
||||||
"filter": "Filter koleksi",
|
"filter": "Filter perpustakaan",
|
||||||
"home": "Beranda"
|
"home": "Beranda",
|
||||||
|
"queued": "{{title}} (Antrian)",
|
||||||
|
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
|
||||||
|
"sign_in": "Masuk"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Pencarian",
|
"search": "Cari game",
|
||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Unduhan",
|
"downloads": "Unduhan",
|
||||||
"search_results": "Hasil pencarian",
|
"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": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Tidak ada unduhan berjalan",
|
"no_downloads_in_progress": "Tidak ada unduhan yang sedang berjalan",
|
||||||
"downloading_metadata": "Mengunduh metadata {{title}}...",
|
"downloading_metadata": "Mengunduh metadata {{title}}…",
|
||||||
"downloading": "Mengunduh {{title}}… ({{percentage}} selesai) - Perkiraan {{eta}} - {{speed}}"
|
"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": {
|
"catalogue": {
|
||||||
"next_page": "Halaman berikutnya",
|
"next_page": "Halaman Berikutnya",
|
||||||
"previous_page": "Halaman sebelumnya"
|
"previous_page": "Halaman Sebelumnya"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Buka opsi unduhan",
|
"open_download_options": "Buka opsi unduhan",
|
||||||
"download_options_zero": "Tidak ada opsi unduhan",
|
"download_options_zero": "Tidak ada opsi unduhan",
|
||||||
"download_options_one": "{{count}} opsi unduhan",
|
"download_options_one": "{{count}} opsi unduhan",
|
||||||
"download_options_other": "{{count}} opsi unduhan",
|
"download_options_other": "{{count}} opsi unduhan",
|
||||||
"updated_at": "Diperbarui {{updated_at}}",
|
"updated_at": "Diperbarui pada {{updated_at}}",
|
||||||
"install": "Install",
|
"install": "Instal",
|
||||||
"resume": "Lanjutkan",
|
"resume": "Lanjutkan",
|
||||||
"pause": "Hentikan sementara",
|
"pause": "Jeda",
|
||||||
"cancel": "Batalkan",
|
"cancel": "Batal",
|
||||||
"remove": "Hapus",
|
"remove": "Hapus",
|
||||||
"space_left_on_disk": "{{space}} tersisa pada disk",
|
"space_left_on_disk": "{{space}} tersisa di disk",
|
||||||
"eta": "Perkiraan {{eta}}",
|
"eta": "Estimasi {{eta}}",
|
||||||
|
"calculating_eta": "Menghitung waktu yang tersisa…",
|
||||||
"downloading_metadata": "Mengunduh metadata…",
|
"downloading_metadata": "Mengunduh metadata…",
|
||||||
"filter": "Saring repacks",
|
"filter": "Filter repack",
|
||||||
"requirements": "Keperluan sistem",
|
"requirements": "Persyaratan sistem",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"recommended": "Rekomendasi",
|
"recommended": "Dianjurkan",
|
||||||
|
"paused": "Dijeda",
|
||||||
"release_date": "Dirilis pada {{date}}",
|
"release_date": "Dirilis pada {{date}}",
|
||||||
"publisher": "Dipublikasikan oleh {{publisher}}",
|
"publisher": "Diterbitkan oleh {{publisher}}",
|
||||||
"hours": "jam",
|
"hours": "jam",
|
||||||
"minutes": "menit",
|
"minutes": "menit",
|
||||||
"amount_hours": "{{amount}} jam",
|
"amount_hours": "{{amount}} jam",
|
||||||
"amount_minutes": "{{amount}} menit",
|
"amount_minutes": "{{amount}} menit",
|
||||||
"accuracy": "{{accuracy}}% akurasi",
|
"accuracy": "{{accuracy}}% akurasi",
|
||||||
"add_to_library": "Tambahkan ke koleksi",
|
"add_to_library": "Tambah ke perpustakaan",
|
||||||
"remove_from_library": "Hapus dari koleksi",
|
"remove_from_library": "Hapus dari perpustakaan",
|
||||||
"no_downloads": "Tidak ada unduhan tersedia",
|
"no_downloads": "Tidak ada yang bisa diunduh",
|
||||||
"play_time": "Dimainkan selama {{amount}}",
|
"play_time": "Dimainkan selama {{amount}}",
|
||||||
"last_time_played": "Terakhir dimainkan {{period}}",
|
"last_time_played": "Terakhir dimainkan {{period}}",
|
||||||
"not_played_yet": "Kamu belum memainkan {{title}}",
|
"not_played_yet": "Kamu belum memainkan {{title}}",
|
||||||
"next_suggestion": "Rekomendasi berikutnya",
|
"next_suggestion": "Saran berikutnya",
|
||||||
"play": "Mainkan",
|
"play": "Main",
|
||||||
"deleting": "Menghapus installer…",
|
"deleting": "Menghapus installer…",
|
||||||
"close": "Tutup",
|
"close": "Tutup",
|
||||||
"playing_now": "Memainkan sekarang",
|
"playing_now": "Sedang dimainkan",
|
||||||
"change": "Ubah",
|
"change": "Ubah",
|
||||||
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
"repacks_modal_description": "Pilih repack yang ingin kamu unduh",
|
||||||
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
"select_folder_hint": "Untuk ganti folder default, buka <0>Pengaturan</0>",
|
||||||
"download_now": "Unduh sekarang"
|
"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": {
|
"activation": {
|
||||||
"title": "Aktivasi Hydra",
|
"title": "Aktifkan Hydra",
|
||||||
"installation_id": "ID instalasi:",
|
"installation_id": "ID Instalasi:",
|
||||||
"enter_activation_code": "Masukkan kode aktivasi",
|
"enter_activation_code": "Masukkan kode aktivasi kamu",
|
||||||
"message": "Jika kamu tidak tau dimana bertanya untuk ini, maka kamu tidak seharusnya memiliki ini.",
|
"message": "Kalau tidak tahu harus tanya ke siapa, berarti kamu tidak perlu ini.",
|
||||||
"activate": "Aktifkan",
|
"activate": "Aktifkan",
|
||||||
"loading": "Memuat…"
|
"loading": "Memuat…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Lanjutkan",
|
"resume": "Lanjutkan",
|
||||||
"pause": "Hentikan sementara",
|
"pause": "Jeda",
|
||||||
"eta": "Perkiraan {{eta}}",
|
"eta": "Estimasi {{eta}}",
|
||||||
"paused": "Terhenti sementara",
|
"paused": "Dijeda",
|
||||||
"verifying": "Memeriksa…",
|
"verifying": "Verifikasi…",
|
||||||
"completed": "Selesai",
|
"completed": "Selesai",
|
||||||
"cancel": "Batalkan",
|
"removed": "Tidak diunduh",
|
||||||
"filter": "Saring game yang diunduh",
|
"cancel": "Batal",
|
||||||
|
"filter": "Filter game yang diunduh",
|
||||||
"remove": "Hapus",
|
"remove": "Hapus",
|
||||||
"downloading_metadata": "Mengunduh metadata…",
|
"downloading_metadata": "Mengunduh metadata…",
|
||||||
"deleting": "Menghapus file instalasi…",
|
"deleting": "Menghapus installer…",
|
||||||
"delete": "Hapus file instalasi",
|
"delete": "Hapus installer",
|
||||||
"delete_modal_title": "Kamu yakin?",
|
"delete_modal_title": "Apa kamu yakin?",
|
||||||
"delete_modal_description": "Proses ini akan menghapus semua file instalasi dari komputer kamu",
|
"delete_modal_description": "Ini akan menghapus semua file instalasi dari komputer kamu",
|
||||||
"install": "Install"
|
"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": {
|
"settings": {
|
||||||
"downloads_path": "Lokasi unduhan",
|
"downloads_path": "Path unduhan",
|
||||||
"change": "Perbarui",
|
"change": "Ganti",
|
||||||
"notifications": "Pengingat",
|
"notifications": "Notifikasi",
|
||||||
"enable_download_notifications": "Saat unduhan selesai",
|
"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",
|
"behavior": "Perilaku",
|
||||||
"quit_app_instead_hiding": "Tutup aplikasi alih-alih menyembunyikan aplikasi",
|
"download_sources": "Sumber unduhan",
|
||||||
"launch_with_system": "Jalankan saat memulai sistem"
|
"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>sini</0>",
|
||||||
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Unduhan selesai",
|
"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_list_updated": "Daftar repack diperbarui",
|
||||||
"repack_count_one": "{{count}} repack ditambahkan",
|
"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": {
|
"system_tray": {
|
||||||
"open": "Buka Hydra",
|
"open": "Buka Hydra",
|
||||||
"quit": "Tutup"
|
"quit": "Keluar"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "Tidak ada unduhan tersedia"
|
"no_downloads": "Tidak ada unduhan yang tersedia"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Program tidak terinstal",
|
"title": "Program tidak terpasang",
|
||||||
"description": "Wine atau Lutris exe tidak ditemukan pada sistem kamu",
|
"description": "Executable Wine atau Lutris tidak ditemukan di sistem kamu",
|
||||||
"instructions": "Periksa cara instalasi yang benar pada Linux distro-mu agar game dapat dimainkan dengan benar"
|
"instructions": "Cek cara instalasi yang benar di distro Linux kamu agar game bisa jalan normal"
|
||||||
},
|
},
|
||||||
"modal": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,6 +250,17 @@
|
|||||||
"add": "Adicionar",
|
"add": "Adicionar",
|
||||||
"sending": "Enviando",
|
"sending": "Enviando",
|
||||||
"friends_list": "Lista de amigos",
|
"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}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,12 @@ import "./auth/sign-out";
|
|||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
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-friend-requests";
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
|
import "./profile/undo-friendship";
|
||||||
import "./profile/update-friend-request";
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
11
src/main/events/profile/undo-friendship.ts
Normal file
11
src/main/events/profile/undo-friendship.ts
Normal file
@ -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);
|
11
src/main/events/user/block-user.ts
Normal file
11
src/main/events/user/block-user.ts
Normal file
@ -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);
|
29
src/main/events/user/get-user-friends.ts
Normal file
29
src/main/events/user/get-user-friends.ts
Normal file
@ -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<UserFriends> => {
|
||||||
|
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);
|
@ -4,13 +4,19 @@ import { steamGamesWorker } from "@main/workers";
|
|||||||
import { UserProfile } from "@types";
|
import { UserProfile } from "@types";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
import { getSteamAppAsset } from "@main/helpers";
|
||||||
|
import { getUserFriends } from "./get-user-friends";
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
try {
|
||||||
const profile = await HydraApi.get(`/user/${userId}`);
|
const [profile, friends] = await Promise.all([
|
||||||
|
HydraApi.get(`/user/${userId}`),
|
||||||
|
getUserFriends(userId, 12, 0).catch(() => {
|
||||||
|
return { totalFriends: 0, friends: [] };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const recentGames = await Promise.all(
|
const recentGames = await Promise.all(
|
||||||
profile.recentGames.map(async (game) => {
|
profile.recentGames.map(async (game) => {
|
||||||
@ -46,7 +52,13 @@ const getUser = async (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...profile, libraryGames, recentGames };
|
return {
|
||||||
|
...profile,
|
||||||
|
libraryGames,
|
||||||
|
recentGames,
|
||||||
|
friends: friends.friends,
|
||||||
|
totalFriends: friends.totalFriends,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
11
src/main/events/user/unblock-user.ts
Normal file
11
src/main/events/user/unblock-user.ts
Normal file
@ -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);
|
@ -72,7 +72,7 @@ export class HydraApi {
|
|||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(request) => {
|
(request) => {
|
||||||
logger.log(" ---- REQUEST -----");
|
logger.log(" ---- REQUEST -----");
|
||||||
logger.log(request.method, request.url, request.data);
|
logger.log(request.method, request.url, request.params, request.data);
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -196,52 +196,52 @@ export class HydraApi {
|
|||||||
throw err;
|
throw err;
|
||||||
};
|
};
|
||||||
|
|
||||||
static async get(url: string) {
|
static async get<T = any>(url: string, params?: any) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.get(url, this.getAxiosConfig())
|
.get<T>(url, { params, ...this.getAxiosConfig() })
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async post(url: string, data?: any) {
|
static async post<T = any>(url: string, data?: any) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.post(url, data, this.getAxiosConfig())
|
.post<T>(url, data, this.getAxiosConfig())
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async put(url: string, data?: any) {
|
static async put<T = any>(url: string, data?: any) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.put(url, data, this.getAxiosConfig())
|
.put<T>(url, data, this.getAxiosConfig())
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async patch(url: string, data?: any) {
|
static async patch<T = any>(url: string, data?: any) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.patch(url, data, this.getAxiosConfig())
|
.patch<T>(url, data, this.getAxiosConfig())
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async delete(url: string) {
|
static async delete<T = any>(url: string) {
|
||||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.delete(url, this.getAxiosConfig())
|
.delete<T>(url, this.getAxiosConfig())
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
@ -135,6 +135,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
|
undoFriendship: (userId: string) =>
|
||||||
|
ipcRenderer.invoke("undoFriendship", userId),
|
||||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
@ -145,6 +147,10 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
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 */
|
/* Auth */
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://cdn.discordapp.com https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #1c1c1c">
|
<body style="background-color: #1c1c1c">
|
||||||
|
@ -42,11 +42,12 @@ export function App() {
|
|||||||
const {
|
const {
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
updateFriendRequests,
|
friendModalUserId,
|
||||||
|
fetchFriendRequests,
|
||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
|
|
||||||
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||||
useUserDetails();
|
useUserDetails();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -104,7 +105,7 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
updateFriendRequests();
|
fetchFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||||
@ -113,7 +114,7 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
updateFriendRequests();
|
fetchFriendRequests();
|
||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -218,11 +219,14 @@ export function App() {
|
|||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserFriendModal
|
{userDetails && (
|
||||||
visible={isFriendsModalVisible}
|
<UserFriendModal
|
||||||
initialTab={friendRequetsModalTab}
|
visible={isFriendsModalVisible}
|
||||||
onClose={hideFriendsModal}
|
initialTab={friendRequetsModalTab}
|
||||||
/>
|
onClose={hideFriendsModal}
|
||||||
|
userId={friendModalUserId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
@ -3,10 +3,11 @@ import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
|
|||||||
import * as styles from "./sidebar-profile.css";
|
import * as styles from "./sidebar-profile.css";
|
||||||
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { profileContainerBackground } from "./sidebar-profile.css";
|
import { profileContainerBackground } from "./sidebar-profile.css";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
import { FriendRequest } from "@types";
|
||||||
|
|
||||||
export function SidebarProfile() {
|
export function SidebarProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -16,6 +17,14 @@ export function SidebarProfile() {
|
|||||||
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
||||||
useUserDetails();
|
useUserDetails();
|
||||||
|
|
||||||
|
const [receivedRequests, setReceivedRequests] = useState<FriendRequest[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setReceivedRequests(
|
||||||
|
friendRequests.filter((request) => request.type === "RECEIVED")
|
||||||
|
);
|
||||||
|
}, [friendRequests]);
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
@ -79,15 +88,17 @@ export function SidebarProfile() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{userDetails && friendRequests.length > 0 && !gameRunning && (
|
{userDetails && receivedRequests.length > 0 && !gameRunning && (
|
||||||
<div className={styles.friendRequestContainer}>
|
<div className={styles.friendRequestContainer}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.friendRequestButton}
|
className={styles.friendRequestButton}
|
||||||
onClick={() => showFriendsModal(UserFriendModalTab.AddFriend)}
|
onClick={() =>
|
||||||
|
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PersonAddIcon size={24} />
|
<PersonAddIcon size={24} />
|
||||||
{friendRequests.length}
|
{receivedRequests.length}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
9
src/renderer/src/declaration.d.ts
vendored
9
src/renderer/src/declaration.d.ts
vendored
@ -16,6 +16,7 @@ import type {
|
|||||||
UserProfile,
|
UserProfile,
|
||||||
FriendRequest,
|
FriendRequest,
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
|
UserFriends,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -127,9 +128,17 @@ declare global {
|
|||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => Promise<UserProfile | null>;
|
getUser: (userId: string) => Promise<UserProfile | null>;
|
||||||
|
blockUser: (userId: string) => Promise<void>;
|
||||||
|
unblockUser: (userId: string) => Promise<void>;
|
||||||
|
getUserFriends: (
|
||||||
|
userId: string,
|
||||||
|
take: number,
|
||||||
|
skip: number
|
||||||
|
) => Promise<UserFriends>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserProfile | null>;
|
||||||
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (
|
||||||
displayName: string,
|
displayName: string,
|
||||||
newProfileImagePath: string | null
|
newProfileImagePath: string | null
|
||||||
|
@ -8,6 +8,7 @@ export interface UserDetailsState {
|
|||||||
friendRequests: FriendRequest[];
|
friendRequests: FriendRequest[];
|
||||||
isFriendsModalVisible: boolean;
|
isFriendsModalVisible: boolean;
|
||||||
friendRequetsModalTab: UserFriendModalTab | null;
|
friendRequetsModalTab: UserFriendModalTab | null;
|
||||||
|
friendModalUserId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserDetailsState = {
|
const initialState: UserDetailsState = {
|
||||||
@ -16,6 +17,7 @@ const initialState: UserDetailsState = {
|
|||||||
friendRequests: [],
|
friendRequests: [],
|
||||||
isFriendsModalVisible: false,
|
isFriendsModalVisible: false,
|
||||||
friendRequetsModalTab: null,
|
friendRequetsModalTab: null,
|
||||||
|
friendModalUserId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userDetailsSlice = createSlice({
|
export const userDetailsSlice = createSlice({
|
||||||
@ -33,10 +35,11 @@ export const userDetailsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setFriendsModalVisible: (
|
setFriendsModalVisible: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<UserFriendModalTab>
|
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
||||||
) => {
|
) => {
|
||||||
state.isFriendsModalVisible = true;
|
state.isFriendsModalVisible = true;
|
||||||
state.friendRequetsModalTab = action.payload;
|
state.friendRequetsModalTab = action.payload.initialTab;
|
||||||
|
state.friendModalUserId = action.payload.userId;
|
||||||
},
|
},
|
||||||
setFriendsModalHidden: (state) => {
|
setFriendsModalHidden: (state) => {
|
||||||
state.isFriendsModalVisible = false;
|
state.isFriendsModalVisible = false;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
import { average } from "color.js";
|
||||||
|
|
||||||
export const steamUrlBuilder = {
|
export const steamUrlBuilder = {
|
||||||
library: (objectID: string) =>
|
library: (objectID: string) =>
|
||||||
@ -45,3 +46,14 @@ export const buildGameDetailsPath = (
|
|||||||
|
|
||||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
|
||||||
new Color(color).darken(amount).alpha(alpha).toString();
|
new Color(color).darken(amount).alpha(alpha).toString();
|
||||||
|
|
||||||
|
export const profileBackgroundFromProfileImage = async (
|
||||||
|
profileImageUrl: string
|
||||||
|
) => {
|
||||||
|
const output = await average(profileImageUrl, {
|
||||||
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { average } from "color.js";
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "./redux";
|
import { useAppDispatch, useAppSelector } from "./redux";
|
||||||
import {
|
import {
|
||||||
setProfileBackground,
|
setProfileBackground,
|
||||||
@ -9,7 +7,7 @@ import {
|
|||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { darkenColor } from "@renderer/helpers";
|
import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
||||||
import { FriendRequestAction, UserDetails } from "@types";
|
import { FriendRequestAction, UserDetails } from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
@ -21,6 +19,7 @@ export function useUserDetails() {
|
|||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
|
friendModalUserId,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
} = useAppSelector((state) => state.userDetails);
|
} = useAppSelector((state) => state.userDetails);
|
||||||
|
|
||||||
@ -42,12 +41,9 @@ export function useUserDetails() {
|
|||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
const output = await average(userDetails.profileImageUrl, {
|
const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
amount: 1,
|
userDetails.profileImageUrl
|
||||||
format: "hex",
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
|
|
||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
@ -89,15 +85,19 @@ export function useUserDetails() {
|
|||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateFriendRequests = useCallback(async () => {
|
const fetchFriendRequests = useCallback(() => {
|
||||||
const friendRequests = await window.electron.getFriendRequests();
|
return window.electron
|
||||||
dispatch(setFriendRequests(friendRequests));
|
.getFriendRequests()
|
||||||
|
.then((friendRequests) => {
|
||||||
|
dispatch(setFriendRequests(friendRequests));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const showFriendsModal = useCallback(
|
const showFriendsModal = useCallback(
|
||||||
(tab: UserFriendModalTab) => {
|
(initialTab: UserFriendModalTab, userId: string) => {
|
||||||
dispatch(setFriendsModalVisible(tab));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
updateFriendRequests();
|
fetchFriendRequests();
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -110,26 +110,39 @@ export function useUserDetails() {
|
|||||||
async (userId: string) => {
|
async (userId: string) => {
|
||||||
return window.electron
|
return window.electron
|
||||||
.sendFriendRequest(userId)
|
.sendFriendRequest(userId)
|
||||||
.then(() => updateFriendRequests());
|
.then(() => fetchFriendRequests());
|
||||||
},
|
},
|
||||||
[updateFriendRequests]
|
[fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateFriendRequestState = useCallback(
|
const updateFriendRequestState = useCallback(
|
||||||
async (userId: string, action: FriendRequestAction) => {
|
async (userId: string, action: FriendRequestAction) => {
|
||||||
return window.electron
|
return window.electron
|
||||||
.updateFriendRequest(userId, action)
|
.updateFriendRequest(userId, action)
|
||||||
.then(() => updateFriendRequests());
|
.then(() => fetchFriendRequests());
|
||||||
},
|
},
|
||||||
[updateFriendRequests]
|
[fetchFriendRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const undoFriendship = (userId: string) => {
|
||||||
|
return window.electron.undoFriendship(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockUser = (userId: string) => {
|
||||||
|
return window.electron.blockUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unblockUser = (userId: string) => {
|
||||||
|
return window.electron.unblockUser(userId);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
|
friendModalUserId,
|
||||||
showFriendsModal,
|
showFriendsModal,
|
||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
@ -138,7 +151,10 @@ export function useUserDetails() {
|
|||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
patchUser,
|
patchUser,
|
||||||
sendFriendRequest,
|
sendFriendRequest,
|
||||||
updateFriendRequests,
|
fetchFriendRequests,
|
||||||
updateFriendRequestState,
|
updateFriendRequestState,
|
||||||
|
blockUser,
|
||||||
|
unblockUser,
|
||||||
|
undoFriendship,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,136 @@
|
|||||||
|
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";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export type UserFriendItemProps = {
|
||||||
|
userId: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
displayName: string;
|
||||||
|
onClickItem: (userId: string) => void;
|
||||||
|
} & (
|
||||||
|
| { type: "ACCEPTED"; onClickUndoFriendship: (userId: string) => void }
|
||||||
|
| {
|
||||||
|
type: "SENT" | "RECEIVED";
|
||||||
|
onClickCancelRequest: (userId: string) => void;
|
||||||
|
onClickAcceptRequest: (userId: string) => void;
|
||||||
|
onClickRefuseRequest: (userId: string) => void;
|
||||||
|
}
|
||||||
|
| { type: null }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UserFriendItem = (props: UserFriendItemProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
const { userId, profileImageUrl, displayName, type, onClickItem } = props;
|
||||||
|
|
||||||
|
const getRequestDescription = () => {
|
||||||
|
if (type === "ACCEPTED" || type === null) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<small>
|
||||||
|
{type == "SENT" ? t("request_sent") : t("request_received")}
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequestActions = () => {
|
||||||
|
if (type === null) return null;
|
||||||
|
|
||||||
|
if (type === "SENT") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickCancelRequest(userId)}
|
||||||
|
title={t("cancel_request")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "RECEIVED") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles.acceptRequestButton}
|
||||||
|
onClick={() => props.onClickAcceptRequest(userId)}
|
||||||
|
title={t("accept_request")}
|
||||||
|
>
|
||||||
|
<CheckCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickRefuseRequest(userId)}
|
||||||
|
title={t("ignore_request")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "ACCEPTED") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => props.onClickUndoFriendship(userId)}
|
||||||
|
title={t("undo_friendship")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.friendListButton}
|
||||||
|
onClick={() => onClickItem(userId)}
|
||||||
|
>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={displayName}
|
||||||
|
src={profileImageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flex: "1",
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||||
|
{getRequestDescription()}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getRequestActions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -4,7 +4,7 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { UserFriendRequest } from "./user-friend-request";
|
import { UserFriendItem } from "./user-friend-item";
|
||||||
|
|
||||||
export interface UserFriendModalAddFriendProps {
|
export interface UserFriendModalAddFriendProps {
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
@ -23,7 +23,7 @@ export const UserFriendModalAddFriend = ({
|
|||||||
const { sendFriendRequest, updateFriendRequestState, friendRequests } =
|
const { sendFriendRequest, updateFriendRequestState, friendRequests } =
|
||||||
useUserDetails();
|
useUserDetails();
|
||||||
|
|
||||||
const { showErrorToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
const handleClickAddFriend = () => {
|
const handleClickAddFriend = () => {
|
||||||
setIsAddingFriend(true);
|
setIsAddingFriend(true);
|
||||||
@ -56,21 +56,25 @@ export const UserFriendModalAddFriend = ({
|
|||||||
navigate(`/user/${friendCode}`);
|
navigate(`/user/${friendCode}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickCancelFriendRequest = (userId: string) => {
|
const handleCancelFriendRequest = (userId: string) => {
|
||||||
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
||||||
showErrorToast("Falha ao cancelar convite");
|
showErrorToast(t("try_again"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickAcceptFriendRequest = (userId: string) => {
|
const handleAcceptFriendRequest = (userId: string) => {
|
||||||
updateFriendRequestState(userId, "ACCEPTED").catch(() => {
|
updateFriendRequestState(userId, "ACCEPTED")
|
||||||
showErrorToast("Falha ao aceitar convite");
|
.then(() => {
|
||||||
});
|
showSuccessToast(t("request_accepted"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickRefuseFriendRequest = (userId: string) => {
|
const handleRefuseFriendRequest = (userId: string) => {
|
||||||
updateFriendRequestState(userId, "REFUSED").catch(() => {
|
updateFriendRequestState(userId, "REFUSED").catch(() => {
|
||||||
showErrorToast("Falha ao recusar convite");
|
showErrorToast(t("try_again"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -121,16 +125,16 @@ export const UserFriendModalAddFriend = ({
|
|||||||
<h3>Pendentes</h3>
|
<h3>Pendentes</h3>
|
||||||
{friendRequests.map((request) => {
|
{friendRequests.map((request) => {
|
||||||
return (
|
return (
|
||||||
<UserFriendRequest
|
<UserFriendItem
|
||||||
key={request.id}
|
key={request.id}
|
||||||
displayName={request.displayName}
|
displayName={request.displayName}
|
||||||
isRequestSent={request.type === "SENT"}
|
type={request.type}
|
||||||
profileImageUrl={request.profileImageUrl}
|
profileImageUrl={request.profileImageUrl}
|
||||||
userId={request.id}
|
userId={request.id}
|
||||||
onClickAcceptRequest={handleClickAcceptFriendRequest}
|
onClickAcceptRequest={handleAcceptFriendRequest}
|
||||||
onClickCancelRequest={handleClickCancelFriendRequest}
|
onClickCancelRequest={handleCancelFriendRequest}
|
||||||
onClickRefuseRequest={handleClickRefuseFriendRequest}
|
onClickRefuseRequest={handleRefuseFriendRequest}
|
||||||
onClickRequest={handleClickRequest}
|
onClickItem={handleClickRequest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { UserFriend } from "@types";
|
||||||
|
import { useEffect, 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";
|
||||||
|
|
||||||
|
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 [maxPage, setMaxPage] = useState(0);
|
||||||
|
const [friends, setFriends] = useState<UserFriend[]>([]);
|
||||||
|
|
||||||
|
const { userDetails, undoFriendship } = useUserDetails();
|
||||||
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (page > maxPage) return;
|
||||||
|
window.electron
|
||||||
|
.getUserFriends(userId, pageSize, page * pageSize)
|
||||||
|
.then((newPage) => {
|
||||||
|
if (page === 0) {
|
||||||
|
setMaxPage(newPage.totalFriends / pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFriends([...friends, ...newPage.friends]);
|
||||||
|
setPage(page + 1);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{friends.map((friend) => {
|
||||||
|
return (
|
||||||
|
<UserFriendItem
|
||||||
|
userId={friend.id}
|
||||||
|
displayName={friend.displayName}
|
||||||
|
profileImageUrl={friend.profileImageUrl}
|
||||||
|
onClickItem={handleClickFriend}
|
||||||
|
onClickUndoFriendship={handleUndoFriendship}
|
||||||
|
type={isMe ? "ACCEPTED" : null}
|
||||||
|
key={friend.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -3,23 +3,27 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||||
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
import { UserFriendModalList } from "./user-friend-modal-list";
|
||||||
|
|
||||||
export enum UserFriendModalTab {
|
export enum UserFriendModalTab {
|
||||||
FriendsList,
|
FriendsList,
|
||||||
AddFriend,
|
AddFriend,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserAddFriendsModalProps {
|
export interface UserFriendsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
initialTab: UserFriendModalTab | null;
|
initialTab: UserFriendModalTab | null;
|
||||||
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserFriendModal = ({
|
export const UserFriendModal = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
initialTab,
|
initialTab,
|
||||||
}: UserAddFriendsModalProps) => {
|
userId,
|
||||||
|
}: UserFriendsModalProps) => {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
const tabs = [t("friends_list"), t("add_friends")];
|
const tabs = [t("friends_list"), t("add_friends")];
|
||||||
@ -28,6 +32,9 @@ export const UserFriendModal = ({
|
|||||||
initialTab || UserFriendModalTab.FriendsList
|
initialTab || UserFriendModalTab.FriendsList
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
const isMe = userDetails?.id == userId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialTab != null) {
|
if (initialTab != null) {
|
||||||
setCurrentTab(initialTab);
|
setCurrentTab(initialTab);
|
||||||
@ -36,7 +43,7 @@ export const UserFriendModal = ({
|
|||||||
|
|
||||||
const renderTab = () => {
|
const renderTab = () => {
|
||||||
if (currentTab == UserFriendModalTab.FriendsList) {
|
if (currentTab == UserFriendModalTab.FriendsList) {
|
||||||
return <></>;
|
return <UserFriendModalList userId={userId} closeModal={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTab == UserFriendModalTab.AddFriend) {
|
if (currentTab == UserFriendModalTab.AddFriend) {
|
||||||
@ -56,20 +63,21 @@ export const UserFriendModal = ({
|
|||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
{isMe && (
|
||||||
{tabs.map((tab, index) => {
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
return (
|
{tabs.map((tab, index) => {
|
||||||
<Button
|
return (
|
||||||
key={tab}
|
<Button
|
||||||
theme={index === currentTab ? "primary" : "outline"}
|
key={tab}
|
||||||
onClick={() => setCurrentTab(index)}
|
theme={index === currentTab ? "primary" : "outline"}
|
||||||
>
|
onClick={() => setCurrentTab(index)}
|
||||||
{tab}
|
>
|
||||||
</Button>
|
{tab}
|
||||||
);
|
</Button>
|
||||||
})}
|
);
|
||||||
</section>
|
})}
|
||||||
<h2>{tabs[currentTab]}</h2>
|
</section>
|
||||||
|
)}
|
||||||
{renderTab()}
|
{renderTab()}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -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 (
|
|
||||||
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.friendListButton}
|
|
||||||
onClick={() => onClickRequest(userId)}
|
|
||||||
>
|
|
||||||
<div className={styles.friendAvatarContainer}>
|
|
||||||
{profileImageUrl ? (
|
|
||||||
<img
|
|
||||||
className={styles.profileAvatar}
|
|
||||||
alt={displayName}
|
|
||||||
src={profileImageUrl}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PersonIcon size={24} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flex: "1",
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className={styles.friendListDisplayName}>{displayName}</p>
|
|
||||||
<small>{isRequestSent ? "Pedido enviado" : "Pedido recebido"}</small>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: "8px",
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isRequestSent ? (
|
|
||||||
<button
|
|
||||||
className={styles.cancelRequestButton}
|
|
||||||
onClick={() => onClickCancelRequest(userId)}
|
|
||||||
>
|
|
||||||
<XCircleIcon size={28} />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className={styles.acceptRequestButton}
|
|
||||||
onClick={() => onClickAcceptRequest(userId)}
|
|
||||||
>
|
|
||||||
<CheckCircleIcon size={28} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.cancelRequestButton}
|
|
||||||
onClick={() => onClickRefuseRequest(userId)}
|
|
||||||
>
|
|
||||||
<XCircleIcon size={28} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
44
src/renderer/src/pages/user/user-block-modal.tsx
Normal file
44
src/renderer/src/pages/user/user-block-modal.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("sign_out_modal_title")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className={styles.signOutModalContent}>
|
||||||
|
<p style={{ fontFamily: "Fira Sans" }}>
|
||||||
|
{t("user_block_modal_text", { displayName })}
|
||||||
|
</p>
|
||||||
|
<div className={styles.signOutModalButtonsContainer}>
|
||||||
|
<Button onClick={onConfirm} theme="danger">
|
||||||
|
{t("block_user")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onClose} theme="primary">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { UserGame, UserProfile } from "@types";
|
import { FriendRequestAction, UserGame, UserProfile } from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
@ -12,12 +12,23 @@ import {
|
|||||||
useUserDetails,
|
useUserDetails,
|
||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
import {
|
||||||
import { PersonIcon, PlusIcon, TelescopeIcon } from "@primer/octicons-react";
|
buildGameDetailsPath,
|
||||||
|
profileBackgroundFromProfileImage,
|
||||||
|
steamUrlBuilder,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
PersonIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TelescopeIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
import { UserEditProfileModal } from "./user-edit-modal";
|
import { UserEditProfileModal } from "./user-edit-modal";
|
||||||
import { UserSignOutModal } from "./user-signout-modal";
|
import { UserSignOutModal } from "./user-sign-out-modal";
|
||||||
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||||
|
import { UserBlockModal } from "./user-block-modal";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
@ -26,6 +37,8 @@ export interface ProfileContentProps {
|
|||||||
updateUserProfile: () => Promise<void>;
|
updateUserProfile: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FriendAction = FriendRequestAction | ("BLOCK" | "UNDO" | "SEND");
|
||||||
|
|
||||||
export function UserContent({
|
export function UserContent({
|
||||||
userProfile,
|
userProfile,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
@ -36,13 +49,20 @@ export function UserContent({
|
|||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
signOut,
|
signOut,
|
||||||
updateFriendRequests,
|
sendFriendRequest,
|
||||||
|
fetchFriendRequests,
|
||||||
showFriendsModal,
|
showFriendsModal,
|
||||||
|
updateFriendRequestState,
|
||||||
|
undoFriendship,
|
||||||
|
blockUser,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [profileContentBoxBackground, setProfileContentBoxBackground] =
|
||||||
|
useState<string | undefined>();
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
const [showSignOutModal, setShowSignOutModal] = useState(false);
|
||||||
|
const [showUserBlockModal, setShowUserBlockModal] = useState(false);
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
@ -93,14 +113,141 @@ export function UserContent({
|
|||||||
const isMe = userDetails?.id == userProfile.id;
|
const isMe = userDetails?.id == userProfile.id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMe) updateFriendRequests();
|
if (isMe) fetchFriendRequests();
|
||||||
}, [isMe]);
|
}, [isMe]);
|
||||||
|
|
||||||
const profileContentBoxBackground = useMemo(() => {
|
useEffect(() => {
|
||||||
if (profileBackground) return profileBackground;
|
if (isMe && profileBackground) {
|
||||||
/* TODO: Render background colors for other users */
|
setProfileContentBoxBackground(profileBackground);
|
||||||
return undefined;
|
}
|
||||||
}, [profileBackground]);
|
|
||||||
|
if (userProfile.profileImageUrl) {
|
||||||
|
profileBackgroundFromProfileImage(userProfile.profileImageUrl).then(
|
||||||
|
(profileBackground) => {
|
||||||
|
setProfileContentBoxBackground(profileBackground);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [profileBackground, isMe]);
|
||||||
|
|
||||||
|
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 getProfileActions = () => {
|
||||||
|
if (isMe) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button theme="outline" onClick={handleEditProfile}>
|
||||||
|
{t("edit_profile")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button theme="danger" onClick={() => setShowSignOutModal(true)}>
|
||||||
|
{t("sign_out")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProfile.relation == null) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => handleFriendAction(userProfile.id, "SEND")}
|
||||||
|
>
|
||||||
|
{t("add_friend")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button theme="danger" onClick={() => setShowUserBlockModal(true)}>
|
||||||
|
{t("block_user")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProfile.relation.status === "ACCEPTED") {
|
||||||
|
const userId =
|
||||||
|
userProfile.relation.AId === userDetails?.id
|
||||||
|
? userProfile.relation.BId
|
||||||
|
: userProfile.relation.AId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => handleFriendAction(userId, "UNDO")}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} /> {t("undo_friendship")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProfile.relation.BId === userProfile.id) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() =>
|
||||||
|
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} /> {t("cancel_request")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
className={styles.acceptRequestButton}
|
||||||
|
onClick={() =>
|
||||||
|
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckCircleIcon size={28} /> {t("accept_request")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() =>
|
||||||
|
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} /> {t("ignore_request")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -117,6 +264,13 @@ export function UserContent({
|
|||||||
onConfirm={handleConfirmSignout}
|
onConfirm={handleConfirmSignout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UserBlockModal
|
||||||
|
visible={showUserBlockModal}
|
||||||
|
onClose={() => setShowUserBlockModal(false)}
|
||||||
|
onConfirm={() => handleFriendAction(userProfile.id, "BLOCK")}
|
||||||
|
displayName={userProfile.displayName}
|
||||||
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className={styles.profileContentBox}
|
className={styles.profileContentBox}
|
||||||
style={{
|
style={{
|
||||||
@ -187,37 +341,24 @@ export function UserContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMe && (
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "end",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "end",
|
flexDirection: "column",
|
||||||
zIndex: 1,
|
gap: `${SPACING_UNIT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{getProfileActions()}
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<Button theme="outline" onClick={handleEditProfile}>
|
|
||||||
{t("edit_profile")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
theme="danger"
|
|
||||||
onClick={() => setShowSignOutModal(true)}
|
|
||||||
>
|
|
||||||
{t("sign_out")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className={styles.profileContent}>
|
<div className={styles.profileContent}>
|
||||||
@ -327,12 +468,16 @@ export function UserContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(isMe ||
|
{showFriends && (
|
||||||
(userProfile.friends && userProfile.friends.length > 0)) && (
|
|
||||||
<div className={styles.friendsSection}>
|
<div className={styles.friendsSection}>
|
||||||
<button
|
<button
|
||||||
className={styles.friendsSectionHeader}
|
className={styles.friendsSectionHeader}
|
||||||
onClick={() => showFriendsModal(UserFriendModalTab.FriendsList)}
|
onClick={() =>
|
||||||
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.FriendsList,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h2>{t("friends")}</h2>
|
<h2>{t("friends")}</h2>
|
||||||
|
|
||||||
@ -344,7 +489,7 @@ export function UserContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
{userProfile.friends.length}
|
{userProfile.totalFriends}
|
||||||
</h3>
|
</h3>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -388,7 +533,10 @@ export function UserContent({
|
|||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showFriendsModal(UserFriendModalTab.AddFriend)
|
showFriendsModal(
|
||||||
|
UserFriendModalTab.AddFriend,
|
||||||
|
userProfile.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PlusIcon /> {t("add")}
|
<PlusIcon /> {t("add")}
|
||||||
|
@ -2,7 +2,7 @@ import { Button, Modal } from "@renderer/components";
|
|||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface UserEditProfileModalProps {
|
export interface UserSignOutModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -12,7 +12,7 @@ export const UserSignOutModal = ({
|
|||||||
visible,
|
visible,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClose,
|
onClose,
|
||||||
}: UserEditProfileModalProps) => {
|
}: UserSignOutModalProps) => {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
return (
|
return (
|
@ -78,6 +78,8 @@ export const profileAvatar = style({
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
|
borderRadius: "50%",
|
||||||
|
overflow: "hidden",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileAvatarEditOverlay = style({
|
export const profileAvatarEditOverlay = style({
|
||||||
@ -277,3 +279,16 @@ export const profileBackground = style({
|
|||||||
top: "0",
|
top: "0",
|
||||||
borderRadius: "4px",
|
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,
|
||||||
|
});
|
||||||
|
@ -277,6 +277,11 @@ export interface UserFriend {
|
|||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserFriends {
|
||||||
|
totalFriends: number;
|
||||||
|
friends: UserFriend[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -284,14 +289,25 @@ export interface FriendRequest {
|
|||||||
type: "SENT" | "RECEIVED";
|
type: "SENT" | "RECEIVED";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserRelation {
|
||||||
|
AId: string;
|
||||||
|
BId: string;
|
||||||
|
status: "ACCEPTED" | "PENDING";
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
|
profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
totalPlayTimeInSeconds: number;
|
totalPlayTimeInSeconds: number;
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
recentGames: UserGame[];
|
recentGames: UserGame[];
|
||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
|
totalFriends: number;
|
||||||
|
relation: UserRelation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
|
Loading…
Reference in New Issue
Block a user