mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 18:33:56 +00:00
Compare commits
94 Commits
feat/playt
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df5486fec | ||
|
|
26dfb6db8e | ||
|
|
ae47498139 | ||
|
|
bc5c5da703 | ||
|
|
5f1026fcd2 | ||
|
|
de4b039d10 | ||
|
|
fd4d293e13 | ||
|
|
e5646240ab | ||
|
|
655de1361b | ||
|
|
ceb236c40c | ||
|
|
570b881c2b | ||
|
|
959bed746b | ||
|
|
f0cb2f9579 | ||
|
|
0f3d6ef76f | ||
|
|
3301f845f5 | ||
|
|
9689c19863 | ||
|
|
bd053a1635 | ||
|
|
a39f9ebb70 | ||
|
|
7e22344f77 | ||
|
|
96d6b90356 | ||
|
|
a87e04a366 | ||
|
|
bd86321d02 | ||
|
|
95fca086ba | ||
|
|
3e93a14deb | ||
|
|
d4b4b25ec8 | ||
|
|
2bed7c0b37 | ||
|
|
f3b4898e9c | ||
|
|
046f6d388a | ||
|
|
dfd640ebda | ||
|
|
88ed335fd4 | ||
|
|
d48f28b3c7 | ||
|
|
8f30f8a4ad | ||
|
|
7bbca91d9f | ||
|
|
dceb3a7509 | ||
|
|
d78631a7f4 | ||
|
|
889f3bb773 | ||
|
|
b62f8c13fc | ||
|
|
f552b759cb | ||
|
|
83e7f676bf | ||
|
|
cb25a55118 | ||
|
|
5d4409525c | ||
|
|
92af5c2b23 | ||
|
|
2cebc73789 | ||
|
|
e96cb3228e | ||
|
|
a71293148d | ||
|
|
aa152385b1 | ||
|
|
b6be03cea3 | ||
|
|
f027f05e02 | ||
|
|
56391837cd | ||
|
|
31bd523038 | ||
|
|
fd1f13225b | ||
|
|
dba4b2c4be | ||
|
|
05f10fd80b | ||
|
|
d59315e322 | ||
|
|
a29f2ba741 | ||
|
|
d33b0099a1 | ||
|
|
cad50649aa | ||
|
|
b3148855bc | ||
|
|
f56a3ea126 | ||
|
|
eea701f046 | ||
|
|
355d38c0a2 | ||
|
|
366da7839f | ||
|
|
a869902cfb | ||
|
|
f9d51ed33d | ||
|
|
092af7e421 | ||
|
|
33c15baf0e | ||
|
|
eeed34adcb | ||
|
|
393a04d7c3 | ||
|
|
de4119988c | ||
|
|
e0496b6449 | ||
|
|
607bc6407c | ||
|
|
de70beb01e | ||
|
|
9f4fd0ce61 | ||
|
|
bf9e3de0b5 | ||
|
|
9ce1c40b21 | ||
|
|
78358d212b | ||
|
|
ea792d0409 | ||
|
|
04be5c754e | ||
|
|
ab163ffa39 | ||
|
|
501ca7190e | ||
|
|
14fc764328 | ||
|
|
672ddff9f8 | ||
|
|
f4e84e46cc | ||
|
|
3409b53268 | ||
|
|
7e59e02d03 | ||
|
|
a7e4e21167 | ||
|
|
b22e082781 | ||
|
|
c056feb26f | ||
|
|
240b0705d5 | ||
|
|
2604dfea22 | ||
|
|
a546b906e9 | ||
|
|
85b40b9a7e | ||
|
|
302ed92018 | ||
|
|
7e5cef6e44 |
@@ -54,6 +54,8 @@
|
|||||||
"diskusage": "^1.2.0",
|
"diskusage": "^1.2.0",
|
||||||
"electron-log": "^5.2.4",
|
"electron-log": "^5.2.4",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"file-type": "^20.5.0",
|
"file-type": "^20.5.0",
|
||||||
"framer-motion": "^12.15.0",
|
"framer-motion": "^12.15.0",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
@@ -63,6 +65,8 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"parse-torrent": "^11.0.18",
|
"parse-torrent": "^11.0.18",
|
||||||
"rc-virtual-list": "^3.18.3",
|
"rc-virtual-list": "^3.18.3",
|
||||||
|
"react-dnd": "^16.0.1",
|
||||||
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "مميز",
|
|
||||||
"surprise_me": "مفاجئني",
|
"surprise_me": "مفاجئني",
|
||||||
"no_results": "لم يتم العثور على نتائج",
|
"no_results": "لم يتم العثور على نتائج",
|
||||||
"start_typing": "ابدأ بالكتابة للبحث...",
|
"start_typing": "ابدأ بالكتابة للبحث...",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "беларуская мова",
|
"language_name": "беларуская мова",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рэкамэндаванае",
|
|
||||||
"surprise_me": "Здзіві мяне",
|
"surprise_me": "Здзіві мяне",
|
||||||
"no_results": "Няма вынікаў"
|
"no_results": "Няма вынікаў"
|
||||||
},
|
},
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
"home": "Галоўная",
|
"home": "Галоўная",
|
||||||
"favorites": "Улюбленыя"
|
"favorites": "Улюбленыя"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Пошук",
|
"search": "Пошук",
|
||||||
"home": "Галоўная",
|
"home": "Галоўная",
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
"downloading_metadata": "Сцягванне мэтаданых {{title}}…",
|
||||||
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
"downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Наступная старонка",
|
|
||||||
"previous_page": "Папярэдняя старонка"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Адкрыць варыянты сцягвання",
|
"open_download_options": "Адкрыць варыянты сцягвання",
|
||||||
"download_options_zero": "Няма варыянтаў сцягвання",
|
"download_options_zero": "Няма варыянтаў сцягвання",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Успешно влизане"
|
"successfully_signed_in": "Успешно влизане"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Препоръчани",
|
|
||||||
"surprise_me": "Изненадай ме",
|
"surprise_me": "Изненадай ме",
|
||||||
"no_results": "Няма намерени резултати",
|
"no_results": "Няма намерени резултати",
|
||||||
"start_typing": "Започнете да пишете за търсене...",
|
"start_typing": "Започнете да пишете за търсене...",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Has entrat correctament"
|
"successfully_signed_in": "Has entrat correctament"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destacats",
|
|
||||||
"surprise_me": "Sorprèn-me",
|
"surprise_me": "Sorprèn-me",
|
||||||
"no_results": "No s'ha trobat res"
|
"no_results": "No s'ha trobat res"
|
||||||
},
|
},
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca jocs",
|
"search": "Cerca jocs",
|
||||||
|
|
||||||
"home": "Inici",
|
"home": "Inici",
|
||||||
"catalogue": "Catàleg",
|
"catalogue": "Catàleg",
|
||||||
"downloads": "Baixades",
|
"downloads": "Baixades",
|
||||||
@@ -41,10 +39,7 @@
|
|||||||
"calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…",
|
"calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…",
|
||||||
"checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)"
|
"checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Pàgina següent",
|
|
||||||
"previous_page": "Pàgina anterior"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Obre les opcions de baixada",
|
"open_download_options": "Obre les opcions de baixada",
|
||||||
"download_options_zero": "No hi ha opcions de baixada",
|
"download_options_zero": "No hi ha opcions de baixada",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Úspěšně přihlášen"
|
"successfully_signed_in": "Úspěšně přihlášen"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Doporučené",
|
|
||||||
"surprise_me": "Překvap mě",
|
"surprise_me": "Překvap mě",
|
||||||
"no_results": "Výsledek nenalezen",
|
"no_results": "Výsledek nenalezen",
|
||||||
"start_typing": "Začni psát pro vyhledávání...",
|
"start_typing": "Začni psát pro vyhledávání...",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Loggede ind successfuldt"
|
"successfully_signed_in": "Loggede ind successfuldt"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalet",
|
|
||||||
"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...",
|
"start_typing": "Begynd at skrive for at søge...",
|
||||||
@@ -29,7 +28,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søg efter spil",
|
"search": "Søg efter spil",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
@@ -45,10 +43,7 @@
|
|||||||
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
|
"calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…",
|
||||||
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
|
"checking_files": "Checker {{title}} filer… ({{percentage}} færdig)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Næste side",
|
|
||||||
"previous_page": "Forrige side"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Åben download muligheder",
|
"open_download_options": "Åben download muligheder",
|
||||||
"download_options_zero": "Ingen download mulighed",
|
"download_options_zero": "Ingen download mulighed",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Erfolgreich angemeldet"
|
"successfully_signed_in": "Erfolgreich angemeldet"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Empfohlen",
|
|
||||||
"surprise_me": "Überrasche mich",
|
"surprise_me": "Überrasche mich",
|
||||||
"no_results": "Keine Ergebnisse gefunden",
|
"no_results": "Keine Ergebnisse gefunden",
|
||||||
"start_typing": "Tippe, um zu suchen...",
|
"start_typing": "Tippe, um zu suchen...",
|
||||||
@@ -59,9 +58,7 @@
|
|||||||
"download_sources": "Download-Quellen",
|
"download_sources": "Download-Quellen",
|
||||||
"result_count": "{{resultCount}} Ergebnisse",
|
"result_count": "{{resultCount}} Ergebnisse",
|
||||||
"filter_count": "{{filterCount}} verfügbar",
|
"filter_count": "{{filterCount}} verfügbar",
|
||||||
"clear_filters": "{{filterCount}} ausgewählte löschen",
|
"clear_filters": "{{filterCount}} ausgewählte löschen"
|
||||||
"next_page": "Nächste Seite",
|
|
||||||
"previous_page": "Vorherige Seite"
|
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Download-Optionen öffnen",
|
"open_download_options": "Download-Optionen öffnen",
|
||||||
|
|||||||
74
src/locales/en/translation.json
Normal file → Executable file
74
src/locales/en/translation.json
Normal file → Executable file
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Successfully signed in"
|
"successfully_signed_in": "Successfully signed in"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
|
||||||
"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...",
|
||||||
@@ -28,7 +27,50 @@
|
|||||||
"friends": "Friends",
|
"friends": "Friends",
|
||||||
"need_help": "Need help?",
|
"need_help": "Need help?",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
"playable_button_title": "Show only games you can play now"
|
"playable_button_title": "Show only games you can play now",
|
||||||
|
"add_custom_game_tooltip": "Add Custom Game",
|
||||||
|
"show_playable_only_tooltip": "Show Playable Only",
|
||||||
|
"custom_game_modal": "Add Custom Game",
|
||||||
|
"custom_game_modal_description": "Add a custom game to your library by selecting an executable file",
|
||||||
|
"custom_game_modal_executable_path": "Executable Path",
|
||||||
|
"custom_game_modal_select_executable": "Select executable file",
|
||||||
|
"custom_game_modal_title": "Title",
|
||||||
|
"custom_game_modal_enter_title": "Enter title",
|
||||||
|
"custom_game_modal_browse": "Browse",
|
||||||
|
"custom_game_modal_cancel": "Cancel",
|
||||||
|
"custom_game_modal_add": "Add Game",
|
||||||
|
"custom_game_modal_adding": "Adding Game...",
|
||||||
|
"custom_game_modal_success": "Custom game added successfully",
|
||||||
|
"custom_game_modal_failed": "Failed to add custom game",
|
||||||
|
"custom_game_modal_executable": "Executable",
|
||||||
|
"edit_game_modal": "Customize Assets",
|
||||||
|
"edit_game_modal_description": "Customize game assets and details",
|
||||||
|
"edit_game_modal_title": "Title",
|
||||||
|
"edit_game_modal_enter_title": "Enter title",
|
||||||
|
"edit_game_modal_image": "Image",
|
||||||
|
"edit_game_modal_select_image": "Select image",
|
||||||
|
"edit_game_modal_browse": "Browse",
|
||||||
|
"edit_game_modal_image_preview": "Image preview",
|
||||||
|
"edit_game_modal_icon": "Icon",
|
||||||
|
"edit_game_modal_select_icon": "Select icon",
|
||||||
|
"edit_game_modal_icon_preview": "Icon preview",
|
||||||
|
"edit_game_modal_logo": "Logo",
|
||||||
|
"edit_game_modal_select_logo": "Select logo",
|
||||||
|
"edit_game_modal_logo_preview": "Logo preview",
|
||||||
|
"edit_game_modal_hero": "Library Hero",
|
||||||
|
"edit_game_modal_select_hero": "Select library hero image",
|
||||||
|
"edit_game_modal_hero_preview": "Library hero image preview",
|
||||||
|
"edit_game_modal_cancel": "Cancel",
|
||||||
|
"edit_game_modal_update": "Update",
|
||||||
|
"edit_game_modal_updating": "Updating...",
|
||||||
|
"edit_game_modal_fill_required": "Please fill in all required fields",
|
||||||
|
"edit_game_modal_success": "Assets updated successfully",
|
||||||
|
"edit_game_modal_failed": "Failed to update assets",
|
||||||
|
"edit_game_modal_image_filter": "Image",
|
||||||
|
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
|
||||||
|
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
|
||||||
|
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
|
||||||
|
"edit_game_modal_assets": "Assets"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@@ -210,6 +252,8 @@
|
|||||||
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
|
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
|
||||||
"game_removed_from_favorites": "Game removed from favorites",
|
"game_removed_from_favorites": "Game removed from favorites",
|
||||||
"game_added_to_favorites": "Game added to favorites",
|
"game_added_to_favorites": "Game added to favorites",
|
||||||
|
"game_removed_from_pinned": "Game removed from pinned",
|
||||||
|
"game_added_to_pinned": "Game added to pinned",
|
||||||
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
|
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
|
||||||
"create_start_menu_shortcut": "Create Start Menu shortcut",
|
"create_start_menu_shortcut": "Create Start Menu shortcut",
|
||||||
"invalid_wine_prefix_path": "Invalid Wine prefix path",
|
"invalid_wine_prefix_path": "Invalid Wine prefix path",
|
||||||
@@ -228,7 +272,21 @@
|
|||||||
"backup_frozen": "Backup pinned",
|
"backup_frozen": "Backup pinned",
|
||||||
"backup_unfrozen": "Backup unpinned",
|
"backup_unfrozen": "Backup unpinned",
|
||||||
"backup_freeze_failed": "Failed to freeze backup",
|
"backup_freeze_failed": "Failed to freeze backup",
|
||||||
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups"
|
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups",
|
||||||
|
"edit_game_modal_button": "Customize game assets",
|
||||||
|
"game_details": "Game Details",
|
||||||
|
"currency_symbol": "$",
|
||||||
|
"currency_country": "us",
|
||||||
|
"prices": "Prices",
|
||||||
|
"no_prices_found": "No prices found",
|
||||||
|
"view_all_prices": "Click to view all prices",
|
||||||
|
"retail_price": "Retail price",
|
||||||
|
"keyshop_price": "Keyshop price",
|
||||||
|
"historical_retail": "Historical retail",
|
||||||
|
"historical_keyshop": "Historical keyshop",
|
||||||
|
"language": "Language",
|
||||||
|
"caption": "Caption",
|
||||||
|
"audio": "Audio"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@@ -448,9 +506,15 @@
|
|||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} hours",
|
"amount_hours": "{{amount}} hours",
|
||||||
"amount_minutes": "{{amount}} minutes",
|
"amount_minutes": "{{amount}} minutes",
|
||||||
|
"amount_hours_short": "{{amount}}h",
|
||||||
|
"amount_minutes_short": "{{amount}}m",
|
||||||
"last_time_played": "Last played {{period}}",
|
"last_time_played": "Last played {{period}}",
|
||||||
"activity": "Recent Activity",
|
"activity": "Recent Activity",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
|
"pinned": "Pinned",
|
||||||
|
"achievements_earned": "Achievements earned",
|
||||||
|
"played_recently": "Played recently",
|
||||||
|
"playtime": "Playtime",
|
||||||
"total_play_time": "Total playtime",
|
"total_play_time": "Total playtime",
|
||||||
"manual_playtime_tooltip": "This playtime has been manually updated",
|
"manual_playtime_tooltip": "This playtime has been manually updated",
|
||||||
"no_recent_activity_title": "Hmmm… nothing here",
|
"no_recent_activity_title": "Hmmm… nothing here",
|
||||||
@@ -527,7 +591,9 @@
|
|||||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||||
"show_points_on_profile": "Show your earned points on your profile",
|
"show_points_on_profile": "Show your earned points on your profile",
|
||||||
"error_adding_friend": "Could not send friend request. Please check friend code",
|
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||||
"friend_code_length_error": "Friend code must have 8 characters"
|
"friend_code_length_error": "Friend code must have 8 characters",
|
||||||
|
"game_removed_from_pinned": "Game removed from pinned",
|
||||||
|
"game_added_to_pinned": "Game added to pinned"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked",
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
|||||||
@@ -1,34 +1,76 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Español",
|
"language_name": "Español",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "Sesión iniciada exitosamente"
|
"successfully_signed_in": "Iniciaste sesión exitosamente"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destacado",
|
|
||||||
"surprise_me": "¡Sorpréndeme!",
|
"surprise_me": "¡Sorpréndeme!",
|
||||||
"no_results": "Sin resultados encontrados",
|
"no_results": "No se encontraron resultados",
|
||||||
"start_typing": "Empieza a escribir para buscar...",
|
"start_typing": "Empezá a escribir para buscar...",
|
||||||
"hot": "Popular Ahora",
|
"hot": "Tendencias",
|
||||||
"weekly": "📅 Mejores juegos de la semana",
|
"weekly": "📅 Mejores juegos de la semana",
|
||||||
"achievements": "🏆 Juegos para completar"
|
"achievements": "🏆 Juegos para platinar"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"catalogue": "Catálogo",
|
"catalogue": "Catálogo",
|
||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"my_library": "Mi biblioteca",
|
"my_library": "Mi Librería",
|
||||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||||
"paused": "{{title}} (Pausado)",
|
"paused": "{{title}} (Pausado)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
"downloading": "{{title}} ({{percentage}} - Descargando…)",
|
||||||
"filter": "Buscar en la biblioteca",
|
"filter": "Filtrar Librería",
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
"queued": "{{title}} (En cola)",
|
"queued": "{{title}} (En cola)",
|
||||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar Sesión",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "¿Necesitas ayuda?",
|
"need_help": "¿Necesitás ayuda?",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
"playable_button_title": "Mostrar solo juegos que puedes jugar ahora"
|
"playable_button_title": "Solo mostrar juegos que podés jugar en este momento",
|
||||||
|
"add_custom_game_tooltip": "Añadir juego personalizado",
|
||||||
|
"show_playable_only_tooltip": "Mostrar Solo Jugable",
|
||||||
|
"custom_game_modal": "Añadir juego personalizado",
|
||||||
|
"custom_game_modal_description": "Añadí un juego personalizado a tu librería seleccionando el ejecutable",
|
||||||
|
"custom_game_modal_executable_path": "Ruta del Ejecutable",
|
||||||
|
"custom_game_modal_select_executable": "Seleccionar archivo ejecutable",
|
||||||
|
"custom_game_modal_title": "Título",
|
||||||
|
"custom_game_modal_enter_title": "Ingresá el título",
|
||||||
|
"custom_game_modal_browse": "Buscar",
|
||||||
|
"custom_game_modal_cancel": "Cancelar",
|
||||||
|
"custom_game_modal_add": "Añadir juego",
|
||||||
|
"custom_game_modal_adding": "Añadiendo juego...",
|
||||||
|
"custom_game_modal_success": "Juego personalizado añadido exitosamente",
|
||||||
|
"custom_game_modal_failed": "Error al añadir juego personalizado",
|
||||||
|
"custom_game_modal_executable": "Ejecutable",
|
||||||
|
"edit_game_modal": "Personalizar recursos",
|
||||||
|
"edit_game_modal_description": "Personaliza los recursos y detalles del juego",
|
||||||
|
"edit_game_modal_title": "Título",
|
||||||
|
"edit_game_modal_enter_title": "Ingresá el título",
|
||||||
|
"edit_game_modal_image": "Imagen",
|
||||||
|
"edit_game_modal_select_image": "Seleccionar imagen",
|
||||||
|
"edit_game_modal_browse": "Navegar",
|
||||||
|
"edit_game_modal_image_preview": "Vista previa de imagen",
|
||||||
|
"edit_game_modal_icon": "Ícono",
|
||||||
|
"edit_game_modal_select_icon": "Seleccionar ícono",
|
||||||
|
"edit_game_modal_icon_preview": "Vista previa de ícono",
|
||||||
|
"edit_game_modal_logo": "Logo",
|
||||||
|
"edit_game_modal_select_logo": "Seleccionar logo",
|
||||||
|
"edit_game_modal_logo_preview": "Vista previa del logo",
|
||||||
|
"edit_game_modal_hero": "Library Hero",
|
||||||
|
"edit_game_modal_select_hero": "Seleccionar una imagen de Library Hero",
|
||||||
|
"edit_game_modal_hero_preview": "Vista previa de library hero",
|
||||||
|
"edit_game_modal_cancel": "Cancelar",
|
||||||
|
"edit_game_modal_update": "Actualizar",
|
||||||
|
"edit_game_modal_updating": "Actualizando...",
|
||||||
|
"edit_game_modal_fill_required": "Por favor rellená todos los espacios requeridos",
|
||||||
|
"edit_game_modal_success": "Recursos actualizados exitosamente",
|
||||||
|
"edit_game_modal_failed": "Error al actualizar los recursos",
|
||||||
|
"edit_game_modal_image_filter": "Imagen",
|
||||||
|
"edit_game_modal_icon_resolution": "Resolución recomendada: 256x256px",
|
||||||
|
"edit_game_modal_logo_resolution": "Resolución recomendada: 640x360px",
|
||||||
|
"edit_game_modal_hero_resolution": "Resolución recomendada: 1920x620px",
|
||||||
|
"edit_game_modal_assets": "Recursos"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
@@ -37,348 +79,409 @@
|
|||||||
"downloads": "Descargas",
|
"downloads": "Descargas",
|
||||||
"search_results": "Resultados de búsqueda",
|
"search_results": "Resultados de búsqueda",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"version_available_install": "Versión {{version}} disponible. Presiona acá para descargar y reinstalar.",
|
"version_available_install": "Versión {{version}} disponible. Presiona acá para reiniciar e instalar.",
|
||||||
"version_available_download": "Versión {{version}} disponible. Presiona aquí para descargar."
|
"version_available_download": "Versión {{version}} disponible. Presiona acá para descargar."
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sin descargas en progreso",
|
"no_downloads_in_progress": "Sin descargas en progreso",
|
||||||
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
"downloading_metadata": "Descargando metadatos de {{title}}…",
|
||||||
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}",
|
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}",
|
||||||
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…",
|
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…",
|
||||||
"installation_complete": "Instalación completada",
|
"checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)",
|
||||||
"installation_complete_message": "Common redistributables instalados exitosamente",
|
|
||||||
"installing_common_redist": "{{log}}…",
|
"installing_common_redist": "{{log}}…",
|
||||||
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
|
"installation_complete": "Instalación completada",
|
||||||
|
"installation_complete_message": "Common redistributables instalados correctamente"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"search": "Filtrar…",
|
"search": "Filtrar…",
|
||||||
"developers": "Desarrolladores",
|
"developers": "Desarrolladores",
|
||||||
"genres": "Géneros",
|
"genres": "Géneros",
|
||||||
"tags": "Marcadores",
|
"tags": "Etiquetas",
|
||||||
"publishers": "Editores",
|
"publishers": "Editores",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Descargando fuentes",
|
||||||
"result_count": "{{resultCount}} resultados",
|
"result_count": "{{resultCount}} resultados",
|
||||||
"filter_count": "{{filterCount}} disponibles",
|
"filter_count": "{{filterCount}} disponible",
|
||||||
"clear_filters": "Limpiar {{filterCount}} seleccionados"
|
"clear_filters": "Limpiar {{filterCount}} seleccionados"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Ver opciones de descargas",
|
"open_download_options": "Abrir opciones de descargas",
|
||||||
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
|
"download_options_zero": "Sin opciones de descargas",
|
||||||
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
|
|
||||||
"download_options_zero": "No hay opciones de descargas disponibles",
|
|
||||||
"download_options_one": "{{count}} opción de descarga",
|
"download_options_one": "{{count}} opción de descarga",
|
||||||
"download_options_other": "{{count}} opciones de descargas",
|
"download_options_other": "{{count}} opciones de descargas",
|
||||||
"updated_at": "Actualizado el: {{updated_at}}",
|
"updated_at": "Actualizado el {{updated_at}}",
|
||||||
"install": "Instalar",
|
"install": "Instalar",
|
||||||
"resume": "Continuar",
|
"resume": "Resumir",
|
||||||
"pause": "Pausa",
|
"pause": "Pausar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"remove": "Eliminar",
|
"remove": "Remover",
|
||||||
"space_left_on_disk": "{{space}} restantes en el disco",
|
"space_left_on_disk": "{{space}} restante en el disco",
|
||||||
"eta": "Tiempo restante: {{eta}}",
|
"eta": "Conclusión {{eta}}",
|
||||||
"calculating_eta": "Calculando tiempo restante…",
|
"calculating_eta": "Calculando tiempo restante…",
|
||||||
"downloading_metadata": "Descargando metadatos…",
|
"downloading_metadata": "Descargando metadatos…",
|
||||||
"filter": "Buscar repacks",
|
"filter": "Filtrar repacks",
|
||||||
"requirements": "Requisitos del Sistema",
|
"requirements": "Requisitos del Sistema",
|
||||||
"minimum": "Mínimos",
|
"minimum": "Mínimos",
|
||||||
"recommended": "Recomendados",
|
"recommended": "Recomendados",
|
||||||
"paused": "Pausado",
|
"paused": "En Pausa",
|
||||||
"release_date": "Fecha de lanzamiento: {{date}}",
|
"release_date": "Lanzado el {{date}}",
|
||||||
"publisher": "Publicado por: {{publisher}}",
|
"publisher": "Públicado por {{publisher}}",
|
||||||
"hours": "horas",
|
"hours": "horas",
|
||||||
"minutes": "minutos",
|
"minutes": "minutos",
|
||||||
"amount_hours": "{{amount}} horas",
|
"amount_hours": "{{amount}} horas",
|
||||||
"amount_minutes": "{{amount}} minutos",
|
"amount_minutes": "{{amount}} minutos",
|
||||||
"accuracy": "{{accuracy}}% precisión",
|
"accuracy": "{{accuracy}}% completista",
|
||||||
"add_to_library": "Agregar a la biblioteca",
|
"add_to_library": "Añadir a la librería",
|
||||||
"remove_from_library": "Eliminar de la biblioteca",
|
"already_in_library": "Ya está en la librería",
|
||||||
"no_downloads": "No hay descargas disponibles",
|
"remove_from_library": "Eliminar de la librería",
|
||||||
"play_time": "Has jugado {{amount}}",
|
"no_downloads": "Sin descargas disponibles",
|
||||||
"last_time_played": "Jugado por última vez: {{period}}",
|
"play_time": "Jugado por {{amount}}",
|
||||||
"not_played_yet": "Aún no has jugado a {{title}}",
|
"last_time_played": "Última vez jugado {{period}}",
|
||||||
|
"not_played_yet": "No has jugado a {{title}} todavía",
|
||||||
"next_suggestion": "Siguiente sugerencia",
|
"next_suggestion": "Siguiente sugerencia",
|
||||||
"play": "Jugar",
|
"play": "Jugar",
|
||||||
"deleting": "Eliminando instalador…",
|
"deleting": "Eliminando instalador…",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"playing_now": "Jugando ahora",
|
"playing_now": "Jugando ahora",
|
||||||
"change": "Cambiar",
|
"change": "Cambiar",
|
||||||
"repacks_modal_description": "Selecciona el repack que quieres descargar",
|
"repacks_modal_description": "Elegí el repack que querés descargar",
|
||||||
"select_folder_hint": "Para cambiar la carpeta predeterminada, ve a <0>Ajustes</0>",
|
"select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes</0>",
|
||||||
"download_now": "Descargar ahora",
|
"download_now": "Descargar ahora",
|
||||||
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
|
"no_shop_details": "No se pudieron obtener detalles de la tienda.",
|
||||||
"download_options": "Opciones de descarga",
|
"download_options": "Opciones de descarga",
|
||||||
"download_path": "Ruta de descarga",
|
"download_path": "Ruta de descarga",
|
||||||
"previous_screenshot": "Anterior captura",
|
"previous_screenshot": "Anterior captura",
|
||||||
"next_screenshot": "Siguiente captura",
|
"next_screenshot": "Siguiente captura",
|
||||||
"screenshot": "Captura {{number}}",
|
"screenshot": "Captura número {{number}}",
|
||||||
"open_screenshot": "Abrir captura {{number}}",
|
"open_screenshot": "Abrir captura número {{number}}",
|
||||||
"download_settings": "Ajustes de descarga",
|
"download_settings": "Descargar ajustes",
|
||||||
"downloader": "Método de descarga",
|
"downloader": "Descargador",
|
||||||
"select_executable": "Seleccionar",
|
"select_executable": "Seleccionar",
|
||||||
"no_executable_selected": "No se seleccionó un ejecutable",
|
"no_executable_selected": "Sin ejecutable seleccionado",
|
||||||
"open_folder": "Abrir carpeta",
|
"open_folder": "Abrir carpeta",
|
||||||
"open_download_location": "Ver archivos descargados",
|
"open_download_location": "Ver archivos descargados",
|
||||||
"create_shortcut": "Crear acceso directo en el escritorio",
|
"create_shortcut": "Crear atajo en el escritorio",
|
||||||
"remove_files": "Eliminar archivos",
|
"clear": "Limpiar",
|
||||||
|
"remove_files": "Remover archivos",
|
||||||
"remove_from_library_title": "¿Estás seguro?",
|
"remove_from_library_title": "¿Estás seguro?",
|
||||||
"remove_from_library_description": "Esto eliminará {{game}} de tu biblioteca",
|
"remove_from_library_description": "Esto va eliminará {{game}} de tu librería",
|
||||||
"options": "Opciones",
|
"options": "Opciones",
|
||||||
"executable_section_title": "Ejecutable",
|
"executable_section_title": "Ejecutable",
|
||||||
"executable_section_description": "Ruta del archivo que se ejecutará cuando se presione \"Jugar\"",
|
"executable_section_description": "Ruta del archivo que se ejecutará cuando presiones \"Jugar\"",
|
||||||
"downloads_section_title": "Descargas",
|
"downloads_section_title": "Descargas",
|
||||||
"downloads_section_description": "Buscar actualizaciones u otras versiones de este juego",
|
"downloads_section_description": "Revisar actualizaciones u otras versiones del juego",
|
||||||
"danger_zone_section_title": "Opciones Avanzadas",
|
"danger_zone_section_title": "Zona de Peligro",
|
||||||
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
|
"danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra",
|
||||||
"download_in_progress": "Descarga en progreso",
|
"download_in_progress": "Descarga en progreso",
|
||||||
"download_paused": "Descarga pausada",
|
"download_paused": "Descarga pausada",
|
||||||
|
"last_downloaded_option": "Última opción de descarga",
|
||||||
"create_steam_shortcut": "Crear atajo de Steam",
|
"create_steam_shortcut": "Crear atajo de Steam",
|
||||||
"last_downloaded_option": "Última opción descargada",
|
|
||||||
"create_shortcut_success": "Atajo creado con éxito",
|
"create_shortcut_success": "Atajo creado con éxito",
|
||||||
"you_might_need_to_restart_steam": "Es posible que necesites reiniciar Steam para ver los cambios",
|
"you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios",
|
||||||
"create_shortcut_error": "Error al crear un atajo",
|
"create_shortcut_error": "Error al crear atajo",
|
||||||
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
|
"nsfw_content_title": "Este juego tiene contenido inapropiado",
|
||||||
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
|
"nsfw_content_description": "{{title}} tiene contenido no apto para todas las edades. ¿Querés continuar igualmente?",
|
||||||
"allow_nsfw_content": "Continuar",
|
"allow_nsfw_content": "Continuar",
|
||||||
"refuse_nsfw_content": "No, gracias",
|
"refuse_nsfw_content": "Regresar",
|
||||||
"stats": "Estadísticas",
|
"stats": "Estadísticas",
|
||||||
"download_count": "Descargas",
|
"download_count": "Descargas",
|
||||||
"player_count": "Jugadores activos",
|
"player_count": "Jugadores activos",
|
||||||
"download_error": "Esta opción de descarga no está disponible.",
|
"download_error": "Esta opción de descarga no está disponible",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"",
|
"executable_path_in_use": "El ejecutable ya se está usando por \"{{game}}\"",
|
||||||
"warning": "Advertencia:",
|
"warning": "Advertencia:",
|
||||||
"hydra_needs_to_remain_open": "Para esta descarga, Hydra necesita mantenerse abierta hasta que concluya. En caso de que Hydra se cierre antes de que concluya, podrías perder todo el progreso.",
|
"hydra_needs_to_remain_open": "para esta descarga, Hydra necesita estar abierta hasta que termine. Si se cierra antes de completar, perderás todo el progreso.",
|
||||||
"achievements": "Logros",
|
"achievements": "Logros",
|
||||||
"achievements_count": "Logros {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "Logros {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Guardado en la nube",
|
"cloud_save": "Guardado en la nube",
|
||||||
"cloud_save_description": "Guarda tu progreso en la nube y continúa jugando en cualquier dispositivo",
|
"cloud_save_description": "Guardá tu progreso en la nube y jugá en cualquier dispositivo",
|
||||||
"backups": "Copias de Seguridad",
|
"backups": "Copia de seguridad",
|
||||||
"install_backup": "Instalar",
|
"install_backup": "Instalar",
|
||||||
"delete_backup": "Eliminar",
|
"delete_backup": "Eliminar",
|
||||||
"create_backup": "Nueva Copia de Seguridad",
|
"create_backup": "Nueva copia de seguridad",
|
||||||
"last_backup_date": "Última copia de seguridad el {{date}}",
|
"last_backup_date": "Última copia de seguridad {{date}}",
|
||||||
"no_backup_preview": "No se encontraron datos de guardados para este juego",
|
"no_backup_preview": "No se han encotrado puntos de guardado para este juego",
|
||||||
"restoring_backup": "Restaurando copia de seguridad ({{progress}} completado)…",
|
"restoring_backup": "Restaurando copia de seguridad ({{progress}} completado)…",
|
||||||
"uploading_backup": "Subiendo copia de seguridad…",
|
"uploading_backup": "Subiendo copia de seguridad…",
|
||||||
"no_backups": "No has creado ninguna copia de seguridad para este juego aún",
|
"no_backups": "No has creado ninguna copia de seguridad para este juego todavía",
|
||||||
"backup_uploaded": "Copia de seguridad subida",
|
"backup_uploaded": "Copia de seguridad subida",
|
||||||
"backup_deleted": "Copia de seguridad eliminada",
|
"backup_deleted": "Copia de seguridad eliminada",
|
||||||
"backup_restored": "Copia de seguridad restaurada",
|
"backup_restored": "Copia de seguridad restaurada",
|
||||||
"see_all_achievements": "Ver todos los logros",
|
"see_all_achievements": "Ver todos los logros",
|
||||||
"sign_in_to_see_achievements": "Inicia sesión para ver los logros",
|
"sign_in_to_see_achievements": "Iniciá sesión para ver los logros",
|
||||||
"mapping_method_automatic": "Automático",
|
"mapping_method_automatic": "Automático",
|
||||||
"mapping_method_manual": "Manual",
|
"mapping_method_manual": "Manual",
|
||||||
"mapping_method_label": "Método de mapeo",
|
"mapping_method_label": "Método de mapeado",
|
||||||
"files_automatically_mapped": "Archivos mapeados automáticamente",
|
"files_automatically_mapped": "Archivos automáticamente mapeados",
|
||||||
"no_backups_created": "Sin copias de seguridad creadas para este juego",
|
"no_backups_created": "Sin copias de seguridad creadas para este juego",
|
||||||
"manage_files": "Gestionar archivos",
|
"manage_files": "Administrar archivos",
|
||||||
"loading_save_preview": "Buscando datos de guardados de juegos…",
|
"loading_save_preview": "Buscando por guardado de juegos…",
|
||||||
"wine_prefix": "Prefijo de Wine",
|
"wine_prefix": "Prefijo de Wine",
|
||||||
"wine_prefix_description": "El prefijo de Wine usado para ejecutar este juego",
|
"wine_prefix_description": "El prefijo de Wine usado para este juego",
|
||||||
|
"launch_options": "Opciones para iniciar",
|
||||||
|
"launch_options_description": "Los usuarios avanzados pueden ingresar sus modificaciones para el inicio de sus juegos (característica experimental)",
|
||||||
|
"launch_options_placeholder": "Sin parámetro específicado",
|
||||||
"no_download_option_info": "Sin información disponible",
|
"no_download_option_info": "Sin información disponible",
|
||||||
"backup_deletion_failed": "La eliminación de la copia de seguridad falló",
|
"backup_deletion_failed": "Error al eliminar copia de seguridad",
|
||||||
"max_number_of_artifacts_reached": "Número máximo de copias de seguridad de este juego alcanzadas",
|
"max_number_of_artifacts_reached": "Máximo de copias de seguridad alcanzadas para este juego",
|
||||||
"achievements_not_sync": "Tus logros no están sincronizados",
|
"achievements_not_sync": "Revisá como sincronizar tus logros'",
|
||||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
"manage_files_description": "Elegí que archivos se guardarán y restaurarán de la copia de seguridad",
|
||||||
"select_folder": "Seleccionar carpeta",
|
"select_folder": "Seleccionar carpeta",
|
||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
|
||||||
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
|
"enable_automatic_cloud_sync": "Habilitar sincronización con la nube",
|
||||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
"custom_backup_location_set": "Ubicación de copia de seguridad personalizada",
|
||||||
"clear": "Limpiar",
|
"no_directory_selected": "Sin directorio seleccionado",
|
||||||
"no_directory_selected": "No se seleccionó un directorio",
|
"no_write_permission": "No se puede descargar en este directorio. Presioná acá para más información.",
|
||||||
"launch_options": "Opciones de Inicio",
|
|
||||||
"launch_options_description": "Los usuarios avanzados pueden introducir sus propias modificaciones de opciones de inicio (característica experimental)",
|
|
||||||
"launch_options_placeholder": "Sin parámetro específicado",
|
|
||||||
"no_write_permission": "No se puede descargar en este directorio. Presiona aquí para aprender más.",
|
|
||||||
"reset_achievements": "Reiniciar logros",
|
"reset_achievements": "Reiniciar logros",
|
||||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
"reset_achievements_description": "Esto va a reiniciar todos los logros para {{game}}",
|
||||||
"reset_achievements_title": "¿Estás seguro?",
|
"reset_achievements_title": "¿Querés continuar?",
|
||||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
"reset_achievements_success": "Logros reiniciados éxitosamente",
|
||||||
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
|
"reset_achievements_error": "Error al reiniciar logros",
|
||||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de GoFile. Esperá a que se reinice.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nuevas descargas. Revisá los ajustes de tu cuenta y probá de nuevo.",
|
||||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y no está disponible el estado de descarga de sondeo todavía.",
|
||||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y aún no se puede verificar el estado de la descarga.",
|
"update_playtime_title": "Actualizar tiempo de juego",
|
||||||
|
"update_playtime_description": "Actualizar manualmente el tiempo de juego para {{game}}",
|
||||||
|
"update_playtime": "Actualizar tiempo de juego",
|
||||||
|
"update_playtime_success": "Tiempo de juego actualizado éxitosamente",
|
||||||
|
"update_playtime_error": "Error al actualizar el tiempo de juego",
|
||||||
|
"update_game_playtime": "Actualizar tu tiempo de juego",
|
||||||
|
"manual_playtime_warning": "Tus horas de juego se marcarán como actualizadas manualmente, y esto no se puede deshacer.",
|
||||||
|
"manual_playtime_tooltip": "Este tiempo de juego se ha actualizad manualmente",
|
||||||
|
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y no está disponible el estado de descarga de sondeo todavía.",
|
||||||
|
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
|
||||||
|
"game_removed_from_favorites": "Juego eliminado de favoritos",
|
||||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||||
"game_removed_from_favorites": "Juego removido de favoritos",
|
"game_removed_from_pinned": "Juego removido de fijados",
|
||||||
"invalid_wine_prefix_path": "Ruta de prefijo de Wine inválida",
|
"game_added_to_pinned": "Juego añadido a fijados",
|
||||||
"invalid_wine_prefix_path_description": "La ruta del prefijo Wine es inválida. Por favor, checa la ruta y vuelve a intentarlo.",
|
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
|
||||||
"missing_wine_prefix": "Se requiere el prefijo Wine para crear una copia de seguridad en Linux"
|
"create_start_menu_shortcut": "Crear un atajo en el Menú de Inicio",
|
||||||
|
"invalid_wine_prefix_path": "Ruta inválida del prefijo de Wine",
|
||||||
|
"invalid_wine_prefix_path_description": "La ruta al prefijo de Wine es inválida. Por favor revisá la ruta y probá de nuevo.",
|
||||||
|
"missing_wine_prefix": "EL prefijo de Wine es requerido para hacer una copia en Linux",
|
||||||
|
"artifact_renamed": "Copia de seguridad renombrada éxitosamente",
|
||||||
|
"rename_artifact": "Renombrar copia de seguridad",
|
||||||
|
"rename_artifact_description": "Renombrar copia de seguridad con un nombre más descriptivo",
|
||||||
|
"artifact_name_label": "Nombre de la copia de seguridad",
|
||||||
|
"artifact_name_placeholder": "Introducí un nombre para la copia de seguridad",
|
||||||
|
"save_changes": "Guardar cambios",
|
||||||
|
"required_field": "Este campo es requerido",
|
||||||
|
"max_length_field": "Este campo debe tener menos de {{length}} carácteres",
|
||||||
|
"freeze_backup": "Fíjalo así no se re-escríbira por copias de seguridad automáticas",
|
||||||
|
"unfreeze_backup": "Dejar de fijar",
|
||||||
|
"backup_frozen": "Copia de seguridad fijada",
|
||||||
|
"backup_unfrozen": "Copia de seguridad desfijada",
|
||||||
|
"backup_freeze_failed": "Error al congelar tu copia de seguridad",
|
||||||
|
"backup_freeze_failed_description": "Tenés que tener mínimo un espacio para copias de seguridad automáticas",
|
||||||
|
"edit_game_modal_button": "Personalizar recursos de juego",
|
||||||
|
"game_details": "Detalles del juego",
|
||||||
|
"currency_symbol": "$",
|
||||||
|
"currency_country": "us",
|
||||||
|
"prices": "Precios",
|
||||||
|
"no_prices_found": "No se encontraron precios",
|
||||||
|
"view_all_prices": "Presioná acá para ver todos los precios",
|
||||||
|
"retail_price": "Precio recomendado",
|
||||||
|
"keyshop_price": "Precio de tiendas de terceros",
|
||||||
|
"historical_retail": "Precio de tiendas",
|
||||||
|
"historical_keyshop": "Precio de tiendas de terceros",
|
||||||
|
"language": "Idioma",
|
||||||
|
"caption": "Subtítulo",
|
||||||
|
"audio": "Audio"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
"installation_id": "ID de la Instalación:",
|
"installation_id": "ID de Instalación:",
|
||||||
"enter_activation_code": "Introduce tu código de activación",
|
"enter_activation_code": "Introducí tu código de activación",
|
||||||
"message": "Si no sabes donde obtener el código, no deberías de tener esto.",
|
"message": "Si no sabes donde preguntar por esto, entonces no tenés que tener esto.",
|
||||||
"activate": "Activar",
|
"activate": "Activar",
|
||||||
"loading": "Cargando…"
|
"loading": "Cargando…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"resume": "Resumir",
|
"resume": "Resumir",
|
||||||
"pause": "Pausa",
|
"pause": "Pausar",
|
||||||
"eta": "Finalizando en {{eta}}",
|
"eta": "Tiempo de finalizción {{eta}}",
|
||||||
"paused": "En Pausa",
|
"paused": "Pausado",
|
||||||
"verifying": "Verificando…",
|
"verifying": "Verificando…",
|
||||||
"completed": "Completado",
|
"completed": "Completado",
|
||||||
"removed": "No descargado",
|
"removed": "No descargado",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"filter": "Buscar juegos descargados",
|
"filter": "Filtrar juegos descargados",
|
||||||
"remove": "Eliminar",
|
"remove": "Remover",
|
||||||
"downloading_metadata": "Descargando metadatos…",
|
"downloading_metadata": "Descargando metadatos…",
|
||||||
"deleting": "Eliminando instalador…",
|
"deleting": "Eliminado instalador…",
|
||||||
"delete": "Eliminar instalador",
|
"delete": "Remover instalador",
|
||||||
"delete_modal_title": "¿Estás seguro?",
|
"delete_modal_title": "¿Querés continuar?",
|
||||||
"delete_modal_description": "Esto eliminará todos los archivos de la instalación del repack del juego de tu computadora. (Si ya instalaste el juego, puedes eliminar esto, no afectará al juego)",
|
"delete_modal_description": "Esto eliminará todos los archivos del instalador de tu computadora",
|
||||||
"install": "Instalar",
|
"install": "Instalar",
|
||||||
"download_in_progress": "En progreso",
|
"download_in_progress": "En progreso",
|
||||||
"queued_downloads": "Descargas en cola",
|
"queued_downloads": "Descargas en cola",
|
||||||
"downloads_completed": "Completado",
|
"downloads_completed": "Completado",
|
||||||
"queued": "En cola",
|
"queued": "En cola",
|
||||||
"no_downloads_title": "Esto está tan... vacío",
|
"no_downloads_title": "Esto está... tan, ¿vacío?",
|
||||||
"no_downloads_description": "No has descargado nada con Hydra... aún, ¡pero nunca es tarde para comenzar!.",
|
"no_downloads_description": "No has descargado nada con Hydra, pero nunca es tarde para comenzar.",
|
||||||
"checking_files": "Verificando archivos…",
|
"checking_files": "Revisando archivos…",
|
||||||
"seeding": "Seeding",
|
"seeding": "Sembrando",
|
||||||
"stop_seeding": "Detener seeding",
|
"stop_seeding": "Dejar de sembrar",
|
||||||
"resume_seeding": "Continuar seeding",
|
"resume_seeding": "Continuar sembrando",
|
||||||
|
"options": "Administrar",
|
||||||
"extract": "Extraer archivos",
|
"extract": "Extraer archivos",
|
||||||
"extracting": "Extrayendo archivos…",
|
"extracting": "Extrayendo archivos…"
|
||||||
"options": "Gestionar"
|
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Ruta de descarga",
|
"downloads_path": "Ruta de descarga",
|
||||||
"common_redist": "Common redistributables",
|
"change": "Actualizar",
|
||||||
"common_redist_description": "Las Common redistributables son requeridos para ejecutar algunos juegos. Es recomendado instalarlos para evitar problemas.",
|
|
||||||
"create_real_debrid_account": "Presiona acá si no tienes una cuenta de Real-Debrid aún",
|
|
||||||
"create_torbox_account": "Presiona acá si no tienes una cuenta de TorBox aún",
|
|
||||||
"install_common_redist": "Instalar",
|
|
||||||
"installing_common_redist": "Instalando…",
|
|
||||||
"show_download_speed_in_megabytes": "Mostrar velocidad de descargar en megabytes por segundo",
|
|
||||||
"change": "Cambiar",
|
|
||||||
"notifications": "Notificaciones",
|
"notifications": "Notificaciones",
|
||||||
"enable_download_notifications": "Cuando se completa una descarga",
|
"enable_download_notifications": "Cuando una descarga se completa",
|
||||||
"enable_repack_list_notifications": "Cuando se añade un repack nuevo",
|
"enable_repack_list_notifications": "Cuando un nuevo repack se añade",
|
||||||
"real_debrid_api_token_label": "Token API de Real-Debrid",
|
"real_debrid_api_token_label": "Real-Debrid API token",
|
||||||
"quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema",
|
"quit_app_instead_hiding": "No ocultar Hydra cuando se cierra",
|
||||||
"launch_with_system": "Iniciar Hydra al inicio del sistema",
|
"launch_with_system": "Iniciar Hydra con el sistema",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Otros",
|
"behavior": "Comportamiento",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Fuentes de descargas",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"api_token": "Token API",
|
"api_token": "API Token",
|
||||||
"enable_real_debrid": "Activar Real-Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.",
|
"real_debrid_description": "Real-Debrid es un descargador que te permite descargar archivos más rápidos, solo límitado por la velocidad de tu internet.",
|
||||||
"debrid_invalid_token": "Token de API inválido",
|
"debrid_invalid_token": "Token API inválido",
|
||||||
"debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>",
|
"debrid_api_token_hint": "Podés obtener la el token de tu API <0>acá</0>",
|
||||||
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid",
|
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratis. Por favor suscribíte a Real-Debrid",
|
||||||
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Guardar cambios",
|
"save_changes": "Guardar cambios",
|
||||||
"changes_saved": "Ajustes guardados exitosamente",
|
"changes_saved": "Cambios guardados éxitosamente",
|
||||||
"download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga",
|
"download_sources_description": "Hydra va a recoger los links de descarga de cada fuente. La URL de origen debe ser un enlace .json que contenga los enlaces de descarga.",
|
||||||
"validate_download_source": "Validar",
|
"validate_download_source": "Validar",
|
||||||
"remove_download_source": "Eliminar",
|
"remove_download_source": "Remover",
|
||||||
"add_download_source": "Añadir fuente de descarga",
|
"add_download_source": "Añadir fuente",
|
||||||
"download_count_zero": "No hay descargas en la lista",
|
"download_count_zero": "Sin opciones de descarga",
|
||||||
"download_count_one": "{{countFormatted}} descarga en la lista",
|
"download_count_one": "{{countFormatted}} opción de descarga",
|
||||||
"download_count_other": "{{countFormatted}} descargas en la lista",
|
"download_count_other": "{{countFormatted}} opciones de descarga",
|
||||||
"download_source_url": "Descargar URL de origen",
|
"download_source_url": "Descargar fuente URL",
|
||||||
"add_download_source_description": "Introduce la URL con el archivo .json",
|
"add_download_source_description": "Introducí la URL del archivo .json",
|
||||||
"download_source_up_to_date": "Al día",
|
"download_source_up_to_date": "Actualizado",
|
||||||
"download_source_errored": "Error",
|
"download_source_errored": "Error",
|
||||||
"sync_download_sources": "Sincronizar fuentes",
|
"sync_download_sources": "Sincronizar fuentes",
|
||||||
"removed_download_source": "Fuente de descarga eliminada",
|
"removed_download_source": "Fuente de descarga eliminada",
|
||||||
|
"removed_download_sources": "Fuente de descarga eliminadas",
|
||||||
"cancel_button_confirmation_delete_all_sources": "No",
|
"cancel_button_confirmation_delete_all_sources": "No",
|
||||||
"confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo",
|
"confirm_button_confirmation_delete_all_sources": "Si, eliminar todo",
|
||||||
"description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga",
|
|
||||||
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
||||||
"removed_download_sources": "Fuentes de descarga eliminadas",
|
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
||||||
"button_delete_all_sources": "Eliminar todas las fuentes de descarga",
|
"button_delete_all_sources": "Eliminar todo",
|
||||||
"added_download_source": "Fuente de descarga añadida",
|
"added_download_source": "Añadir fuente de descarga",
|
||||||
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
|
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
||||||
"insert_valid_json_url": "Introduce una URL JSON válida",
|
"insert_valid_json_url": "Introducí una URL de json válida",
|
||||||
"found_download_option_zero": "No se encontró una opción de descarga",
|
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
||||||
"found_download_option_one": "Se encontró {{countFormatted}} opción de descarga",
|
"found_download_option_one": "Encontrada {{countFormatted}} fuente de descarga",
|
||||||
"found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga",
|
"found_download_option_other": "Encontradas {{countFormatted}} opciones de descargas",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"public": "Público",
|
"public": "Público",
|
||||||
"private": "Privado",
|
"private": "Privado",
|
||||||
"friends_only": "Solo amigos",
|
"friends_only": "Sólo amigos",
|
||||||
"privacy": "Privacidad",
|
"privacy": "Privacidad",
|
||||||
"profile_visibility": "Visibilidad del perfil",
|
"profile_visibility": "Visibilidad del perfil",
|
||||||
"profile_visibility_description": "Elige quién puede ver tu perfil y biblioteca",
|
"profile_visibility_description": "Elegí quién puede ver tú perfil y biblioteca",
|
||||||
"required_field": "Este campo es obligatorio",
|
"required_field": "Este campo es requerido",
|
||||||
"source_already_exists": "Esta fuente ya ha sido agregada.",
|
"source_already_exists": "Esta fuente ya está añadida",
|
||||||
"must_be_valid_url": "La fuente debe ser una URL válida.",
|
"must_be_valid_url": "La fuente debe ser una URL válida",
|
||||||
"blocked_users": "Usuarios bloqueados",
|
"blocked_users": "Usuarios bloqueados",
|
||||||
"user_unblocked": "El usuario ha sido desbloqueado",
|
"user_unblocked": "Has desbloqueado a este usuario",
|
||||||
"enable_achievement_notifications": "Cuando un logro se desbloquea",
|
"enable_achievement_notifications": "Cuando desbloqueás un logro",
|
||||||
"launch_minimized": "Iniciar Hydra minimizado",
|
"launch_minimized": "Iniciar Hydra minimizado",
|
||||||
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
"disable_nsfw_alert": "Deshabilitar alerta de NSFW",
|
||||||
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
"seed_after_download_complete": "Sembrar después de completar una descarga",
|
||||||
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos",
|
"show_hidden_achievement_description": "Mostrar logros ocultos antes de desbloquearlos",
|
||||||
"account": "Cuenta",
|
"account": "Cuenta",
|
||||||
"account_data_updated_successfully": "Datos de la cuenta actualizados",
|
"no_users_blocked": "No has bloqueado a ningún usuario",
|
||||||
"bill_sent_until": "Tú próxima factura se enviará el {{date}}",
|
"subscription_active_until": "Tu Hydra Cloud está activo hasta {{date}}",
|
||||||
"current_email": "Correo actual:",
|
"manage_subscription": "Administrar suscripción",
|
||||||
"manage_subscription": "Gestionar suscripción",
|
|
||||||
"no_email_account": "No has configurado un correo aún",
|
|
||||||
"no_subscription": "Disfruta Hydra de la mejor manera",
|
|
||||||
"no_users_blocked": "No tienes usuarios bloqueados",
|
|
||||||
"renew_subscription": "Renovar Hydra Cloud",
|
|
||||||
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
|
|
||||||
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
|
|
||||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
|
||||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
|
||||||
"update_email": "Actualizar correo",
|
"update_email": "Actualizar correo",
|
||||||
"update_password": "Actualizar contraseña",
|
"update_password": "Cambiar contraseña",
|
||||||
"appearance": "Apariencia",
|
"current_email": "Correo actual:",
|
||||||
"become_subscriber": "Sé Hydra Cloud",
|
"no_email_account": "No tenés ningún correo vinculado aún",
|
||||||
"cancel": "Cancelar",
|
"account_data_updated_successfully": "Datos de la cuenta actualizados correctamente",
|
||||||
"clear_themes": "Limpiar",
|
"renew_subscription": "Renovar Hydra Cloud",
|
||||||
"create_theme": "Crear",
|
"subscription_expired_at": "Tu suscripción expiró el {{date}}",
|
||||||
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
|
"no_subscription": "Disfrutá Hydra de la mejor forma",
|
||||||
"create_theme_modal_title": "Crear tema personalizado",
|
"become_subscriber": "Sé parte de Hydra Cloud",
|
||||||
"delete_all_themes": "Eliminar todos los temas",
|
"subscription_renew_cancelled": "Renovación automática desactivada",
|
||||||
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
|
"subscription_renews_on": "Tu suscripción se renueva el {{date}}",
|
||||||
"delete_theme": "Eliminar tema",
|
"bill_sent_until": "Tu próxima factura se enviará este día",
|
||||||
"delete_theme_description": "Esto eliminará el tema {{theme}}",
|
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupés, presiona acá para hacer tu primera obra maestra.",
|
||||||
"edit_theme": "Editar tema",
|
|
||||||
"editor_tab_code": "Código",
|
"editor_tab_code": "Código",
|
||||||
"editor_tab_info": "Info",
|
"editor_tab_info": "Info",
|
||||||
"editor_tab_save": "Guardar",
|
"editor_tab_save": "Guardar",
|
||||||
"enable_torbox": "Habilitar TorBox",
|
"web_store": "Tienda Web",
|
||||||
"error_importing_theme": "Error al importar el tema",
|
"clear_themes": "Limpiar",
|
||||||
|
"create_theme": "Crear",
|
||||||
|
"create_theme_modal_title": "Crear tema personalizado",
|
||||||
|
"create_theme_modal_description": "Crear un nuevo tema para personalizar el estilo de Hydra",
|
||||||
|
"theme_name": "Nombre",
|
||||||
|
"insert_theme_name": "Introducí un nombre del tema",
|
||||||
|
"set_theme": "Usar tema",
|
||||||
|
"unset_theme": "Dejar de usar tema",
|
||||||
|
"delete_theme": "Eliminar tema",
|
||||||
|
"edit_theme": "Editar tema",
|
||||||
|
"delete_all_themes": "Eliminar todos los temas",
|
||||||
|
"delete_all_themes_description": "Esto va a eliminar todos los temas personalizados",
|
||||||
|
"delete_theme_description": "Esto va a eliminar el tema {{theme}}",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"appearance": "Apariencia",
|
||||||
|
"enable_torbox": "Activar TorBox",
|
||||||
|
"torbox_description": "TorBox es un servicio premium de seedbox que incluso rivaliza los mejores servidores.",
|
||||||
|
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
||||||
|
"create_real_debrid_account": "Presioná acá si todavía no tenés una cuenta de Real-Debrid",
|
||||||
|
"create_torbox_account": "Presioná acá si todavía no tenés una cuenta de TorBox",
|
||||||
|
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
||||||
|
"name_min_length": "El nombre del tema debe tener mínimo 3 carácteres",
|
||||||
"import_theme": "Importar tema",
|
"import_theme": "Importar tema",
|
||||||
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
|
||||||
"insert_theme_name": "Introducí el nombre del tema",
|
"error_importing_theme": "Error al importar el tema",
|
||||||
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
|
"theme_imported": "Tema importado correctamente",
|
||||||
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
|
"enable_friend_request_notifications": "Cuando recibís una solicitud de amistad",
|
||||||
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
|
"enable_auto_install": "Descargar actualizaciones automáticamente",
|
||||||
"set_theme": "Establecer tema",
|
"common_redist": "Common redistributables",
|
||||||
"theme_imported": "Tema importado exitosamente",
|
"common_redist_description": "Common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.",
|
||||||
"theme_name": "Nombre",
|
"install_common_redist": "Instalar",
|
||||||
"torbox_account_linked": "Cuenta de TorBox vinculada",
|
"installing_common_redist": "Instalando…",
|
||||||
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
|
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
|
||||||
"unset_theme": "Desactivar tema",
|
"extract_files_by_default": "Extraer archivos por defecto después de descargar",
|
||||||
"web_store": "Tienda Web",
|
"enable_steam_achievements": "Habilitar búsqueda de logros de Steam",
|
||||||
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
|
"achievement_custom_notification_position": "Posición de notificación de logros",
|
||||||
"enable_auto_install": "Descargar actualizaciones automáticamente"
|
"top-left": "Superior Izquierda",
|
||||||
|
"top-center": "Superior Centro",
|
||||||
|
"top-right": "Superior Derecha",
|
||||||
|
"bottom-left": "Inferior Izquierda",
|
||||||
|
"bottom-center": "Inferior Centro",
|
||||||
|
"bottom-right": "Inferior Derecha",
|
||||||
|
"enable_achievement_custom_notifications": "Habilitar notificación personalizada de logros",
|
||||||
|
"alignment": "Centrado",
|
||||||
|
"variation": "Variación",
|
||||||
|
"default": "Defecto",
|
||||||
|
"rare": "Raro",
|
||||||
|
"platinum": "Platino",
|
||||||
|
"hidden": "Oculto",
|
||||||
|
"test_notification": "Probar notificación",
|
||||||
|
"notification_preview": "Probar notificación de logro",
|
||||||
|
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
"game_ready_to_install": "{{title}} está listo para instalar",
|
||||||
|
"repack_list_updated": "Lista de repacks actualizadas",
|
||||||
|
"repack_count_one": "{{count}} repack añadido",
|
||||||
|
"repack_count_other": "{{count}} repacks añadidos",
|
||||||
|
"new_update_available": "Versión {{version}} disponible",
|
||||||
|
"restart_to_install_update": "Reiniciá Hydra para instalar la actualización",
|
||||||
|
"notification_achievement_unlocked_title": "Logro desbloqueado para {{game}}",
|
||||||
|
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
||||||
|
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
|
||||||
|
"new_friend_request_title": "Nueva solicitud de amistad",
|
||||||
"extraction_complete": "Extracción completada",
|
"extraction_complete": "Extracción completada",
|
||||||
"game_extracted": "{{title}} extraído exitosamente",
|
"game_extracted": "{{title}} extraído exitosamente",
|
||||||
"game_ready_to_install": "{{title}} está listo para instalarse",
|
"friend_started_playing_game": "{{displayName}} empezó a jugar un juego",
|
||||||
"repack_list_updated": "Lista de repacks actualizadas",
|
"test_achievement_notification_title": "Esto es una notificación de prueba",
|
||||||
"repack_count_one": "{{count}} repack ha sido añadido",
|
"test_achievement_notification_description": "Piola, ¿verdad?"
|
||||||
"repack_count_other": "{{count}} repacks añadidos",
|
|
||||||
"new_update_available": "Version {{version}} disponible",
|
|
||||||
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
|
|
||||||
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
|
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
|
|
||||||
"new_friend_request_title": "Nueva solicitud de amistad",
|
|
||||||
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
|
|
||||||
"friend_started_playing_game": "{{displayName}} está jugando"
|
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Abrir Hydra",
|
"open": "Abrir Hydra",
|
||||||
@@ -387,15 +490,15 @@
|
|||||||
"game_card": {
|
"game_card": {
|
||||||
"available_one": "Disponible",
|
"available_one": "Disponible",
|
||||||
"available_other": "Disponibles",
|
"available_other": "Disponibles",
|
||||||
"no_downloads": "No hay descargas disponibles"
|
"no_downloads": "Sin descargas disponibles"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "Programas no instalados",
|
"title": "Programas no instalados",
|
||||||
"description": "Los ejecutables de Wine o Lutris no se encontraron en tu sistema",
|
"description": "Ejecutables de Wine o Lutris executables no encontrados en tu sistema",
|
||||||
"instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad"
|
"instructions": "Comprobá la forma correcta de instalar cualquiera de ellos en tu distribución de Linux para que el juego pueda ejecutarse con normalidad"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Botón de cierre"
|
"close": "Botón de cerrar"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
"toggle_password_visibility": "Cambiar visibilidad de contraseña"
|
"toggle_password_visibility": "Cambiar visibilidad de contraseña"
|
||||||
@@ -403,111 +506,120 @@
|
|||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} horas",
|
"amount_hours": "{{amount}} horas",
|
||||||
"amount_minutes": "{{amount}} minutos",
|
"amount_minutes": "{{amount}} minutos",
|
||||||
"last_time_played": "Última vez jugado: {{period}}",
|
"last_time_played": "Jugado por última vez el {{period}}",
|
||||||
"activity": "Actividad reciente",
|
"activity": "Actividad reciente",
|
||||||
"library": "Biblioteca",
|
"library": "Librería",
|
||||||
"total_play_time": "Has jugado",
|
"pinned": "Fijado",
|
||||||
"no_recent_activity_title": "Que raro, no hay nada por acá...",
|
"total_play_time": "Total de tiempo de juego",
|
||||||
"no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!",
|
"achievements_earned": "Logros conseguidos",
|
||||||
"display_name": "Nombre en pantalla",
|
"played_recently": "Jugado recientemente",
|
||||||
|
"playtime": "Tiempo de juego",
|
||||||
|
"manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente",
|
||||||
|
"no_recent_activity_title": "Hmmm… nada por acá",
|
||||||
|
"no_recent_activity_description": "No has jugado nada recientemente. ¡Te toca cambiar eso!",
|
||||||
|
"display_name": "Nombre a mostar",
|
||||||
"saving": "Guardando",
|
"saving": "Guardando",
|
||||||
"save": "Guardar",
|
"save": "Guardado",
|
||||||
"edit_profile": "Editar perfil",
|
"edit_profile": "Editar perfil",
|
||||||
"saved_successfully": "Guardado exitosamente",
|
"saved_successfully": "Guardado exitosamente",
|
||||||
"try_again": "Por favor, intenta de nuevo",
|
"try_again": "Por favor, intentá de nuevo",
|
||||||
"sign_out_modal_title": "¿Estás seguro?",
|
"sign_out_modal_title": "¿Querés continuar?",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"successfully_signed_out": "Sesión cerrada exitosamente",
|
"successfully_signed_out": "Cerraste sesión exitosamente",
|
||||||
"sign_out": "Cerrar sesión",
|
"sign_out": "Cerrar sesión",
|
||||||
"playing_for": "Llevas jugando {{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 librería está vinculada con esta cuenta. Cuando cerrés sesión, tu librería ya no será visible, y cualquier progreso no se guardará. ¿Querés continuar con el cierre de sesión?",
|
||||||
"add_friends": "Añadir amigos",
|
"add_friends": "Añadir amistades",
|
||||||
"add": "Añadir",
|
"add": "Añadir",
|
||||||
"friend_code": "Código de amigo",
|
"friend_code": "Código de amistad",
|
||||||
"see_profile": "Ver perfil",
|
"see_profile": "Ver perfil",
|
||||||
"sending": "Enviando",
|
"sending": "Enviando",
|
||||||
"friend_request_sent": "Solicitud de amistad enviada",
|
"friend_request_sent": "Solicitud de amistad enviada",
|
||||||
"friends": "Amigos",
|
"friends": "Amistades",
|
||||||
"friends_list": "Lista de amigos",
|
"friends_list": "Lista de amistades",
|
||||||
"user_not_found": "Usuario no encontrado",
|
"user_not_found": "Usuario no encontrado",
|
||||||
"block_user": "Bloquear usuario",
|
"block_user": "Bloquear usuario",
|
||||||
"add_friend": "Añadir amigo",
|
"add_friend": "Añadir amistad",
|
||||||
"request_sent": "Solicitud enviada",
|
"request_sent": "Solicitud enviada",
|
||||||
"request_received": "Solicitud recibida",
|
"request_received": "Solicitud recibida",
|
||||||
"accept_request": "Aceptar solicitud",
|
"accept_request": "Aceptar solicitud",
|
||||||
"ignore_request": "Ignorar solicitud",
|
"ignore_request": "Ignorar solicitud",
|
||||||
"cancel_request": "Cancelar solicitud",
|
"cancel_request": "Cancelar solicitud",
|
||||||
"undo_friendship": "Eliminar amistad",
|
"undo_friendship": "Deshacer amistad",
|
||||||
"request_accepted": "Solicitud aceptada",
|
"request_accepted": "Solicitud aceptada",
|
||||||
"user_blocked_successfully": "Usuario bloqueado exitosamente",
|
"user_blocked_successfully": "Usuario bloqueado exitosamente",
|
||||||
"user_block_modal_text": "Esto va a bloquear a {{displayName}}",
|
"user_block_modal_text": "Esto va a bloquear a {{displayName}}",
|
||||||
"blocked_users": "Usuarios bloqueados",
|
"blocked_users": "Usuarios bloqueados",
|
||||||
"unblock": "Desbloquear",
|
"unblock": "Desbloquear",
|
||||||
"no_friends_added": "Todavía no tienes amigos añadidos",
|
"no_friends_added": "No tenés amistades añadidas",
|
||||||
"pending": "Pendiente",
|
"pending": "Pendiente",
|
||||||
"no_pending_invites": "No tienes invitaciones pendientes",
|
"no_pending_invites": "No tenés invitaciones pendientes",
|
||||||
"no_blocked_users": "No has bloqueado a ningún usuario",
|
"no_blocked_users": "No has bloqueado a nadie",
|
||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amistad copiado",
|
||||||
"undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}",
|
"undo_friendship_modal_text": "Esto va a deshacer tu amistad con {{displayName}}",
|
||||||
"privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración</0>.",
|
"privacy_hint": "Para cambiar quién puede ver esto, andá a <0>Ajustes</0>",
|
||||||
"locked_profile": "Este perfil es privado",
|
"locked_profile": "Este perfil es privado",
|
||||||
"image_process_failure": "Error al procesar la imagen",
|
"image_process_failure": "Errpr al procesar la imagen",
|
||||||
"required_field": "Este campo es obligatorio",
|
"required_field": "Este campo es requerido",
|
||||||
"displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres",
|
"displayname_min_length": "El nombre en pantalla debe tener mínimo 3 caracteres",
|
||||||
"displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres",
|
"displayname_max_length": "El nombre en pantalla debe tener máximo 50 caracteres",
|
||||||
"report_profile": "Reportar este perfil",
|
"report_profile": "Reportar este perfil",
|
||||||
"report_reason": "¿Cual es el motivo del reporte?",
|
"report_reason": "¿Porque estás reportando este perfil?",
|
||||||
"report_description": "Información adicional",
|
"report_description": "Información adicional",
|
||||||
"report_description_placeholder": "Información adicional",
|
"report_description_placeholder": "Información adicional",
|
||||||
"report": "Reportar",
|
"report": "Reportar",
|
||||||
"report_reason_hate": "Discursos de odio",
|
"report_reason_hate": "Discursos de odio",
|
||||||
"report_reason_sexual_content": "Contenido sexual",
|
"report_reason_sexual_content": "Contenido sexual",
|
||||||
"report_reason_violence": "Violencia",
|
"report_reason_violence": "Violencia",
|
||||||
"report_reason_spam": "Spam / Contenido no deseado",
|
"report_reason_spam": "Spam",
|
||||||
"report_reason_other": "Otro",
|
"report_reason_other": "Otros",
|
||||||
"profile_reported": "Perfil reportado",
|
"profile_reported": "Perfil reportado",
|
||||||
"your_friend_code": "Tu código de amigo:",
|
"your_friend_code": "Tu código de amistad:",
|
||||||
"upload_banner": "Subir un banner",
|
"upload_banner": "Subir banner",
|
||||||
"uploading_banner": "Subiendo banner…",
|
"uploading_banner": "Subiendo banner…",
|
||||||
"background_image_updated": "Imagen de fondo actualizada",
|
"background_image_updated": "Imagen de fondo actualizada",
|
||||||
"playing": "Jugando {{game}}",
|
|
||||||
"achievements": "logros",
|
|
||||||
"achievements_unlocked": "Logros desbloqueados",
|
|
||||||
"earned_points": "Puntos Obtenidos",
|
|
||||||
"show_achievements_on_profile": "Mostrar tus logros en tu perfil",
|
|
||||||
"show_points_on_profile": "Mostrar tus puntos obtenidos en tu perfil",
|
|
||||||
"games": "Juegos",
|
|
||||||
"ranking_updated_weekly": "El Ranking se actualiza semanalmente",
|
|
||||||
"stats": "Estadísticas",
|
"stats": "Estadísticas",
|
||||||
"top_percentile": "Top {{percentile}}%"
|
"achievements": "logros",
|
||||||
|
"games": "Juegos",
|
||||||
|
"top_percentile": "Top {{percentile}}%",
|
||||||
|
"ranking_updated_weekly": "El ranking se actualiza semanalmente",
|
||||||
|
"playing": "Jugando {{game}}",
|
||||||
|
"achievements_unlocked": "Logros desbloqueados",
|
||||||
|
"earned_points": "Puntos obtenidos",
|
||||||
|
"show_achievements_on_profile": "Mostrá tus logros en tu perfil",
|
||||||
|
"show_points_on_profile": "Mostrá los puntos obtenidos en tu perfil",
|
||||||
|
"error_adding_friend": "No se pudo enviar la solicitud de amistad. Por favor revisá el código",
|
||||||
|
"friend_code_length_error": "El código de amistad debe tener mínimo 8 caracteres",
|
||||||
|
"game_removed_from_pinned": "Juego removido de fijados",
|
||||||
|
"game_added_to_pinned": "Juego añadido a fijados"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Logro desbloqueado",
|
"achievement_unlocked": "Logro desbloqueado",
|
||||||
"user_achievements": "Logros de {{displayName}}",
|
"user_achievements": "Logros de {{displayName}}",
|
||||||
"your_achievements": "Tus Logros",
|
"your_achievements": "Tus logros",
|
||||||
"unlocked_at": "Desbloqueado el: {{date}}",
|
"unlocked_at": "Desbloqueado el: {{date}}",
|
||||||
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
|
"subscription_needed": "Se requiere una suscripción a Hydra Cloud para ver esto",
|
||||||
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos",
|
"new_achievements_unlocked": "Desbloqueaste {{achievementCount}} nuevos logros de {{gameCount}} juegos",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} logros",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} logros",
|
||||||
"achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}",
|
"achievements_unlocked_for_game": "Desbloqueaste {{achievementCount}} nuevos logros para {{gameTitle}}",
|
||||||
"hidden_achievement_tooltip": "Este es un logro oculto",
|
"hidden_achievement_tooltip": "Este es un logro oculto",
|
||||||
"achievement_earn_points": "Obtén {{points}} puntos con este logro",
|
"achievement_earn_points": "Conseguí {{points}} puntos por este logro",
|
||||||
"earned_points": "Puntos obtenidos:",
|
"earned_points": "Puntos obtenidos:",
|
||||||
"available_points": "Puntos disponibles:",
|
"available_points": "Puntos disponibles:",
|
||||||
"how_to_earn_achievements_points": "¿Cómo obtener puntos de logros?"
|
"how_to_earn_achievements_points": "¿Como conseguir puntos por logros?"
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"hydra_cloud": {
|
||||||
"subscription_tour_title": "Suscripción Hydra Cloud",
|
"subscription_tour_title": "Suscripción Hydra Cloud",
|
||||||
"debrid_description": "Descargas hasta x4 más rápidas con Nimbus",
|
"subscribe_now": "suscribíte ahora",
|
||||||
"subscribe_now": "Suscribirse ahora",
|
|
||||||
"cloud_saving": "Guardado en la nube",
|
"cloud_saving": "Guardado en la nube",
|
||||||
"cloud_achievements": "Guarda tus logros en la nube",
|
"cloud_achievements": "Guardá tus logros en la nube",
|
||||||
"animated_profile_picture": "Fotos de perfil animadas",
|
"animated_profile_picture": "Foto de perfil animada",
|
||||||
"premium_support": "Soporte Premium",
|
"premium_support": "Soporte Premium",
|
||||||
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
|
"show_and_compare_achievements": "Mostrá y compará tus logros con otros usuarios",
|
||||||
"animated_profile_banner": "Fondo de perfil animado",
|
"animated_profile_banner": "Banner de perfil animado",
|
||||||
"hydra_cloud": "Hydra Cloud",
|
"hydra_cloud": "Hydra Cloud",
|
||||||
"hydra_cloud_feature_found": "¡Has descubierto una característica de Hydra Cloud!",
|
"hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!",
|
||||||
"learn_more": "Aprender más"
|
"learn_more": "Descubrir más",
|
||||||
|
"debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Edukalt sisse logitud"
|
"successfully_signed_in": "Edukalt sisse logitud"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Esile toodud",
|
|
||||||
"surprise_me": "Üllata mind",
|
"surprise_me": "Üllata mind",
|
||||||
"no_results": "Tulemusi ei leitud",
|
"no_results": "Tulemusi ei leitud",
|
||||||
"start_typing": "Alusta otsimiseks kirjutamist...",
|
"start_typing": "Alusta otsimiseks kirjutamist...",
|
||||||
@@ -45,10 +44,7 @@
|
|||||||
"calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…",
|
"calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…",
|
||||||
"checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)"
|
"checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Järgmine leht",
|
|
||||||
"previous_page": "Eelmine leht"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Ava allalaadimise valikud",
|
"open_download_options": "Ava allalaadimise valikud",
|
||||||
"download_options_zero": "Allalaadimise valikuid pole",
|
"download_options_zero": "Allalaadimise valikuid pole",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "فارسی",
|
"language_name": "فارسی",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "پیشنهادی",
|
|
||||||
"surprise_me": "سوپرایزم کن",
|
"surprise_me": "سوپرایزم کن",
|
||||||
"no_results": "اتمامای پیدا نشد"
|
"no_results": "اتمامای پیدا نشد"
|
||||||
},
|
},
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
"home": "خانه",
|
"home": "خانه",
|
||||||
"favorites": "علاقهمندیها"
|
"favorites": "علاقهمندیها"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "جستجوی بازیها",
|
"search": "جستجوی بازیها",
|
||||||
"home": "خانه",
|
"home": "خانه",
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
"downloading_metadata": "درحال دانلود متادیتاهای {{title}}…",
|
||||||
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
"downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "صفحهی بعدی",
|
|
||||||
"previous_page": "صفحهی قبلی"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "بازکردن آپشنهای دانلود",
|
"open_download_options": "بازکردن آپشنهای دانلود",
|
||||||
"download_options_zero": "هیچ آپشن دانلودی وجود ندارد",
|
"download_options_zero": "هیچ آپشن دانلودی وجود ندارد",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Connecté avec succès"
|
"successfully_signed_in": "Connecté avec succès"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "En vedette",
|
|
||||||
"surprise_me": "Surprenez-moi",
|
"surprise_me": "Surprenez-moi",
|
||||||
"no_results": "Aucun résultat trouvé",
|
"no_results": "Aucun résultat trouvé",
|
||||||
"start_typing": "Commencez à taper pour rechercher...",
|
"start_typing": "Commencez à taper pour rechercher...",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Magyar",
|
"language_name": "Magyar",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
|
||||||
"surprise_me": "Lepj meg",
|
"surprise_me": "Lepj meg",
|
||||||
"no_results": "Nem található"
|
"no_results": "Nem található"
|
||||||
},
|
},
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
|
|
||||||
"home": "Főoldal",
|
"home": "Főoldal",
|
||||||
"catalogue": "Katalógus",
|
"catalogue": "Katalógus",
|
||||||
"downloads": "Letöltések",
|
"downloads": "Letöltések",
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
"downloading_metadata": "{{title}} metaadatainak letöltése…",
|
||||||
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
"downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Következő olda",
|
|
||||||
"previous_page": "Előző olda"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Letöltési lehetőségek",
|
"open_download_options": "Letöltési lehetőségek",
|
||||||
"download_options_zero": "Nincs letöltési lehetőség",
|
"download_options_zero": "Nincs letöltési lehetőség",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Berhasil masuk"
|
"successfully_signed_in": "Berhasil masuk"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Unggulan",
|
|
||||||
"surprise_me": "Kejutkan saya",
|
"surprise_me": "Kejutkan saya",
|
||||||
"no_results": "Tidak ada hasil ditemukan"
|
"no_results": "Tidak ada hasil ditemukan"
|
||||||
},
|
},
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cari game",
|
"search": "Cari game",
|
||||||
|
|
||||||
"home": "Beranda",
|
"home": "Beranda",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Unduhan",
|
"downloads": "Unduhan",
|
||||||
@@ -41,10 +39,7 @@
|
|||||||
"calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…",
|
"calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…",
|
||||||
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)"
|
"checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Halaman Berikutnya",
|
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Italiano",
|
"language_name": "Italiano",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "In primo piano",
|
|
||||||
"surprise_me": "Sorprendimi",
|
"surprise_me": "Sorprendimi",
|
||||||
"no_results": "Nessun risultato trovato"
|
"no_results": "Nessun risultato trovato"
|
||||||
},
|
},
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Catalogo",
|
"catalogue": "Catalogo",
|
||||||
"downloads": "Download",
|
"downloads": "Download",
|
||||||
@@ -32,10 +30,7 @@
|
|||||||
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
"downloading_metadata": "Scaricamento metadati di {{title}}…",
|
||||||
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
"downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Pagina successiva",
|
|
||||||
"previous_page": "Pagina precedente"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Apri opzioni di download",
|
"open_download_options": "Apri opzioni di download",
|
||||||
"download_options_zero": "Nessuna opzione di download",
|
"download_options_zero": "Nessuna opzione di download",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Сәтті кіру"
|
"successfully_signed_in": "Сәтті кіру"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Ұсынылған",
|
|
||||||
"surprise_me": "Таңқалдыр",
|
"surprise_me": "Таңқалдыр",
|
||||||
"no_results": "Ештеңе табылмады"
|
"no_results": "Ештеңе табылмады"
|
||||||
},
|
},
|
||||||
@@ -23,7 +22,6 @@
|
|||||||
"sign_in": "Кіру",
|
"sign_in": "Кіру",
|
||||||
"favorites": "Таңдаулылар"
|
"favorites": "Таңдаулылар"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Іздеу",
|
"search": "Іздеу",
|
||||||
"home": "Басты бет",
|
"home": "Басты бет",
|
||||||
@@ -40,10 +38,7 @@
|
|||||||
"downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}",
|
"downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}",
|
||||||
"calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…"
|
"calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Келесі бет",
|
|
||||||
"previous_page": "Алдыңғы бет"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Жүктеу нұсқаларын ашу",
|
"open_download_options": "Жүктеу нұсқаларын ашу",
|
||||||
"download_options_zero": "Жүктеу нұсқалары жоқ",
|
"download_options_zero": "Жүктеу нұсқалары жоқ",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "한국어",
|
"language_name": "한국어",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "추천",
|
|
||||||
"surprise_me": "무작위 추천",
|
"surprise_me": "무작위 추천",
|
||||||
"no_results": "결과 없음"
|
"no_results": "결과 없음"
|
||||||
},
|
},
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
"home": "홈",
|
"home": "홈",
|
||||||
"favorites": "즐겨찾기"
|
"favorites": "즐겨찾기"
|
||||||
},
|
},
|
||||||
|
|
||||||
"header": {
|
"header": {
|
||||||
"search": "게임 검색하기",
|
"search": "게임 검색하기",
|
||||||
"home": "홈",
|
"home": "홈",
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
"downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…",
|
||||||
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
"downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "다음 페이지",
|
|
||||||
"previous_page": "이전 페이지"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "다운로드 선택지 열기",
|
"open_download_options": "다운로드 선택지 열기",
|
||||||
"download_options_zero": "다운로드 선택지 없음",
|
"download_options_zero": "다운로드 선택지 없음",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Logget inn vellykket"
|
"successfully_signed_in": "Logget inn vellykket"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Anbefalinger",
|
|
||||||
"surprise_me": "Overrask meg",
|
"surprise_me": "Overrask meg",
|
||||||
"no_results": "Ingen resultater fundet",
|
"no_results": "Ingen resultater fundet",
|
||||||
"start_typing": "Begynn å skrive for å søke...",
|
"start_typing": "Begynn å skrive for å søke...",
|
||||||
@@ -29,7 +28,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Søk efter spill",
|
"search": "Søk efter spill",
|
||||||
|
|
||||||
"home": "Hjem",
|
"home": "Hjem",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Nedlastinger",
|
"downloads": "Nedlastinger",
|
||||||
@@ -45,10 +43,7 @@
|
|||||||
"calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…",
|
"calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…",
|
||||||
"checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)"
|
"checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Neste side",
|
|
||||||
"previous_page": "Forrige side"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Åpne nedlastingsmuligheter",
|
"open_download_options": "Åpne nedlastingsmuligheter",
|
||||||
"download_options_zero": "Ingen nedlastingsmulighet",
|
"download_options_zero": "Ingen nedlastingsmulighet",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Nederlands",
|
"language_name": "Nederlands",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Uitgelicht",
|
|
||||||
"surprise_me": "Verrasing",
|
"surprise_me": "Verrasing",
|
||||||
"no_results": "Geen resultaten gevonden"
|
"no_results": "Geen resultaten gevonden"
|
||||||
},
|
},
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Zoek spellen",
|
"search": "Zoek spellen",
|
||||||
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalogue": "Bibliotheek",
|
"catalogue": "Bibliotheek",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"downloading_metadata": "Downloading {{title}} metadata…",
|
"downloading_metadata": "Downloading {{title}} metadata…",
|
||||||
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
|
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Volgende Pagina",
|
|
||||||
"previous_page": "Vorige Pagina"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Open download Instellingen",
|
"open_download_options": "Open download Instellingen",
|
||||||
"download_options_zero": "Geen download Instellingen",
|
"download_options_zero": "Geen download Instellingen",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Polski",
|
"language_name": "Polski",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Wyróżnione",
|
|
||||||
"surprise_me": "Zaskocz mnie",
|
"surprise_me": "Zaskocz mnie",
|
||||||
"no_results": "Nie znaleziono wyników"
|
"no_results": "Nie znaleziono wyników"
|
||||||
},
|
},
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
|
|
||||||
"home": "Główna",
|
"home": "Główna",
|
||||||
"catalogue": "Katalog",
|
"catalogue": "Katalog",
|
||||||
"downloads": "Pobrane",
|
"downloads": "Pobrane",
|
||||||
@@ -32,10 +30,7 @@
|
|||||||
"downloading_metadata": "Pobieranie {{title}} metadata…",
|
"downloading_metadata": "Pobieranie {{title}} metadata…",
|
||||||
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
|
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Następna strona",
|
|
||||||
"previous_page": "Poprzednia strona"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Otwórz opcje pobierania",
|
"open_download_options": "Otwórz opcje pobierania",
|
||||||
"download_options_zero": "Brak opcji pobierania",
|
"download_options_zero": "Brak opcji pobierania",
|
||||||
|
|||||||
57
src/locales/pt-BR/translation.json
Normal file → Executable file
57
src/locales/pt-BR/translation.json
Normal file → Executable file
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Autenticado com sucesso"
|
"successfully_signed_in": "Autenticado com sucesso"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
|
||||||
"hot": "Populares",
|
"hot": "Populares",
|
||||||
"weekly": "📅 Mais baixados da semana",
|
"weekly": "📅 Mais baixados da semana",
|
||||||
"achievements": "🏆 Pra platinar",
|
"achievements": "🏆 Pra platinar",
|
||||||
@@ -27,7 +26,22 @@
|
|||||||
"sign_in": "Login",
|
"sign_in": "Login",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "Precisa de ajuda?",
|
"need_help": "Precisa de ajuda?",
|
||||||
"favorites": "Favoritos"
|
"favorites": "Favoritos",
|
||||||
|
"add_custom_game_tooltip": "Adicionar jogo personalizado",
|
||||||
|
"custom_game_modal": "Adicionar jogo personalizado",
|
||||||
|
"edit_game_modal_title": "Título",
|
||||||
|
"playable_button_title": "",
|
||||||
|
"custom_game_modal_add": "Adicionar Jogo",
|
||||||
|
"custom_game_modal_adding": "Adicionando...",
|
||||||
|
"custom_game_modal_browse": "Buscar",
|
||||||
|
"custom_game_modal_cancel": "Cancelar",
|
||||||
|
"edit_game_modal_assets": "Imagens",
|
||||||
|
"edit_game_modal_icon": "Ícone",
|
||||||
|
"edit_game_modal_browse": "Buscar",
|
||||||
|
"edit_game_modal_cancel": "Cancelar",
|
||||||
|
"edit_game_modal_enter_title": "Insira o título",
|
||||||
|
"edit_game_modal_logo": "Logo",
|
||||||
|
"edit_game_modal": "Personalizar detalhes"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
@@ -207,7 +221,31 @@
|
|||||||
"backup_frozen": "Backup fixado",
|
"backup_frozen": "Backup fixado",
|
||||||
"backup_unfrozen": "Backup removido dos fixados",
|
"backup_unfrozen": "Backup removido dos fixados",
|
||||||
"backup_freeze_failed": "Falha ao fixar backup",
|
"backup_freeze_failed": "Falha ao fixar backup",
|
||||||
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos"
|
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos",
|
||||||
|
"game_details": "Detalhes do Jogo",
|
||||||
|
"currency_symbol": "R$",
|
||||||
|
"currency_country": "br",
|
||||||
|
"prices": "Preços",
|
||||||
|
"no_prices_found": "Nenhum preço encontrado",
|
||||||
|
"view_all_prices": "Clique para ver todos os preços",
|
||||||
|
"retail_price": "Preço de lojas oficiais",
|
||||||
|
"keyshop_price": "Preço em keyshops",
|
||||||
|
"historical_retail": "Preço histórico de lojas oficiais",
|
||||||
|
"historical_keyshop": "Preço histórico em keyshops",
|
||||||
|
"language": "Idioma",
|
||||||
|
"caption": "Legenda",
|
||||||
|
"audio": "Áudio",
|
||||||
|
"edit_game_modal_button": "Alterar detalhes do jogo",
|
||||||
|
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||||
|
"game_removed_from_pinned": "Jogo removido dos fixados",
|
||||||
|
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
|
||||||
|
"manual_playtime_warning": "As suas horas de jogo serão marcadas como atualizadas manualmente. Esta ação não pode ser desfeita.",
|
||||||
|
"missing_wine_prefix": "Um prefixo Wine é necessário para criar um backup no Linux",
|
||||||
|
"update_game_playtime": "Modificar tempo de jogo",
|
||||||
|
"update_playtime": "Modificar tempo de jogo",
|
||||||
|
"update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}",
|
||||||
|
"update_playtime_error": "Falha ao atualizar tempo de jogo",
|
||||||
|
"update_playtime_title": "Atualizar tempo de jogo"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@@ -382,7 +420,8 @@
|
|||||||
"hidden": "Oculta",
|
"hidden": "Oculta",
|
||||||
"test_notification": "Testar notificação",
|
"test_notification": "Testar notificação",
|
||||||
"notification_preview": "Prévia da Notificação de Conquistas",
|
"notification_preview": "Prévia da Notificação de Conquistas",
|
||||||
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
|
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
|
||||||
|
"editor_tab_code": "Código"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@@ -511,7 +550,15 @@
|
|||||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
||||||
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
|
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
|
||||||
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
|
"friend_code_length_error": "Código de amigo deve ter 8 caracteres",
|
||||||
|
"top_percentile": "Top {{percentile}}%",
|
||||||
|
"playtime": "Tempo de jogo",
|
||||||
|
"played_recently": "Jogado recentemente",
|
||||||
|
"pinned": "Fixado",
|
||||||
|
"amount_minutes_short": "{{amount}}h",
|
||||||
|
"amount_hours_short": "{{amount}}h",
|
||||||
|
"game_added_to_pinned": "Jogo adicionado aos fixados",
|
||||||
|
"achievements_earned": "Conquistas recebidas"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada",
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Sessão iniciada com sucesso"
|
"successfully_signed_in": "Sessão iniciada com sucesso"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaques",
|
|
||||||
"hot": "Populares",
|
"hot": "Populares",
|
||||||
"weekly": "📅 Mais descarregados esta semana",
|
"weekly": "📅 Mais descarregados esta semana",
|
||||||
"achievements": "🏆 Para completar",
|
"achievements": "🏆 Para completar",
|
||||||
@@ -26,7 +25,8 @@
|
|||||||
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
"game_has_no_executable": "O jogo não tem um executável selecionado",
|
||||||
"sign_in": "Iniciar sessão",
|
"sign_in": "Iniciar sessão",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"favorites": "Favoritos"
|
"favorites": "Favoritos",
|
||||||
|
"edit_game_modal_cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Procurar jogos",
|
"search": "Procurar jogos",
|
||||||
@@ -247,9 +247,6 @@
|
|||||||
"download_count_zero": "Sem downloads na lista",
|
"download_count_zero": "Sem downloads na lista",
|
||||||
"download_count_one": "{{countFormatted}} download na lista",
|
"download_count_one": "{{countFormatted}} download na lista",
|
||||||
"download_count_other": "{{countFormatted}} downloads na lista",
|
"download_count_other": "{{countFormatted}} downloads na lista",
|
||||||
"download_options_zero": "Sem downloads disponíveis",
|
|
||||||
"download_options_one": "{{countFormatted}} download disponível",
|
|
||||||
"download_options_other": "{{countFormatted}} downloads disponíveis",
|
|
||||||
"download_source_url": "URL da fonte",
|
"download_source_url": "URL da fonte",
|
||||||
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
|
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
|
||||||
"download_source_up_to_date": "Sincronizada",
|
"download_source_up_to_date": "Sincronizada",
|
||||||
@@ -359,8 +356,6 @@
|
|||||||
"instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo"
|
"instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Página seguinte",
|
|
||||||
"previous_page": "Página anterior",
|
|
||||||
"search": "Filtrar…",
|
"search": "Filtrar…",
|
||||||
"developers": "Desenvolvedores",
|
"developers": "Desenvolvedores",
|
||||||
"genres": "Géneros",
|
"genres": "Géneros",
|
||||||
@@ -427,7 +422,6 @@
|
|||||||
"friend_code_copied": "Código de amigo copiado",
|
"friend_code_copied": "Código de amigo copiado",
|
||||||
"undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}",
|
"undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}",
|
||||||
"privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições</0>",
|
"privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições</0>",
|
||||||
"profile_locked": "Este perfil é privado",
|
|
||||||
"image_process_failure": "Falha ao processar a imagem",
|
"image_process_failure": "Falha ao processar a imagem",
|
||||||
"required_field": "Este campo é obrigatório",
|
"required_field": "Este campo é obrigatório",
|
||||||
"displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres",
|
"displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"language_name": "Română",
|
"language_name": "Română",
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Recomandate",
|
|
||||||
"surprise_me": "Surprinde-mă",
|
"surprise_me": "Surprinde-mă",
|
||||||
"no_results": "Niciun rezultat găsit"
|
"no_results": "Niciun rezultat găsit"
|
||||||
},
|
},
|
||||||
@@ -19,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Caută jocuri",
|
"search": "Caută jocuri",
|
||||||
|
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
"catalogue": "Catalog",
|
"catalogue": "Catalog",
|
||||||
"downloads": "Descărcări",
|
"downloads": "Descărcări",
|
||||||
@@ -32,10 +30,7 @@
|
|||||||
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
|
"downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}",
|
||||||
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
|
"calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..."
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {},
|
||||||
"next_page": "Pagina următoare",
|
|
||||||
"previous_page": "Pagina anterioară"
|
|
||||||
},
|
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "Deschide opțiunile de descărcare",
|
"open_download_options": "Deschide opțiunile de descărcare",
|
||||||
"download_options_zero": "Nicio opțiune de descărcare",
|
"download_options_zero": "Nicio opțiune de descărcare",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Успешный вход"
|
"successfully_signed_in": "Успешный вход"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рекомендации",
|
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас популярно",
|
"hot": "Сейчас популярно",
|
||||||
@@ -28,7 +27,47 @@
|
|||||||
"friends": "Друзья",
|
"friends": "Друзья",
|
||||||
"need_help": "Нужна помощь?",
|
"need_help": "Нужна помощь?",
|
||||||
"favorites": "Избранное",
|
"favorites": "Избранное",
|
||||||
"playable_button_title": "Показать только игры, в которые можно играть сейчас"
|
"playable_button_title": "Показать только установленные игры.",
|
||||||
|
"custom_game_modal": "Добавить пользовательскую игру",
|
||||||
|
"custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл",
|
||||||
|
"custom_game_modal_executable_path": "Путь к исполняемому файлу",
|
||||||
|
"custom_game_modal_select_executable": "Выберите исполняемый файл",
|
||||||
|
"custom_game_modal_title": "Название игры",
|
||||||
|
"custom_game_modal_enter_title": "Введите название игры",
|
||||||
|
"custom_game_modal_browse": "Обзор",
|
||||||
|
"custom_game_modal_cancel": "Отмена",
|
||||||
|
"custom_game_modal_add": "Добавить игру",
|
||||||
|
"custom_game_modal_adding": "Добавление игры...",
|
||||||
|
"custom_game_modal_success": "Пользовательская игра успешно добавлена",
|
||||||
|
"custom_game_modal_failed": "Не удалось добавить пользовательскую игру",
|
||||||
|
"custom_game_modal_executable": "Исполняемый файл",
|
||||||
|
"edit_game_modal": "Настроить ресурсы",
|
||||||
|
"edit_game_modal_description": "Настройте ресурсы и детали игры",
|
||||||
|
"edit_game_modal_title": "Название",
|
||||||
|
"edit_game_modal_enter_title": "Введите название",
|
||||||
|
"edit_game_modal_image": "Изображение",
|
||||||
|
"edit_game_modal_select_image": "Выберите изображение",
|
||||||
|
"edit_game_modal_browse": "Обзор",
|
||||||
|
"edit_game_modal_image_preview": "Предпросмотр изображения",
|
||||||
|
"edit_game_modal_icon": "Иконка",
|
||||||
|
"edit_game_modal_select_icon": "Выберите иконку",
|
||||||
|
"edit_game_modal_icon_preview": "Предпросмотр иконки",
|
||||||
|
"edit_game_modal_logo": "Логотип",
|
||||||
|
"edit_game_modal_select_logo": "Выберите логотип",
|
||||||
|
"edit_game_modal_logo_preview": "Предпросмотр логотипа",
|
||||||
|
"edit_game_modal_hero": "Изображение обложку игры",
|
||||||
|
"edit_game_modal_select_hero": "Выберите обложку игры",
|
||||||
|
"edit_game_modal_hero_preview": "Предпросмотр обложки игры",
|
||||||
|
"edit_game_modal_cancel": "Отмена",
|
||||||
|
"edit_game_modal_update": "Обновить",
|
||||||
|
"edit_game_modal_updating": "Обновление...",
|
||||||
|
"edit_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля",
|
||||||
|
"edit_game_modal_success": "Ресурсы успешно обновлены",
|
||||||
|
"edit_game_modal_failed": "Не удалось обновить ресурсы",
|
||||||
|
"edit_game_modal_image_filter": "Изображение",
|
||||||
|
"edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px",
|
||||||
|
"edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px",
|
||||||
|
"edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
@@ -209,7 +248,23 @@
|
|||||||
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
|
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
|
||||||
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
|
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
|
||||||
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
|
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
|
||||||
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus."
|
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus.",
|
||||||
|
"update_playtime_success": "Время игры успешно обновлено",
|
||||||
|
"update_playtime_error": "Не удалось обновить время игры",
|
||||||
|
"manual_playtime_warning": "Ваши часы будут отмечены как обновленные вручную. Это действие нельзя отменить.",
|
||||||
|
"artifact_renamed": "Резервная копия успешно переименована",
|
||||||
|
"rename_artifact": "Переименовать резервную копию",
|
||||||
|
"rename_artifact_description": "Переименуйте резервную копию, присвоив ей более описательное имя.",
|
||||||
|
"artifact_name_label": "Название резервной копии",
|
||||||
|
"artifact_name_placeholder": "Введите название для резервной копии",
|
||||||
|
"max_length_field": "Это поле должно содержать менее {{length}} символов",
|
||||||
|
"freeze_backup": "Закрепить, чтобы она не была перезаписана автоматическими резервными копиями",
|
||||||
|
"unfreeze_backup": "Открепить",
|
||||||
|
"backup_frozen": "Резервная копия закреплена",
|
||||||
|
"backup_unfrozen": "Резервная копия откреплена",
|
||||||
|
"backup_freeze_failed": "Не удалось закрепить резервную копию",
|
||||||
|
"backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий",
|
||||||
|
"manual_playtime_tooltip": "Это время игры было обновлено вручную"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Активировать Hydra",
|
"title": "Активировать Hydra",
|
||||||
@@ -385,7 +440,8 @@
|
|||||||
"hidden": "Скрытый",
|
"hidden": "Скрытый",
|
||||||
"test_notification": "Тестовое уведомление",
|
"test_notification": "Тестовое уведомление",
|
||||||
"notification_preview": "Предварительный просмотр уведомления о достижении",
|
"notification_preview": "Предварительный просмотр уведомления о достижении",
|
||||||
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру"
|
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
|
||||||
|
"enable_steam_achievements": "Включить поиск достижений Steam"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
@@ -398,12 +454,12 @@
|
|||||||
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
|
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
|
||||||
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
|
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
|
||||||
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
"new_friend_request_title": "Новый запрос на добавление в друзья",
|
||||||
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья",
|
|
||||||
"extraction_complete": "Распаковка завершена",
|
"extraction_complete": "Распаковка завершена",
|
||||||
"game_extracted": "{{title}} успешно распакован",
|
"game_extracted": "{{title}} успешно распакован",
|
||||||
"friend_started_playing_game": "{{displayName}} начал играть в игру",
|
"friend_started_playing_game": "{{displayName}} начал играть в игру",
|
||||||
"test_achievement_notification_title": "Это тестовое уведомление",
|
"test_achievement_notification_title": "Это тестовое уведомление",
|
||||||
"test_achievement_notification_description": "Довольно круто, да?"
|
"test_achievement_notification_description": "Довольно круто, да?",
|
||||||
|
"new_friend_request_description": "{{displayName}} отправил вам запрос в друзья"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "Открыть Hydra",
|
"open": "Открыть Hydra",
|
||||||
@@ -428,6 +484,8 @@
|
|||||||
"user_profile": {
|
"user_profile": {
|
||||||
"amount_hours": "{{amount}} часов",
|
"amount_hours": "{{amount}} часов",
|
||||||
"amount_minutes": "{{amount}} минут",
|
"amount_minutes": "{{amount}} минут",
|
||||||
|
"amount_hours_short": "{{amount}}ч",
|
||||||
|
"amount_minutes_short": "{{amount}}м",
|
||||||
"last_time_played": "Последняя игра {{period}}",
|
"last_time_played": "Последняя игра {{period}}",
|
||||||
"activity": "Недавняя активность",
|
"activity": "Недавняя активность",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
@@ -505,7 +563,9 @@
|
|||||||
"achievements_unlocked": "Достижения разблокированы",
|
"achievements_unlocked": "Достижения разблокированы",
|
||||||
"earned_points": "Заработано очков:",
|
"earned_points": "Заработано очков:",
|
||||||
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
"show_achievements_on_profile": "Покажите свои достижения в профиле",
|
||||||
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
|
"show_points_on_profile": "Показывать заработанные очки в своем профиле",
|
||||||
|
"error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга",
|
||||||
|
"friend_code_length_error": "Код друга должен содержать 8 символов"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Достижение разблокировано",
|
"achievement_unlocked": "Достижение разблокировано",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Inloggningen lyckades"
|
"successfully_signed_in": "Inloggningen lyckades"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Utvalt",
|
|
||||||
"surprise_me": "Överraska mig",
|
"surprise_me": "Överraska mig",
|
||||||
"no_results": "Inga resultat hittades",
|
"no_results": "Inga resultat hittades",
|
||||||
"start_typing": "Börja skriva för att söka...",
|
"start_typing": "Börja skriva för att söka...",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Başarıyla giriş yapıldı"
|
"successfully_signed_in": "Başarıyla giriş yapıldı"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Öne Çıkanlar",
|
|
||||||
"surprise_me": "Beni Şaşırt",
|
"surprise_me": "Beni Şaşırt",
|
||||||
"no_results": "Sonuç bulunamadı",
|
"no_results": "Sonuç bulunamadı",
|
||||||
"start_typing": "Aramak için yazmaya başlayın...",
|
"start_typing": "Aramak için yazmaya başlayın...",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Успішний вхід в систему"
|
"successfully_signed_in": "Успішний вхід в систему"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Рекомендоване",
|
|
||||||
"surprise_me": "Здивуй мене",
|
"surprise_me": "Здивуй мене",
|
||||||
"no_results": "Результатів не знайдено",
|
"no_results": "Результатів не знайдено",
|
||||||
"start_typing": "Почніть набирати текст для пошуку...",
|
"start_typing": "Почніть набирати текст для пошуку...",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi"
|
"successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Tavsiya etilgan",
|
|
||||||
"surprise_me": "Hayratda qoldir",
|
"surprise_me": "Hayratda qoldir",
|
||||||
"no_results": "Natijalar topilmadi",
|
"no_results": "Natijalar topilmadi",
|
||||||
"hot": "Eng mashhur",
|
"hot": "Eng mashhur",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"successfully_signed_in": "已成功登录"
|
"successfully_signed_in": "已成功登录"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "特色推荐",
|
|
||||||
"surprise_me": "向我推荐",
|
"surprise_me": "向我推荐",
|
||||||
"no_results": "没有找到结果",
|
"no_results": "没有找到结果",
|
||||||
"start_typing": "键入以开始搜素...",
|
"start_typing": "键入以开始搜素...",
|
||||||
@@ -51,8 +50,6 @@
|
|||||||
"installing_common_redist": "{{log}}…"
|
"installing_common_redist": "{{log}}…"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "下一页",
|
|
||||||
"previous_page": "上一页",
|
|
||||||
"clear_filters": "清除已选的 {{filterCount}} 项",
|
"clear_filters": "清除已选的 {{filterCount}} 项",
|
||||||
"developers": "开发商",
|
"developers": "开发商",
|
||||||
"download_sources": "下载源",
|
"download_sources": "下载源",
|
||||||
|
|||||||
@@ -10,7 +10,16 @@ const saveGameShopAssets = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const key = levelKeys.game(shop, objectId);
|
const key = levelKeys.game(shop, objectId);
|
||||||
const existingAssets = await gamesShopAssetsSublevel.get(key);
|
const existingAssets = await gamesShopAssetsSublevel.get(key);
|
||||||
return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets });
|
|
||||||
|
// Preserve existing title if it differs from the incoming title (indicating it was customized)
|
||||||
|
const shouldPreserveTitle =
|
||||||
|
existingAssets?.title && existingAssets.title !== assets.title;
|
||||||
|
|
||||||
|
return gamesShopAssetsSublevel.put(key, {
|
||||||
|
...existingAssets,
|
||||||
|
...assets,
|
||||||
|
title: shouldPreserveTitle ? existingAssets.title : assets.title,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ import "./catalogue/get-developers";
|
|||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./hardware/check-folder-write-permission";
|
import "./hardware/check-folder-write-permission";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
|
import "./library/add-custom-game-to-library";
|
||||||
|
import "./library/update-custom-game";
|
||||||
|
import "./library/update-game-custom-assets";
|
||||||
import "./library/add-game-to-favorites";
|
import "./library/add-game-to-favorites";
|
||||||
import "./library/remove-game-from-favorites";
|
import "./library/remove-game-from-favorites";
|
||||||
|
import "./library/toggle-game-pin";
|
||||||
import "./library/create-game-shortcut";
|
import "./library/create-game-shortcut";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./library/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
@@ -36,7 +40,9 @@ import "./library/reset-game-achievements";
|
|||||||
import "./library/change-game-playtime";
|
import "./library/change-game-playtime";
|
||||||
import "./library/toggle-automatic-cloud-sync";
|
import "./library/toggle-automatic-cloud-sync";
|
||||||
import "./library/get-default-wine-prefix-selection-path";
|
import "./library/get-default-wine-prefix-selection-path";
|
||||||
|
import "./library/cleanup-unused-assets";
|
||||||
import "./library/create-steam-shortcut";
|
import "./library/create-steam-shortcut";
|
||||||
|
import "./library/copy-custom-game-asset";
|
||||||
import "./misc/open-checkout";
|
import "./misc/open-checkout";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
@@ -45,6 +51,8 @@ import "./misc/show-item-in-folder";
|
|||||||
import "./misc/get-badges";
|
import "./misc/get-badges";
|
||||||
import "./misc/install-common-redist";
|
import "./misc/install-common-redist";
|
||||||
import "./misc/can-install-common-redist";
|
import "./misc/can-install-common-redist";
|
||||||
|
import "./misc/save-temp-file";
|
||||||
|
import "./misc/delete-temp-file";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
import "./torrenting/pause-game-download";
|
import "./torrenting/pause-game-download";
|
||||||
import "./torrenting/resume-game-download";
|
import "./torrenting/resume-game-download";
|
||||||
@@ -64,6 +72,7 @@ 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/get-user-library";
|
||||||
import "./user/get-blocked-users";
|
import "./user/get-blocked-users";
|
||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
|
|||||||
65
src/main/events/library/add-custom-game-to-library.ts
Normal file
65
src/main/events/library/add-custom-game-to-library.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
const addCustomGameToLibrary = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
title: string,
|
||||||
|
executablePath: string,
|
||||||
|
iconUrl?: string,
|
||||||
|
logoImageUrl?: string,
|
||||||
|
libraryHeroImageUrl?: string
|
||||||
|
) => {
|
||||||
|
const objectId = randomUUID();
|
||||||
|
const shop: GameShop = "custom";
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const existingGames = await gamesSublevel.iterator().all();
|
||||||
|
const existingGame = existingGames.find(
|
||||||
|
([_key, game]) => game.executablePath === executablePath && !game.isDeleted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingGame) {
|
||||||
|
throw new Error(
|
||||||
|
"A game with this executable path already exists in your library"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assets = {
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
title,
|
||||||
|
iconUrl: iconUrl || null,
|
||||||
|
libraryHeroImageUrl: libraryHeroImageUrl || "",
|
||||||
|
libraryImageUrl: iconUrl || "",
|
||||||
|
logoImageUrl: logoImageUrl || "",
|
||||||
|
logoPosition: null,
|
||||||
|
coverImageUrl: iconUrl || "",
|
||||||
|
};
|
||||||
|
await gamesShopAssetsSublevel.put(gameKey, assets);
|
||||||
|
|
||||||
|
const game = {
|
||||||
|
title,
|
||||||
|
iconUrl: iconUrl || null,
|
||||||
|
logoImageUrl: logoImageUrl || null,
|
||||||
|
libraryHeroImageUrl: libraryHeroImageUrl || null,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
remoteId: null,
|
||||||
|
isDeleted: false,
|
||||||
|
playTimeInMilliseconds: 0,
|
||||||
|
lastTimePlayed: null,
|
||||||
|
executablePath,
|
||||||
|
launchOptions: null,
|
||||||
|
favorite: false,
|
||||||
|
automaticCloudSync: false,
|
||||||
|
hasManuallyUpdatedPlaytime: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, game);
|
||||||
|
|
||||||
|
return game;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("addCustomGameToLibrary", addCustomGameToLibrary);
|
||||||
@@ -30,6 +30,8 @@ const addGameToLibrary = async (
|
|||||||
game = {
|
game = {
|
||||||
title,
|
title,
|
||||||
iconUrl: gameAssets?.iconUrl ?? null,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
|
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||||
|
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
@@ -41,12 +43,14 @@ const addGameToLibrary = async (
|
|||||||
await gamesSublevel.put(gameKey, game);
|
await gamesSublevel.put(gameKey, game);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createGame(game).catch(() => {});
|
if (game) {
|
||||||
|
await createGame(game).catch(() => {});
|
||||||
|
|
||||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||||
game.shop,
|
game.shop,
|
||||||
game.objectId
|
game.objectId
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||||
|
|||||||
@@ -13,16 +13,20 @@ const changeGamePlaytime = async (
|
|||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
|
||||||
playTimeInSeconds,
|
if (game.remoteId) {
|
||||||
});
|
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
||||||
|
playTimeInSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await gamesSublevel.put(gameKey, {
|
await gamesSublevel.put(gameKey, {
|
||||||
...game,
|
...game,
|
||||||
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
||||||
hasManuallyUpdatedPlaytime: true,
|
hasManuallyUpdatedPlaytime: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to update game favorite status: ${error}`);
|
throw new Error(`Failed to update game playtime: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
76
src/main/events/library/cleanup-unused-assets.ts
Normal file
76
src/main/events/library/cleanup-unused-assets.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { ASSETS_PATH } from "@main/constants";
|
||||||
|
|
||||||
|
const getCustomGamesAssetsPath = () => {
|
||||||
|
return path.join(ASSETS_PATH, "custom-games");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllCustomGameAssets = async (): Promise<string[]> => {
|
||||||
|
const assetsPath = getCustomGamesAssetsPath();
|
||||||
|
|
||||||
|
if (!fs.existsSync(assetsPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await fs.promises.readdir(assetsPath);
|
||||||
|
return files.map((file) => path.join(assetsPath, file));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsedAssetPaths = async (): Promise<Set<string>> => {
|
||||||
|
// Get all custom games from the level database
|
||||||
|
const { gamesSublevel } = await import("@main/level");
|
||||||
|
const allGames = await gamesSublevel.iterator().all();
|
||||||
|
|
||||||
|
const customGames = allGames
|
||||||
|
.map(([_key, game]) => game)
|
||||||
|
.filter((game) => game.shop === "custom" && !game.isDeleted);
|
||||||
|
|
||||||
|
const usedPaths = new Set<string>();
|
||||||
|
|
||||||
|
customGames.forEach((game) => {
|
||||||
|
// Extract file paths from local URLs
|
||||||
|
if (game.iconUrl?.startsWith("local:")) {
|
||||||
|
usedPaths.add(game.iconUrl.replace("local:", ""));
|
||||||
|
}
|
||||||
|
if (game.logoImageUrl?.startsWith("local:")) {
|
||||||
|
usedPaths.add(game.logoImageUrl.replace("local:", ""));
|
||||||
|
}
|
||||||
|
if (game.libraryHeroImageUrl?.startsWith("local:")) {
|
||||||
|
usedPaths.add(game.libraryHeroImageUrl.replace("local:", ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return usedPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupUnusedAssets = async (): Promise<{
|
||||||
|
deletedCount: number;
|
||||||
|
errors: string[];
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const allAssets = await getAllCustomGameAssets();
|
||||||
|
const usedAssets = await getUsedAssetPaths();
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
let deletedCount = 0;
|
||||||
|
|
||||||
|
for (const assetPath of allAssets) {
|
||||||
|
if (!usedAssets.has(assetPath)) {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(assetPath);
|
||||||
|
deletedCount++;
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`Failed to delete ${assetPath}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deletedCount, errors };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to cleanup unused assets: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets);
|
||||||
42
src/main/events/library/copy-custom-game-asset.ts
Normal file
42
src/main/events/library/copy-custom-game-asset.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { ASSETS_PATH } from "@main/constants";
|
||||||
|
|
||||||
|
const copyCustomGameAsset = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
sourcePath: string,
|
||||||
|
assetType: "icon" | "logo" | "hero"
|
||||||
|
): Promise<string> => {
|
||||||
|
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
||||||
|
throw new Error("Source file does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure assets directory exists
|
||||||
|
if (!fs.existsSync(ASSETS_PATH)) {
|
||||||
|
fs.mkdirSync(ASSETS_PATH, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom games assets subdirectory
|
||||||
|
const customGamesAssetsPath = path.join(ASSETS_PATH, "custom-games");
|
||||||
|
if (!fs.existsSync(customGamesAssetsPath)) {
|
||||||
|
fs.mkdirSync(customGamesAssetsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file extension
|
||||||
|
const fileExtension = path.extname(sourcePath);
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const uniqueId = randomUUID();
|
||||||
|
const fileName = `${assetType}-${uniqueId}${fileExtension}`;
|
||||||
|
const destinationPath = path.join(customGamesAssetsPath, fileName);
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
await fs.promises.copyFile(sourcePath, destinationPath);
|
||||||
|
|
||||||
|
// Return the local URL format
|
||||||
|
return `local:${destinationPath}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("copyCustomGameAsset", copyCustomGameAsset);
|
||||||
@@ -23,7 +23,10 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
|||||||
...game,
|
...game,
|
||||||
download: download ?? null,
|
download: download ?? null,
|
||||||
...gameAssets,
|
...gameAssets,
|
||||||
};
|
// Ensure compatibility with LibraryGame type
|
||||||
|
libraryHeroImageUrl:
|
||||||
|
game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl,
|
||||||
|
} as LibraryGame;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,70 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi, logger } from "@main/services";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop, Game } from "@types";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const collectAssetPathsToDelete = (game: Game): string[] => {
|
||||||
|
const assetPathsToDelete: string[] = [];
|
||||||
|
|
||||||
|
const assetUrls =
|
||||||
|
game.shop === "custom"
|
||||||
|
? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl]
|
||||||
|
: [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl];
|
||||||
|
|
||||||
|
for (const url of assetUrls) {
|
||||||
|
if (url?.startsWith("local:")) {
|
||||||
|
assetPathsToDelete.push(url.replace("local:", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assetPathsToDelete;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGameAsDeleted = async (
|
||||||
|
game: Game,
|
||||||
|
gameKey: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const updatedGame = {
|
||||||
|
...game,
|
||||||
|
isDeleted: true,
|
||||||
|
executablePath: null,
|
||||||
|
...(game.shop !== "custom" && {
|
||||||
|
customIconUrl: null,
|
||||||
|
customLogoImageUrl: null,
|
||||||
|
customHeroImageUrl: null,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, updatedGame);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetShopAssets = async (gameKey: string): Promise<void> => {
|
||||||
|
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
if (existingAssets) {
|
||||||
|
const resetAssets = {
|
||||||
|
...existingAssets,
|
||||||
|
title: existingAssets.title,
|
||||||
|
};
|
||||||
|
await gamesShopAssetsSublevel.put(gameKey, resetAssets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAssetFiles = async (
|
||||||
|
assetPathsToDelete: string[]
|
||||||
|
): Promise<void> => {
|
||||||
|
if (assetPathsToDelete.length === 0) return;
|
||||||
|
|
||||||
|
for (const assetPath of assetPathsToDelete) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(assetPath)) {
|
||||||
|
await fs.promises.unlink(assetPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to delete asset ${assetPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeGameFromLibrary = async (
|
const removeGameFromLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -11,17 +74,21 @@ const removeGameFromLibrary = async (
|
|||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
if (game) {
|
if (!game) return;
|
||||||
await gamesSublevel.put(gameKey, {
|
|
||||||
...game,
|
|
||||||
isDeleted: true,
|
|
||||||
executablePath: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (game?.remoteId) {
|
const assetPathsToDelete = collectAssetPathsToDelete(game);
|
||||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
|
||||||
}
|
await updateGameAsDeleted(game, gameKey);
|
||||||
|
|
||||||
|
if (game.shop !== "custom") {
|
||||||
|
await resetShopAssets(gameKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (game?.remoteId) {
|
||||||
|
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAssetFiles(assetPathsToDelete);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
||||||
|
|||||||
43
src/main/events/library/toggle-game-pin.ts
Normal file
43
src/main/events/library/toggle-game-pin.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { HydraApi, logger } from "@main/services";
|
||||||
|
import type { GameShop, UserGame } from "@types";
|
||||||
|
|
||||||
|
const toggleGamePin = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
pin: boolean
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
if (pin) {
|
||||||
|
const response = await HydraApi.put<UserGame>(
|
||||||
|
`/profile/games/${shop}/${objectId}/pin`
|
||||||
|
);
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
isPinned: pin,
|
||||||
|
pinnedDate: new Date(response.pinnedDate!),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`);
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
isPinned: pin,
|
||||||
|
pinnedDate: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update game pinned status", error);
|
||||||
|
throw new Error(`Failed to update game pinned status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("toggleGamePin", toggleGamePin);
|
||||||
98
src/main/events/library/update-custom-game.ts
Normal file
98
src/main/events/library/update-custom-game.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
interface UpdateCustomGameParams {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
iconUrl?: string;
|
||||||
|
logoImageUrl?: string;
|
||||||
|
libraryHeroImageUrl?: string;
|
||||||
|
originalIconPath?: string;
|
||||||
|
originalLogoPath?: string;
|
||||||
|
originalHeroPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCustomGame = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
params: UpdateCustomGameParams
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
title,
|
||||||
|
iconUrl,
|
||||||
|
logoImageUrl,
|
||||||
|
libraryHeroImageUrl,
|
||||||
|
originalIconPath,
|
||||||
|
originalLogoPath,
|
||||||
|
originalHeroPath,
|
||||||
|
} = params;
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const existingGame = await gamesSublevel.get(gameKey);
|
||||||
|
if (!existingGame) {
|
||||||
|
throw new Error("Game not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldAssetPaths: string[] = [];
|
||||||
|
|
||||||
|
const assetPairs = [
|
||||||
|
{ existing: existingGame.iconUrl, new: iconUrl },
|
||||||
|
{ existing: existingGame.logoImageUrl, new: logoImageUrl },
|
||||||
|
{ existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { existing, new: newUrl } of assetPairs) {
|
||||||
|
if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) {
|
||||||
|
oldAssetPaths.push(existing.replace("local:", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedGame = {
|
||||||
|
...existingGame,
|
||||||
|
title,
|
||||||
|
iconUrl: iconUrl || null,
|
||||||
|
logoImageUrl: logoImageUrl || null,
|
||||||
|
libraryHeroImageUrl: libraryHeroImageUrl || null,
|
||||||
|
originalIconPath: originalIconPath || existingGame.originalIconPath || null,
|
||||||
|
originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null,
|
||||||
|
originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, updatedGame);
|
||||||
|
|
||||||
|
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
if (existingAssets) {
|
||||||
|
const updatedAssets = {
|
||||||
|
...existingAssets,
|
||||||
|
title,
|
||||||
|
iconUrl: iconUrl || null,
|
||||||
|
libraryHeroImageUrl: libraryHeroImageUrl || "",
|
||||||
|
libraryImageUrl: iconUrl || "",
|
||||||
|
logoImageUrl: logoImageUrl || "",
|
||||||
|
coverImageUrl: iconUrl || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldAssetPaths.length > 0) {
|
||||||
|
for (const assetPath of oldAssetPaths) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(assetPath)) {
|
||||||
|
await fs.promises.unlink(assetPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to delete old asset ${assetPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedGame;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateCustomGame", updateCustomGame);
|
||||||
162
src/main/events/library/update-game-custom-assets.ts
Normal file
162
src/main/events/library/update-game-custom-assets.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop, Game } from "@types";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { logger } from "@main/services";
|
||||||
|
|
||||||
|
const collectOldAssetPaths = (
|
||||||
|
existingGame: Game,
|
||||||
|
customIconUrl?: string | null,
|
||||||
|
customLogoImageUrl?: string | null,
|
||||||
|
customHeroImageUrl?: string | null
|
||||||
|
): string[] => {
|
||||||
|
const oldAssetPaths: string[] = [];
|
||||||
|
|
||||||
|
const assetPairs = [
|
||||||
|
{ existing: existingGame.customIconUrl, new: customIconUrl },
|
||||||
|
{ existing: existingGame.customLogoImageUrl, new: customLogoImageUrl },
|
||||||
|
{ existing: existingGame.customHeroImageUrl, new: customHeroImageUrl },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { existing, new: newUrl } of assetPairs) {
|
||||||
|
if (
|
||||||
|
existing &&
|
||||||
|
newUrl !== undefined &&
|
||||||
|
existing !== newUrl &&
|
||||||
|
existing.startsWith("local:")
|
||||||
|
) {
|
||||||
|
oldAssetPaths.push(existing.replace("local:", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldAssetPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UpdateGameDataParams {
|
||||||
|
gameKey: string;
|
||||||
|
existingGame: Game;
|
||||||
|
title: string;
|
||||||
|
customIconUrl?: string | null;
|
||||||
|
customLogoImageUrl?: string | null;
|
||||||
|
customHeroImageUrl?: string | null;
|
||||||
|
customOriginalIconPath?: string | null;
|
||||||
|
customOriginalLogoPath?: string | null;
|
||||||
|
customOriginalHeroPath?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGameData = async (params: UpdateGameDataParams): Promise<Game> => {
|
||||||
|
const {
|
||||||
|
gameKey,
|
||||||
|
existingGame,
|
||||||
|
title,
|
||||||
|
customIconUrl,
|
||||||
|
customLogoImageUrl,
|
||||||
|
customHeroImageUrl,
|
||||||
|
customOriginalIconPath,
|
||||||
|
customOriginalLogoPath,
|
||||||
|
customOriginalHeroPath,
|
||||||
|
} = params;
|
||||||
|
const updatedGame = {
|
||||||
|
...existingGame,
|
||||||
|
title,
|
||||||
|
...(customIconUrl !== undefined && { customIconUrl }),
|
||||||
|
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
|
||||||
|
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
|
||||||
|
...(customOriginalIconPath !== undefined && { customOriginalIconPath }),
|
||||||
|
...(customOriginalLogoPath !== undefined && { customOriginalLogoPath }),
|
||||||
|
...(customOriginalHeroPath !== undefined && { customOriginalHeroPath }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, updatedGame);
|
||||||
|
return updatedGame;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateShopAssets = async (
|
||||||
|
gameKey: string,
|
||||||
|
title: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
if (existingAssets) {
|
||||||
|
const updatedAssets = {
|
||||||
|
...existingAssets,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise<void> => {
|
||||||
|
if (oldAssetPaths.length === 0) return;
|
||||||
|
|
||||||
|
for (const assetPath of oldAssetPaths) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(assetPath)) {
|
||||||
|
await fs.promises.unlink(assetPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to delete old custom asset ${assetPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UpdateGameCustomAssetsParams {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
customIconUrl?: string | null;
|
||||||
|
customLogoImageUrl?: string | null;
|
||||||
|
customHeroImageUrl?: string | null;
|
||||||
|
customOriginalIconPath?: string | null;
|
||||||
|
customOriginalLogoPath?: string | null;
|
||||||
|
customOriginalHeroPath?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGameCustomAssets = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
params: UpdateGameCustomAssetsParams
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
title,
|
||||||
|
customIconUrl,
|
||||||
|
customLogoImageUrl,
|
||||||
|
customHeroImageUrl,
|
||||||
|
customOriginalIconPath,
|
||||||
|
customOriginalLogoPath,
|
||||||
|
customOriginalHeroPath,
|
||||||
|
} = params;
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const existingGame = await gamesSublevel.get(gameKey);
|
||||||
|
if (!existingGame) {
|
||||||
|
throw new Error("Game not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldAssetPaths = collectOldAssetPaths(
|
||||||
|
existingGame,
|
||||||
|
customIconUrl,
|
||||||
|
customLogoImageUrl,
|
||||||
|
customHeroImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedGame = await updateGameData({
|
||||||
|
gameKey,
|
||||||
|
existingGame,
|
||||||
|
title,
|
||||||
|
customIconUrl,
|
||||||
|
customLogoImageUrl,
|
||||||
|
customHeroImageUrl,
|
||||||
|
customOriginalIconPath,
|
||||||
|
customOriginalLogoPath,
|
||||||
|
customOriginalHeroPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateShopAssets(gameKey, title);
|
||||||
|
|
||||||
|
await deleteOldAssetFiles(oldAssetPaths);
|
||||||
|
|
||||||
|
return updatedGame;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateGameCustomAssets", updateGameCustomAssets);
|
||||||
18
src/main/events/misc/delete-temp-file.ts
Normal file
18
src/main/events/misc/delete-temp-file.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const deleteTempFile = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
filePath: string
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - temp files will be cleaned up by OS eventually
|
||||||
|
console.warn(`Failed to delete temp file: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("deleteTempFile", deleteTempFile);
|
||||||
27
src/main/events/misc/save-temp-file.ts
Normal file
27
src/main/events/misc/save-temp-file.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { app } from "electron";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const saveTempFile = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
fileName: string,
|
||||||
|
fileData: Uint8Array
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const tempDir = app.getPath("temp");
|
||||||
|
const tempFilePath = path.join(
|
||||||
|
tempDir,
|
||||||
|
`hydra-temp-${Date.now()}-${fileName}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the file data to temp directory
|
||||||
|
fs.writeFileSync(tempFilePath, fileData);
|
||||||
|
|
||||||
|
return tempFilePath;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to save temp file: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("saveTempFile", saveTempFile);
|
||||||
@@ -53,6 +53,8 @@ const startGameDownload = async (
|
|||||||
await gamesSublevel.put(gameKey, {
|
await gamesSublevel.put(gameKey, {
|
||||||
title,
|
title,
|
||||||
iconUrl: gameAssets?.iconUrl ?? null,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
|
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||||
|
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
|
|||||||
28
src/main/events/user/get-user-library.ts
Normal file
28
src/main/events/user/get-user-library.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import type { UserLibraryResponse } from "@types";
|
||||||
|
|
||||||
|
const getUserLibrary = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
userId: string,
|
||||||
|
take: number = 12,
|
||||||
|
skip: number = 0,
|
||||||
|
sortBy?: string
|
||||||
|
): Promise<UserLibraryResponse | null> => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
params.append("take", take.toString());
|
||||||
|
params.append("skip", skip.toString());
|
||||||
|
|
||||||
|
if (sortBy) {
|
||||||
|
params.append("sortBy", sortBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
const baseUrl = `/users/${userId}/library`;
|
||||||
|
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||||
|
|
||||||
|
return HydraApi.get<UserLibraryResponse>(url).catch(() => null);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getUserLibrary", getUserLibrary);
|
||||||
@@ -64,6 +64,71 @@ app.whenReady().then(async () => {
|
|||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protocol.handle("gradient", (request) => {
|
||||||
|
const gradientCss = decodeURIComponent(
|
||||||
|
request.url.slice("gradient:".length)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse gradient CSS safely without regex to prevent ReDoS
|
||||||
|
let direction = "45deg";
|
||||||
|
let color1 = "#4a90e2";
|
||||||
|
let color2 = "#7b68ee";
|
||||||
|
|
||||||
|
// Simple string parsing approach - more secure than regex
|
||||||
|
if (
|
||||||
|
gradientCss.startsWith("linear-gradient(") &&
|
||||||
|
gradientCss.endsWith(")")
|
||||||
|
) {
|
||||||
|
const content = gradientCss.slice(16, -1); // Remove "linear-gradient(" and ")"
|
||||||
|
const parts = content.split(",").map((part) => part.trim());
|
||||||
|
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
direction = parts[0];
|
||||||
|
color1 = parts[1];
|
||||||
|
color2 = parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let x1 = "0%",
|
||||||
|
y1 = "0%",
|
||||||
|
x2 = "100%",
|
||||||
|
y2 = "100%";
|
||||||
|
|
||||||
|
if (direction === "to right") {
|
||||||
|
y2 = "0%";
|
||||||
|
} else if (direction === "to bottom") {
|
||||||
|
x2 = "0%";
|
||||||
|
} else if (direction === "45deg") {
|
||||||
|
y1 = "100%";
|
||||||
|
y2 = "0%";
|
||||||
|
} else if (direction === "225deg") {
|
||||||
|
x1 = "100%";
|
||||||
|
x2 = "0%";
|
||||||
|
} else if (direction === "315deg") {
|
||||||
|
x1 = "100%";
|
||||||
|
y1 = "100%";
|
||||||
|
x2 = "0%";
|
||||||
|
y2 = "0%";
|
||||||
|
}
|
||||||
|
// Note: "135deg" case removed as it uses all default values
|
||||||
|
|
||||||
|
const svgContent = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
||||||
|
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#grad)" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return new Response(svgContent, {
|
||||||
|
headers: { "Content-Type": "image/svg+xml" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await loadState();
|
await loadState();
|
||||||
|
|
||||||
const language = await db
|
const language = await db
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type ProfileGame = {
|
|||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
hasManuallyUpdatedPlaytime: boolean;
|
hasManuallyUpdatedPlaytime: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
isPinned?: boolean;
|
||||||
} & ShopAssets;
|
} & ShopAssets;
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
@@ -36,6 +37,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
lastTimePlayed: updatedLastTimePlayed,
|
lastTimePlayed: updatedLastTimePlayed,
|
||||||
playTimeInMilliseconds: updatedPlayTime,
|
playTimeInMilliseconds: updatedPlayTime,
|
||||||
favorite: game.isFavorite ?? localGame.favorite,
|
favorite: game.isFavorite ?? localGame.favorite,
|
||||||
|
isPinned: game.isPinned ?? localGame.isPinned,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await gamesSublevel.put(gameKey, {
|
await gamesSublevel.put(gameKey, {
|
||||||
@@ -44,18 +46,21 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
iconUrl: game.iconUrl,
|
iconUrl: game.iconUrl,
|
||||||
|
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||||
|
logoImageUrl: game.logoImageUrl,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,
|
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
favorite: game.isFavorite ?? false,
|
favorite: game.isFavorite ?? false,
|
||||||
|
isPinned: game.isPinned ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await gamesShopAssetsSublevel.put(gameKey, {
|
await gamesShopAssetsSublevel.put(gameKey, {
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: game.title,
|
title: localGame?.title || game.title, // Preserve local title if it exists
|
||||||
coverImageUrl: game.coverImageUrl,
|
coverImageUrl: game.coverImageUrl,
|
||||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||||
libraryImageUrl: game.libraryImageUrl,
|
libraryImageUrl: game.libraryImageUrl,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const uploadGamesBatch = async () => {
|
|||||||
.all()
|
.all()
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
return results.filter(
|
return results.filter(
|
||||||
(game) => !game.isDeleted && game.remoteId === null
|
(game) =>
|
||||||
|
!game.isDeleted && game.remoteId === null && game.shop !== "custom"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export const uploadGamesBatch = async () => {
|
|||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
isFavorite: game.favorite,
|
isFavorite: game.favorite,
|
||||||
|
isPinned: game.isPinned ?? false,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
|
|||||||
@@ -128,6 +128,52 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
),
|
),
|
||||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||||
|
addCustomGameToLibrary: (
|
||||||
|
title: string,
|
||||||
|
executablePath: string,
|
||||||
|
iconUrl?: string,
|
||||||
|
logoImageUrl?: string,
|
||||||
|
libraryHeroImageUrl?: string
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
"addCustomGameToLibrary",
|
||||||
|
title,
|
||||||
|
executablePath,
|
||||||
|
iconUrl,
|
||||||
|
logoImageUrl,
|
||||||
|
libraryHeroImageUrl
|
||||||
|
),
|
||||||
|
copyCustomGameAsset: (
|
||||||
|
sourcePath: string,
|
||||||
|
assetType: "icon" | "logo" | "hero"
|
||||||
|
) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType),
|
||||||
|
saveTempFile: (fileName: string, fileData: Uint8Array) =>
|
||||||
|
ipcRenderer.invoke("saveTempFile", fileName, fileData),
|
||||||
|
deleteTempFile: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke("deleteTempFile", filePath),
|
||||||
|
cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"),
|
||||||
|
updateCustomGame: (params: {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
iconUrl?: string;
|
||||||
|
logoImageUrl?: string;
|
||||||
|
libraryHeroImageUrl?: string;
|
||||||
|
originalIconPath?: string;
|
||||||
|
originalLogoPath?: string;
|
||||||
|
originalHeroPath?: string;
|
||||||
|
}) => ipcRenderer.invoke("updateCustomGame", params),
|
||||||
|
updateGameCustomAssets: (params: {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
customIconUrl?: string | null;
|
||||||
|
customLogoImageUrl?: string | null;
|
||||||
|
customHeroImageUrl?: string | null;
|
||||||
|
customOriginalIconPath?: string | null;
|
||||||
|
customOriginalLogoPath?: string | null;
|
||||||
|
customOriginalHeroPath?: string | null;
|
||||||
|
}) => ipcRenderer.invoke("updateGameCustomAssets", params),
|
||||||
createGameShortcut: (
|
createGameShortcut: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -143,6 +189,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||||
|
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||||
|
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -366,6 +414,12 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||||
|
getUserLibrary: (
|
||||||
|
userId: string,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
sortBy?: string
|
||||||
|
) => ipcRenderer.invoke("getUserLibrary", userId, take, skip, sortBy),
|
||||||
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
blockUser: (userId: string) => ipcRenderer.invoke("blockUser", userId),
|
||||||
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
unblockUser: (userId: string) => ipcRenderer.invoke("unblockUser", userId),
|
||||||
getUserFriends: (userId: string, take: number, skip: number) =>
|
getUserFriends: (userId: string, take: number, skip: number) =>
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.16);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|||||||
4
src/renderer/src/assets/play-logo.svg
Normal file
4
src/renderer/src/assets/play-logo.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="white" class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 409 B |
@@ -4,9 +4,14 @@
|
|||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
||||||
border: solid 1px globals.$muted-color;
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all ease 0.2s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,36 @@
|
|||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 280px;
|
height: 180px;
|
||||||
min-height: 280px;
|
min-height: 150px;
|
||||||
max-height: 280px;
|
border-radius: 0;
|
||||||
border-radius: 4px;
|
|
||||||
color: #dadbe1;
|
color: #dadbe1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0px 0px 15px 0px #000000;
|
box-shadow: 0px 0px 15px 0px #000000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
height: 220px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
height: 300px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) and (min-height: 800px) {
|
||||||
|
height: 400px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) and (max-height: 799px) {
|
||||||
|
height: 300px;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
&__media {
|
&__media {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@@ -47,10 +67,42 @@
|
|||||||
&__content {
|
&__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 3);
|
||||||
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 3);
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
width: 120px;
|
||||||
|
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) and (min-height: 800px) {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) and (max-height: 799px) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function Hero() {
|
|||||||
width="250px"
|
width="250px"
|
||||||
alt={game.description ?? ""}
|
alt={game.description ?? ""}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
|
className="hero__logo"
|
||||||
/>
|
/>
|
||||||
<p className="hero__description">{game.description}</p>
|
<p className="hero__description">{game.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
@use "../../scss/globals.scss";
|
||||||
|
|
||||||
|
.sidebar-adding-custom-game-modal {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image-preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
border: 1px solid globals.$border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview-image {
|
||||||
|
max-width: 120px;
|
||||||
|
max-height: 80px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FileDirectoryIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import { Modal, TextField, Button } from "@renderer/components";
|
||||||
|
import { useLibrary, useToast } from "@renderer/hooks";
|
||||||
|
import {
|
||||||
|
buildGameDetailsPath,
|
||||||
|
generateRandomGradient,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
|
||||||
|
import "./sidebar-adding-custom-game-modal.scss";
|
||||||
|
|
||||||
|
export interface SidebarAddingCustomGameModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidebarAddingCustomGameModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
}: Readonly<SidebarAddingCustomGameModalProps>) {
|
||||||
|
const { t } = useTranslation("sidebar");
|
||||||
|
const { updateLibrary } = useLibrary();
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [gameName, setGameName] = useState("");
|
||||||
|
const [executablePath, setExecutablePath] = useState("");
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
|
||||||
|
const handleSelectExecutable = async () => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: t("custom_game_modal_executable"),
|
||||||
|
extensions: ["exe", "msi", "app", "deb", "rpm", "dmg"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const selectedPath = filePaths[0];
|
||||||
|
setExecutablePath(selectedPath);
|
||||||
|
|
||||||
|
if (!gameName.trim()) {
|
||||||
|
const fileName = selectedPath.split(/[\\/]/).pop() || "";
|
||||||
|
const gameNameFromFile = fileName.replace(/\.[^/.]+$/, "");
|
||||||
|
setGameName(gameNameFromFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGameNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setGameName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddGame = async () => {
|
||||||
|
if (!gameName.trim() || !executablePath.trim()) {
|
||||||
|
showErrorToast(t("custom_game_modal_fill_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAdding(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate gradient URL only for hero image
|
||||||
|
const gameNameForSeed = gameName.trim();
|
||||||
|
const iconUrl = ""; // Don't use gradient for icon
|
||||||
|
const logoImageUrl = ""; // Don't use gradient for logo
|
||||||
|
const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero
|
||||||
|
|
||||||
|
const newGame = await window.electron.addCustomGameToLibrary(
|
||||||
|
gameNameForSeed,
|
||||||
|
executablePath,
|
||||||
|
iconUrl,
|
||||||
|
logoImageUrl,
|
||||||
|
libraryHeroImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
showSuccessToast(t("custom_game_modal_success"));
|
||||||
|
updateLibrary();
|
||||||
|
|
||||||
|
const gameDetailsPath = buildGameDetailsPath({
|
||||||
|
shop: "custom",
|
||||||
|
objectId: newGame.objectId,
|
||||||
|
title: newGame.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(gameDetailsPath);
|
||||||
|
|
||||||
|
setGameName("");
|
||||||
|
setExecutablePath("");
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add custom game:", error);
|
||||||
|
showErrorToast(
|
||||||
|
error instanceof Error ? error.message : t("custom_game_modal_failed")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsAdding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!isAdding) {
|
||||||
|
setGameName("");
|
||||||
|
setExecutablePath("");
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = gameName.trim() && executablePath.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("custom_game_modal")}
|
||||||
|
description={t("custom_game_modal_description")}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<div className="sidebar-adding-custom-game-modal__container">
|
||||||
|
<div className="sidebar-adding-custom-game-modal__form">
|
||||||
|
<TextField
|
||||||
|
label={t("custom_game_modal_executable_path")}
|
||||||
|
placeholder={t("custom_game_modal_select_executable")}
|
||||||
|
value={executablePath}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
rightContent={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleSelectExecutable}
|
||||||
|
disabled={isAdding}
|
||||||
|
>
|
||||||
|
<FileDirectoryIcon />
|
||||||
|
{t("custom_game_modal_browse")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t("custom_game_modal_title")}
|
||||||
|
placeholder={t("custom_game_modal_enter_title")}
|
||||||
|
value={gameName}
|
||||||
|
onChange={handleGameNameChange}
|
||||||
|
theme="dark"
|
||||||
|
disabled={isAdding}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sidebar-adding-custom-game-modal__actions">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isAdding}
|
||||||
|
>
|
||||||
|
{t("custom_game_modal_cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="primary"
|
||||||
|
onClick={handleAddGame}
|
||||||
|
disabled={!isFormValid || isAdding}
|
||||||
|
>
|
||||||
|
{isAdding
|
||||||
|
? t("custom_game_modal_adding")
|
||||||
|
: t("custom_game_modal_add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
import PlayLogo from "@renderer/assets/play-logo.svg?react";
|
||||||
import { LibraryGame } from "@types";
|
import { LibraryGame } from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
@@ -16,6 +17,19 @@ export function SidebarGameItem({
|
|||||||
}: Readonly<SidebarGameItemProps>) {
|
}: Readonly<SidebarGameItemProps>) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isCustomGame = game.shop === "custom";
|
||||||
|
const sidebarIcon = isCustomGame
|
||||||
|
? game.libraryImageUrl || game.iconUrl
|
||||||
|
: game.customIconUrl || game.iconUrl;
|
||||||
|
|
||||||
|
// Determine fallback icon based on game type
|
||||||
|
const getFallbackIcon = () => {
|
||||||
|
if (isCustomGame) {
|
||||||
|
return <PlayLogo className="sidebar__game-icon" />;
|
||||||
|
}
|
||||||
|
return <SteamLogo className="sidebar__game-icon" />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
@@ -30,15 +44,15 @@ export function SidebarGameItem({
|
|||||||
className="sidebar__menu-item-button"
|
className="sidebar__menu-item-button"
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
>
|
>
|
||||||
{game.iconUrl ? (
|
{sidebarIcon ? (
|
||||||
<img
|
<img
|
||||||
className="sidebar__game-icon"
|
className="sidebar__game-icon"
|
||||||
src={game.iconUrl}
|
src={sidebarIcon}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SteamLogo className="sidebar__game-icon" />
|
getFallbackIcon()
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="sidebar__menu-item-button-label">
|
<span className="sidebar__menu-item-button-label">
|
||||||
|
|||||||
@@ -172,4 +172,24 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__add-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
@@ -21,8 +22,13 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
|||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { CommentDiscussionIcon, PlayIcon } from "@primer/octicons-react";
|
import {
|
||||||
|
CommentDiscussionIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
import { SidebarGameItem } from "./sidebar-game-item";
|
import { SidebarGameItem } from "./sidebar-game-item";
|
||||||
|
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
|
||||||
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
@@ -63,11 +69,20 @@ export function Sidebar() {
|
|||||||
const { showWarningToast } = useToast();
|
const { showWarningToast } = useToast();
|
||||||
|
|
||||||
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
const [showPlayableOnly, setShowPlayableOnly] = useState(false);
|
||||||
|
const [showAddGameModal, setShowAddGameModal] = useState(false);
|
||||||
|
|
||||||
const handlePlayButtonClick = () => {
|
const handlePlayButtonClick = () => {
|
||||||
setShowPlayableOnly(!showPlayableOnly);
|
setShowPlayableOnly(!showPlayableOnly);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddGameButtonClick = () => {
|
||||||
|
setShowAddGameModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAddGameModal = () => {
|
||||||
|
setShowAddGameModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.gameId, updateLibrary]);
|
}, [lastPacket?.gameId, updateLibrary]);
|
||||||
@@ -254,15 +269,32 @@ export function Sidebar() {
|
|||||||
<small className="sidebar__section-title">
|
<small className="sidebar__section-title">
|
||||||
{t("my_library")}
|
{t("my_library")}
|
||||||
</small>
|
</small>
|
||||||
<button
|
<div
|
||||||
type="button"
|
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
||||||
className={cn("sidebar__play-button", {
|
|
||||||
"sidebar__play-button--active": showPlayableOnly,
|
|
||||||
})}
|
|
||||||
onClick={handlePlayButtonClick}
|
|
||||||
>
|
>
|
||||||
<PlayIcon size={16} />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
className="sidebar__add-button"
|
||||||
|
onClick={handleAddGameButtonClick}
|
||||||
|
data-tooltip-id="add-custom-game-tooltip"
|
||||||
|
data-tooltip-content={t("add_custom_game_tooltip")}
|
||||||
|
data-tooltip-place="top"
|
||||||
|
>
|
||||||
|
<PlusIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("sidebar__play-button", {
|
||||||
|
"sidebar__play-button--active": showPlayableOnly,
|
||||||
|
})}
|
||||||
|
onClick={handlePlayButtonClick}
|
||||||
|
data-tooltip-id="show-playable-only-tooltip"
|
||||||
|
data-tooltip-content={t("show_playable_only_tooltip")}
|
||||||
|
data-tooltip-place="top"
|
||||||
|
>
|
||||||
|
<PlayIcon size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -307,6 +339,14 @@ export function Sidebar() {
|
|||||||
className="sidebar__handle"
|
className="sidebar__handle"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SidebarAddingCustomGameModal
|
||||||
|
visible={showAddGameModal}
|
||||||
|
onClose={handleCloseAddGameModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip id="add-custom-game-tooltip" />
|
||||||
|
<Tooltip id="show-playable-only-tooltip" />
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,12 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||||||
isGameRunning: false,
|
isGameRunning: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
objectId: undefined,
|
objectId: undefined,
|
||||||
gameColor: "",
|
|
||||||
showRepacksModal: false,
|
showRepacksModal: false,
|
||||||
showGameOptionsModal: false,
|
showGameOptionsModal: false,
|
||||||
stats: null,
|
stats: null,
|
||||||
achievements: null,
|
achievements: null,
|
||||||
hasNSFWContentBlocked: false,
|
hasNSFWContentBlocked: false,
|
||||||
lastDownloadedOption: null,
|
lastDownloadedOption: null,
|
||||||
setGameColor: () => {},
|
|
||||||
selectGameExecutable: async () => null,
|
selectGameExecutable: async () => null,
|
||||||
updateGame: async () => {},
|
updateGame: async () => {},
|
||||||
setShowGameOptionsModal: () => {},
|
setShowGameOptionsModal: () => {},
|
||||||
@@ -82,7 +80,6 @@ export function GameDetailsContextProvider({
|
|||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [gameColor, setGameColor] = useState("");
|
|
||||||
const [isGameRunning, setIsGameRunning] = useState(false);
|
const [isGameRunning, setIsGameRunning] = useState(false);
|
||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||||
@@ -201,6 +198,12 @@ export function GameDetailsContextProvider({
|
|||||||
dispatch(setHeaderTitle(gameTitle));
|
dispatch(setHeaderTitle(gameTitle));
|
||||||
}, [objectId, gameTitle, dispatch]);
|
}, [objectId, gameTitle, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (game?.title) {
|
||||||
|
dispatch(setHeaderTitle(game.title));
|
||||||
|
}
|
||||||
|
}, [game?.title, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
|
||||||
const updatedIsGameRunning =
|
const updatedIsGameRunning =
|
||||||
@@ -286,7 +289,6 @@ export function GameDetailsContextProvider({
|
|||||||
isGameRunning,
|
isGameRunning,
|
||||||
isLoading,
|
isLoading,
|
||||||
objectId,
|
objectId,
|
||||||
gameColor,
|
|
||||||
showGameOptionsModal,
|
showGameOptionsModal,
|
||||||
showRepacksModal,
|
showRepacksModal,
|
||||||
stats,
|
stats,
|
||||||
@@ -294,7 +296,6 @@ export function GameDetailsContextProvider({
|
|||||||
hasNSFWContentBlocked,
|
hasNSFWContentBlocked,
|
||||||
lastDownloadedOption,
|
lastDownloadedOption,
|
||||||
setHasNSFWContentBlocked,
|
setHasNSFWContentBlocked,
|
||||||
setGameColor,
|
|
||||||
selectGameExecutable,
|
selectGameExecutable,
|
||||||
updateGame,
|
updateGame,
|
||||||
setShowRepacksModal,
|
setShowRepacksModal,
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ export interface GameDetailsContext {
|
|||||||
isGameRunning: boolean;
|
isGameRunning: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
objectId: string | undefined;
|
objectId: string | undefined;
|
||||||
gameColor: string;
|
|
||||||
showRepacksModal: boolean;
|
showRepacksModal: boolean;
|
||||||
showGameOptionsModal: boolean;
|
showGameOptionsModal: boolean;
|
||||||
stats: GameStats | null;
|
stats: GameStats | null;
|
||||||
achievements: UserAchievement[] | null;
|
achievements: UserAchievement[] | null;
|
||||||
hasNSFWContentBlocked: boolean;
|
hasNSFWContentBlocked: boolean;
|
||||||
lastDownloadedOption: GameRepack | null;
|
lastDownloadedOption: GameRepack | null;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
updateGame: () => Promise<void>;
|
updateGame: () => Promise<void>;
|
||||||
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { darkenColor } from "@renderer/helpers";
|
import { darkenColor } from "@renderer/helpers";
|
||||||
import { useAppSelector, useToast } from "@renderer/hooks";
|
import { useAppSelector, useToast } from "@renderer/hooks";
|
||||||
import type { Badge, UserProfile, UserStats } from "@types";
|
import type { Badge, UserProfile, UserStats, UserGame } from "@types";
|
||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
|
|
||||||
import { createContext, useCallback, useEffect, useState } from "react";
|
import { createContext, useCallback, useEffect, useState } from "react";
|
||||||
@@ -14,9 +14,12 @@ export interface UserProfileContext {
|
|||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
userStats: UserStats | null;
|
userStats: UserStats | null;
|
||||||
getUserProfile: () => Promise<void>;
|
getUserProfile: () => Promise<void>;
|
||||||
|
getUserLibraryGames: (sortBy?: string) => Promise<void>;
|
||||||
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
badges: Badge[];
|
badges: Badge[];
|
||||||
|
libraryGames: UserGame[];
|
||||||
|
pinnedGames: UserGame[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
|
||||||
@@ -27,9 +30,12 @@ export const userProfileContext = createContext<UserProfileContext>({
|
|||||||
isMe: false,
|
isMe: false,
|
||||||
userStats: null,
|
userStats: null,
|
||||||
getUserProfile: async () => {},
|
getUserProfile: async () => {},
|
||||||
|
getUserLibraryGames: async (_sortBy?: string) => {},
|
||||||
setSelectedBackgroundImage: () => {},
|
setSelectedBackgroundImage: () => {},
|
||||||
backgroundImage: "",
|
backgroundImage: "",
|
||||||
badges: [],
|
badges: [],
|
||||||
|
libraryGames: [],
|
||||||
|
pinnedGames: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Provider } = userProfileContext;
|
const { Provider } = userProfileContext;
|
||||||
@@ -49,6 +55,8 @@ export function UserProfileContextProvider({
|
|||||||
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
const [userStats, setUserStats] = useState<UserStats | null>(null);
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
const [libraryGames, setLibraryGames] = useState<UserGame[]>([]);
|
||||||
|
const [pinnedGames, setPinnedGames] = useState<UserGame[]>([]);
|
||||||
const [badges, setBadges] = useState<Badge[]>([]);
|
const [badges, setBadges] = useState<Badge[]>([]);
|
||||||
const [heroBackground, setHeroBackground] = useState(
|
const [heroBackground, setHeroBackground] = useState(
|
||||||
DEFAULT_USER_PROFILE_BACKGROUND
|
DEFAULT_USER_PROFILE_BACKGROUND
|
||||||
@@ -85,8 +93,34 @@ export function UserProfileContextProvider({
|
|||||||
});
|
});
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
|
const getUserLibraryGames = useCallback(
|
||||||
|
async (sortBy?: string) => {
|
||||||
|
try {
|
||||||
|
const response = await window.electron.getUserLibrary(
|
||||||
|
userId,
|
||||||
|
12,
|
||||||
|
0,
|
||||||
|
sortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
setLibraryGames(response.library);
|
||||||
|
setPinnedGames(response.pinnedGames);
|
||||||
|
} else {
|
||||||
|
setLibraryGames([]);
|
||||||
|
setPinnedGames([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setLibraryGames([]);
|
||||||
|
setPinnedGames([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
const getUserProfile = useCallback(async () => {
|
const getUserProfile = useCallback(async () => {
|
||||||
getUserStats();
|
getUserStats();
|
||||||
|
getUserLibraryGames();
|
||||||
|
|
||||||
return window.electron.getUser(userId).then((userProfile) => {
|
return window.electron.getUser(userId).then((userProfile) => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
@@ -102,7 +136,7 @@ export function UserProfileContextProvider({
|
|||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [navigate, getUserStats, showErrorToast, userId, t]);
|
}, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t]);
|
||||||
|
|
||||||
const getBadges = useCallback(async () => {
|
const getBadges = useCallback(async () => {
|
||||||
const badges = await window.electron.getBadges();
|
const badges = await window.electron.getBadges();
|
||||||
@@ -111,6 +145,8 @@ export function UserProfileContextProvider({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUserProfile(null);
|
setUserProfile(null);
|
||||||
|
setLibraryGames([]);
|
||||||
|
setPinnedGames([]);
|
||||||
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
|
||||||
|
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
@@ -124,10 +160,13 @@ export function UserProfileContextProvider({
|
|||||||
heroBackground,
|
heroBackground,
|
||||||
isMe,
|
isMe,
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
|
getUserLibraryGames,
|
||||||
setSelectedBackgroundImage,
|
setSelectedBackgroundImage,
|
||||||
backgroundImage: getBackgroundImageUrl(),
|
backgroundImage: getBackgroundImageUrl(),
|
||||||
userStats,
|
userStats,
|
||||||
badges,
|
badges,
|
||||||
|
libraryGames,
|
||||||
|
pinnedGames,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
51
src/renderer/src/declaration.d.ts
vendored
51
src/renderer/src/declaration.d.ts
vendored
@@ -37,6 +37,7 @@ import type {
|
|||||||
ShopDetailsWithAssets,
|
ShopDetailsWithAssets,
|
||||||
AchievementCustomNotificationPosition,
|
AchievementCustomNotificationPosition,
|
||||||
AchievementNotificationInfo,
|
AchievementNotificationInfo,
|
||||||
|
UserLibraryResponse,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
@@ -111,6 +112,43 @@ declare global {
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
title: string
|
title: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
addCustomGameToLibrary: (
|
||||||
|
title: string,
|
||||||
|
executablePath: string,
|
||||||
|
iconUrl?: string,
|
||||||
|
logoImageUrl?: string,
|
||||||
|
libraryHeroImageUrl?: string
|
||||||
|
) => Promise<Game>;
|
||||||
|
updateCustomGame: (params: {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
iconUrl?: string;
|
||||||
|
logoImageUrl?: string;
|
||||||
|
libraryHeroImageUrl?: string;
|
||||||
|
originalIconPath?: string;
|
||||||
|
originalLogoPath?: string;
|
||||||
|
originalHeroPath?: string;
|
||||||
|
}) => Promise<Game>;
|
||||||
|
copyCustomGameAsset: (
|
||||||
|
sourcePath: string,
|
||||||
|
assetType: "icon" | "logo" | "hero"
|
||||||
|
) => Promise<string>;
|
||||||
|
cleanupUnusedAssets: () => Promise<{
|
||||||
|
deletedCount: number;
|
||||||
|
errors: string[];
|
||||||
|
}>;
|
||||||
|
updateGameCustomAssets: (params: {
|
||||||
|
shop: GameShop;
|
||||||
|
objectId: string;
|
||||||
|
title: string;
|
||||||
|
customIconUrl?: string | null;
|
||||||
|
customLogoImageUrl?: string | null;
|
||||||
|
customHeroImageUrl?: string | null;
|
||||||
|
customOriginalIconPath?: string | null;
|
||||||
|
customOriginalLogoPath?: string | null;
|
||||||
|
customOriginalHeroPath?: string | null;
|
||||||
|
}) => Promise<Game>;
|
||||||
createGameShortcut: (
|
createGameShortcut: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -126,6 +164,11 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
toggleGamePin: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
pinned: boolean
|
||||||
|
) => Promise<void>;
|
||||||
updateLaunchOptions: (
|
updateLaunchOptions: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -267,6 +310,8 @@ declare global {
|
|||||||
onCommonRedistProgress: (
|
onCommonRedistProgress: (
|
||||||
cb: (value: { log: string; complete: boolean }) => void
|
cb: (value: { log: string; complete: boolean }) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
saveTempFile: (fileName: string, fileData: Uint8Array) => Promise<string>;
|
||||||
|
deleteTempFile: (filePath: string) => Promise<void>;
|
||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
|
|
||||||
/* Auto update */
|
/* Auto update */
|
||||||
@@ -287,6 +332,12 @@ declare global {
|
|||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => Promise<UserProfile | null>;
|
getUser: (userId: string) => Promise<UserProfile | null>;
|
||||||
|
getUserLibrary: (
|
||||||
|
userId: string,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
sortBy?: string
|
||||||
|
) => Promise<UserLibraryResponse>;
|
||||||
blockUser: (userId: string) => Promise<void>;
|
blockUser: (userId: string) => Promise<void>;
|
||||||
unblockUser: (userId: string) => Promise<void>;
|
unblockUser: (userId: string) => Promise<void>;
|
||||||
getUserFriends: (
|
getUserFriends: (
|
||||||
|
|||||||
@@ -84,3 +84,23 @@ export const injectCustomCss = (
|
|||||||
export const removeCustomCss = (target: HTMLElement = document.head) => {
|
export const removeCustomCss = (target: HTMLElement = document.head) => {
|
||||||
target.querySelector("#custom-css")?.remove();
|
target.querySelector("#custom-css")?.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateRandomGradient = (): string => {
|
||||||
|
// Use a single consistent gradient with softer colors for custom games as placeholder
|
||||||
|
const color1 = "#2c3e50"; // Dark blue-gray
|
||||||
|
const color2 = "#34495e"; // Darker slate
|
||||||
|
|
||||||
|
// Create SVG data URL that works in img tags
|
||||||
|
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#grad)" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
// Return as data URL that works in img tags
|
||||||
|
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
|
||||||
|
};
|
||||||
|
|||||||
27
src/renderer/src/hooks/use-section-collapse.ts
Normal file
27
src/renderer/src/hooks/use-section-collapse.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
interface SectionCollapseState {
|
||||||
|
pinned: boolean;
|
||||||
|
library: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSectionCollapse() {
|
||||||
|
const [collapseState, setCollapseState] = useState<SectionCollapseState>({
|
||||||
|
pinned: false,
|
||||||
|
library: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSection = useCallback((section: keyof SectionCollapseState) => {
|
||||||
|
setCollapseState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[section]: !prevState[section],
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapseState,
|
||||||
|
toggleSection,
|
||||||
|
isPinnedCollapsed: collapseState.pinned,
|
||||||
|
isLibraryCollapsed: collapseState.library,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import type { ComparedAchievements } from "@types";
|
import type { ComparedAchievements } from "@types";
|
||||||
import { average } from "color.js";
|
|
||||||
import Color from "color";
|
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||||
import { AchievementList } from "./achievement-list";
|
import { AchievementList } from "./achievement-list";
|
||||||
@@ -119,15 +117,8 @@ export function AchievementsContent({
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||||
|
|
||||||
const {
|
const { gameTitle, objectId, shop, shopDetails, achievements } =
|
||||||
gameTitle,
|
useContext(gameDetailsContext);
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
shopDetails,
|
|
||||||
achievements,
|
|
||||||
gameColor,
|
|
||||||
setGameColor,
|
|
||||||
} = useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -136,22 +127,6 @@ export function AchievementsContent({
|
|||||||
dispatch(setHeaderTitle(gameTitle));
|
dispatch(setHeaderTitle(gameTitle));
|
||||||
}, [dispatch, gameTitle]);
|
}, [dispatch, gameTitle]);
|
||||||
|
|
||||||
const handleHeroLoad = async () => {
|
|
||||||
const output = await average(
|
|
||||||
shopDetails?.assets?.libraryHeroImageUrl ?? "",
|
|
||||||
{
|
|
||||||
amount: 1,
|
|
||||||
format: "hex",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor = output
|
|
||||||
? (new Color(output).darken(0.7).toString() as string)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
setGameColor(backgroundColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||||
const heroHeight = heroRef.current?.clientHeight ?? 150;
|
const heroHeight = heroRef.current?.clientHeight ?? 150;
|
||||||
|
|
||||||
@@ -191,7 +166,6 @@ export function AchievementsContent({
|
|||||||
src={shopDetails?.assets?.libraryHeroImageUrl ?? ""}
|
src={shopDetails?.assets?.libraryHeroImageUrl ?? ""}
|
||||||
className="achievements-content__achievements-list__image"
|
className="achievements-content__achievements-list__image"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
onLoad={handleHeroLoad}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -199,12 +173,7 @@ export function AchievementsContent({
|
|||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className="achievements-content__achievements-list__section"
|
className="achievements-content__achievements-list__section"
|
||||||
>
|
>
|
||||||
<div
|
<div className="achievements-content__achievements-list__section__container">
|
||||||
className="achievements-content__achievements-list__section__container"
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(0deg, #151515 0%, ${gameColor} 100%)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={heroRef}
|
ref={heroRef}
|
||||||
className="achievements-content__achievements-list__section__container__hero"
|
className="achievements-content__achievements-list__section__container__hero"
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ export function DeleteGameModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="delete-game-modal__actions">
|
<div className="delete-game-modal__actions">
|
||||||
<Button onClick={handleDeleteGame} theme="outline">
|
<Button onClick={onClose} theme="outline">
|
||||||
{t("delete")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onClose} theme="primary">
|
<Button onClick={handleDeleteGame} theme="primary">
|
||||||
{t("cancel")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
@use "../../../scss/globals.scss";
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
.description-header {
|
.description-header {
|
||||||
width: 100%;
|
width: calc(100% - calc(globals.$spacing-unit * 2));
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
margin: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 1);
|
||||||
|
padding: calc(globals.$spacing-unit * 1.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: globals.$background-color;
|
background-color: globals.$background-color;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,34 +2,64 @@
|
|||||||
|
|
||||||
.gallery-slider {
|
.gallery-slider {
|
||||||
&__container {
|
&__container {
|
||||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
max-height: 80vh;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__viewport {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
width: 80%;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
width: 60%;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
width: 50%;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container-inner {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__media {
|
&__media {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
transition: translate 0.3s ease-in-out;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
align-self: center;
|
object-fit: cover;
|
||||||
}
|
|
||||||
|
|
||||||
&__animation-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__preview {
|
&__preview {
|
||||||
@@ -42,10 +72,18 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
gap: calc(globals.$spacing-unit / 2);
|
gap: calc(globals.$spacing-unit / 2);
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
@@ -68,6 +106,20 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px globals.$border-color;
|
border: solid 1px globals.$border-color;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
@@ -83,49 +135,73 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__play-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.gallery-slider__preview-button:hover & {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
align-self: center;
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.95);
|
transform: translateY(-50%) scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--left {
|
&--left {
|
||||||
left: 0;
|
left: globals.$spacing-unit;
|
||||||
margin-left: globals.$spacing-unit;
|
transform: translateY(-50%) translateX(-100px);
|
||||||
transform: translateX(calc(-1 * (48px + globals.$spacing-unit)));
|
opacity: 0;
|
||||||
|
|
||||||
&.gallery-slider__button--visible {
|
.gallery-slider__viewport:hover & {
|
||||||
transform: translateX(0);
|
transform: translateY(-50%) translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--right {
|
&--right {
|
||||||
right: 0;
|
right: globals.$spacing-unit;
|
||||||
margin-right: globals.$spacing-unit;
|
transform: translateY(-50%) translateX(100px);
|
||||||
transform: translateX(calc(48px + globals.$spacing-unit));
|
opacity: 0;
|
||||||
|
|
||||||
&.gallery-slider__button--visible {
|
.gallery-slider__viewport:hover & {
|
||||||
transform: translateX(0);
|
transform: translateY(-50%) translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,144 @@
|
|||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { useContext, useCallback, useMemo, useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
PlayIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import "./gallery-slider.scss";
|
import "./gallery-slider.scss";
|
||||||
|
|
||||||
export function GallerySlider() {
|
export function GallerySlider() {
|
||||||
const { shopDetails } = useContext(gameDetailsContext);
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const hasScreenshots = shopDetails && shopDetails.screenshots?.length;
|
const hasScreenshots = shopDetails && shopDetails.screenshots?.length;
|
||||||
const hasMovies = shopDetails && shopDetails.movies?.length;
|
|
||||||
|
|
||||||
const mediaCount = useMemo(() => {
|
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false });
|
||||||
if (!shopDetails) return 0;
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
if (shopDetails.screenshots && shopDetails.movies) {
|
const scrollPrev = useCallback(() => {
|
||||||
return shopDetails.screenshots.length + shopDetails.movies.length;
|
if (emblaApi) emblaApi.scrollPrev();
|
||||||
} else if (shopDetails.movies) {
|
}, [emblaApi]);
|
||||||
return shopDetails.movies.length;
|
|
||||||
} else if (shopDetails.screenshots) {
|
|
||||||
return shopDetails.screenshots.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
const scrollNext = useCallback(() => {
|
||||||
}, [shopDetails]);
|
if (emblaApi) emblaApi.scrollNext();
|
||||||
|
}, [emblaApi]);
|
||||||
|
|
||||||
const [mediaIndex, setMediaIndex] = useState(0);
|
const scrollTo = useCallback(
|
||||||
const [showArrows, setShowArrows] = useState(false);
|
(index: number) => {
|
||||||
|
if (emblaApi) emblaApi.scrollTo(index);
|
||||||
|
},
|
||||||
|
[emblaApi]
|
||||||
|
);
|
||||||
|
|
||||||
const showNextImage = () => {
|
const scrollToPreview = useCallback(
|
||||||
setMediaIndex((index: number) => {
|
(index: number, event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (index === mediaCount - 1) return 0;
|
scrollTo(index);
|
||||||
|
|
||||||
return index + 1;
|
const button = event.currentTarget;
|
||||||
});
|
const previewContainer = button.parentElement;
|
||||||
};
|
|
||||||
|
|
||||||
const showPrevImage = () => {
|
if (previewContainer) {
|
||||||
setMediaIndex((index: number) => {
|
const containerRect = previewContainer.getBoundingClientRect();
|
||||||
if (index === 0) return mediaCount - 1;
|
const buttonRect = button.getBoundingClientRect();
|
||||||
|
|
||||||
return index - 1;
|
const isOffScreenLeft = buttonRect.left < containerRect.left;
|
||||||
});
|
const isOffScreenRight = buttonRect.right > containerRect.right;
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (isOffScreenLeft || isOffScreenRight) {
|
||||||
setMediaIndex(0);
|
button.scrollIntoView({
|
||||||
}, [shopDetails]);
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
useEffect(() => {
|
inline: "center",
|
||||||
if (hasMovies && mediaContainerRef.current) {
|
});
|
||||||
mediaContainerRef.current.childNodes.forEach((node, index) => {
|
|
||||||
if (node instanceof HTMLVideoElement) {
|
|
||||||
if (index !== mediaIndex) {
|
|
||||||
node.pause();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollTo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emblaApi) return;
|
||||||
|
|
||||||
|
let isInitialLoad = true;
|
||||||
|
|
||||||
|
const onSelect = () => {
|
||||||
|
const newIndex = emblaApi.selectedScrollSnap();
|
||||||
|
setSelectedIndex(newIndex);
|
||||||
|
|
||||||
|
if (!isInitialLoad) {
|
||||||
|
const videos = document.querySelectorAll(".gallery-slider__media");
|
||||||
|
videos.forEach((video) => {
|
||||||
|
if (video instanceof HTMLVideoElement) {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialLoad = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
emblaApi.on("select", onSelect);
|
||||||
|
onSelect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
emblaApi.off("select", onSelect);
|
||||||
|
};
|
||||||
|
}, [emblaApi]);
|
||||||
|
|
||||||
|
const mediaItems = useMemo(() => {
|
||||||
|
const items: Array<{
|
||||||
|
id: string;
|
||||||
|
type: "video" | "image";
|
||||||
|
src?: string;
|
||||||
|
poster?: string;
|
||||||
|
videoSrc?: string;
|
||||||
|
alt: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (shopDetails?.movies) {
|
||||||
|
shopDetails.movies.forEach((video, index) => {
|
||||||
|
items.push({
|
||||||
|
id: String(video.id),
|
||||||
|
type: "video",
|
||||||
|
poster: video.thumbnail,
|
||||||
|
videoSrc: video.mp4.max.startsWith("http://")
|
||||||
|
? video.mp4.max.replace("http://", "https://")
|
||||||
|
: video.mp4.max,
|
||||||
|
alt: t("video", { number: String(index + 1) }),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [hasMovies, mediaContainerRef, mediaIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (shopDetails?.screenshots) {
|
||||||
if (scrollContainerRef.current) {
|
shopDetails.screenshots.forEach((image, index) => {
|
||||||
const container = scrollContainerRef.current;
|
items.push({
|
||||||
const totalWidth = container.scrollWidth - container.clientWidth;
|
id: String(image.id),
|
||||||
const itemWidth = totalWidth / (mediaCount - 1);
|
type: "image",
|
||||||
const scrollLeft = mediaIndex * itemWidth;
|
src: image.path_full,
|
||||||
container.scrollLeft = scrollLeft;
|
alt: t("screenshot", { number: String(index + 1) }),
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [shopDetails, mediaIndex, mediaCount]);
|
|
||||||
|
return items;
|
||||||
|
}, [shopDetails, t]);
|
||||||
|
|
||||||
const previews = useMemo(() => {
|
const previews = useMemo(() => {
|
||||||
const screenshotPreviews =
|
const screenshotPreviews =
|
||||||
shopDetails?.screenshots?.map(({ id, path_thumbnail }) => ({
|
shopDetails?.screenshots?.map(({ id, path_thumbnail }) => ({
|
||||||
id,
|
id,
|
||||||
thumbnail: path_thumbnail,
|
thumbnail: path_thumbnail,
|
||||||
|
type: "image" as const,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
if (shopDetails?.movies) {
|
if (shopDetails?.movies) {
|
||||||
const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({
|
const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({
|
||||||
id,
|
id,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
type: "video" as const,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...moviePreviews, ...screenshotPreviews];
|
return [...moviePreviews, ...screenshotPreviews];
|
||||||
@@ -93,96 +147,87 @@ export function GallerySlider() {
|
|||||||
return screenshotPreviews;
|
return screenshotPreviews;
|
||||||
}, [shopDetails]);
|
}, [shopDetails]);
|
||||||
|
|
||||||
|
if (!hasScreenshots) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="gallery-slider__container">
|
||||||
{hasScreenshots && (
|
<div className="gallery-slider__viewport" ref={emblaRef}>
|
||||||
<div className="gallery-slider__container">
|
<div className="gallery-slider__container-inner">
|
||||||
<div
|
{mediaItems.map((item) => (
|
||||||
onMouseEnter={() => setShowArrows(true)}
|
<div key={item.id} className="gallery-slider__slide">
|
||||||
onMouseLeave={() => setShowArrows(false)}
|
{item.type === "video" ? (
|
||||||
className="gallery-slider__animation-container"
|
|
||||||
ref={mediaContainerRef}
|
|
||||||
>
|
|
||||||
{shopDetails.movies &&
|
|
||||||
shopDetails.movies.map((video) => (
|
|
||||||
<video
|
<video
|
||||||
key={video.id}
|
|
||||||
controls
|
controls
|
||||||
className="gallery-slider__media"
|
className="gallery-slider__media"
|
||||||
poster={video.thumbnail}
|
poster={item.poster}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
|
autoPlay
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<source src={video.mp4.max.replace("http", "https")} />
|
<source src={item.videoSrc} />
|
||||||
</video>
|
</video>
|
||||||
))}
|
) : (
|
||||||
|
|
||||||
{hasScreenshots &&
|
|
||||||
shopDetails.screenshots?.map((image, i) => (
|
|
||||||
<img
|
<img
|
||||||
key={image.id}
|
|
||||||
className="gallery-slider__media"
|
className="gallery-slider__media"
|
||||||
src={image.path_full}
|
src={item.src}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
alt={item.alt}
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
|
</div>
|
||||||
<button
|
))}
|
||||||
onClick={showPrevImage}
|
|
||||||
type="button"
|
|
||||||
className={`gallery-slider__button gallery-slider__button--left ${
|
|
||||||
showArrows
|
|
||||||
? "gallery-slider__button--visible"
|
|
||||||
: "gallery-slider__button--hidden"
|
|
||||||
}`}
|
|
||||||
aria-label={t("previous_screenshot")}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon size={36} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={showNextImage}
|
|
||||||
type="button"
|
|
||||||
className={`gallery-slider__button gallery-slider__button--right ${
|
|
||||||
showArrows
|
|
||||||
? "gallery-slider__button--visible"
|
|
||||||
: "gallery-slider__button--hidden"
|
|
||||||
}`}
|
|
||||||
aria-label={t("next_screenshot")}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon size={36} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="gallery-slider__preview" ref={scrollContainerRef}>
|
|
||||||
{previews.map((media, i) => (
|
|
||||||
<button
|
|
||||||
key={media.id}
|
|
||||||
type="button"
|
|
||||||
className={`gallery-slider__preview-button ${
|
|
||||||
mediaIndex === i
|
|
||||||
? "gallery-slider__preview-button--active"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setMediaIndex(i)}
|
|
||||||
aria-label={t("open_screenshot", { number: i + 1 })}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={media.thumbnail}
|
|
||||||
className="gallery-slider__preview-image"
|
|
||||||
alt={t("screenshot", { number: i + 1 })}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
<button
|
||||||
|
onClick={scrollPrev}
|
||||||
|
type="button"
|
||||||
|
className="gallery-slider__button gallery-slider__button--left"
|
||||||
|
aria-label={t("previous_screenshot")}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon size={36} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={scrollNext}
|
||||||
|
type="button"
|
||||||
|
className="gallery-slider__button gallery-slider__button--right"
|
||||||
|
aria-label={t("next_screenshot")}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon size={36} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gallery-slider__preview">
|
||||||
|
{previews.map((media, i) => (
|
||||||
|
<button
|
||||||
|
key={media.id}
|
||||||
|
type="button"
|
||||||
|
className={`gallery-slider__preview-button ${
|
||||||
|
selectedIndex === i
|
||||||
|
? "gallery-slider__preview-button--active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={(e) => scrollToPreview(i, e)}
|
||||||
|
aria-label={t("open_screenshot", { number: String(i + 1) })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={media.thumbnail}
|
||||||
|
className="gallery-slider__preview-image"
|
||||||
|
alt={t("screenshot", { number: String(i + 1) })}
|
||||||
|
/>
|
||||||
|
{media.type === "video" && (
|
||||||
|
<div className="gallery-slider__play-overlay">
|
||||||
|
<PlayIcon size={20} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { average } from "color.js";
|
import { PencilIcon } from "@primer/octicons-react";
|
||||||
import Color from "color";
|
|
||||||
|
|
||||||
import { HeroPanel } from "./hero";
|
import { HeroPanel } from "./hero";
|
||||||
import { DescriptionHeader } from "./description-header/description-header";
|
import { DescriptionHeader } from "./description-header/description-header";
|
||||||
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
import { GallerySlider } from "./gallery-slider/gallery-slider";
|
||||||
import { Sidebar } from "./sidebar/sidebar";
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
import { EditGameModal } from "./modals";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails, useLibrary } from "@renderer/hooks";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import "./game-details.scss";
|
import "./game-details.scss";
|
||||||
|
|
||||||
@@ -21,18 +21,13 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const {
|
const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame } =
|
||||||
objectId,
|
useContext(gameDetailsContext);
|
||||||
shopDetails,
|
|
||||||
game,
|
|
||||||
gameColor,
|
|
||||||
setGameColor,
|
|
||||||
hasNSFWContentBlocked,
|
|
||||||
} = useContext(gameDetailsContext);
|
|
||||||
|
|
||||||
const { showHydraCloudModal } = useSubscription();
|
const { showHydraCloudModal } = useSubscription();
|
||||||
|
|
||||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||||
|
const { updateLibrary } = useLibrary();
|
||||||
|
|
||||||
const { setShowCloudSyncModal, getGameArtifacts } =
|
const { setShowCloudSyncModal, getGameArtifacts } =
|
||||||
useContext(cloudSyncContext);
|
useContext(cloudSyncContext);
|
||||||
@@ -48,31 +43,43 @@ export function GameDetailsContent() {
|
|||||||
const $images = Array.from(document.querySelectorAll("img"));
|
const $images = Array.from(document.querySelectorAll("img"));
|
||||||
$images.forEach(($image) => {
|
$images.forEach(($image) => {
|
||||||
$image.loading = "lazy";
|
$image.loading = "lazy";
|
||||||
|
// Remove any inline width/height styles that might cause overflow
|
||||||
|
$image.removeAttribute("width");
|
||||||
|
$image.removeAttribute("height");
|
||||||
|
$image.removeAttribute("style");
|
||||||
|
// Set max-width to prevent overflow
|
||||||
|
$image.style.maxWidth = "100%";
|
||||||
|
$image.style.width = "auto";
|
||||||
|
$image.style.height = "auto";
|
||||||
|
$image.style.boxSizing = "border-box";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle videos the same way
|
||||||
|
const $videos = Array.from(document.querySelectorAll("video"));
|
||||||
|
$videos.forEach(($video) => {
|
||||||
|
// Remove any inline width/height styles that might cause overflow
|
||||||
|
$video.removeAttribute("width");
|
||||||
|
$video.removeAttribute("height");
|
||||||
|
$video.removeAttribute("style");
|
||||||
|
// Set max-width to prevent overflow
|
||||||
|
$video.style.maxWidth = "100%";
|
||||||
|
$video.style.width = "auto";
|
||||||
|
$video.style.height = "auto";
|
||||||
|
$video.style.boxSizing = "border-box";
|
||||||
});
|
});
|
||||||
|
|
||||||
return document.body.outerHTML;
|
return document.body.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (game?.shop === "custom") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return t("no_shop_details");
|
return t("no_shop_details");
|
||||||
}, [shopDetails, t]);
|
}, [shopDetails, t, game?.shop]);
|
||||||
|
|
||||||
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
||||||
|
const [showEditGameModal, setShowEditGameModal] = useState(false);
|
||||||
const handleHeroLoad = async () => {
|
|
||||||
const output = await average(
|
|
||||||
shopDetails?.assets?.libraryHeroImageUrl ?? "",
|
|
||||||
{
|
|
||||||
amount: 1,
|
|
||||||
format: "hex",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor = output
|
|
||||||
? new Color(output).darken(0.7).toString()
|
|
||||||
: "";
|
|
||||||
|
|
||||||
setGameColor(backgroundColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBackdropOpacity(1);
|
setBackdropOpacity(1);
|
||||||
@@ -92,10 +99,72 @@ export function GameDetailsContent() {
|
|||||||
setShowCloudSyncModal(true);
|
setShowCloudSyncModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditGameClick = () => {
|
||||||
|
setShowEditGameModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGameUpdated = (_updatedGame: any) => {
|
||||||
|
updateGame();
|
||||||
|
updateLibrary();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getGameArtifacts();
|
getGameArtifacts();
|
||||||
}, [getGameArtifacts]);
|
}, [getGameArtifacts]);
|
||||||
|
|
||||||
|
const isCustomGame = game?.shop === "custom";
|
||||||
|
|
||||||
|
// Helper function to get image with custom asset priority
|
||||||
|
const getImageWithCustomPriority = (
|
||||||
|
customUrl: string | null | undefined,
|
||||||
|
originalUrl: string | null | undefined,
|
||||||
|
fallbackUrl?: string | null | undefined
|
||||||
|
) => {
|
||||||
|
return customUrl || originalUrl || fallbackUrl || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const heroImage = isCustomGame
|
||||||
|
? game?.libraryHeroImageUrl || game?.iconUrl || ""
|
||||||
|
: getImageWithCustomPriority(
|
||||||
|
game?.customHeroImageUrl,
|
||||||
|
shopDetails?.assets?.libraryHeroImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const logoImage = isCustomGame
|
||||||
|
? game?.logoImageUrl || ""
|
||||||
|
: getImageWithCustomPriority(
|
||||||
|
game?.customLogoImageUrl,
|
||||||
|
shopDetails?.assets?.logoImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGameLogo = () => {
|
||||||
|
if (isCustomGame) {
|
||||||
|
// For custom games, show logo image if available, otherwise show game title as text
|
||||||
|
if (logoImage) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={logoImage}
|
||||||
|
className="game-details__game-logo"
|
||||||
|
alt={game?.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="game-details__game-logo-text">{game?.title}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-custom games, show logo image if available
|
||||||
|
return logoImage ? (
|
||||||
|
<img
|
||||||
|
src={logoImage}
|
||||||
|
className="game-details__game-logo"
|
||||||
|
alt={game?.title}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
|
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
|
||||||
@@ -103,15 +172,13 @@ export function GameDetailsContent() {
|
|||||||
<section className="game-details__container">
|
<section className="game-details__container">
|
||||||
<div ref={heroRef} className="game-details__hero">
|
<div ref={heroRef} className="game-details__hero">
|
||||||
<img
|
<img
|
||||||
src={shopDetails?.assets?.libraryHeroImageUrl ?? ""}
|
src={heroImage}
|
||||||
className="game-details__hero-image"
|
className="game-details__hero-image"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onLoad={handleHeroLoad}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="game-details__hero-backdrop"
|
className="game-details__hero-backdrop"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: gameColor,
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -121,26 +188,35 @@ export function GameDetailsContent() {
|
|||||||
style={{ opacity: backdropOpacity }}
|
style={{ opacity: backdropOpacity }}
|
||||||
>
|
>
|
||||||
<div className="game-details__hero-content">
|
<div className="game-details__hero-content">
|
||||||
<img
|
{renderGameLogo()}
|
||||||
src={shopDetails?.assets?.logoImageUrl ?? ""}
|
|
||||||
className="game-details__game-logo"
|
|
||||||
alt={game?.title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<div className="game-details__hero-buttons game-details__hero-buttons--right">
|
||||||
type="button"
|
<button
|
||||||
className="game-details__cloud-sync-button"
|
type="button"
|
||||||
onClick={handleCloudSaveButtonClick}
|
className="game-details__edit-custom-game-button"
|
||||||
>
|
onClick={handleEditGameClick}
|
||||||
<div className="game-details__cloud-icon-container">
|
title={t("edit_game_modal_button")}
|
||||||
<img
|
>
|
||||||
src={cloudIconAnimated}
|
<PencilIcon size={16} />
|
||||||
alt="Cloud icon"
|
</button>
|
||||||
className="game-details__cloud-icon"
|
|
||||||
/>
|
{game?.shop !== "custom" && (
|
||||||
</div>
|
<button
|
||||||
{t("cloud_save")}
|
type="button"
|
||||||
</button>
|
className="game-details__cloud-sync-button"
|
||||||
|
onClick={handleCloudSaveButtonClick}
|
||||||
|
>
|
||||||
|
<div className="game-details__cloud-icon-container">
|
||||||
|
<img
|
||||||
|
src={cloudIconAnimated}
|
||||||
|
alt="Cloud icon"
|
||||||
|
className="game-details__cloud-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{t("cloud_save")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,9 +236,17 @@ export function GameDetailsContent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sidebar />
|
{game?.shop !== "custom" && <Sidebar />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<EditGameModal
|
||||||
|
visible={showEditGameModal}
|
||||||
|
onClose={() => setShowEditGameModal(false)}
|
||||||
|
game={game}
|
||||||
|
shopDetails={shopDetails}
|
||||||
|
onGameUpdated={handleGameUpdated}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ $hero-height: 300px;
|
|||||||
&__wrapper {
|
&__wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
@@ -44,12 +43,53 @@ $hero-height: 300px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__hero-content {
|
&__hero-content {
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 1.5);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hero-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&--right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__edit-custom-game-button {
|
||||||
|
padding: calc(globals.$spacing-unit * 1.5);
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
border: solid 1px globals.$border-color;
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
|
||||||
|
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: globals.$body-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-logo-backdrop {
|
&__hero-logo-backdrop {
|
||||||
@@ -64,8 +104,8 @@ $hero-height: 300px;
|
|||||||
|
|
||||||
&__hero-image {
|
&__hero-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $hero-height;
|
height: calc($hero-height + 72px);
|
||||||
min-height: $hero-height;
|
min-height: calc($hero-height + 72px);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: top;
|
object-position: top;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
@@ -74,14 +114,46 @@ $hero-height: 300px;
|
|||||||
|
|
||||||
@media (min-width: 1250px) {
|
@media (min-width: 1250px) {
|
||||||
object-position: center;
|
object-position: center;
|
||||||
height: 350px;
|
height: calc(350px + 72px);
|
||||||
min-height: 350px;
|
min-height: calc(350px + 72px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__game-logo {
|
&__game-logo {
|
||||||
width: 300px;
|
width: 200px;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__game-logo-text {
|
||||||
|
width: 200px;
|
||||||
|
align-self: flex-end;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.2;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 250px;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
width: 300px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__hero-image-skeleton {
|
&__hero-image-skeleton {
|
||||||
@@ -97,7 +169,6 @@ $hero-height: 300px;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +176,7 @@ $hero-height: 300px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
globals.$background-color 50%,
|
globals.$background-color 50%,
|
||||||
@@ -114,30 +186,53 @@ $hero-height: 300px;
|
|||||||
|
|
||||||
&__description-content {
|
&__description-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
font-size: globals.$body-font-size;
|
font-size: globals.$body-font-size;
|
||||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
min-height: auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
@media (min-width: 1536px) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-top: globals.$spacing-unit;
|
margin-top: globals.$spacing-unit;
|
||||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
margin-bottom: calc(globals.$spacing-unit * 3);
|
||||||
display: block;
|
display: block !important;
|
||||||
width: 100%;
|
max-width: 100% !important;
|
||||||
height: auto;
|
width: auto !important;
|
||||||
object-fit: cover;
|
height: auto !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -154,15 +249,28 @@ $hero-height: 300px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__randomizer-button {
|
&__randomizer-button {
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export default function GameDetails() {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowGameOptionsModal(false);
|
setShowGameOptionsModal(false);
|
||||||
}}
|
}}
|
||||||
|
onNavigateHome={() => navigate("/")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ import {
|
|||||||
GearIcon,
|
GearIcon,
|
||||||
HeartFillIcon,
|
HeartFillIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
|
PinIcon,
|
||||||
|
PinSlashIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusCircleIcon,
|
PlusCircleIcon,
|
||||||
} from "@primer/octicons-react";
|
} from "@primer/octicons-react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
import {
|
||||||
|
useDownload,
|
||||||
|
useLibrary,
|
||||||
|
useToast,
|
||||||
|
useUserDetails,
|
||||||
|
} from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
@@ -19,6 +26,7 @@ export function HeroPanelActions() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const { isGameDeleting } = useDownload();
|
const { isGameDeleting } = useDownload();
|
||||||
|
const { userDetails } = useUserDetails();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
game,
|
game,
|
||||||
@@ -82,6 +90,29 @@ export function HeroPanelActions() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleGamePinned = async () => {
|
||||||
|
setToggleLibraryGameDisabled(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (game?.isPinned && objectId) {
|
||||||
|
await window.electron.toggleGamePin(shop, objectId, false).then(() => {
|
||||||
|
showSuccessToast(t("game_removed_from_pinned"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!objectId) return;
|
||||||
|
|
||||||
|
await window.electron.toggleGamePin(shop, objectId, true).then(() => {
|
||||||
|
showSuccessToast(t("game_added_to_pinned"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLibrary();
|
||||||
|
updateGame();
|
||||||
|
} finally {
|
||||||
|
setToggleLibraryGameDisabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openGame = async () => {
|
const openGame = async () => {
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
@@ -198,6 +229,17 @@ export function HeroPanelActions() {
|
|||||||
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{userDetails && game.shop !== "custom" && (
|
||||||
|
<Button
|
||||||
|
onClick={toggleGamePinned}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
className="hero-panel-actions__action"
|
||||||
|
>
|
||||||
|
{game.isPinned ? <PinSlashIcon /> : <PinIcon />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowGameOptionsModal(true)}
|
onClick={() => setShowGameOptionsModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__manual-warning {
|
&__manual-warning {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import "./hero-panel-playtime.scss";
|
|||||||
|
|
||||||
export function HeroPanelPlaytime() {
|
export function HeroPanelPlaytime() {
|
||||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||||
|
|
||||||
|
|
||||||
const { game, isGameRunning } = useContext(gameDetailsContext);
|
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
@@ -89,19 +88,23 @@ export function HeroPanelPlaytime() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p
|
<p
|
||||||
className="hero-panel-playtime__play-time"
|
className="hero-panel-playtime__play-time"
|
||||||
data-tooltip-place="top"
|
data-tooltip-place="right"
|
||||||
data-tooltip-content={
|
data-tooltip-content={
|
||||||
game.hasManuallyUpdatedPlaytime
|
game.hasManuallyUpdatedPlaytime
|
||||||
? t("manual_playtime_tooltip")
|
? t("manual_playtime_tooltip")
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
data-tooltip-id={game.hasManuallyUpdatedPlaytime ? "manual-playtime-warning" : undefined}
|
data-tooltip-id={
|
||||||
|
game.hasManuallyUpdatedPlaytime
|
||||||
|
? "manual-playtime-warning"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{game.hasManuallyUpdatedPlaytime && (
|
{game.hasManuallyUpdatedPlaytime && (
|
||||||
<AlertFillIcon
|
<AlertFillIcon
|
||||||
size={16}
|
size={16}
|
||||||
className="hero-panel-playtime__manual-warning"
|
className="hero-panel-playtime__manual-warning"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -119,7 +122,7 @@ export function HeroPanelPlaytime() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{game.hasManuallyUpdatedPlaytime && (
|
{game.hasManuallyUpdatedPlaytime && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
id="manual-playtime-warning"
|
id="manual-playtime-warning"
|
||||||
@@ -127,7 +130,6 @@ export function HeroPanelPlaytime() {
|
|||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
}}
|
}}
|
||||||
openOnClick={false}
|
openOnClick={false}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -5,18 +5,24 @@
|
|||||||
height: 72px;
|
height: 72px;
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 3);
|
||||||
background-color: globals.$dark-background-color;
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: solid 1px rgba(255, 255, 255, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
transition: all ease 0.2s;
|
transition: all ease 0.2s;
|
||||||
border-bottom: solid 1px globals.$border-color;
|
border-bottom: solid 1px globals.$border-color;
|
||||||
position: sticky;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
&--stuck {
|
&--stuck {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function HeroPanel() {
|
|||||||
|
|
||||||
const { formatDate } = useDate();
|
const { formatDate } = useDate();
|
||||||
|
|
||||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
const { game, repacks } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { lastPacket } = useDownload();
|
const { lastPacket } = useDownload();
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export function HeroPanel() {
|
|||||||
game?.download?.status === "paused";
|
game?.download?.status === "paused";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: gameColor }} className="hero-panel">
|
<div className="hero-panel">
|
||||||
<div className="hero-panel__content">{getInfo()}</div>
|
<div className="hero-panel__content">{getInfo()}</div>
|
||||||
<div className="hero-panel__actions">
|
<div className="hero-panel__actions">
|
||||||
<HeroPanelActions />
|
<HeroPanelActions />
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export function ChangeGamePlaytimeModal({
|
|||||||
onSuccess?.(t("update_playtime_success"));
|
onSuccess?.(t("update_playtime_success"));
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
onError?.(t("update_playtime_error"));
|
onError?.(t("update_playtime_error"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@@ -150,17 +149,17 @@ export function ChangeGamePlaytimeModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="change-game-playtime-modal__actions">
|
<div className="change-game-playtime-modal__actions">
|
||||||
|
<Button onClick={onClose} theme="outline">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleChangePlaytime}
|
onClick={handleChangePlaytime}
|
||||||
theme="outline"
|
theme="primary"
|
||||||
disabled={!isValid || isSubmitting}
|
disabled={!isValid || isSubmitting}
|
||||||
>
|
>
|
||||||
{t("update_playtime")}
|
{t("update_playtime")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onClose} theme="primary">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
181
src/renderer/src/pages/game-details/modals/edit-game-modal.scss
Normal file
181
src/renderer/src/pages/game-details/modals/edit-game-modal.scss
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
.edit-game-modal__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__asset-selector {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__asset-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__asset-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__image-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__resolution-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__image-preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 255, 255, 0.1) 25%,
|
||||||
|
transparent 25%
|
||||||
|
),
|
||||||
|
linear-gradient(-45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
0 8px,
|
||||||
|
8px -8px,
|
||||||
|
-8px 0px;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
/* Reset button styles when used as button element */
|
||||||
|
&[type="button"] {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-align: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__drop-zone {
|
||||||
|
min-height: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-style: dashed !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background-color: rgba(var(--color-primary-rgb), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-color: var(--color-primary) !important;
|
||||||
|
background-color: rgba(var(--color-primary-rgb), 0.1) !important;
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(var(--color-primary-rgb), 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__drop-zone-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__icon-preview {
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-game-modal__actions button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
642
src/renderer/src/pages/game-details/modals/edit-game-modal.tsx
Normal file
642
src/renderer/src/pages/game-details/modals/edit-game-modal.tsx
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ImageIcon, XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import { Modal, TextField, Button } from "@renderer/components";
|
||||||
|
import { useToast } from "@renderer/hooks";
|
||||||
|
import type { LibraryGame, Game, ShopDetailsWithAssets } from "@types";
|
||||||
|
|
||||||
|
import "./edit-game-modal.scss";
|
||||||
|
|
||||||
|
export interface EditGameModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
game: LibraryGame | Game | null;
|
||||||
|
shopDetails?: ShopDetailsWithAssets | null;
|
||||||
|
onGameUpdated: (updatedGame: LibraryGame | Game) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetType = "icon" | "logo" | "hero";
|
||||||
|
|
||||||
|
export function EditGameModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
game,
|
||||||
|
shopDetails,
|
||||||
|
onGameUpdated,
|
||||||
|
}: Readonly<EditGameModalProps>) {
|
||||||
|
const { t } = useTranslation("sidebar");
|
||||||
|
const { showSuccessToast, showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const [gameName, setGameName] = useState("");
|
||||||
|
const [assetPaths, setAssetPaths] = useState({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
const [assetDisplayPaths, setAssetDisplayPaths] = useState({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
const [originalAssetPaths, setOriginalAssetPaths] = useState({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
const [defaultUrls, setDefaultUrls] = useState({
|
||||||
|
icon: null as string | null,
|
||||||
|
logo: null as string | null,
|
||||||
|
hero: null as string | null,
|
||||||
|
});
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [selectedAssetType, setSelectedAssetType] = useState<AssetType>("icon");
|
||||||
|
|
||||||
|
const isCustomGame = (game: LibraryGame | Game): boolean => {
|
||||||
|
return game.shop === "custom";
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractLocalPath = (url: string | null | undefined): string => {
|
||||||
|
return url?.startsWith("local:") ? url.replace("local:", "") : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
|
||||||
|
setAssetPaths({
|
||||||
|
icon: extractLocalPath(game.iconUrl),
|
||||||
|
logo: extractLocalPath(game.logoImageUrl),
|
||||||
|
hero: extractLocalPath(game.libraryHeroImageUrl),
|
||||||
|
});
|
||||||
|
setAssetDisplayPaths({
|
||||||
|
icon: extractLocalPath(game.iconUrl),
|
||||||
|
logo: extractLocalPath(game.logoImageUrl),
|
||||||
|
hero: extractLocalPath(game.libraryHeroImageUrl),
|
||||||
|
});
|
||||||
|
setOriginalAssetPaths({
|
||||||
|
icon: (game as any).originalIconPath || extractLocalPath(game.iconUrl),
|
||||||
|
logo:
|
||||||
|
(game as any).originalLogoPath || extractLocalPath(game.logoImageUrl),
|
||||||
|
hero:
|
||||||
|
(game as any).originalHeroPath ||
|
||||||
|
extractLocalPath(game.libraryHeroImageUrl),
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setNonCustomGameAssets = useCallback(
|
||||||
|
(game: LibraryGame) => {
|
||||||
|
setAssetPaths({
|
||||||
|
icon: extractLocalPath(game.customIconUrl),
|
||||||
|
logo: extractLocalPath(game.customLogoImageUrl),
|
||||||
|
hero: extractLocalPath(game.customHeroImageUrl),
|
||||||
|
});
|
||||||
|
setAssetDisplayPaths({
|
||||||
|
icon: extractLocalPath(game.customIconUrl),
|
||||||
|
logo: extractLocalPath(game.customLogoImageUrl),
|
||||||
|
hero: extractLocalPath(game.customHeroImageUrl),
|
||||||
|
});
|
||||||
|
setOriginalAssetPaths({
|
||||||
|
icon:
|
||||||
|
(game as any).customOriginalIconPath ||
|
||||||
|
extractLocalPath(game.customIconUrl),
|
||||||
|
logo:
|
||||||
|
(game as any).customOriginalLogoPath ||
|
||||||
|
extractLocalPath(game.customLogoImageUrl),
|
||||||
|
hero:
|
||||||
|
(game as any).customOriginalHeroPath ||
|
||||||
|
extractLocalPath(game.customHeroImageUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
setDefaultUrls({
|
||||||
|
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
|
||||||
|
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
|
||||||
|
hero:
|
||||||
|
shopDetails?.assets?.libraryHeroImageUrl ||
|
||||||
|
game.libraryHeroImageUrl ||
|
||||||
|
null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[shopDetails]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (game && visible) {
|
||||||
|
setGameName(game.title || "");
|
||||||
|
|
||||||
|
if (isCustomGame(game)) {
|
||||||
|
setCustomGameAssets(game);
|
||||||
|
} else {
|
||||||
|
setNonCustomGameAssets(game as LibraryGame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [game, visible, shopDetails, setCustomGameAssets, setNonCustomGameAssets]);
|
||||||
|
|
||||||
|
const handleGameNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setGameName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssetTypeChange = (assetType: AssetType) => {
|
||||||
|
setSelectedAssetType(assetType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAssetPath = (assetType: AssetType): string => {
|
||||||
|
return assetPaths[assetType];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAssetDisplayPath = (assetType: AssetType): string => {
|
||||||
|
// Use original path if available, otherwise fall back to display path
|
||||||
|
return originalAssetPaths[assetType] || assetDisplayPaths[assetType];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAssetPath = (assetType: AssetType, path: string): void => {
|
||||||
|
setAssetPaths((prev) => ({ ...prev, [assetType]: path }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAssetDisplayPath = (assetType: AssetType, path: string): void => {
|
||||||
|
setAssetDisplayPaths((prev) => ({ ...prev, [assetType]: path }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultUrl = (assetType: AssetType): string | null => {
|
||||||
|
return defaultUrls[assetType];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAsset = async (assetType: AssetType) => {
|
||||||
|
const { filePaths } = await window.electron.showOpenDialog({
|
||||||
|
properties: ["openFile"],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: t("edit_game_modal_image_filter"),
|
||||||
|
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePaths && filePaths.length > 0) {
|
||||||
|
const originalPath = filePaths[0];
|
||||||
|
try {
|
||||||
|
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
|
||||||
|
originalPath,
|
||||||
|
assetType
|
||||||
|
);
|
||||||
|
setAssetPath(assetType, copiedAssetUrl.replace("local:", ""));
|
||||||
|
setAssetDisplayPath(assetType, originalPath);
|
||||||
|
// Store the original path for display purposes
|
||||||
|
setOriginalAssetPaths((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[assetType]: originalPath,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to copy ${assetType} asset:`, error);
|
||||||
|
setAssetPath(assetType, originalPath);
|
||||||
|
setAssetDisplayPath(assetType, originalPath);
|
||||||
|
setOriginalAssetPaths((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[assetType]: originalPath,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreDefault = (assetType: AssetType) => {
|
||||||
|
setAssetPath(assetType, "");
|
||||||
|
setAssetDisplayPath(assetType, "");
|
||||||
|
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOriginalTitle = (): string => {
|
||||||
|
if (!game) return "";
|
||||||
|
|
||||||
|
// For non-custom games, the original title is from shopDetails assets
|
||||||
|
return shopDetails?.assets?.title || game.title || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreDefaultTitle = () => {
|
||||||
|
const originalTitle = getOriginalTitle();
|
||||||
|
setGameName(originalTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTitleChanged = useMemo((): boolean => {
|
||||||
|
if (!game || isCustomGame(game)) return false;
|
||||||
|
const originalTitle = getOriginalTitle();
|
||||||
|
return gameName.trim() !== originalTitle.trim();
|
||||||
|
}, [game, gameName, shopDetails]);
|
||||||
|
|
||||||
|
const [dragOverTarget, setDragOverTarget] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnter = (e: React.DragEvent, target: string) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragOverTarget(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
|
setDragOverTarget(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateImageFile = (file: File): boolean => {
|
||||||
|
const validTypes = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
];
|
||||||
|
return validTypes.includes(file.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processDroppedFile = async (file: File, assetType: AssetType) => {
|
||||||
|
setDragOverTarget(null);
|
||||||
|
|
||||||
|
if (!validateImageFile(file)) {
|
||||||
|
showErrorToast("Invalid file type. Please select an image file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let filePath: string;
|
||||||
|
|
||||||
|
interface ElectronFile extends File {
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("path" in file && typeof (file as ElectronFile).path === "string") {
|
||||||
|
filePath = (file as ElectronFile).path!;
|
||||||
|
} else {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
const tempFileName = `temp_${Date.now()}_${file.name}`;
|
||||||
|
const tempPath = await window.electron.saveTempFile?.(
|
||||||
|
tempFileName,
|
||||||
|
uint8Array
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tempPath) {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to process file. Drag and drop may not be fully supported."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = tempPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
|
||||||
|
filePath,
|
||||||
|
assetType
|
||||||
|
);
|
||||||
|
|
||||||
|
const assetPath = copiedAssetUrl.replace("local:", "");
|
||||||
|
setAssetPath(assetType, assetPath);
|
||||||
|
setAssetDisplayPath(assetType, filePath);
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
`${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!("path" in file) && filePath) {
|
||||||
|
try {
|
||||||
|
await window.electron.deleteTempFile?.(filePath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn("Failed to clean up temporary file:", cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to process dropped ${assetType}:`, error);
|
||||||
|
showErrorToast(
|
||||||
|
`Failed to process dropped ${assetType}. Please try again.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssetDrop = async (e: React.DragEvent, assetType: AssetType) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragOverTarget(null);
|
||||||
|
|
||||||
|
if (isUpdating) return;
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await processDroppedFile(files[0], assetType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to prepare custom game assets
|
||||||
|
const prepareCustomGameAssets = (game: LibraryGame | Game) => {
|
||||||
|
const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl;
|
||||||
|
const logoImageUrl = assetPaths.logo
|
||||||
|
? `local:${assetPaths.logo}`
|
||||||
|
: game.logoImageUrl;
|
||||||
|
const libraryHeroImageUrl = assetPaths.hero
|
||||||
|
? `local:${assetPaths.hero}`
|
||||||
|
: game.libraryHeroImageUrl;
|
||||||
|
|
||||||
|
return { iconUrl, logoImageUrl, libraryHeroImageUrl };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to prepare non-custom game assets
|
||||||
|
const prepareNonCustomGameAssets = () => {
|
||||||
|
return {
|
||||||
|
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null,
|
||||||
|
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null,
|
||||||
|
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update custom game
|
||||||
|
const updateCustomGame = async (game: LibraryGame | Game) => {
|
||||||
|
const { iconUrl, logoImageUrl, libraryHeroImageUrl } =
|
||||||
|
prepareCustomGameAssets(game);
|
||||||
|
|
||||||
|
return window.electron.updateCustomGame({
|
||||||
|
shop: game.shop,
|
||||||
|
objectId: game.objectId,
|
||||||
|
title: gameName.trim(),
|
||||||
|
iconUrl: iconUrl || undefined,
|
||||||
|
logoImageUrl: logoImageUrl || undefined,
|
||||||
|
libraryHeroImageUrl: libraryHeroImageUrl || undefined,
|
||||||
|
originalIconPath: originalAssetPaths.icon || undefined,
|
||||||
|
originalLogoPath: originalAssetPaths.logo || undefined,
|
||||||
|
originalHeroPath: originalAssetPaths.hero || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to update non-custom game
|
||||||
|
const updateNonCustomGame = async (game: LibraryGame) => {
|
||||||
|
const { customIconUrl, customLogoImageUrl, customHeroImageUrl } =
|
||||||
|
prepareNonCustomGameAssets();
|
||||||
|
|
||||||
|
return window.electron.updateGameCustomAssets({
|
||||||
|
shop: game.shop,
|
||||||
|
objectId: game.objectId,
|
||||||
|
title: gameName.trim(),
|
||||||
|
customIconUrl,
|
||||||
|
customLogoImageUrl,
|
||||||
|
customHeroImageUrl,
|
||||||
|
customOriginalIconPath: originalAssetPaths.icon || undefined,
|
||||||
|
customOriginalLogoPath: originalAssetPaths.logo || undefined,
|
||||||
|
customOriginalHeroPath: originalAssetPaths.hero || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateGame = async () => {
|
||||||
|
if (!game || !gameName.trim()) {
|
||||||
|
showErrorToast(t("edit_game_modal_fill_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedGame =
|
||||||
|
game && isCustomGame(game)
|
||||||
|
? await updateCustomGame(game)
|
||||||
|
: await updateNonCustomGame(game as LibraryGame);
|
||||||
|
|
||||||
|
showSuccessToast(t("edit_game_modal_success"));
|
||||||
|
onGameUpdated(updatedGame);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update game:", error);
|
||||||
|
showErrorToast(
|
||||||
|
error instanceof Error ? error.message : t("edit_game_modal_failed")
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to reset form to initial state
|
||||||
|
const resetFormToInitialState = useCallback(
|
||||||
|
(game: LibraryGame | Game) => {
|
||||||
|
setGameName(game.title || "");
|
||||||
|
|
||||||
|
if (isCustomGame(game)) {
|
||||||
|
setCustomGameAssets(game);
|
||||||
|
// Clear default URLs for custom games
|
||||||
|
setDefaultUrls({
|
||||||
|
icon: null,
|
||||||
|
logo: null,
|
||||||
|
hero: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setNonCustomGameAssets(game as LibraryGame);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setCustomGameAssets, setNonCustomGameAssets]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!isUpdating && game) {
|
||||||
|
resetFormToInitialState(game);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = gameName.trim();
|
||||||
|
|
||||||
|
const getPreviewUrl = (assetType: AssetType): string | undefined => {
|
||||||
|
const assetPath = getAssetPath(assetType);
|
||||||
|
const defaultUrl = getDefaultUrl(assetType);
|
||||||
|
|
||||||
|
if (game && !isCustomGame(game)) {
|
||||||
|
return assetPath ? `local:${assetPath}` : defaultUrl || undefined;
|
||||||
|
}
|
||||||
|
return assetPath ? `local:${assetPath}` : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImageSection = (assetType: AssetType) => {
|
||||||
|
const assetPath = getAssetPath(assetType);
|
||||||
|
const assetDisplayPath = getAssetDisplayPath(assetType);
|
||||||
|
const defaultUrl = getDefaultUrl(assetType);
|
||||||
|
const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl);
|
||||||
|
const isDragOver = dragOverTarget === assetType;
|
||||||
|
|
||||||
|
const getTranslationKey = (suffix: string) =>
|
||||||
|
`edit_game_modal_${assetType}${suffix}`;
|
||||||
|
const getResolutionKey = () => `edit_game_modal_${assetType}_resolution`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="edit-game-modal__image-section">
|
||||||
|
<TextField
|
||||||
|
placeholder={t(`edit_game_modal_select_${assetType}`)}
|
||||||
|
value={assetDisplayPath}
|
||||||
|
readOnly
|
||||||
|
theme="dark"
|
||||||
|
rightContent={
|
||||||
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => handleSelectAsset(assetType)}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
<ImageIcon />
|
||||||
|
{t("edit_game_modal_browse")}
|
||||||
|
</Button>
|
||||||
|
{game && !isCustomGame(game) && assetPath && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={() => handleRestoreDefault(assetType)}
|
||||||
|
disabled={isUpdating}
|
||||||
|
title={`Remove ${assetType}`}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="edit-game-modal__resolution-info">
|
||||||
|
{t(getResolutionKey())}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasImage && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t(getTranslationKey("_drop_zone"))}
|
||||||
|
className={`edit-game-modal__image-preview ${
|
||||||
|
assetType === "icon" ? "edit-game-modal__icon-preview" : ""
|
||||||
|
} ${isDragOver ? "edit-game-modal__drop-zone--active" : ""}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnter={(e) => handleDragEnter(e, assetType)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={(e) => handleAssetDrop(e, assetType)}
|
||||||
|
onClick={() => handleSelectAsset(assetType)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getPreviewUrl(assetType)}
|
||||||
|
alt={t(getTranslationKey("_preview"))}
|
||||||
|
className="edit-game-modal__preview-image"
|
||||||
|
/>
|
||||||
|
{isDragOver && (
|
||||||
|
<div className="edit-game-modal__drop-overlay">
|
||||||
|
<span>Drop to replace {assetType}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasImage && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t(getTranslationKey("_drop_zone_empty"))}
|
||||||
|
className={`edit-game-modal__image-preview ${
|
||||||
|
assetType === "icon" ? "edit-game-modal__icon-preview" : ""
|
||||||
|
} edit-game-modal__drop-zone ${
|
||||||
|
isDragOver ? "edit-game-modal__drop-zone--active" : ""
|
||||||
|
}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnter={(e) => handleDragEnter(e, assetType)}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={(e) => handleAssetDrop(e, assetType)}
|
||||||
|
onClick={() => handleSelectAsset(assetType)}
|
||||||
|
>
|
||||||
|
<div className="edit-game-modal__drop-zone-content">
|
||||||
|
<ImageIcon />
|
||||||
|
<span>Drop {assetType} image here</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("edit_game_modal")}
|
||||||
|
description={t("edit_game_modal_description")}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<div className="edit-game-modal__container">
|
||||||
|
<div className="edit-game-modal__form">
|
||||||
|
<TextField
|
||||||
|
label={t("edit_game_modal_title")}
|
||||||
|
placeholder={t("edit_game_modal_enter_title")}
|
||||||
|
value={gameName}
|
||||||
|
onChange={handleGameNameChange}
|
||||||
|
theme="dark"
|
||||||
|
disabled={isUpdating}
|
||||||
|
rightContent={
|
||||||
|
isTitleChanged && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleRestoreDefaultTitle}
|
||||||
|
disabled={isUpdating}
|
||||||
|
title="Restore default title"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="edit-game-modal__asset-selector">
|
||||||
|
<div className="edit-game-modal__asset-label">
|
||||||
|
{t("edit_game_modal_assets")}
|
||||||
|
</div>
|
||||||
|
<div className="edit-game-modal__asset-tabs">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme={selectedAssetType === "icon" ? "primary" : "outline"}
|
||||||
|
onClick={() => handleAssetTypeChange("icon")}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{t("edit_game_modal_icon")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme={selectedAssetType === "logo" ? "primary" : "outline"}
|
||||||
|
onClick={() => handleAssetTypeChange("logo")}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{t("edit_game_modal_logo")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme={selectedAssetType === "hero" ? "primary" : "outline"}
|
||||||
|
onClick={() => handleAssetTypeChange("hero")}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{t("edit_game_modal_hero")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderImageSection(selectedAssetType)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="edit-game-modal__actions">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{t("edit_game_modal_cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="primary"
|
||||||
|
onClick={handleUpdateGame}
|
||||||
|
disabled={!isFormValid || isUpdating}
|
||||||
|
>
|
||||||
|
{isUpdating
|
||||||
|
? t("edit_game_modal_updating")
|
||||||
|
: t("edit_game_modal_update")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,12 +18,14 @@ export interface GameOptionsModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
game: LibraryGame;
|
game: LibraryGame;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onNavigateHome?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GameOptionsModal({
|
export function GameOptionsModal({
|
||||||
visible,
|
visible,
|
||||||
game,
|
game,
|
||||||
onClose,
|
onClose,
|
||||||
|
onNavigateHome,
|
||||||
}: Readonly<GameOptionsModalProps>) {
|
}: Readonly<GameOptionsModalProps>) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
@@ -90,6 +92,11 @@ export function GameOptionsModal({
|
|||||||
await removeGameFromLibrary(game.shop, game.objectId);
|
await removeGameFromLibrary(game.shop, game.objectId);
|
||||||
updateGame();
|
updateGame();
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
|
// Redirect to home page if it's a custom game
|
||||||
|
if (game.shop === "custom" && onNavigateHome) {
|
||||||
|
onNavigateHome();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeExecutableLocation = async () => {
|
const handleChangeExecutableLocation = async () => {
|
||||||
@@ -346,14 +353,16 @@ export function GameOptionsModal({
|
|||||||
>
|
>
|
||||||
{t("create_shortcut")}
|
{t("create_shortcut")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{game.shop !== "custom" && (
|
||||||
onClick={handleCreateSteamShortcut}
|
<Button
|
||||||
theme="outline"
|
onClick={handleCreateSteamShortcut}
|
||||||
disabled={creatingSteamShortcut}
|
theme="outline"
|
||||||
>
|
disabled={creatingSteamShortcut}
|
||||||
<SteamLogo />
|
>
|
||||||
{t("create_steam_shortcut")}
|
<SteamLogo />
|
||||||
</Button>
|
{t("create_steam_shortcut")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{shouldShowCreateStartMenuShortcut && (
|
{shouldShowCreateStartMenuShortcut && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleCreateShortcut("start_menu")}
|
onClick={() => handleCreateShortcut("start_menu")}
|
||||||
@@ -367,19 +376,21 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CheckboxField
|
{game.shop !== "custom" && (
|
||||||
label={
|
<CheckboxField
|
||||||
<div className="game-options-modal__cloud-sync-label">
|
label={
|
||||||
{t("enable_automatic_cloud_sync")}
|
<div className="game-options-modal__cloud-sync-label">
|
||||||
<span className="game-options-modal__cloud-sync-hydra-cloud">
|
{t("enable_automatic_cloud_sync")}
|
||||||
Hydra Cloud
|
<span className="game-options-modal__cloud-sync-hydra-cloud">
|
||||||
</span>
|
Hydra Cloud
|
||||||
</div>
|
</span>
|
||||||
}
|
</div>
|
||||||
checked={automaticCloudSync}
|
}
|
||||||
disabled={!hasActiveSubscription || !game.executablePath}
|
checked={automaticCloudSync}
|
||||||
onChange={handleToggleAutomaticCloudSync}
|
disabled={!hasActiveSubscription || !game.executablePath}
|
||||||
/>
|
onChange={handleToggleAutomaticCloudSync}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowWinePrefixConfiguration && (
|
{shouldShowWinePrefixConfiguration && (
|
||||||
<div className="game-options-modal__wine-prefix">
|
<div className="game-options-modal__wine-prefix">
|
||||||
@@ -441,33 +452,35 @@ export function GameOptionsModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="game-options-modal__downloads">
|
{game.shop !== "custom" && (
|
||||||
<div className="game-options-modal__header">
|
<div className="game-options-modal__downloads">
|
||||||
<h2>{t("downloads_section_title")}</h2>
|
<div className="game-options-modal__header">
|
||||||
<h4 className="game-options-modal__header-description">
|
<h2>{t("downloads_section_title")}</h2>
|
||||||
{t("downloads_section_description")}
|
<h4 className="game-options-modal__header-description">
|
||||||
</h4>
|
{t("downloads_section_description")}
|
||||||
</div>
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="game-options-modal__row">
|
<div className="game-options-modal__row">
|
||||||
<Button
|
|
||||||
onClick={() => setShowRepacksModal(true)}
|
|
||||||
theme="outline"
|
|
||||||
disabled={deleting || isGameDownloading || !repacks.length}
|
|
||||||
>
|
|
||||||
{t("open_download_options")}
|
|
||||||
</Button>
|
|
||||||
{game.download?.downloadPath && (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenDownloadFolder}
|
onClick={() => setShowRepacksModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting || isGameDownloading || !repacks.length}
|
||||||
>
|
>
|
||||||
{t("open_download_location")}
|
{t("open_download_options")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{game.download?.downloadPath && (
|
||||||
|
<Button
|
||||||
|
onClick={handleOpenDownloadFolder}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("open_download_location")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="game-options-modal__danger-zone">
|
<div className="game-options-modal__danger-zone">
|
||||||
<div className="game-options-modal__header">
|
<div className="game-options-modal__header">
|
||||||
@@ -486,18 +499,20 @@ export function GameOptionsModal({
|
|||||||
{t("remove_from_library")}
|
{t("remove_from_library")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
{game.shop !== "custom" && (
|
||||||
onClick={() => setShowResetAchievementsModal(true)}
|
<Button
|
||||||
theme="danger"
|
onClick={() => setShowResetAchievementsModal(true)}
|
||||||
disabled={
|
theme="danger"
|
||||||
deleting ||
|
disabled={
|
||||||
isDeletingAchievements ||
|
deleting ||
|
||||||
!hasAchievements ||
|
isDeletingAchievements ||
|
||||||
!userDetails
|
!hasAchievements ||
|
||||||
}
|
!userDetails
|
||||||
>
|
}
|
||||||
{t("reset_achievements")}
|
>
|
||||||
</Button>
|
{t("reset_achievements")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowChangePlaytimeModal(true)}
|
onClick={() => setShowChangePlaytimeModal(true)}
|
||||||
@@ -506,17 +521,21 @@ export function GameOptionsModal({
|
|||||||
{t("update_game_playtime")}
|
{t("update_game_playtime")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
{game.shop !== "custom" && (
|
||||||
onClick={() => {
|
<Button
|
||||||
setShowDeleteModal(true);
|
onClick={() => {
|
||||||
}}
|
setShowDeleteModal(true);
|
||||||
theme="danger"
|
}}
|
||||||
disabled={
|
theme="danger"
|
||||||
isGameDownloading || deleting || !game.download?.downloadPath
|
disabled={
|
||||||
}
|
isGameDownloading ||
|
||||||
>
|
deleting ||
|
||||||
{t("remove_files")}
|
!game.download?.downloadPath
|
||||||
</Button>
|
}
|
||||||
|
>
|
||||||
|
{t("remove_files")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./repacks-modal";
|
export * from "./repacks-modal";
|
||||||
export * from "./download-settings-modal";
|
export * from "./download-settings-modal";
|
||||||
export * from "./game-options-modal";
|
export * from "./game-options-modal";
|
||||||
|
export * from "./edit-game-modal";
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ export function RemoveGameFromLibraryModal({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<div className="remove-from-library-modal__actions">
|
<div className="remove-from-library-modal__actions">
|
||||||
<Button onClick={handleRemoveGame} theme="outline">
|
<Button onClick={onClose} theme="outline">
|
||||||
{t("remove")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onClose} theme="primary">
|
<Button onClick={handleRemoveGame} theme="primary">
|
||||||
{t("cancel")}
|
{t("remove")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export function ResetAchievementsModal({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="reset-achievements-modal__actions">
|
<div className="reset-achievements-modal__actions">
|
||||||
<Button onClick={handleResetAchievements} theme="outline">
|
<Button onClick={onClose} theme="outline">
|
||||||
{t("reset_achievements")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={onClose} theme="primary">
|
<Button onClick={handleResetAchievements} theme="primary">
|
||||||
{t("cancel")}
|
{t("reset_achievements")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
@use "../../../scss/globals.scss";
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
.sidebar-section {
|
.sidebar-section {
|
||||||
|
background-color: globals.$dark-background-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
height: 72px;
|
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
|
||||||
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 2);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: globals.$background-color;
|
background-color: globals.$background-color;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.game-language-section {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-bottom: 1px solid globals.$border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2.5);
|
||||||
|
|
||||||
|
&--center {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cell {
|
||||||
|
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2.5);
|
||||||
|
font-size: globals.$body-font-size;
|
||||||
|
color: globals.$body-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&--language {
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--center {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 0 0 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__check {
|
||||||
|
color: globals.$body-color;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cross {
|
||||||
|
color: globals.$body-color;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
&__header,
|
||||||
|
&__cell {
|
||||||
|
padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 0.5);
|
||||||
|
font-size: calc(globals.$small-font-size * 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/renderer/src/pages/game-details/sidebar/game-language-section.tsx
Executable file
72
src/renderer/src/pages/game-details/sidebar/game-language-section.tsx
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CheckIcon, XIcon } from "@primer/octicons-react";
|
||||||
|
import { gameDetailsContext } from "@renderer/context/game-details/game-details.context";
|
||||||
|
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||||
|
import "./game-language-section.scss";
|
||||||
|
|
||||||
|
export function GameLanguageSection() {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
const { shopDetails } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const languages = useMemo(() => {
|
||||||
|
const supportedLanguages = shopDetails?.supported_languages;
|
||||||
|
if (!supportedLanguages) return [];
|
||||||
|
|
||||||
|
const languagesString = supportedLanguages.split("<br>")[0];
|
||||||
|
const languageArray = languagesString?.split(",") || [];
|
||||||
|
|
||||||
|
return languageArray.map((lang) => ({
|
||||||
|
language: lang.replace("<strong>*</strong>", "").trim(),
|
||||||
|
hasAudio: lang.includes("*"),
|
||||||
|
}));
|
||||||
|
}, [shopDetails?.supported_languages]);
|
||||||
|
|
||||||
|
if (languages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarSection title={t("language")}>
|
||||||
|
<div className="game-language-section">
|
||||||
|
<div className="game-language-section__header">
|
||||||
|
<div className="game-language-section__header-item">
|
||||||
|
<span>{t("language")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="game-language-section__header-item game-language-section__header-item--center">
|
||||||
|
<span>{t("caption")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="game-language-section__header-item game-language-section__header-item--center">
|
||||||
|
<span>{t("audio")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="game-language-section__content">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<div key={lang.language} className="game-language-section__row">
|
||||||
|
<div
|
||||||
|
className="game-language-section__cell game-language-section__cell--language"
|
||||||
|
title={lang.language}
|
||||||
|
>
|
||||||
|
{lang.language}
|
||||||
|
</div>
|
||||||
|
<div className="game-language-section__cell game-language-section__cell--center">
|
||||||
|
<CheckIcon size={14} className="game-language-section__check" />
|
||||||
|
</div>
|
||||||
|
<div className="game-language-section__cell game-language-section__cell--center">
|
||||||
|
{lang.hasAudio ? (
|
||||||
|
<CheckIcon
|
||||||
|
size={14}
|
||||||
|
className="game-language-section__check"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<XIcon size={14} className="game-language-section__cross" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ export function HowLongToBeatSection({
|
|||||||
return `${value} ${t(durationTranslation[unit])}`;
|
return `${value} ${t(durationTranslation[unit])}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!howLongToBeatData && !isLoading) return null;
|
if (!howLongToBeatData || !isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||||
|
|||||||
23
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file → Executable file
23
src/renderer/src/pages/game-details/sidebar/sidebar.scss
Normal file → Executable file
@@ -3,17 +3,30 @@
|
|||||||
.content-sidebar {
|
.content-sidebar {
|
||||||
border-left: solid 1px globals.$border-color;
|
border-left: solid 1px globals.$border-color;
|
||||||
background-color: globals.$dark-background-color;
|
background-color: globals.$dark-background-color;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
max-width: 300px;
|
width: 320px;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
width: 100%;
|
width: 380px;
|
||||||
max-width: 400px;
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 35%;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
width: 40%;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/renderer/src/pages/game-details/sidebar/sidebar.tsx
Normal file → Executable file
3
src/renderer/src/pages/game-details/sidebar/sidebar.tsx
Normal file → Executable file
@@ -21,6 +21,7 @@ import { SidebarSection } from "../sidebar-section/sidebar-section";
|
|||||||
import { buildGameAchievementPath } from "@renderer/helpers";
|
import { buildGameAchievementPath } from "@renderer/helpers";
|
||||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||||
import "./sidebar.scss";
|
import "./sidebar.scss";
|
||||||
|
import { GameLanguageSection } from "./game-language-section";
|
||||||
|
|
||||||
const achievementsPlaceholder: UserAchievement[] = [
|
const achievementsPlaceholder: UserAchievement[] = [
|
||||||
{
|
{
|
||||||
@@ -263,6 +264,8 @@ export function Sidebar() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
|
|
||||||
|
<GameLanguageSection />
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(globals.$spacing-unit * 3);
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
padding: calc(globals.$spacing-unit * 3);
|
padding: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: calc(globals.$spacing-unit * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__buttons-list {
|
&__buttons-list {
|
||||||
@@ -27,25 +28,6 @@
|
|||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cards {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(1, 1fr);
|
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
|
||||||
transition: all ease 0.2s;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1250px) {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1600px) {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__card-skeleton {
|
&__card-skeleton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
@@ -94,10 +76,40 @@
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-flame-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
|
padding: 0 calc(globals.$spacing-unit * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
gap: calc(globals.$spacing-unit * 2);
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
padding: 0 calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 3);
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1250px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||||
<section className="home__content">
|
<section className="home__content">
|
||||||
<h2>{t("featured")}</h2>
|
|
||||||
|
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
<section className="home__header">
|
<section className="home__header">
|
||||||
@@ -160,7 +158,7 @@ export default function Home() {
|
|||||||
<img
|
<img
|
||||||
src={flameIconAnimated}
|
src={flameIconAnimated}
|
||||||
alt="Flame animation"
|
alt="Flame animation"
|
||||||
className="home__flame-icon"
|
className="home__title-flame-icon"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -31,10 +31,14 @@ export function FriendsBox() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="friends-box__section-header">
|
<div className="friends-box__section-header">
|
||||||
<h2>{t("friends")}</h2>
|
<div className="profile-content__section-title-group">
|
||||||
{userStats && (
|
<h2>{t("friends")}</h2>
|
||||||
<span>{numberFormatter.format(userStats.friendsCount)}</span>
|
{userStats && (
|
||||||
)}
|
<span className="profile-content__section-badge">
|
||||||
|
{numberFormatter.format(userStats.friendsCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="friends-box__box">
|
<div className="friends-box__box">
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export const sectionVariants = {
|
||||||
|
collapsed: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -20,
|
||||||
|
height: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: [0.25, 0.1, 0.25, 1],
|
||||||
|
opacity: { duration: 0.1 },
|
||||||
|
y: { duration: 0.1 },
|
||||||
|
height: { duration: 0.2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expanded: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
height: "auto",
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: [0.25, 0.1, 0.25, 1],
|
||||||
|
opacity: { duration: 0.2, delay: 0.1 },
|
||||||
|
y: { duration: 0.3 },
|
||||||
|
height: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gameCardVariants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20,
|
||||||
|
scale: 0.95,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.1, 0.25, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -20,
|
||||||
|
scale: 0.95,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: [0.25, 0.1, 0.25, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gameGridVariants = {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chevronVariants = {
|
||||||
|
collapsed: {
|
||||||
|
rotate: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "easeInOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expanded: {
|
||||||
|
rotate: 90,
|
||||||
|
transition: {
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "easeInOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||||
@@ -54,8 +54,74 @@
|
|||||||
&__section-header {
|
&__section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-title-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__collapse-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tab {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
padding: calc(globals.$spacing-unit) calc(globals.$spacing-unit * 2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: white;
|
||||||
|
border-bottom-color: #c9aa71;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__games-grid {
|
&__games-grid {
|
||||||
@@ -85,5 +151,29 @@
|
|||||||
@container #{globals.$app-container} (min-width: 3000px) {
|
@container #{globals.$app-container} (min-width: 3000px) {
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--drag-over {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.3);
|
||||||
|
position: relative;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "Drop here to " attr(data-action);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: globals.$muted-color;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { TelescopeIcon } from "@primer/octicons-react";
|
import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
import { ReportProfile } from "../report-profile/report-profile";
|
import { ReportProfile } from "../report-profile/report-profile";
|
||||||
@@ -11,15 +11,32 @@ import { FriendsBox } from "./friends-box";
|
|||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserStatsBox } from "./user-stats-box";
|
import { UserStatsBox } from "./user-stats-box";
|
||||||
import { UserLibraryGameCard } from "./user-library-game-card";
|
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||||
|
import { SortOptions } from "./sort-options";
|
||||||
|
import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
sectionVariants,
|
||||||
|
chevronVariants,
|
||||||
|
GAME_STATS_ANIMATION_DURATION_IN_MS,
|
||||||
|
} from "./profile-animations";
|
||||||
import "./profile-content.scss";
|
import "./profile-content.scss";
|
||||||
|
|
||||||
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
const {
|
||||||
|
userProfile,
|
||||||
|
isMe,
|
||||||
|
userStats,
|
||||||
|
libraryGames,
|
||||||
|
pinnedGames,
|
||||||
|
getUserLibraryGames,
|
||||||
|
} = useContext(userProfileContext);
|
||||||
const [statsIndex, setStatsIndex] = useState(0);
|
const [statsIndex, setStatsIndex] = useState(0);
|
||||||
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||||
|
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
|
||||||
const statsAnimation = useRef(-1);
|
const statsAnimation = useRef(-1);
|
||||||
|
const { toggleSection, isPinnedCollapsed } = useSectionCollapse();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -33,6 +50,12 @@ export function ProfileContent() {
|
|||||||
}
|
}
|
||||||
}, [userProfile, dispatch]);
|
}, [userProfile, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userProfile) {
|
||||||
|
getUserLibraryGames(sortBy);
|
||||||
|
}
|
||||||
|
}, [sortBy, getUserLibraryGames, userProfile]);
|
||||||
|
|
||||||
const handleOnMouseEnterGameCard = () => {
|
const handleOnMouseEnterGameCard = () => {
|
||||||
setIsAnimationRunning(false);
|
setIsAnimationRunning(false);
|
||||||
};
|
};
|
||||||
@@ -79,14 +102,21 @@ export function ProfileContent() {
|
|||||||
return <LockedProfile />;
|
return <LockedProfile />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasGames = userProfile?.libraryGames.length > 0;
|
const hasGames = libraryGames.length > 0;
|
||||||
|
const hasPinnedGames = pinnedGames.length > 0;
|
||||||
|
const hasAnyGames = hasGames || hasPinnedGames;
|
||||||
|
|
||||||
const shouldShowRightContent = hasGames || userProfile.friends.length > 0;
|
const shouldShowRightContent =
|
||||||
|
hasAnyGames || userProfile.friends.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="profile-content__section">
|
<section className="profile-content__section">
|
||||||
<div className="profile-content__main">
|
<div className="profile-content__main">
|
||||||
{!hasGames && (
|
{hasAnyGames && (
|
||||||
|
<SortOptions sortBy={sortBy} onSortChange={setSortBy} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasAnyGames && (
|
||||||
<div className="profile-content__no-games">
|
<div className="profile-content__no-games">
|
||||||
<div className="profile-content__telescope-icon">
|
<div className="profile-content__telescope-icon">
|
||||||
<TelescopeIcon size={24} />
|
<TelescopeIcon size={24} />
|
||||||
@@ -96,28 +126,95 @@ export function ProfileContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasGames && (
|
{hasAnyGames && (
|
||||||
<>
|
<div>
|
||||||
<div className="profile-content__section-header">
|
{hasPinnedGames && (
|
||||||
<h2>{t("library")}</h2>
|
<div style={{ marginBottom: "2rem" }}>
|
||||||
|
<div className="profile-content__section-header">
|
||||||
|
<div className="profile-content__section-title-group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="profile-content__collapse-button"
|
||||||
|
onClick={() => toggleSection("pinned")}
|
||||||
|
aria-label={
|
||||||
|
isPinnedCollapsed
|
||||||
|
? "Expand pinned section"
|
||||||
|
: "Collapse pinned section"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={chevronVariants}
|
||||||
|
animate={isPinnedCollapsed ? "collapsed" : "expanded"}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon size={16} />
|
||||||
|
</motion.div>
|
||||||
|
</button>
|
||||||
|
<h2>{t("pinned")}</h2>
|
||||||
|
<span className="profile-content__section-badge">
|
||||||
|
{pinnedGames.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{userStats && (
|
<AnimatePresence initial={true} mode="wait">
|
||||||
<span>{numberFormatter.format(userStats.libraryCount)}</span>
|
{!isPinnedCollapsed && (
|
||||||
)}
|
<motion.div
|
||||||
</div>
|
key="pinned-content"
|
||||||
|
variants={sectionVariants}
|
||||||
|
initial="collapsed"
|
||||||
|
animate="expanded"
|
||||||
|
exit="collapsed"
|
||||||
|
layout
|
||||||
|
>
|
||||||
|
<ul className="profile-content__games-grid">
|
||||||
|
{pinnedGames?.map((game) => (
|
||||||
|
<li
|
||||||
|
key={game.objectId}
|
||||||
|
style={{ listStyle: "none" }}
|
||||||
|
>
|
||||||
|
<UserLibraryGameCard
|
||||||
|
game={game}
|
||||||
|
statIndex={statsIndex}
|
||||||
|
onMouseEnter={handleOnMouseEnterGameCard}
|
||||||
|
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ul className="profile-content__games-grid">
|
{hasGames && (
|
||||||
{userProfile?.libraryGames?.map((game) => (
|
<div>
|
||||||
<UserLibraryGameCard
|
<div className="profile-content__section-header">
|
||||||
game={game}
|
<div className="profile-content__section-title-group">
|
||||||
key={game.objectId}
|
<h2>{t("library")}</h2>
|
||||||
statIndex={statsIndex}
|
{userStats && (
|
||||||
onMouseEnter={handleOnMouseEnterGameCard}
|
<span className="profile-content__section-badge">
|
||||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
{numberFormatter.format(userStats.libraryCount)}
|
||||||
/>
|
</span>
|
||||||
))}
|
)}
|
||||||
</ul>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
|
||||||
|
<ul className="profile-content__games-grid">
|
||||||
|
{libraryGames?.map((game) => (
|
||||||
|
<li key={game.objectId} style={{ listStyle: "none" }}>
|
||||||
|
<UserLibraryGameCard
|
||||||
|
game={game}
|
||||||
|
statIndex={statsIndex}
|
||||||
|
onMouseEnter={handleOnMouseEnterGameCard}
|
||||||
|
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,6 +236,11 @@ export function ProfileContent() {
|
|||||||
numberFormatter,
|
numberFormatter,
|
||||||
t,
|
t,
|
||||||
statsIndex,
|
statsIndex,
|
||||||
|
libraryGames,
|
||||||
|
pinnedGames,
|
||||||
|
isPinnedCollapsed,
|
||||||
|
toggleSection,
|
||||||
|
sortBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.sort-options {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(globals.$spacing-unit);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 300;
|
||||||
|
transition: all ease 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { TrophyIcon, ClockIcon, HistoryIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./sort-options.scss";
|
||||||
|
|
||||||
|
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
||||||
|
|
||||||
|
interface SortOptionsProps {
|
||||||
|
sortBy: SortOption;
|
||||||
|
onSortChange: (sortBy: SortOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortOptions({ sortBy, onSortChange }: SortOptionsProps) {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sort-options__container">
|
||||||
|
<span className="sort-options__label">Sort by:</span>
|
||||||
|
<div className="sort-options__options">
|
||||||
|
<button
|
||||||
|
className={`sort-options__option ${sortBy === "achievementCount" ? "active" : ""}`}
|
||||||
|
onClick={() => onSortChange("achievementCount")}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={16} />
|
||||||
|
<span>{t("achievements_earned")}</span>
|
||||||
|
</button>
|
||||||
|
<span className="sort-options__separator">|</span>
|
||||||
|
<button
|
||||||
|
className={`sort-options__option ${sortBy === "playedRecently" ? "active" : ""}`}
|
||||||
|
onClick={() => onSortChange("playedRecently")}
|
||||||
|
>
|
||||||
|
<HistoryIcon size={16} />
|
||||||
|
<span>{t("played_recently")}</span>
|
||||||
|
</button>
|
||||||
|
<span className="sort-options__separator">|</span>
|
||||||
|
<button
|
||||||
|
className={`sort-options__option ${sortBy === "playtime" ? "active" : ""}`}
|
||||||
|
onClick={() => onSortChange("playtime")}
|
||||||
|
>
|
||||||
|
<ClockIcon size={16} />
|
||||||
|
<span>{t("playtime")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user