mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 17:23:57 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
613898b32d | ||
|
|
2bc98317e9 | ||
|
|
2e87ae8486 | ||
|
|
df5f34119f | ||
|
|
2271368199 | ||
|
|
e139423b52 | ||
|
|
aa2ecfad96 | ||
|
|
b76441a763 | ||
|
|
2d160ba8ee | ||
|
|
5863b15b12 | ||
|
|
bc175e635f | ||
|
|
f7f89cb778 | ||
|
|
16c45692da | ||
|
|
30aa3f5470 | ||
|
|
ef16732c0a | ||
|
|
84c472a3fa | ||
|
|
2610f8b17b | ||
|
|
705b12019f | ||
|
|
39be8fdf53 | ||
|
|
cc7c3455fa | ||
|
|
d1f4bc7207 | ||
|
|
aa4ca25653 | ||
|
|
405ea0a824 | ||
|
|
43bc0cb08f | ||
|
|
3c200aa2eb | ||
|
|
cc5967814b | ||
|
|
ec16efed2f | ||
|
|
09d0e5b4ef | ||
|
|
5b18aba2b8 | ||
|
|
192008c76c | ||
|
|
1de3a9836c | ||
|
|
ee02811aea | ||
|
|
c21ebe1ce2 |
21
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
21
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a report to help us improve. Write in English, please.
|
description: Create a report to help us improve. Write in English, please.
|
||||||
title: "[BUG] "
|
title: "[BUG] Write a title for your bug"
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -28,11 +28,21 @@ body:
|
|||||||
description: A clear and concise description of what you expected to happen.
|
description: A clear and concise description of what you expected to happen.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional information and data
|
||||||
|
description: |
|
||||||
|
If possible, add screenshots and upload your logs file here.
|
||||||
|
Logs location on Windows: "%appdata%/hydra"
|
||||||
|
Logs location on Linux: "~/.config/hydra/"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots
|
label: Screenshots
|
||||||
description: If applicable, add screenshots to help explain your problem.
|
description: If possible, add screenshots to help explain your problem.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: input
|
||||||
@@ -49,13 +59,6 @@ body:
|
|||||||
description: Please provide the version of Hydra you are using.
|
description: Please provide the version of Hydra you are using.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
id: additional-info
|
|
||||||
attributes:
|
|
||||||
label: Additional Information
|
|
||||||
description: Please provide any additional information and context about your problem.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
6
build/installer.nsh
Normal file
6
build/installer.nsh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
!macro customUnInstall
|
||||||
|
${ifNot} ${isUpdated}
|
||||||
|
RMDir /r "$APPDATA\${APP_PACKAGE_NAME}"
|
||||||
|
RMDir /r "$APPDATA\hydra"
|
||||||
|
${endIf}
|
||||||
|
!macroend
|
||||||
@@ -27,6 +27,7 @@ nsis:
|
|||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
oneClick: false
|
oneClick: false
|
||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
|
include: installer.nsh
|
||||||
portable:
|
portable:
|
||||||
artifactName: ${name}-${version}-portable.${ext}
|
artifactName: ${name}-${version}-portable.${ext}
|
||||||
mac:
|
mac:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "2.1.2",
|
"version": "2.1.5",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Dansk",
|
"language_name": "Dansk",
|
||||||
|
"app": {
|
||||||
|
"successfully_signed_in": "Loggede ind successfuldt"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalet",
|
"featured": "Anbefalet",
|
||||||
"trending": "Trender",
|
"trending": "Trender",
|
||||||
"surprise_me": "Overrask mig",
|
"surprise_me": "Overrask mig",
|
||||||
"no_results": "Ingen resultater fundet"
|
"no_results": "Ingen resultater fundet",
|
||||||
|
"start_typing": "Begynd at skrive for at søge...",
|
||||||
|
"hot": "Populært lige nu",
|
||||||
|
"weekly": "📅 Mest populære spil denne uge"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
@@ -12,27 +18,35 @@
|
|||||||
"settings": "Indstillinger",
|
"settings": "Indstillinger",
|
||||||
"my_library": "Mit bibliotek",
|
"my_library": "Mit bibliotek",
|
||||||
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
"downloading_metadata": "{{title}} (Downloader metadata…)",
|
||||||
"paused": "{{title}} (Paused)",
|
"paused": "{{title}} (Sat på pause)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloader…)",
|
||||||
"filter": "Filtrer bibliotek",
|
"filter": "Filtrér bibliotek",
|
||||||
"home": "Hjem"
|
"home": "Hjem",
|
||||||
|
"queued": "{{title}} (I køen)",
|
||||||
|
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
|
||||||
|
"sign_in": "Log ind",
|
||||||
|
"friends": "Venner"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søg spil",
|
"search": "Søg efter spil",
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"search_results": "Søge resultater",
|
"search_results": "Søge resultater",
|
||||||
"settings": "Indstillinger"
|
"settings": "Indstillinger",
|
||||||
|
"version_available_install": "Version {{version}} tilgængelig. Klik her for at genstarte og installere.",
|
||||||
|
"version_available_download": "Version {{version}} tilgængelig. Klik her for at downloade."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Ingen downloads igang",
|
"no_downloads_in_progress": "Ingen downloads igang",
|
||||||
"downloading_metadata": "Downloader {{title}} metadata…",
|
"downloading_metadata": "Downloader {{title}} metadata…",
|
||||||
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}"
|
"downloading": "Downloader {{title}}… ({{percentage}} færdig) - Fuldt downloadet {{eta}} - {{speed}}",
|
||||||
|
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
|
||||||
|
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Næste side",
|
"next_page": "Næste side",
|
||||||
"previous_page": "Tidligere side"
|
"previous_page": "Forrige side"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Åben download muligheder",
|
"open_download_options": "Åben download muligheder",
|
||||||
@@ -47,11 +61,13 @@
|
|||||||
"remove": "Fjern",
|
"remove": "Fjern",
|
||||||
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
"space_left_on_disk": "{{space}} tilbage på harddisken",
|
||||||
"eta": "Konklusion {{eta}}",
|
"eta": "Konklusion {{eta}}",
|
||||||
|
"calculating_eta": "Udregner resterende tid…",
|
||||||
"downloading_metadata": "Downloader metadata…",
|
"downloading_metadata": "Downloader metadata…",
|
||||||
"filter": "Filtrer repacks",
|
"filter": "Filtrér repacks",
|
||||||
"requirements": "System behov",
|
"requirements": "System behov",
|
||||||
"minimum": "Mindste",
|
"minimum": "Mindste",
|
||||||
"recommended": "Anbefalet",
|
"recommended": "Anbefalet",
|
||||||
|
"paused": "Sat på pause",
|
||||||
"release_date": "Offentliggjort den {{date}}",
|
"release_date": "Offentliggjort den {{date}}",
|
||||||
"publisher": "Udgivet af {{publisher}}",
|
"publisher": "Udgivet af {{publisher}}",
|
||||||
"hours": "timer",
|
"hours": "timer",
|
||||||
@@ -70,10 +86,51 @@
|
|||||||
"deleting": "Sletter installatør…",
|
"deleting": "Sletter installatør…",
|
||||||
"close": "Luk",
|
"close": "Luk",
|
||||||
"playing_now": "Spiller nu",
|
"playing_now": "Spiller nu",
|
||||||
"change": "Ændré",
|
"change": "Ændre",
|
||||||
"repacks_modal_description": "Vælg den repack du vil downloade",
|
"repacks_modal_description": "Vælg den repack du vil downloade",
|
||||||
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
"select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne</0>",
|
||||||
"download_now": "Download nu"
|
"download_now": "Download nu",
|
||||||
|
"no_shop_details": "Kunne ikke modtage butiks detaljerne.",
|
||||||
|
"download_options": "Download muligheder",
|
||||||
|
"download_path": "Download sti",
|
||||||
|
"previous_screenshot": "Forrige skærmbillede",
|
||||||
|
"next_screenshot": "Næste skærmbillede",
|
||||||
|
"screenshot": "Skærmbillede {{number}}",
|
||||||
|
"open_screenshot": "Åben skærmbillede {{number}}",
|
||||||
|
"download_settings": "Download indstillinger",
|
||||||
|
"downloader": "Downloader",
|
||||||
|
"select_executable": "Vælg",
|
||||||
|
"no_executable_selected": "Ingen eksekverbar fil valgt",
|
||||||
|
"open_folder": "Åben mappe",
|
||||||
|
"open_download_location": "Se downloadede filer",
|
||||||
|
"create_shortcut": "Lav skrivebords genvej",
|
||||||
|
"remove_files": "Fjern filer",
|
||||||
|
"remove_from_library_title": "Er du sikker?",
|
||||||
|
"remove_from_library_description": "Dette vil fjerne {{game}} fra dit bibliotek",
|
||||||
|
"options": "Valgmuligheder",
|
||||||
|
"executable_section_title": "Eksekverbar fil",
|
||||||
|
"executable_section_description": "Sti til filen som skal bruges når \"Spil\" bliver klikket",
|
||||||
|
"downloads_secion_title": "Downloads",
|
||||||
|
"downloads_section_description": "Undersøg opdateringer eller andre versioner af dette spil",
|
||||||
|
"danger_zone_section_title": "Farezonen",
|
||||||
|
"danger_zone_section_description": "Fjern dette spil fra dit bibliotek eller filerne der er blevet downloadet af Hydra",
|
||||||
|
"download_in_progress": "Download undervejs",
|
||||||
|
"download_paused": "Download sat på pause",
|
||||||
|
"last_downloaded_option": "Sidste download mulighed",
|
||||||
|
"create_shortcut_success": "Genvej lavet successfuldt",
|
||||||
|
"create_shortcut_error": "Fejl under skabelsen af genvej",
|
||||||
|
"nsfw_content_title": "Dette spil indeholder upassende indhold",
|
||||||
|
"nsfw_content_description": "{{title}} indeholder indhold der ikke egner sig til alle aldre. Er du sikker på at du vil fortsætte?",
|
||||||
|
"allow_nsfw_content": "Fortsæt",
|
||||||
|
"refuse_nsfw_content": "Gå tilbage",
|
||||||
|
"stats": "Statistik",
|
||||||
|
"download_count": "Downloads",
|
||||||
|
"player_count": "Aktive spillere",
|
||||||
|
"download_error": "Denne download mulighed er ikke tilgængelig",
|
||||||
|
"download": "Download",
|
||||||
|
"executable_path_in_use": "Eksekverbar allerede i brug af \"{{game}}\"",
|
||||||
|
"warning": "Advarsel:",
|
||||||
|
"hydra_needs_to_remain_open": "Hydra skal forblive åbent for at denne download kan gennemføres. I tilfælde af at Hydra lukker før downloaden er færdig, mister du dit fremskridt."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Aktivér Hydra",
|
"title": "Aktivér Hydra",
|
||||||
@@ -81,45 +138,95 @@
|
|||||||
"enter_activation_code": "Indtast din aktiverings kode",
|
"enter_activation_code": "Indtast din aktiverings kode",
|
||||||
"message": "Hvis du ikke ved hvor du skal spørge om dette, burde du ikke have dette.",
|
"message": "Hvis du ikke ved hvor du skal spørge om dette, burde du ikke have dette.",
|
||||||
"activate": "Aktivér",
|
"activate": "Aktivér",
|
||||||
"loading": "Loader…"
|
"loading": "Indlæser…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Fortsæt",
|
"resume": "Fortsæt",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"eta": "Konklusion {{eta}}",
|
"eta": "Konklusion {{eta}}",
|
||||||
"paused": "Pauset",
|
"paused": "Sat på pause",
|
||||||
"verifying": "Verificerer…",
|
"verifying": "Verificerer…",
|
||||||
"completed": "Færdigt",
|
"completed": "Færdigt",
|
||||||
|
"removed": "Ikke downloadet",
|
||||||
"cancel": "Annullér",
|
"cancel": "Annullér",
|
||||||
"filter": "Filtrer downloadet spil",
|
"filter": "Filtrér downloadet spil",
|
||||||
"remove": "Fjern",
|
"remove": "Fjern",
|
||||||
"downloading_metadata": "Downloader metadata…",
|
"downloading_metadata": "Downloader metadata…",
|
||||||
"deleting": "Sletter installatør…",
|
"deleting": "Sletter installatør…",
|
||||||
"delete": "Fjern installatør",
|
"delete": "Fjern installatør",
|
||||||
"delete_modal_title": "Er du sikker?",
|
"delete_modal_title": "Er du sikker?",
|
||||||
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
"delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer",
|
||||||
"install": "Installér"
|
"install": "Installér",
|
||||||
|
"download_in_progress": "Undervejs",
|
||||||
|
"queued_downloads": "Downloadkø",
|
||||||
|
"downloads_completed": "Gennemførte",
|
||||||
|
"queued": "I kø",
|
||||||
|
"no_downloads_title": "Rimelig tomt",
|
||||||
|
"no_downloads_description": "Du har ikke downloadet noget med Hydra endnu, men det er aldrig for sent at begynde.",
|
||||||
|
"checking_files": "Undersøger filer…"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads sti",
|
"downloads_path": "Downloads sti",
|
||||||
"change": "Opdatering",
|
"change": "Opdatér",
|
||||||
"notifications": "Notifikationer",
|
"notifications": "Notifikationer",
|
||||||
"enable_download_notifications": "Når et download bliver færdigt",
|
"enable_download_notifications": "Når en download bliver færdigt",
|
||||||
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
"enable_repack_list_notifications": "Når en ny repack bliver tilføjet",
|
||||||
|
"real_debrid_api_token_label": "Real-Debrid API nøgle",
|
||||||
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
"quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen",
|
||||||
"launch_with_system": "Åben Hydra ved start af systemet",
|
"launch_with_system": "Åben Hydra ved start af systemet",
|
||||||
"general": "Generelt",
|
"general": "Generelt",
|
||||||
"behavior": "Opførsel",
|
"behavior": "Opførsel",
|
||||||
|
"download_sources": "Download kilder",
|
||||||
|
"language": "Sprog",
|
||||||
|
"real_debrid_api_token": "API nøgle",
|
||||||
"enable_real_debrid": "Slå Real-Debrid til",
|
"enable_real_debrid": "Slå Real-Debrid til",
|
||||||
|
"real_debrid_description": "Real-Debrid er en ubegrænset downloader der gør det muligt for dig at downloade filer med det samme og med den bedste udnyttelse af din internet hastighed.",
|
||||||
|
"real_debrid_invalid_token": "Ugyldig API nøgle",
|
||||||
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
"real_debrid_api_token_hint": "Du kan få din API nøgle <0>her</0>",
|
||||||
"save_changes": "Gem ændringer"
|
"real_debrid_free_account_error": "Brugeren \"{{username}}\" er en gratis bruger. Venligst abbonér på Real-Debrid",
|
||||||
|
"real_debrid_linked_message": "Brugeren \"{{username}}\" er forbundet",
|
||||||
|
"save_changes": "Gem ændringer",
|
||||||
|
"changes_saved": "Ændringer gemt successfuldt",
|
||||||
|
"download_sources_description": "Hydra vil hente download links fra disse kilder. Kilde URLen skal være et direkte link til en .json fil der indeholder download linkene.",
|
||||||
|
"validate_download_source": "Validér",
|
||||||
|
"remove_download_source": "Fjern",
|
||||||
|
"add_download_source": "Tilføj kilde",
|
||||||
|
"download_count_zero": "Ingen download muligheder",
|
||||||
|
"download_count_one": "{{countFormatted}} download mulighed",
|
||||||
|
"download_count_other": "{{countFormatted}} download muligheder",
|
||||||
|
"download_source_url": "Download kilde URL",
|
||||||
|
"add_download_source_description": "Indsæt URLen der indeholder .json filen",
|
||||||
|
"download_source_up_to_date": "Op til dato",
|
||||||
|
"download_source_errored": "Fejlede",
|
||||||
|
"sync_download_sources": "Synkronisér kilder",
|
||||||
|
"removed_download_source": "Download kilde fjernet",
|
||||||
|
"added_download_source": "Tilføjede download kilde",
|
||||||
|
"download_sources_synced": "Alle download kilder er synkroniserede",
|
||||||
|
"insert_valid_json_url": "Indsæt en gyldig JSON url",
|
||||||
|
"found_download_option_zero": "Ingen download mulighed fundet",
|
||||||
|
"found_download_option_one": "Fandt {{countFormatted}} download mulighed",
|
||||||
|
"found_download_option_other": "Fandt {{countFormatted}} download mulighed",
|
||||||
|
"import": "Importér",
|
||||||
|
"public": "Offentlig",
|
||||||
|
"private": "Privat",
|
||||||
|
"friends_only": "Kun blandt venner",
|
||||||
|
"privacy": "Privatliv",
|
||||||
|
"profile_visibility": "Synlighed af profil",
|
||||||
|
"profile_visibility_description": "Vælg hvem der kan se din profil og dit bibliotek",
|
||||||
|
"required_field": "Dette felt er påkrævet",
|
||||||
|
"source_already_exists": "Denne kilde er allerede blevet tilføjet",
|
||||||
|
"must_be_valid_url": "Kilden skal være en gyldig URL",
|
||||||
|
"blocked_users": "Blokerede brugere",
|
||||||
|
"user_unblocked": "Brugeren er blevet afblokeret"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download færdig",
|
"download_complete": "Download færdig",
|
||||||
"game_ready_to_install": "{{title}} er klar til at installeret",
|
"game_ready_to_install": "{{title}} er klar til at installeret",
|
||||||
"repack_list_updated": "Repack liste opdateret",
|
"repack_list_updated": "Repack liste opdateret",
|
||||||
"repack_count_one": "{{count}} repack tilføjet",
|
"repack_count_one": "{{count}} repack tilføjet",
|
||||||
"repack_count_other": "{{count}} repacks tilføjet"
|
"repack_count_other": "{{count}} repacks tilføjet",
|
||||||
|
"new_update_available": "Version {{version}} tilgængelig",
|
||||||
|
"restart_to_install_update": "Genstart Hydra for at installere opdateringen"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Åben Hydra",
|
"open": "Åben Hydra",
|
||||||
@@ -130,10 +237,80 @@
|
|||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programmer ikke installeret",
|
"title": "Programmer ikke installeret",
|
||||||
"description": "Wine eller Lutris eksekverbare blev ikke fundet på dit system",
|
"description": "Wine eller Lutris eksekverbar blev ikke fundet på dit system",
|
||||||
"instructions": "Tjek den korrekte måde at installere nogle af dem, på din Linux distribution, så spillet kan køre normalt"
|
"instructions": "Tjek den korrekte måde at installere nogle af dem, på din Linux distribution, så spillet kan køre normalt"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Luk knap"
|
"close": "Luk knap"
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"toggle_password_visibility": "Skift synlighed af kodeord"
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"amount_hours": "{{amount}} timer",
|
||||||
|
"amount_minutes": "{{amount}} minuter",
|
||||||
|
"last_time_played": "Sidst spillet {{period}}",
|
||||||
|
"activity": "Seneste aktivitet",
|
||||||
|
"library": "Bibliotek",
|
||||||
|
"total_play_time": "Samlet spiltid: {{amount}}",
|
||||||
|
"no_recent_activity_title": "Hmmm… ikke noget her",
|
||||||
|
"no_recent_activity_description": "Du har ikke spillet nogen spil for nyligt. Dét er det på tide at lave om på!",
|
||||||
|
"display_name": "Brugernavn",
|
||||||
|
"saving": "Gemmer",
|
||||||
|
"save": "Gem",
|
||||||
|
"edit_profile": "Redigér Profil",
|
||||||
|
"saved_successfully": "Gemt successfuldt",
|
||||||
|
"try_again": "Venligst, prøv igen",
|
||||||
|
"sign_out_modal_title": "Er du sikker?",
|
||||||
|
"cancel": "Annullér",
|
||||||
|
"successfully_signed_out": "Loggede ud successfuldt",
|
||||||
|
"sign_out": "Log ud",
|
||||||
|
"playing_for": "Spiller i {{amount}}",
|
||||||
|
"sign_out_modal_text": "Dit bibliotek er koblet sammen med din nuværende bruger. Når du logger ud er dit bibliotek ikke synligt længere, og nogen som helst form for fremskridt bliver ikke gemt. Vil du fortsætte med at logge ud?",
|
||||||
|
"add_friends": "Tilføj venner",
|
||||||
|
"add": "Tilføj",
|
||||||
|
"friend_code": "Venne kode",
|
||||||
|
"see_profile": "Se profil",
|
||||||
|
"sending": "Sender",
|
||||||
|
"friend_request_sent": "Venne anmodning sendt",
|
||||||
|
"friends": "Venner",
|
||||||
|
"friends_list": "Venne liste",
|
||||||
|
"user_not_found": "Bruger ikke fundet",
|
||||||
|
"block_user": "Blokér bruger",
|
||||||
|
"add_friend": "Tilføj ven",
|
||||||
|
"request_sent": "Anmodning sendt",
|
||||||
|
"request_received": "Anmodning modtaget",
|
||||||
|
"accept_request": "Acceptér anmodning",
|
||||||
|
"ignore_request": "Ignorér anmodning",
|
||||||
|
"cancel_request": "Annullér anmodning",
|
||||||
|
"undo_friendship": "Fortryd venskab",
|
||||||
|
"request_accepted": "Anmodning accepteret",
|
||||||
|
"user_blocked_successfully": "Bruger blokeret successfuldt",
|
||||||
|
"user_block_modal_text": "Dette blokerer {{displayName}}",
|
||||||
|
"blocked_users": "Blokerede brugere",
|
||||||
|
"unblock": "Afblokér",
|
||||||
|
"no_friends_added": "Du har stadig ikke tilføjet nogen venner",
|
||||||
|
"pending": "Afventer",
|
||||||
|
"no_pending_invites": "Du har ingen afventende invitationer",
|
||||||
|
"no_blocked_users": "Du har ingen blokerede brugere",
|
||||||
|
"friend_code_copied": "Venne kode kopieret",
|
||||||
|
"undo_friendship_modal_text": "Dette vil fortryde dit venskab med {{displayName}}",
|
||||||
|
"privacy_hint": "For at justere hvem der kan se dette, gå til <0>Indstillingerne</0>",
|
||||||
|
"locked_profile": "Denne profil er privat",
|
||||||
|
"image_process_failure": "Fejlede under håndteringen af billedet",
|
||||||
|
"required_field": "Dette felt er påkrævet",
|
||||||
|
"displayname_min_length": "Brugernavnet skal være mindst 3 karakterer langt",
|
||||||
|
"displayname_max_length": "Brugernavnet skal være højest 50 karakterer langt",
|
||||||
|
"report_profile": "Rapportér denne profil",
|
||||||
|
"report_reason": "Hvorfor rapportérer du denne profil?",
|
||||||
|
"report_description": "Yderligere information",
|
||||||
|
"report_description_placeholder": "Yderligere information",
|
||||||
|
"report": "Rapportér",
|
||||||
|
"report_reason_hate": "Hadefuld tale",
|
||||||
|
"report_reason_sexual_content": "Seksuelt indhold",
|
||||||
|
"report_reason_violence": "Vold",
|
||||||
|
"report_reason_spam": "Spam",
|
||||||
|
"report_reason_other": "Andet",
|
||||||
|
"profile_reported": "Profil rapporteret"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"surprise_me": "Surprise me",
|
"surprise_me": "Surprise me",
|
||||||
"no_results": "No results found",
|
"no_results": "No results found",
|
||||||
"start_typing": "Starting typing to search...",
|
"start_typing": "Starting typing to search...",
|
||||||
"hot": "🔥 Hot now",
|
"hot": "Hot now",
|
||||||
"weekly": "📅 Top games of the week"
|
"weekly": "📅 Top games of the week"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"trending": "Tendencias",
|
"trending": "Tendencias",
|
||||||
"surprise_me": "¡Sorpréndeme!",
|
"surprise_me": "¡Sorpréndeme!",
|
||||||
"no_results": "No se encontraron resultados",
|
"no_results": "No se encontraron resultados",
|
||||||
"hot": "🔥 Caliente ahora",
|
"hot": "Caliente ahora",
|
||||||
"weekly": "📅 Los mejores juegos de la semana",
|
"weekly": "📅 Los mejores juegos de la semana",
|
||||||
"start_typing": "Empieza a escribir para buscar..."
|
"start_typing": "Empieza a escribir para buscar..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
"featured": "Destaques",
|
||||||
"trending": "Populares",
|
"trending": "Populares",
|
||||||
"hot": "🔥 Populares agora",
|
"hot": "Populares agora",
|
||||||
"weekly": "📅 Mais baixados da semana",
|
"weekly": "📅 Mais baixados da semana",
|
||||||
"surprise_me": "Surpreenda-me",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado",
|
"no_results": "Nenhum resultado encontrado",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"trending": "В тренде",
|
"trending": "В тренде",
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "🔥 Сейчас жарко",
|
"hot": "Сейчас жарко",
|
||||||
"start_typing": "Начинаю вводить текст для поиска...",
|
"start_typing": "Начинаю вводить текст для поиска...",
|
||||||
"weekly": "📅 Лучшие игры недели"
|
"weekly": "📅 Лучшие игры недели"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
|||||||
export const seedsPath = app.isPackaged
|
export const seedsPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "seeds")
|
? path.join(process.resourcesPath, "seeds")
|
||||||
: path.join(__dirname, "..", "..", "seeds");
|
: path.join(__dirname, "..", "..", "seeds");
|
||||||
|
|
||||||
|
export const appVersion = app.getVersion();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defaultDownloadsPath } from "@main/constants";
|
import { appVersion, defaultDownloadsPath } from "@main/constants";
|
||||||
import { app, ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
import "./catalogue/get-catalogue";
|
import "./catalogue/get-catalogue";
|
||||||
import "./catalogue/get-game-shop-details";
|
import "./catalogue/get-game-shop-details";
|
||||||
@@ -59,9 +59,10 @@ import "./profile/update-friend-request";
|
|||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
import "./profile/process-profile-image";
|
import "./profile/process-profile-image";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
|
import "./profile/sync-friend-requests";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => appVersion);
|
||||||
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserProfile } from "@types";
|
import { ProfileVisibility, UserDetails } from "@types";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { steamUrlBuilder, UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
|
|
||||||
const getSteamGame = async (objectId: string) => {
|
|
||||||
try {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: steamGame.name,
|
|
||||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to get Steam game", err);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserDetails | null> => {
|
||||||
return HydraApi.get(`/profile/me`)
|
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||||
.then(async (me) => {
|
.then(async (me) => {
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
@@ -38,17 +20,6 @@ const getMe = async (
|
|||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (me.currentGame) {
|
|
||||||
const steamGame = await getSteamGame(me.currentGame.objectId);
|
|
||||||
|
|
||||||
if (steamGame) {
|
|
||||||
me.currentGame = {
|
|
||||||
...me.currentGame,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.setUser({ id: me.id, username: me.username });
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
return me;
|
||||||
@@ -61,7 +32,13 @@ const getMe = async (
|
|||||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
if (loggedUser) {
|
if (loggedUser) {
|
||||||
return { ...loggedUser, id: loggedUser.userId };
|
return {
|
||||||
|
...loggedUser,
|
||||||
|
id: loggedUser.userId,
|
||||||
|
username: "",
|
||||||
|
bio: "",
|
||||||
|
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
9
src/main/events/profile/sync-friend-requests.ts
Normal file
9
src/main/events/profile/sync-friend-requests.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { FriendRequestSync } from "@types";
|
||||||
|
|
||||||
|
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
|
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("syncFriendRequests", syncFriendRequests);
|
||||||
@@ -2,7 +2,7 @@ import { registerEvent } from "../register-event";
|
|||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
import { getFileBase64 } from "@main/helpers";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
@@ -101,6 +101,17 @@ const startGameDownload = async (
|
|||||||
|
|
||||||
createGame(updatedGame!).catch(() => {});
|
createGame(updatedGame!).catch(() => {});
|
||||||
|
|
||||||
|
HydraApi.post(
|
||||||
|
"/games/download",
|
||||||
|
{
|
||||||
|
objectId: updatedGame!.objectID,
|
||||||
|
shop: updatedGame!.shop,
|
||||||
|
},
|
||||||
|
{ needsAuth: false }
|
||||||
|
).catch((err) => {
|
||||||
|
logger.error("Failed to create game download", err);
|
||||||
|
});
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
|||||||
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
||||||
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
||||||
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
RepackUris,
|
RepackUris,
|
||||||
UpdateUserLanguage,
|
UpdateUserLanguage,
|
||||||
EnsureRepackUris,
|
EnsureRepackUris,
|
||||||
|
FixMissingColumns,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
@@ -25,6 +28,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const knexClient = knex({
|
export const knexClient = knex({
|
||||||
|
debug: !app.isPackaged,
|
||||||
client: "better-sqlite3",
|
client: "better-sqlite3",
|
||||||
connection: {
|
connection: {
|
||||||
filename: databasePath,
|
filename: databasePath,
|
||||||
|
|||||||
@@ -4,55 +4,15 @@ import type { Knex } from "knex";
|
|||||||
export const RepackUris: HydraMigration = {
|
export const RepackUris: HydraMigration = {
|
||||||
name: "RepackUris",
|
name: "RepackUris",
|
||||||
up: async (knex: Knex) => {
|
up: async (knex: Knex) => {
|
||||||
await knex.schema.createTable("temporary_repack", (table) => {
|
await knex.schema.alterTable("repack", (table) => {
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.text("title")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
|
||||||
table
|
|
||||||
.text("magnet")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
|
||||||
table.text("repacker").notNullable();
|
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
table.text("uris").notNullable().defaultTo("[]");
|
table.text("uris").notNullable().defaultTo("[]");
|
||||||
});
|
});
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("repack");
|
|
||||||
await knex.schema.renameTable("temporary_repack", "repack");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
down: async (knex: Knex) => {
|
||||||
await knex.schema.renameTable("repack", "temporary_repack");
|
await knex.schema.alterTable("repack", (table) => {
|
||||||
await knex.schema.createTable("repack", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("title").notNullable().unique();
|
|
||||||
table.text("magnet").notNullable().unique();
|
|
||||||
table.integer("page");
|
table.integer("page");
|
||||||
table.text("repacker").notNullable();
|
table.dropColumn("uris");
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
});
|
});
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("temporary_repack");
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,55 +4,14 @@ import type { Knex } from "knex";
|
|||||||
export const EnsureRepackUris: HydraMigration = {
|
export const EnsureRepackUris: HydraMigration = {
|
||||||
name: "EnsureRepackUris",
|
name: "EnsureRepackUris",
|
||||||
up: async (knex: Knex) => {
|
up: async (knex: Knex) => {
|
||||||
await knex.schema.createTable("temporary_repack", (table) => {
|
await knex.schema.hasColumn("repack", "uris").then(async (exists) => {
|
||||||
const timestamp = new Date().getTime();
|
if (!exists) {
|
||||||
table.increments("id").primary();
|
await knex.schema.table("repack", (table) => {
|
||||||
table
|
table.text("uris").notNullable().defaultTo("[]");
|
||||||
.text("title")
|
});
|
||||||
.notNullable()
|
}
|
||||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
|
||||||
table
|
|
||||||
.text("magnet")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
|
||||||
table.text("repacker").notNullable();
|
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
table.text("uris").notNullable().defaultTo("[]");
|
|
||||||
});
|
});
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("repack");
|
|
||||||
await knex.schema.renameTable("temporary_repack", "repack");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
down: async (_knex: Knex) => {},
|
||||||
await knex.schema.renameTable("repack", "temporary_repack");
|
|
||||||
await knex.schema.createTable("repack", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("title").notNullable().unique();
|
|
||||||
table.text("magnet").notNullable().unique();
|
|
||||||
table.integer("page");
|
|
||||||
table.text("repacker").notNullable();
|
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
});
|
|
||||||
await knex.raw(
|
|
||||||
`INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`
|
|
||||||
);
|
|
||||||
await knex.schema.dropTable("temporary_repack");
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
41
src/main/migrations/20240918001920_FixMissingColumns.ts
Normal file
41
src/main/migrations/20240918001920_FixMissingColumns.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const FixMissingColumns: HydraMigration = {
|
||||||
|
name: "FixMissingColumns",
|
||||||
|
up: async (knex: Knex) => {
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
await knex.schema
|
||||||
|
.hasColumn("repack", "downloadSourceId")
|
||||||
|
.then(async (exists) => {
|
||||||
|
if (!exists) {
|
||||||
|
await knex.schema.table("repack", (table) => {
|
||||||
|
table
|
||||||
|
.integer("downloadSourceId")
|
||||||
|
.references("download_source.id")
|
||||||
|
.onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.hasColumn("game", "remoteId").then(async (exists) => {
|
||||||
|
if (!exists) {
|
||||||
|
await knex.schema.table("game", (table) => {
|
||||||
|
table
|
||||||
|
.text("remoteId")
|
||||||
|
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.hasColumn("game", "uri").then(async (exists) => {
|
||||||
|
if (!exists) {
|
||||||
|
await knex.schema.table("game", (table) => {
|
||||||
|
table.text("uri");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (_knex: Knex) => {},
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@ import { uploadGamesBatch } from "./library-sync";
|
|||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
import { omit } from "lodash-es";
|
||||||
|
import { appVersion } from "@main/constants";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth: boolean;
|
needsAuth: boolean;
|
||||||
@@ -79,12 +81,16 @@ export class HydraApi {
|
|||||||
static async setupApi() {
|
static async setupApi() {
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||||
|
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
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.params, request.data);
|
const data = Array.isArray(request.data)
|
||||||
|
? request.data
|
||||||
|
: omit(request.data, ["refreshToken"]);
|
||||||
|
logger.log(request.method, request.url, request.params, data);
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -96,11 +102,14 @@ export class HydraApi {
|
|||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
logger.log(" ---- RESPONSE -----");
|
logger.log(" ---- RESPONSE -----");
|
||||||
|
const data = Array.isArray(response.data)
|
||||||
|
? response.data
|
||||||
|
: omit(response.data, ["username", "accessToken", "refreshToken"]);
|
||||||
logger.log(
|
logger.log(
|
||||||
response.status,
|
response.status,
|
||||||
response.config.method,
|
response.config.method,
|
||||||
response.config.url,
|
response.config.url,
|
||||||
response.data
|
data
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
@@ -166,7 +175,10 @@ export class HydraApi {
|
|||||||
this.userAuth.authToken = accessToken;
|
this.userAuth.authToken = accessToken;
|
||||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||||
|
|
||||||
logger.log("Token refreshed", this.userAuth);
|
logger.log(
|
||||||
|
"Token refreshed. New expiration:",
|
||||||
|
this.userAuth.expirationTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
export const createGame = async (game: Game) => {
|
export const createGame = async (game: Game) => {
|
||||||
HydraApi.post(
|
|
||||||
"/games/download",
|
|
||||||
{
|
|
||||||
objectId: game.objectID,
|
|
||||||
shop: game.shop,
|
|
||||||
},
|
|
||||||
{ needsAuth: false }
|
|
||||||
).catch((err) => {
|
|
||||||
logger.error("Failed to create game download", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.post(`/profile/games`, {
|
return HydraApi.post(`/profile/games`, {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const onCloseGame = (game: Game) => {
|
|||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
updateGamePlaytime(
|
updateGamePlaytime(
|
||||||
game,
|
game,
|
||||||
performance.now() - gamePlaytime.firstTick,
|
performance.now() - gamePlaytime.lastSyncTick,
|
||||||
game.lastTimePlayed!
|
game.lastTimePlayed!
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
processProfileImage: (imagePath: string) =>
|
processProfileImage: (imagePath: string) =>
|
||||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||||
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
|
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
||||||
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
sendFriendRequest: (userId: string) =>
|
sendFriendRequest: (userId: string) =>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function App() {
|
|||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
fetchFriendRequests,
|
syncFriendRequests,
|
||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
|
|
||||||
@@ -105,22 +105,22 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, fetchFriendRequests, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
fetchFriendRequests,
|
syncFriendRequests,
|
||||||
t,
|
t,
|
||||||
showSuccessToast,
|
showSuccessToast,
|
||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
|
|||||||
1735
src/renderer/src/assets/lottie/flame.json
Normal file
1735
src/renderer/src/assets/lottie/flame.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import Lottie from "lottie-react";
|
import Lottie from "lottie-react";
|
||||||
|
|
||||||
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
||||||
@@ -8,11 +7,8 @@ export interface DownloadIconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
||||||
const lottieRef = useRef(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Lottie
|
<Lottie
|
||||||
lottieRef={lottieRef}
|
|
||||||
animationData={downloadingAnimation}
|
animationData={downloadingAnimation}
|
||||||
loop={isDownloading}
|
loop={isDownloading}
|
||||||
autoplay={isDownloading}
|
autoplay={isDownloading}
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, friendRequests, showFriendsModal, fetchFriendRequests } =
|
const {
|
||||||
useUserDetails();
|
userDetails,
|
||||||
|
friendRequestCount,
|
||||||
|
showFriendsModal,
|
||||||
|
syncFriendRequests,
|
||||||
|
} = useUserDetails();
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
const receivedRequests = useMemo(() => {
|
|
||||||
return friendRequests.filter((request) => request.type === "RECEIVED");
|
|
||||||
}, [friendRequests]);
|
|
||||||
|
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
if (userDetails === null) {
|
if (userDetails === null) {
|
||||||
window.electron.openAuthWindow();
|
window.electron.openAuthWindow();
|
||||||
@@ -35,7 +35,7 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pollingInterval.current = setInterval(() => {
|
pollingInterval.current = setInterval(() => {
|
||||||
fetchFriendRequests();
|
syncFriendRequests();
|
||||||
}, LONG_POLLING_INTERVAL);
|
}, LONG_POLLING_INTERVAL);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -43,7 +43,7 @@ export function SidebarProfile() {
|
|||||||
clearInterval(pollingInterval.current);
|
clearInterval(pollingInterval.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [fetchFriendRequests]);
|
}, [syncFriendRequests]);
|
||||||
|
|
||||||
const friendsButton = useMemo(() => {
|
const friendsButton = useMemo(() => {
|
||||||
if (!userDetails) return null;
|
if (!userDetails) return null;
|
||||||
@@ -57,16 +57,16 @@ export function SidebarProfile() {
|
|||||||
}
|
}
|
||||||
title={t("friends")}
|
title={t("friends")}
|
||||||
>
|
>
|
||||||
{receivedRequests.length > 0 && (
|
{friendRequestCount > 0 && (
|
||||||
<small className={styles.friendsButtonBadge}>
|
<small className={styles.friendsButtonBadge}>
|
||||||
{receivedRequests.length > 99 ? "99+" : receivedRequests.length}
|
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PeopleIcon size={16} />
|
<PeopleIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}, [userDetails, t, receivedRequests, showFriendsModal]);
|
}, [userDetails, t, friendRequestCount, showFriendsModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.profileContainer}>
|
<div className={styles.profileContainer}>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function GameDetailsContextProvider({
|
|||||||
setShopDetails(appDetailsResult.value);
|
setShopDetails(appDetailsResult.value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appDetailsResult.value!.content_descriptors.ids.includes(
|
appDetailsResult.value?.content_descriptors.ids.includes(
|
||||||
SteamContentDescriptor.AdultOnlySexualContent
|
SteamContentDescriptor.AdultOnlySexualContent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -23,6 +23,8 @@ import type {
|
|||||||
GameStats,
|
GameStats,
|
||||||
TrendingGame,
|
TrendingGame,
|
||||||
UserStats,
|
UserStats,
|
||||||
|
UserDetails,
|
||||||
|
FriendRequestSync,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ declare global {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
/* Profile */
|
/* Profile */
|
||||||
getMe: () => Promise<UserProfile | null>;
|
getMe: () => Promise<UserDetails | null>;
|
||||||
undoFriendship: (userId: string) => Promise<void>;
|
undoFriendship: (userId: string) => Promise<void>;
|
||||||
updateProfile: (
|
updateProfile: (
|
||||||
updateProfile: UpdateProfileRequest
|
updateProfile: UpdateProfileRequest
|
||||||
@@ -163,6 +165,7 @@ declare global {
|
|||||||
path: string
|
path: string
|
||||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||||
getFriendRequests: () => Promise<FriendRequest[]>;
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
|
syncFriendRequests: () => Promise<FriendRequestSync>;
|
||||||
updateFriendRequest: (
|
updateFriendRequest: (
|
||||||
userId: string,
|
userId: string,
|
||||||
action: FriendRequestAction
|
action: FriendRequestAction
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import type { FriendRequest, UserProfile } from "@types";
|
import type { FriendRequest, UserDetails } from "@types";
|
||||||
|
|
||||||
export interface UserDetailsState {
|
export interface UserDetailsState {
|
||||||
userDetails: UserProfile | null;
|
userDetails: UserDetails | null;
|
||||||
profileBackground: null | string;
|
profileBackground: null | string;
|
||||||
friendRequests: FriendRequest[];
|
friendRequests: FriendRequest[];
|
||||||
|
friendRequestCount: number;
|
||||||
isFriendsModalVisible: boolean;
|
isFriendsModalVisible: boolean;
|
||||||
friendRequetsModalTab: UserFriendModalTab | null;
|
friendRequetsModalTab: UserFriendModalTab | null;
|
||||||
friendModalUserId: string;
|
friendModalUserId: string;
|
||||||
@@ -15,6 +16,7 @@ const initialState: UserDetailsState = {
|
|||||||
userDetails: null,
|
userDetails: null,
|
||||||
profileBackground: null,
|
profileBackground: null,
|
||||||
friendRequests: [],
|
friendRequests: [],
|
||||||
|
friendRequestCount: 0,
|
||||||
isFriendsModalVisible: false,
|
isFriendsModalVisible: false,
|
||||||
friendRequetsModalTab: null,
|
friendRequetsModalTab: null,
|
||||||
friendModalUserId: "",
|
friendModalUserId: "",
|
||||||
@@ -24,7 +26,7 @@ export const userDetailsSlice = createSlice({
|
|||||||
name: "user-details",
|
name: "user-details",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setUserDetails: (state, action: PayloadAction<UserProfile | null>) => {
|
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
||||||
state.userDetails = action.payload;
|
state.userDetails = action.payload;
|
||||||
},
|
},
|
||||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||||
@@ -33,6 +35,9 @@ export const userDetailsSlice = createSlice({
|
|||||||
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
||||||
state.friendRequests = action.payload;
|
state.friendRequests = action.payload;
|
||||||
},
|
},
|
||||||
|
setFriendRequestCount: (state, action: PayloadAction<number>) => {
|
||||||
|
state.friendRequestCount = action.payload;
|
||||||
|
},
|
||||||
setFriendsModalVisible: (
|
setFriendsModalVisible: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }>
|
||||||
@@ -52,6 +57,7 @@ export const {
|
|||||||
setUserDetails,
|
setUserDetails,
|
||||||
setProfileBackground,
|
setProfileBackground,
|
||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
|
setFriendRequestCount,
|
||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} = userDetailsSlice.actions;
|
} = userDetailsSlice.actions;
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import {
|
|||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
|
setFriendRequestCount,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import type {
|
import type {
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UserProfile,
|
UserDetails,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export function useUserDetails() {
|
|||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
|
friendRequestCount,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
@@ -40,7 +42,7 @@ export function useUserDetails() {
|
|||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const updateUserDetails = useCallback(
|
const updateUserDetails = useCallback(
|
||||||
async (userDetails: UserProfile) => {
|
async (userDetails: UserDetails) => {
|
||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
@@ -83,7 +85,10 @@ export function useUserDetails() {
|
|||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (values: UpdateProfileRequest) => {
|
async (values: UpdateProfileRequest) => {
|
||||||
const response = await window.electron.updateProfile(values);
|
const response = await window.electron.updateProfile(values);
|
||||||
return updateUserDetails(response);
|
return updateUserDetails({
|
||||||
|
...response,
|
||||||
|
username: userDetails?.username || "",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
);
|
);
|
||||||
@@ -92,11 +97,21 @@ export function useUserDetails() {
|
|||||||
return window.electron
|
return window.electron
|
||||||
.getFriendRequests()
|
.getFriendRequests()
|
||||||
.then((friendRequests) => {
|
.then((friendRequests) => {
|
||||||
|
syncFriendRequests();
|
||||||
dispatch(setFriendRequests(friendRequests));
|
dispatch(setFriendRequests(friendRequests));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const syncFriendRequests = useCallback(async () => {
|
||||||
|
return window.electron
|
||||||
|
.syncFriendRequests()
|
||||||
|
.then((sync) => {
|
||||||
|
dispatch(setFriendRequestCount(sync.friendRequestCount));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const showFriendsModal = useCallback(
|
const showFriendsModal = useCallback(
|
||||||
(initialTab: UserFriendModalTab, userId: string) => {
|
(initialTab: UserFriendModalTab, userId: string) => {
|
||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
@@ -140,6 +155,7 @@ export function useUserDetails() {
|
|||||||
userDetails,
|
userDetails,
|
||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
|
friendRequestCount,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendModalUserId,
|
friendModalUserId,
|
||||||
@@ -152,6 +168,7 @@ export function useUserDetails() {
|
|||||||
patchUser,
|
patchUser,
|
||||||
sendFriendRequest,
|
sendFriendRequest,
|
||||||
fetchFriendRequests,
|
fetchFriendRequests,
|
||||||
|
syncFriendRequests,
|
||||||
updateFriendRequestState,
|
updateFriendRequestState,
|
||||||
blockUser,
|
blockUser,
|
||||||
unblockUser,
|
unblockUser,
|
||||||
|
|||||||
@@ -149,8 +149,7 @@ export const randomizerButton = style({
|
|||||||
animationName: slideIn,
|
animationName: slideIn,
|
||||||
animationDuration: "0.2s",
|
animationDuration: "0.2s",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
/* Bottom panel height + spacing */
|
bottom: `${SPACING_UNIT * 3}px`,
|
||||||
bottom: `${26 + SPACING_UNIT * 2}px`,
|
|
||||||
/* Scroll bar + spacing */
|
/* Scroll bar + spacing */
|
||||||
right: `${9 + SPACING_UNIT * 2}px`,
|
right: `${9 + SPACING_UNIT * 2}px`,
|
||||||
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
@@ -9,7 +9,7 @@ import { useFormat } from "@renderer/hooks";
|
|||||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [_howLongToBeat, setHowLongToBeat] = useState<{
|
const [_howLongToBeat, _setHowLongToBeat] = useState<{
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
data: HowLongToBeatCategory[] | null;
|
data: HowLongToBeatCategory[] | null;
|
||||||
}>({ isLoading: true, data: null });
|
}>({ isLoading: true, data: null });
|
||||||
@@ -17,27 +17,26 @@ export function Sidebar() {
|
|||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
const { gameTitle, shopDetails, objectID, stats } =
|
const { gameTitle, shopDetails, stats } = useContext(gameDetailsContext);
|
||||||
useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (objectID) {
|
// if (objectID) {
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
// setHowLongToBeat({ isLoading: true, data: null });
|
||||||
|
|
||||||
window.electron
|
// window.electron
|
||||||
.getHowLongToBeat(objectID, "steam", gameTitle)
|
// .getHowLongToBeat(objectID, "steam", gameTitle)
|
||||||
.then((howLongToBeat) => {
|
// .then((howLongToBeat) => {
|
||||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
// setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||||
})
|
// })
|
||||||
.catch(() => {
|
// .catch(() => {
|
||||||
setHowLongToBeat({ isLoading: false, data: null });
|
// setHowLongToBeat({ isLoading: false, data: null });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}, [objectID, gameTitle]);
|
// }, [objectID, gameTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.contentSidebar}>
|
<aside className={styles.contentSidebar}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, 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";
|
||||||
|
|
||||||
@@ -8,10 +8,11 @@ import { Button, GameCard, Hero } from "@renderer/components";
|
|||||||
import type { Steam250Game, CatalogueEntry } from "@types";
|
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
import flameAnimation from "@renderer/assets/lottie/flame.json";
|
||||||
|
|
||||||
import * as styles from "./home.css";
|
import * as styles from "./home.css";
|
||||||
import { vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import Lottie from "lottie-react";
|
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ export function Home() {
|
|||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const flameAnimationRef = useRef<LottieRefCurrentProps>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
|
|
||||||
@@ -82,6 +85,18 @@ export function Home() {
|
|||||||
|
|
||||||
const categories = Object.values(CatalogueCategory);
|
const categories = Object.values(CatalogueCategory);
|
||||||
|
|
||||||
|
const handleMouseEnterCategory = (category: CatalogueCategory) => {
|
||||||
|
if (category === CatalogueCategory.Hot) {
|
||||||
|
flameAnimationRef?.current?.play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeaveCategory = (category: CatalogueCategory) => {
|
||||||
|
if (category === CatalogueCategory.Hot) {
|
||||||
|
flameAnimationRef?.current?.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
@@ -100,7 +115,28 @@ export function Home() {
|
|||||||
: "outline"
|
: "outline"
|
||||||
}
|
}
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
|
onMouseEnter={() => handleMouseEnterCategory(category)}
|
||||||
|
onMouseLeave={() => handleMouseLeaveCategory(category)}
|
||||||
>
|
>
|
||||||
|
{category === CatalogueCategory.Hot && (
|
||||||
|
<div
|
||||||
|
style={{ width: 16, height: 16, position: "relative" }}
|
||||||
|
>
|
||||||
|
<Lottie
|
||||||
|
lottieRef={flameAnimationRef}
|
||||||
|
animationData={flameAnimation}
|
||||||
|
loop
|
||||||
|
autoplay={false}
|
||||||
|
style={{
|
||||||
|
width: 30,
|
||||||
|
top: -10,
|
||||||
|
left: -5,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{t(category)}
|
{t(category)}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
@@ -116,14 +152,32 @@ export function Home() {
|
|||||||
<Lottie
|
<Lottie
|
||||||
animationData={starsAnimation}
|
animationData={starsAnimation}
|
||||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||||
loop
|
loop={Boolean(randomGame)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{t("surprise_me")}
|
{t("surprise_me")}
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2>{t(currentCatalogueCategory)}</h2>
|
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
||||||
|
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
||||||
|
<div style={{ width: 24, height: 24, position: "relative" }}>
|
||||||
|
<Lottie
|
||||||
|
animationData={flameAnimation}
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
top: -10,
|
||||||
|
left: -5,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{t(currentCatalogueCategory)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<section className={styles.cards}>
|
<section className={styles.cards}>
|
||||||
{isLoading
|
{isLoading
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const listItemImage = style({
|
|||||||
width: "32px",
|
width: "32px",
|
||||||
height: "32px",
|
height: "32px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listItemDetails = style({
|
export const listItemDetails = style({
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ export interface UserBlocks {
|
|||||||
blocks: UserFriend[];
|
blocks: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FriendRequestSync {
|
||||||
|
friendRequestCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -190,23 +194,34 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
|
|||||||
sessionDurationInSeconds: number;
|
sessionDurationInSeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
|
||||||
|
export interface UserDetails {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
profileVisibility: ProfileVisibility;
|
||||||
|
bio: 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";
|
profileVisibility: ProfileVisibility;
|
||||||
totalPlayTimeInSeconds: number;
|
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
recentGames: UserGame[];
|
recentGames: UserGame[];
|
||||||
friends: UserFriend[];
|
friends: UserFriend[];
|
||||||
totalFriends: number;
|
totalFriends: number;
|
||||||
relation: UserRelation | null;
|
relation: UserRelation | null;
|
||||||
currentGame: UserProfileCurrentGame | null;
|
currentGame: UserProfileCurrentGame | null;
|
||||||
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProfileRequest {
|
export interface UpdateProfileRequest {
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
profileVisibility?: "PUBLIC" | "PRIVATE" | "FRIENDS";
|
profileVisibility?: ProfileVisibility;
|
||||||
profileImageUrl?: string | null;
|
profileImageUrl?: string | null;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user