mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 10:03:56 +00:00
Compare commits
136 Commits
v3.6.2
...
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 | ||
|
|
0adea20565 | ||
|
|
f182c7c8e9 | ||
|
|
d9379fbcb9 | ||
|
|
adc4af731e | ||
|
|
af1b3d4535 | ||
|
|
291935a1bc | ||
|
|
665365abdc | ||
|
|
86da92aa3f | ||
|
|
6ff694c078 | ||
|
|
a546b906e9 | ||
|
|
85b40b9a7e | ||
|
|
302ed92018 | ||
|
|
7e5cef6e44 | ||
|
|
01ac5239dc | ||
|
|
1dc2176813 | ||
|
|
a7ec632a21 | ||
|
|
2b6d8eba78 | ||
|
|
6bc6a10d66 | ||
|
|
51f8b12e13 | ||
|
|
957a6b512e | ||
|
|
8bc1c1c58c | ||
|
|
fbb67af1f6 | ||
|
|
4211f97dd0 | ||
|
|
f569b142f3 | ||
|
|
18e4baec3d | ||
|
|
b7199f4d95 | ||
|
|
2b8cc506df | ||
|
|
e0c5f80b68 | ||
|
|
63374ccd74 | ||
|
|
8eeacf478d | ||
|
|
87a4c27977 | ||
|
|
8177c3fd0c | ||
|
|
d28a5b828f | ||
|
|
68288adef3 | ||
|
|
51894d9924 | ||
|
|
2107261f7b | ||
|
|
737dc37433 | ||
|
|
6dcc6bfe56 | ||
|
|
714b30d6da | ||
|
|
6a94c3c812 | ||
|
|
c7ecd541d5 | ||
|
|
2dc700188d | ||
|
|
07ac13f4cc | ||
|
|
de605ecd02 | ||
|
|
81654d7688 | ||
|
|
14a8336695 |
@@ -1,5 +1,5 @@
|
|||||||
MAIN_VITE_API_URL=API_URL
|
MAIN_VITE_API_URL=
|
||||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
MAIN_VITE_AUTH_URL=
|
||||||
MAIN_VITE_WS_URL=
|
MAIN_VITE_WS_URL=
|
||||||
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
|
||||||
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
RENDERER_VITE_TORBOX_REFERRAL_CODE=
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "electron-vite";
|
} from "electron-vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
loadEnv(mode);
|
loadEnv(mode);
|
||||||
@@ -48,15 +47,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"@shared": resolve("src/shared"),
|
"@shared": resolve("src/shared"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [svgr(), react()],
|
||||||
svgr(),
|
|
||||||
react(),
|
|
||||||
sentryVitePlugin({
|
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
||||||
org: "hydra-launcher",
|
|
||||||
project: "hydra-renderer",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.6.2",
|
"version": "3.6.8",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -40,8 +40,6 @@
|
|||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@sentry/react": "^8.47.0",
|
|
||||||
"@sentry/vite-plugin": "^2.22.7",
|
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-cookiejar-support": "^5.0.5",
|
"axios-cookiejar-support": "^5.0.5",
|
||||||
@@ -56,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",
|
||||||
@@ -65,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",
|
||||||
|
|||||||
85
src/locales/en/translation.json
Normal file → Executable file
85
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",
|
||||||
@@ -89,6 +131,7 @@
|
|||||||
"amount_minutes": "{{amount}} minutes",
|
"amount_minutes": "{{amount}} minutes",
|
||||||
"accuracy": "{{accuracy}}% accuracy",
|
"accuracy": "{{accuracy}}% accuracy",
|
||||||
"add_to_library": "Add to library",
|
"add_to_library": "Add to library",
|
||||||
|
"already_in_library": "Already in library",
|
||||||
"remove_from_library": "Remove from library",
|
"remove_from_library": "Remove from library",
|
||||||
"no_downloads": "No downloads available",
|
"no_downloads": "No downloads available",
|
||||||
"play_time": "Played for {{amount}}",
|
"play_time": "Played for {{amount}}",
|
||||||
@@ -197,10 +240,20 @@
|
|||||||
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
"download_error_not_cached_on_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
"download_error_not_cached_on_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
|
"update_playtime_title": "Update playtime",
|
||||||
|
"update_playtime_description": "Manually update the playtime for {{game}}",
|
||||||
|
"update_playtime": "Update playtime",
|
||||||
|
"update_playtime_success": "Playtime updated successfully",
|
||||||
|
"update_playtime_error": "Failed to update playtime",
|
||||||
|
"update_game_playtime": "Update game playtime",
|
||||||
|
"manual_playtime_warning": "Your hours will be marked as manually updated, and this cannot be undone.",
|
||||||
|
"manual_playtime_tooltip": "This playtime has been manually updated",
|
||||||
"download_error_not_cached_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
|
"download_error_not_cached_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
|
||||||
"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",
|
||||||
@@ -219,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",
|
||||||
@@ -379,6 +446,7 @@
|
|||||||
"installing_common_redist": "Installing…",
|
"installing_common_redist": "Installing…",
|
||||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
||||||
"extract_files_by_default": "Extract files by default after download",
|
"extract_files_by_default": "Extract files by default after download",
|
||||||
|
"enable_steam_achievements": "Enable search for Steam achievements",
|
||||||
"achievement_custom_notification_position": "Achievement custom notification position",
|
"achievement_custom_notification_position": "Achievement custom notification position",
|
||||||
"top-left": "Top left",
|
"top-left": "Top left",
|
||||||
"top-center": "Top center",
|
"top-center": "Top center",
|
||||||
@@ -438,10 +506,17 @@
|
|||||||
"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",
|
||||||
"no_recent_activity_title": "Hmmm… nothing here",
|
"no_recent_activity_title": "Hmmm… nothing here",
|
||||||
"no_recent_activity_description": "You haven't played any games recently. It's time to change that!",
|
"no_recent_activity_description": "You haven't played any games recently. It's time to change that!",
|
||||||
"display_name": "Display name",
|
"display_name": "Display name",
|
||||||
@@ -516,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",
|
||||||
|
|||||||
59
src/locales/pt-BR/translation.json
Normal file → Executable file
59
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",
|
||||||
@@ -76,6 +90,7 @@
|
|||||||
"amount_minutes": "{{amount}} minutos",
|
"amount_minutes": "{{amount}} minutos",
|
||||||
"accuracy": "{{accuracy}}% de precisão",
|
"accuracy": "{{accuracy}}% de precisão",
|
||||||
"add_to_library": "Adicionar à biblioteca",
|
"add_to_library": "Adicionar à biblioteca",
|
||||||
|
"already_in_library": "Já está na biblioteca",
|
||||||
"remove_from_library": "Remover da biblioteca",
|
"remove_from_library": "Remover da biblioteca",
|
||||||
"no_downloads": "Nenhum download disponível",
|
"no_downloads": "Nenhum download disponível",
|
||||||
"play_time": "Jogou por {{amount}}",
|
"play_time": "Jogou por {{amount}}",
|
||||||
@@ -206,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",
|
||||||
@@ -364,6 +403,7 @@
|
|||||||
"installing_common_redist": "Instalando…",
|
"installing_common_redist": "Instalando…",
|
||||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
||||||
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
|
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
|
||||||
|
"enable_steam_achievements": "Habilitar busca por conquistas da Steam",
|
||||||
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
|
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
|
||||||
"top-left": "Superior esquerdo",
|
"top-left": "Superior esquerdo",
|
||||||
"top-center": "Superior central",
|
"top-center": "Superior central",
|
||||||
@@ -380,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",
|
||||||
@@ -509,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": "Поиск",
|
||||||
@@ -195,6 +234,10 @@
|
|||||||
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
|
||||||
"download_error_not_cached_on_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
"download_error_not_cached_on_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
|
||||||
|
"update_playtime_title": "Обновить время игры",
|
||||||
|
"update_playtime_description": "Вручную обновите время игры для {{game}}",
|
||||||
|
"update_playtime": "Обновить время игры",
|
||||||
|
"update_game_playtime": "Обновить время игры",
|
||||||
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
|
||||||
"game_added_to_favorites": "Игра добавлена в избранное",
|
"game_added_to_favorites": "Игра добавлена в избранное",
|
||||||
"game_removed_from_favorites": "Игра удалена из избранного",
|
"game_removed_from_favorites": "Игра удалена из избранного",
|
||||||
@@ -205,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",
|
||||||
@@ -381,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": "Загрузка завершена",
|
||||||
@@ -394,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",
|
||||||
@@ -424,10 +484,13 @@
|
|||||||
"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": "Библиотека",
|
||||||
"total_play_time": "Всего сыграно",
|
"total_play_time": "Всего сыграно",
|
||||||
|
"manual_playtime_tooltip": "Время игры было обновлено вручную",
|
||||||
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
"no_recent_activity_title": "Хммм... Тут ничего нет",
|
||||||
"no_recent_activity_description": "Вы давно ни во что не играли. Пора это изменить!",
|
"no_recent_activity_description": "Вы давно ни во что не играли. Пора это изменить!",
|
||||||
"display_name": "Отображаемое имя",
|
"display_name": "Отображаемое имя",
|
||||||
@@ -500,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",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"language_name": "中文",
|
"language_name": "简体中文",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "已成功登录"
|
"successfully_signed_in": "已成功登录"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "特色推荐",
|
|
||||||
"surprise_me": "向我推荐",
|
"surprise_me": "向我推荐",
|
||||||
"no_results": "没有找到结果",
|
"no_results": "没有找到结果",
|
||||||
"start_typing": "键入以开始搜素...",
|
"start_typing": "键入以开始搜素...",
|
||||||
@@ -26,7 +25,9 @@
|
|||||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||||
"sign_in": "登入",
|
"sign_in": "登入",
|
||||||
"friends": "好友",
|
"friends": "好友",
|
||||||
"favorites": "收藏"
|
"favorites": "收藏",
|
||||||
|
"need_help": "需要帮助?",
|
||||||
|
"playable_button_title": "仅显示现在可以游玩的游戏"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "搜索游戏",
|
"search": "搜索游戏",
|
||||||
@@ -43,11 +44,21 @@
|
|||||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
|
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
|
||||||
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间...",
|
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间...",
|
||||||
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)"
|
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)",
|
||||||
|
"installation_complete": "安装完成",
|
||||||
|
"installation_complete_message": "通用可再发行组件安装成功",
|
||||||
|
"installing_common_redist": "{{log}}…"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "下一页",
|
"clear_filters": "清除已选的 {{filterCount}} 项",
|
||||||
"previous_page": "上一页"
|
"developers": "开发商",
|
||||||
|
"download_sources": "下载源",
|
||||||
|
"filter_count": "{{filterCount}} 项可用",
|
||||||
|
"genres": "类型",
|
||||||
|
"publishers": "发行商",
|
||||||
|
"result_count": "{{resultCount}} 个结果",
|
||||||
|
"search": "筛选…",
|
||||||
|
"tags": "标签"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
"open_download_options": "打开下载菜单",
|
"open_download_options": "打开下载菜单",
|
||||||
@@ -166,7 +177,48 @@
|
|||||||
"manage_files_description": "管理哪些文件要备份和恢复",
|
"manage_files_description": "管理哪些文件要备份和恢复",
|
||||||
"select_folder": "选择文件夹",
|
"select_folder": "选择文件夹",
|
||||||
"backup_from": "{{date}} 时备份",
|
"backup_from": "{{date}} 时备份",
|
||||||
"custom_backup_location_set": "自定义备份文件位置"
|
"custom_backup_location_set": "自定义备份文件位置",
|
||||||
|
"artifact_name_label": "备份名称",
|
||||||
|
"artifact_name_placeholder": "为备份输入名称",
|
||||||
|
"artifact_renamed": "备份重命名成功",
|
||||||
|
"automatic_backup_from": "{{date}} 的自动备份",
|
||||||
|
"automatically_extract_downloaded_files": "自动解压下载的文件",
|
||||||
|
"backup_freeze_failed": "固定备份失败",
|
||||||
|
"backup_freeze_failed_description": "您必须至少保留一个空位用于自动备份",
|
||||||
|
"backup_frozen": "备份已固定",
|
||||||
|
"backup_unfrozen": "备份已取消固定",
|
||||||
|
"clear": "清除",
|
||||||
|
"create_start_menu_shortcut": "创建开始菜单快捷方式",
|
||||||
|
"create_steam_shortcut": "创建Steam快捷方式",
|
||||||
|
"download_error_gofile_quota_exceeded": "您已超出Gofile的月度配额。请等待配额重置。",
|
||||||
|
"download_error_not_cached_on_hydra": "此下载在Nimbus上不可用。",
|
||||||
|
"download_error_not_cached_on_real_debrid": "此下载在Real-Debrid上不可用,且暂不支持从Real-Debrid轮询下载状态。",
|
||||||
|
"download_error_not_cached_on_torbox": "此下载在TorBox上不可用,且暂不支持从TorBox轮询下载状态。",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "您的Real-Debrid账户未被授权进行新下载。请检查您的账户设置并重试。",
|
||||||
|
"enable_automatic_cloud_sync": "启用自动云同步",
|
||||||
|
"freeze_backup": "固定以免被自动备份覆盖",
|
||||||
|
"game_added_to_favorites": "游戏已添加到收藏",
|
||||||
|
"game_removed_from_favorites": "游戏已从收藏中移除",
|
||||||
|
"invalid_wine_prefix_path": "无效的Wine前置路径",
|
||||||
|
"invalid_wine_prefix_path_description": "Wine前置的路径无效。请检查路径并重试。",
|
||||||
|
"launch_options": "启动选项",
|
||||||
|
"launch_options_description": "高级用户可以选择修改启动选项(实验性功能)",
|
||||||
|
"launch_options_placeholder": "未指定参数",
|
||||||
|
"max_length_field": "此字段必须少于 {{length}} 个字符",
|
||||||
|
"missing_wine_prefix": "在Linux上创建备份需要Wine前置",
|
||||||
|
"no_directory_selected": "未选择目录",
|
||||||
|
"no_write_permission": "无法下载到此目录。点击此处了解更多。",
|
||||||
|
"rename_artifact": "重命名备份",
|
||||||
|
"rename_artifact_description": "将备份重命名为更具描述性的名称",
|
||||||
|
"required_field": "此字段为必填项",
|
||||||
|
"reset_achievements": "重置成就",
|
||||||
|
"reset_achievements_description": "这将重置 {{game}} 的所有成就",
|
||||||
|
"reset_achievements_error": "重置成就失败",
|
||||||
|
"reset_achievements_success": "成就重置成功",
|
||||||
|
"reset_achievements_title": "您确定吗?",
|
||||||
|
"save_changes": "保存更改",
|
||||||
|
"unfreeze_backup": "取消固定",
|
||||||
|
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "激活 Hydra",
|
"title": "激活 Hydra",
|
||||||
@@ -199,7 +251,13 @@
|
|||||||
"queued": "下载列表",
|
"queued": "下载列表",
|
||||||
"no_downloads_title": "空空如也",
|
"no_downloads_title": "空空如也",
|
||||||
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。",
|
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。",
|
||||||
"checking_files": "正在校验文件…"
|
"checking_files": "正在校验文件…",
|
||||||
|
"extract": "解压文件",
|
||||||
|
"extracting": "正在解压文件…",
|
||||||
|
"options": "管理",
|
||||||
|
"resume_seeding": "恢复做种",
|
||||||
|
"seeding": "做种中",
|
||||||
|
"stop_seeding": "停止做种"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "下载路径",
|
"downloads_path": "下载路径",
|
||||||
@@ -260,7 +318,83 @@
|
|||||||
"must_be_valid_url": "来源必须是有效的 URL",
|
"must_be_valid_url": "来源必须是有效的 URL",
|
||||||
"blocked_users": "已屏蔽用户",
|
"blocked_users": "已屏蔽用户",
|
||||||
"user_unblocked": "用户已经被屏蔽",
|
"user_unblocked": "用户已经被屏蔽",
|
||||||
"enable_achievement_notifications": "当成就解锁时"
|
"enable_achievement_notifications": "当成就解锁时",
|
||||||
|
"account": "账户",
|
||||||
|
"account_data_updated_successfully": "账户数据更新成功",
|
||||||
|
"achievement_custom_notification_position": "成就自定义通知位置",
|
||||||
|
"alignment": "对齐",
|
||||||
|
"appearance": "外观",
|
||||||
|
"become_subscriber": "成为Hydra Cloud用户",
|
||||||
|
"bill_sent_until": "您的下一张账单将在此日期前发送",
|
||||||
|
"bottom-center": "底部中央",
|
||||||
|
"bottom-left": "底部左侧",
|
||||||
|
"bottom-right": "底部右侧",
|
||||||
|
"cancel": "取消",
|
||||||
|
"clear_themes": "清除",
|
||||||
|
"common_redist": "通用可再发行组件",
|
||||||
|
"common_redist_description": "运行某些游戏需要通用可再发行组件。建议安装以避免问题。",
|
||||||
|
"create_real_debrid_account": "如果您还没有Real-Debrid账户,请点击此处",
|
||||||
|
"create_theme": "创建",
|
||||||
|
"create_theme_modal_description": "创建新主题以自定义Hydra的外观",
|
||||||
|
"create_theme_modal_title": "创建自定义主题",
|
||||||
|
"create_torbox_account": "如果您还没有TorBox账户,请点击此处",
|
||||||
|
"current_email": "当前邮箱:",
|
||||||
|
"default": "默认",
|
||||||
|
"delete_all_themes": "删除所有主题",
|
||||||
|
"delete_all_themes_description": "这将删除所有您的自定义主题",
|
||||||
|
"delete_theme": "删除主题",
|
||||||
|
"delete_theme_description": "这将删除主题 {{theme}}",
|
||||||
|
"disable_nsfw_alert": "禁用NSFW警告",
|
||||||
|
"edit_theme": "编辑主题",
|
||||||
|
"editor_tab_code": "代码",
|
||||||
|
"editor_tab_info": "信息",
|
||||||
|
"editor_tab_save": "保存",
|
||||||
|
"enable_achievement_custom_notifications": "启用成就自定义通知",
|
||||||
|
"enable_auto_install": "自动下载更新",
|
||||||
|
"enable_friend_request_notifications": "当收到好友请求时",
|
||||||
|
"enable_friend_start_game_notifications": "当好友开始游戏时",
|
||||||
|
"enable_torbox": "启用TorBox",
|
||||||
|
"error_importing_theme": "导入主题时出错",
|
||||||
|
"extract_files_by_default": "下载后默认解压文件",
|
||||||
|
"hidden": "隐藏",
|
||||||
|
"import_theme": "导入主题",
|
||||||
|
"import_theme_description": "您将从主题商店导入 {{theme}}",
|
||||||
|
"insert_theme_name": "输入主题名称",
|
||||||
|
"install_common_redist": "安装",
|
||||||
|
"installing_common_redist": "正在安装…",
|
||||||
|
"launch_minimized": "最小化启动Hydra",
|
||||||
|
"manage_subscription": "管理订阅",
|
||||||
|
"name_min_length": "主题名称必须至少3个字符长",
|
||||||
|
"no_email_account": "您尚未设置邮箱",
|
||||||
|
"no_subscription": "以最佳方式享受Hydra",
|
||||||
|
"no_themes": "看起来您还没有任何主题,但别担心,点击这里创建您的第一个杰作。",
|
||||||
|
"no_users_blocked": "您没有屏蔽任何用户",
|
||||||
|
"notification_preview": "成就通知预览",
|
||||||
|
"platinum": "白金",
|
||||||
|
"rare": "稀有",
|
||||||
|
"real_debrid_account_linked": "Real-Debrid账户已连接",
|
||||||
|
"renew_subscription": "续费Hydra Cloud",
|
||||||
|
"seed_after_download_complete": "下载完成后做种",
|
||||||
|
"set_theme": "设置主题",
|
||||||
|
"show_download_speed_in_megabytes": "以兆字节每秒显示下载速度",
|
||||||
|
"show_hidden_achievement_description": "在解锁前显示隐藏成就描述",
|
||||||
|
"subscription_active_until": "您的Hydra Cloud活跃至 {{date}}",
|
||||||
|
"subscription_expired_at": "您的订阅已于 {{date}} 到期",
|
||||||
|
"subscription_renew_cancelled": "自动续费已禁用",
|
||||||
|
"subscription_renews_on": "您的订阅将于 {{date}} 续费",
|
||||||
|
"test_notification": "测试通知",
|
||||||
|
"theme_imported": "主题导入成功",
|
||||||
|
"theme_name": "名称",
|
||||||
|
"top-center": "顶部中央",
|
||||||
|
"top-left": "顶部左侧",
|
||||||
|
"top-right": "顶部右侧",
|
||||||
|
"torbox_account_linked": "TorBox账户已连接",
|
||||||
|
"torbox_description": "TorBox是您的高级种子盒服务,甚至可与市场上最好的服务器相媲美。",
|
||||||
|
"unset_theme": "取消设置主题",
|
||||||
|
"update_email": "更新邮箱",
|
||||||
|
"update_password": "更新密码",
|
||||||
|
"variation": "变体",
|
||||||
|
"web_store": "网络商店"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "下载完成",
|
"download_complete": "下载完成",
|
||||||
@@ -271,14 +405,23 @@
|
|||||||
"new_update_available": "版本 {{version}} 可用",
|
"new_update_available": "版本 {{version}} 可用",
|
||||||
"restart_to_install_update": "重启 Hydra 以安装更新",
|
"restart_to_install_update": "重启 Hydra 以安装更新",
|
||||||
"notification_achievement_unlocked_title": "{{game}} 的成绩已解锁",
|
"notification_achievement_unlocked_title": "{{game}} 的成绩已解锁",
|
||||||
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁"
|
"notification_achievement_unlocked_body": "{{achievement}} 和其他 {{count}} 已解锁",
|
||||||
|
"extraction_complete": "解压完成",
|
||||||
|
"friend_started_playing_game": "{{displayName}} 开始玩游戏",
|
||||||
|
"game_extracted": "{{title}} 解压成功",
|
||||||
|
"new_friend_request_description": "{{displayName}} 向您发送了好友请求",
|
||||||
|
"new_friend_request_title": "新好友请求",
|
||||||
|
"test_achievement_notification_description": "非常酷,对吧?",
|
||||||
|
"test_achievement_notification_title": "这是一个测试通知"
|
||||||
},
|
},
|
||||||
"system_tray": {
|
"system_tray": {
|
||||||
"open": "打开 Hydra",
|
"open": "打开 Hydra",
|
||||||
"quit": "退出"
|
"quit": "退出"
|
||||||
},
|
},
|
||||||
"game_card": {
|
"game_card": {
|
||||||
"no_downloads": "无可用下载选项"
|
"no_downloads": "无可用下载选项",
|
||||||
|
"available_one": "可用",
|
||||||
|
"available_other": "可用"
|
||||||
},
|
},
|
||||||
"binary_not_found_modal": {
|
"binary_not_found_modal": {
|
||||||
"title": "程序未安装",
|
"title": "程序未安装",
|
||||||
@@ -351,7 +494,7 @@
|
|||||||
"report_description": "额外信息",
|
"report_description": "额外信息",
|
||||||
"report_description_placeholder": "额外信息",
|
"report_description_placeholder": "额外信息",
|
||||||
"report": "举报",
|
"report": "举报",
|
||||||
"report_reason_hate": "Hate speech",
|
"report_reason_hate": "仇恨言论",
|
||||||
"report_reason_sexual_content": "色情内容",
|
"report_reason_sexual_content": "色情内容",
|
||||||
"report_reason_violence": "暴力",
|
"report_reason_violence": "暴力",
|
||||||
"report_reason_spam": "骚扰",
|
"report_reason_spam": "骚扰",
|
||||||
@@ -360,7 +503,19 @@
|
|||||||
"your_friend_code": "你的好友代码:",
|
"your_friend_code": "你的好友代码:",
|
||||||
"upload_banner": "上传横幅",
|
"upload_banner": "上传横幅",
|
||||||
"uploading_banner": "上传横幅中…",
|
"uploading_banner": "上传横幅中…",
|
||||||
"background_image_updated": "背景图片已更新"
|
"background_image_updated": "背景图片已更新",
|
||||||
|
"achievements": "成就",
|
||||||
|
"achievements_unlocked": "成就已解锁",
|
||||||
|
"earned_points": "获得积分",
|
||||||
|
"error_adding_friend": "无法发送好友请求。请检查好友代码",
|
||||||
|
"friend_code_length_error": "好友代码必须为8个字符",
|
||||||
|
"games": "游戏",
|
||||||
|
"playing": "正在玩 {{game}}",
|
||||||
|
"ranking_updated_weekly": "排名每周更新",
|
||||||
|
"show_achievements_on_profile": "在您的个人资料上显示成就",
|
||||||
|
"show_points_on_profile": "在您的个人资料上显示获得的积分",
|
||||||
|
"stats": "统计",
|
||||||
|
"top_percentile": "前 {{percentile}}%"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "成就已解锁",
|
"achievement_unlocked": "成就已解锁",
|
||||||
@@ -368,7 +523,14 @@
|
|||||||
"your_achievements": "你的成就",
|
"your_achievements": "你的成就",
|
||||||
"unlocked_at": "解锁于: {{date}}",
|
"unlocked_at": "解锁于: {{date}}",
|
||||||
"subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容",
|
"subscription_needed": "需要订阅 Hydra Cloud 才能看到此内容",
|
||||||
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就"
|
"new_achievements_unlocked": "从 {{gameCount}} 游戏中解锁 {{achievementCount}} 新成就",
|
||||||
|
"achievement_earn_points": "通过此成就获得 {{points}} 积分",
|
||||||
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} 成就",
|
||||||
|
"achievements_unlocked_for_game": "为 {{gameTitle}} 解锁了 {{achievementCount}} 个新成就",
|
||||||
|
"available_points": "可用积分:",
|
||||||
|
"earned_points": "获得积分:",
|
||||||
|
"hidden_achievement_tooltip": "这是一个隐藏成就",
|
||||||
|
"how_to_earn_achievements_points": "如何获得成就积分?"
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"hydra_cloud": {
|
||||||
"subscription_tour_title": "Hydra 云订阅",
|
"subscription_tour_title": "Hydra 云订阅",
|
||||||
@@ -378,6 +540,10 @@
|
|||||||
"animated_profile_picture": "动画头像",
|
"animated_profile_picture": "动画头像",
|
||||||
"premium_support": "高级技术支持",
|
"premium_support": "高级技术支持",
|
||||||
"show_and_compare_achievements": "展示并与其他用户比较您的成就",
|
"show_and_compare_achievements": "展示并与其他用户比较您的成就",
|
||||||
"animated_profile_banner": "动态个人简介横幅"
|
"animated_profile_banner": "动态个人简介横幅",
|
||||||
|
"debrid_description": "使用Nimbus下载速度提升4倍",
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"hydra_cloud_feature_found": "您刚刚发现了一个Hydra Cloud功能!",
|
||||||
|
"learn_more": "了解更多"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,4 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
|
|||||||
|
|
||||||
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
|
||||||
|
|
||||||
export const MAIN_LOOP_INTERVAL = 1500;
|
export const MAIN_LOOP_INTERVAL = 2000;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -33,9 +37,12 @@ import "./library/remove-game";
|
|||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./library/select-game-wine-prefix";
|
import "./library/select-game-wine-prefix";
|
||||||
import "./library/reset-game-achievements";
|
import "./library/reset-game-achievements";
|
||||||
|
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";
|
||||||
@@ -44,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";
|
||||||
@@ -63,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);
|
||||||
|
|||||||
33
src/main/events/library/change-game-playtime.ts
Normal file
33
src/main/events/library/change-game-playtime.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
|
const changeGamePlaytime = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
playTimeInSeconds: number
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
if (game.remoteId) {
|
||||||
|
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
||||||
|
playTimeInSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
||||||
|
hasManuallyUpdatedPlaytime: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update game playtime: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("changeGamePlayTime", changeGamePlaytime);
|
||||||
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);
|
||||||
@@ -94,7 +94,7 @@ const createSteamShortcut = async (
|
|||||||
|
|
||||||
if (!steamUserIds.length) {
|
if (!steamUserIds.length) {
|
||||||
logger.error("No Steam user ID found");
|
logger.error("No Steam user ID found");
|
||||||
return;
|
throw new Error("No Steam user ID found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const getUnlockedAchievementsEvent = async (
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
): Promise<UserAchievement[]> => {
|
): Promise<UserAchievement[]> => {
|
||||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||||
return getUnlockedAchievements(objectId, shop, false);
|
return getUnlockedAchievements(objectId, shop, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Downloader } from "@shared";
|
|||||||
import { levelKeys, db } from "./level";
|
import { levelKeys, db } from "./level";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import {
|
import {
|
||||||
WSClient,
|
|
||||||
SystemPath,
|
SystemPath,
|
||||||
CommonRedistManager,
|
CommonRedistManager,
|
||||||
TorBoxClient,
|
TorBoxClient,
|
||||||
@@ -47,7 +46,7 @@ export const loadState = async () => {
|
|||||||
|
|
||||||
await HydraApi.setupApi().then(() => {
|
await HydraApi.setupApi().then(() => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
WSClient.connect();
|
// WSClient.connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloads = await downloadsSublevel
|
const downloads = await downloadsSublevel
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mergeAchievements } from "./merge-achievements";
|
|||||||
import fs, { readdirSync } from "node:fs";
|
import fs, { readdirSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
findAchievementFileInExecutableDirectory,
|
findAchievementFileInExecutableDirectory,
|
||||||
|
findAchievementFileInSteamPath,
|
||||||
findAchievementFiles,
|
findAchievementFiles,
|
||||||
findAllAchievementFiles,
|
findAllAchievementFiles,
|
||||||
getAlternativeObjectIds,
|
getAlternativeObjectIds,
|
||||||
@@ -43,10 +44,14 @@ const watchAchievementsWindows = async () => {
|
|||||||
gameAchievementFiles.push(
|
gameAchievementFiles.push(
|
||||||
...findAchievementFileInExecutableDirectory(game)
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(
|
||||||
|
...(await findAchievementFileInSteamPath(game))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of gameAchievementFiles) {
|
for (const file of gameAchievementFiles) {
|
||||||
compareFile(game, file);
|
await compareFile(game, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -61,13 +66,11 @@ const watchAchievementsWithWine = async () => {
|
|||||||
|
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
const achievementFileInsideDirectory =
|
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||||
|
|
||||||
for (const file of gameAchievementFiles) {
|
for (const file of gameAchievementFiles) {
|
||||||
compareFile(game, file);
|
await compareFile(game, file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -167,15 +170,14 @@ export class AchievementWatcherManager {
|
|||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
if (this.alreadySyncedGames.get(gameKey)) return;
|
if (this.alreadySyncedGames.get(gameKey)) return;
|
||||||
|
|
||||||
|
this.alreadySyncedGames.set(gameKey, true);
|
||||||
|
|
||||||
const game = await gamesSublevel.get(gameKey).catch(() => null);
|
const game = await gamesSublevel.get(gameKey).catch(() => null);
|
||||||
if (!game || game.isDeleted) return;
|
if (!game || game.isDeleted) return;
|
||||||
|
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
||||||
const achievementFileInsideDirectory =
|
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
|
||||||
|
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
@@ -190,8 +192,6 @@ export class AchievementWatcherManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.alreadySyncedGames.set(gameKey, true);
|
|
||||||
|
|
||||||
const newAchievements = await mergeAchievements(
|
const newAchievements = await mergeAchievements(
|
||||||
game,
|
game,
|
||||||
unlockedAchievements,
|
unlockedAchievements,
|
||||||
@@ -259,7 +259,7 @@ export class AchievementWatcherManager {
|
|||||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
games.map((game) => {
|
games.map(async (game) => {
|
||||||
const achievementFiles: AchievementFile[] = [];
|
const achievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
@@ -270,6 +270,10 @@ export class AchievementWatcherManager {
|
|||||||
achievementFiles.push(
|
achievementFiles.push(
|
||||||
...findAchievementFileInExecutableDirectory(game)
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
achievementFiles.push(
|
||||||
|
...(await findAchievementFileInSteamPath(game))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { game, achievementFiles };
|
return { game, achievementFiles };
|
||||||
@@ -284,12 +288,10 @@ export class AchievementWatcherManager {
|
|||||||
.then((games) => games.filter((game) => !game.isDeleted));
|
.then((games) => games.filter((game) => !game.isDeleted));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
games.map((game) => {
|
games.map(async (game) => {
|
||||||
const achievementFiles = findAchievementFiles(game);
|
const achievementFiles = findAchievementFiles(game);
|
||||||
const achievementFileInsideDirectory =
|
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
achievementFiles.push(...achievementFileInsideDirectory);
|
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||||
|
|
||||||
return { game, achievementFiles };
|
return { game, achievementFiles };
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import type { Game, AchievementFile } from "@types";
|
import type { Game, AchievementFile, UserPreferences } from "@types";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { SystemPath } from "../system-path";
|
import { SystemPath } from "../system-path";
|
||||||
|
import { getSteamLocation, getSteamUsersIds } from "../steam";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getAppDataPath = () => {
|
const getAppDataPath = () => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@@ -270,6 +272,55 @@ export const findAchievementFiles = (game: Game) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
|
return achievementFiles.concat(achievementFileInsideDirectory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const steamUserIds = await getSteamUsersIds();
|
||||||
|
const steamPath = await getSteamLocation().catch(() => null);
|
||||||
|
|
||||||
|
export const findAchievementFileInSteamPath = async (game: Game) => {
|
||||||
|
if (!steamUserIds.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!steamPath) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userPreferences?.enableSteamAchievements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const achievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
|
for (const steamUserId of steamUserIds) {
|
||||||
|
const gameAchievementPath = path.join(
|
||||||
|
steamPath,
|
||||||
|
"userdata",
|
||||||
|
steamUserId.toString(),
|
||||||
|
"config",
|
||||||
|
"librarycache",
|
||||||
|
`${game.objectId}.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(gameAchievementPath)) {
|
||||||
|
achievementFiles.push({
|
||||||
|
type: Cracker.Steam,
|
||||||
|
filePath: gameAchievementPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return achievementFiles;
|
return achievementFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ export const parseAchievementFile = (
|
|||||||
return processRazor1911(filePath);
|
return processRazor1911(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === Cracker.Steam) {
|
||||||
|
const parsed = jsonParse(filePath);
|
||||||
|
return processSteamCacheAchievement(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
`Unprocessed ${type} achievements found on ${filePath}`
|
`Unprocessed ${type} achievements found on ${filePath}`
|
||||||
);
|
);
|
||||||
@@ -234,6 +239,35 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
|||||||
return newUnlockedAchievements;
|
return newUnlockedAchievements;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processSteamCacheAchievement = (
|
||||||
|
unlockedAchievements: any[]
|
||||||
|
): UnlockedAchievement[] => {
|
||||||
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
const achievementIndex = unlockedAchievements.findIndex(
|
||||||
|
(element) => element[0] === "achievements"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (achievementIndex === -1) {
|
||||||
|
achievementsLogger.info("No achievements found in Steam cache file");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockedAchievementsData =
|
||||||
|
unlockedAchievements[achievementIndex][1]["data"]["vecHighlight"];
|
||||||
|
|
||||||
|
for (const achievement of unlockedAchievementsData) {
|
||||||
|
if (achievement.bAchieved) {
|
||||||
|
newUnlockedAchievements.push({
|
||||||
|
name: achievement.strID,
|
||||||
|
unlockTime: achievement.rtUnlocked * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUnlockedAchievements;
|
||||||
|
};
|
||||||
|
|
||||||
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
|
const process3DM = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||||
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
const newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { getUserData } from "./user/get-user-data";
|
|||||||
import { db } from "@main/level";
|
import { db } from "@main/level";
|
||||||
import { levelKeys } from "@main/level/sublevels";
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
import type { Auth, User } from "@types";
|
import type { Auth, User } from "@types";
|
||||||
import { WSClient } from "./ws/ws-client";
|
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
@@ -103,8 +102,8 @@ export class HydraApi {
|
|||||||
WindowManager.mainWindow.webContents.send("on-signin");
|
WindowManager.mainWindow.webContents.send("on-signin");
|
||||||
await clearGamesRemoteIds();
|
await clearGamesRemoteIds();
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
WSClient.close();
|
// WSClient.close();
|
||||||
WSClient.connect();
|
// WSClient.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ type ProfileGame = {
|
|||||||
id: string;
|
id: string;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
|
hasManuallyUpdatedPlaytime: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
isPinned?: boolean;
|
||||||
} & ShopAssets;
|
} & ShopAssets;
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
@@ -35,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, {
|
||||||
@@ -43,17 +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,
|
||||||
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(() => {});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CloudSync } from "./cloud-sync";
|
|||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||||
|
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -25,7 +26,7 @@ interface GameExecutables {
|
|||||||
[key: string]: ExecutableInfo[];
|
[key: string]: ExecutableInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TICKS_TO_UPDATE_API = 120;
|
const TICKS_TO_UPDATE_API = (3 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 3 minutes
|
||||||
let currentTick = 1;
|
let currentTick = 1;
|
||||||
|
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export const getSteamLocation = async () => {
|
|||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
reject(new Error("SteamPath not found in registry"));
|
||||||
|
}
|
||||||
|
|
||||||
resolve(value.value);
|
resolve(value.value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -78,9 +82,19 @@ export const getSteamAppDetails = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getSteamUsersIds = async () => {
|
export const getSteamUsersIds = async () => {
|
||||||
const userDataPath = await getSteamLocation();
|
const steamLocation = await getSteamLocation().catch(() => null);
|
||||||
|
|
||||||
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
|
if (!steamLocation) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataPath = path.join(steamLocation, "userdata");
|
||||||
|
|
||||||
|
if (!fs.existsSync(userDataPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = fs.readdirSync(userDataPath, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ export class WindowManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.mainWindow.on("close", async () => {
|
this.mainWindow.on("close", async () => {
|
||||||
|
const mainWindow = this.mainWindow;
|
||||||
|
this.mainWindow = null;
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
@@ -188,9 +191,11 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.mainWindow) {
|
if (mainWindow) {
|
||||||
const lastBounds = this.mainWindow.getBounds();
|
mainWindow.setProgressBar(-1);
|
||||||
const isMaximized = this.mainWindow.isMaximized() ?? false;
|
|
||||||
|
const lastBounds = mainWindow.getBounds();
|
||||||
|
const isMaximized = mainWindow.isMaximized() ?? false;
|
||||||
const screenConfig = isMaximized
|
const screenConfig = isMaximized
|
||||||
? {
|
? {
|
||||||
x: undefined,
|
x: undefined,
|
||||||
@@ -207,9 +212,6 @@ export class WindowManager {
|
|||||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
|
||||||
WindowManager.mainWindow = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
|
||||||
@@ -582,9 +584,9 @@ export class WindowManager {
|
|||||||
tray.popUpContextMenu(contextMenu);
|
tray.popUpContextMenu(contextMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
tray.setToolTip("Hydra");
|
tray.setToolTip("Hydra Launcher");
|
||||||
|
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform === "win32") {
|
||||||
await updateSystemTray();
|
await updateSystemTray();
|
||||||
|
|
||||||
tray.addListener("double-click", () => {
|
tray.addListener("double-click", () => {
|
||||||
@@ -595,6 +597,18 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tray.addListener("right-click", showContextMenu);
|
||||||
|
} else if (process.platform === "linux") {
|
||||||
|
await updateSystemTray();
|
||||||
|
|
||||||
|
tray.addListener("click", () => {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.show();
|
||||||
|
} else {
|
||||||
|
this.createMainWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tray.addListener("right-click", showContextMenu);
|
tray.addListener("right-click", showContextMenu);
|
||||||
} else {
|
} else {
|
||||||
tray.addListener("click", showContextMenu);
|
tray.addListener("click", showContextMenu);
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -189,6 +237,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||||
|
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
|
||||||
|
ipcRenderer.invoke("changeGamePlayTime", shop, objectId, playtime),
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
ipcRenderer.invoke("extractGameDownload", shop, objectId),
|
||||||
getDefaultWinePrefixSelectionPath: () =>
|
getDefaultWinePrefixSelectionPath: () =>
|
||||||
@@ -364,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) =>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Hydra</title>
|
<title>Hydra Launcher</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' 'unsafe-inline' * data: local:;"
|
content="default-src 'self' 'unsafe-inline' * data: local:;"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
56
src/renderer/src/declaration.d.ts
vendored
56
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,
|
||||||
@@ -162,6 +205,11 @@ declare global {
|
|||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||||
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
|
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
|
changeGamePlayTime: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
playtimeInSeconds: number
|
||||||
|
) => Promise<void>;
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||||
@@ -262,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 */
|
||||||
@@ -282,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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import type {
|
|||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
@@ -29,8 +28,6 @@ export function useUserDetails() {
|
|||||||
} = useAppSelector((state) => state.userDetails);
|
} = useAppSelector((state) => state.userDetails);
|
||||||
|
|
||||||
const clearUserDetails = useCallback(async () => {
|
const clearUserDetails = useCallback(async () => {
|
||||||
Sentry.setUser(null);
|
|
||||||
|
|
||||||
dispatch(setUserDetails(null));
|
dispatch(setUserDetails(null));
|
||||||
dispatch(setProfileBackground(null));
|
dispatch(setProfileBackground(null));
|
||||||
|
|
||||||
@@ -45,12 +42,6 @@ export function useUserDetails() {
|
|||||||
|
|
||||||
const updateUserDetails = useCallback(
|
const updateUserDetails = useCallback(
|
||||||
async (userDetails: UserDetails) => {
|
async (userDetails: UserDetails) => {
|
||||||
Sentry.setUser({
|
|
||||||
id: userDetails.id,
|
|
||||||
username: userDetails.username,
|
|
||||||
email: userDetails.email ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
|
window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import resources from "@locales";
|
|||||||
|
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { addCookieInterceptor } from "./cookies";
|
import { addCookieInterceptor } from "./cookies";
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import Catalogue from "./pages/catalogue/catalogue";
|
import Catalogue from "./pages/catalogue/catalogue";
|
||||||
import Home from "./pages/home/home";
|
import Home from "./pages/home/home";
|
||||||
import Downloads from "./pages/downloads/downloads";
|
import Downloads from "./pages/downloads/downloads";
|
||||||
@@ -32,18 +31,6 @@ import Achievements from "./pages/achievements/achievements";
|
|||||||
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
import ThemeEditor from "./pages/theme-editor/theme-editor";
|
||||||
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
|
|
||||||
integrations: [
|
|
||||||
Sentry.browserTracingIntegration(),
|
|
||||||
Sentry.replayIntegration(),
|
|
||||||
],
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
replaysSessionSampleRate: 0.1,
|
|
||||||
replaysOnErrorSampleRate: 1.0,
|
|
||||||
release: await window.electron.getVersion(),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log = logger.log;
|
console.log = logger.log;
|
||||||
|
|
||||||
const isStaging = await window.electron.isStaging();
|
const isStaging = await window.electron.isStaging();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -16,6 +16,25 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
.game-item__plus-wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__plus-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__plus-wrapper--added {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__cover {
|
&__cover {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Badge } from "@renderer/components";
|
import { Badge } from "@renderer/components";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { useAppSelector, useRepacks } from "@renderer/hooks";
|
import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import "./game-item.scss";
|
import "./game-item.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CatalogueSearchResult } from "@types";
|
import { CatalogueSearchResult } from "@types";
|
||||||
import { QuestionIcon } from "@primer/octicons-react";
|
import { QuestionIcon, PlusIcon, CheckIcon } from "@primer/octicons-react";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
export interface GameItemProps {
|
export interface GameItemProps {
|
||||||
game: CatalogueSearchResult;
|
game: CatalogueSearchResult;
|
||||||
@@ -16,7 +17,9 @@ export interface GameItemProps {
|
|||||||
export function GameItem({ game }: GameItemProps) {
|
export function GameItem({ game }: GameItemProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n, t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const language = i18n.language.split("-")[0];
|
||||||
|
|
||||||
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
|
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
|
||||||
|
|
||||||
@@ -24,7 +27,41 @@ export function GameItem({ game }: GameItemProps) {
|
|||||||
|
|
||||||
const repacks = getRepacksForObjectId(game.objectId);
|
const repacks = getRepacksForObjectId(game.objectId);
|
||||||
|
|
||||||
const language = i18n.language.split("-")[0];
|
const [isAddingToLibrary, setIsAddingToLibrary] = useState(false);
|
||||||
|
|
||||||
|
const [added, setAdded] = useState(false);
|
||||||
|
|
||||||
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const exists = library.some(
|
||||||
|
(libItem) =>
|
||||||
|
libItem.shop === game.shop && libItem.objectId === game.objectId
|
||||||
|
);
|
||||||
|
setAdded(exists);
|
||||||
|
}, [library, game.shop, game.objectId]);
|
||||||
|
|
||||||
|
const addGameToLibrary = async (
|
||||||
|
event: React.MouseEvent | React.KeyboardEvent
|
||||||
|
) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (added || isAddingToLibrary) return;
|
||||||
|
|
||||||
|
setIsAddingToLibrary(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electron.addGameToLibrary(
|
||||||
|
game.shop,
|
||||||
|
game.objectId,
|
||||||
|
game.title
|
||||||
|
);
|
||||||
|
updateLibrary();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsAddingToLibrary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const uniqueRepackers = useMemo(() => {
|
const uniqueRepackers = useMemo(() => {
|
||||||
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
|
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
|
||||||
@@ -36,7 +73,11 @@ export function GameItem({ game }: GameItemProps) {
|
|||||||
(steamGenre) => steamGenre === genre
|
(steamGenre) => steamGenre === genre
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index && steamGenres[language] && steamGenres[language][index]) {
|
if (
|
||||||
|
index !== undefined &&
|
||||||
|
steamGenres[language] &&
|
||||||
|
steamGenres[language][index]
|
||||||
|
) {
|
||||||
return steamGenres[language][index];
|
return steamGenres[language][index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +122,23 @@ export function GameItem({ game }: GameItemProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn("game-item__plus-wrapper", {
|
||||||
|
"game-item__plus-wrapper--added": added,
|
||||||
|
})}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={addGameToLibrary}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
addGameToLibrary(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={added ? t("already_in_library") : t("add_to_library")}
|
||||||
|
>
|
||||||
|
{added ? <CheckIcon size={16} /> : <PlusIcon size={16} />}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -12,4 +12,16 @@
|
|||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__play-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__manual-warning {
|
||||||
|
color: #f59e0b; // Warning amber color
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import { useDate, useDownload, useFormat } from "@renderer/hooks";
|
|||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import { AlertFillIcon } from "@primer/octicons-react";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
import "./hero-panel-playtime.scss";
|
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");
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
@@ -85,7 +88,26 @@ export function HeroPanelPlaytime() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p
|
||||||
|
className="hero-panel-playtime__play-time"
|
||||||
|
data-tooltip-place="right"
|
||||||
|
data-tooltip-content={
|
||||||
|
game.hasManuallyUpdatedPlaytime
|
||||||
|
? t("manual_playtime_tooltip")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
data-tooltip-id={
|
||||||
|
game.hasManuallyUpdatedPlaytime
|
||||||
|
? "manual-playtime-warning"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{game.hasManuallyUpdatedPlaytime && (
|
||||||
|
<AlertFillIcon
|
||||||
|
size={16}
|
||||||
|
className="hero-panel-playtime__manual-warning"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{t("play_time", {
|
{t("play_time", {
|
||||||
amount: formattedPlayTime,
|
amount: formattedPlayTime,
|
||||||
})}
|
})}
|
||||||
@@ -100,6 +122,16 @@ export function HeroPanelPlaytime() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{game.hasManuallyUpdatedPlaytime && (
|
||||||
|
<Tooltip
|
||||||
|
id="manual-playtime-warning"
|
||||||
|
style={{
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
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 />
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
|
.change-game-playtime-modal {
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: globals.$spacing-unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
padding: globals.$spacing-unit;
|
||||||
|
background-color: rgba(255, 193, 7, 0.1);
|
||||||
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffc107;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: globals.$spacing-unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
|
import type { Game } from "@types";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { AlertIcon } from "@primer/octicons-react";
|
||||||
|
import "./change-game-playtime-modal.scss";
|
||||||
|
|
||||||
|
export interface ChangeGamePlaytimeModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
game: Game;
|
||||||
|
onClose: () => void;
|
||||||
|
changePlaytime: (playTimeInSeconds: number) => Promise<void>;
|
||||||
|
onSuccess?: (message: string) => void;
|
||||||
|
onError?: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangeGamePlaytimeModal({
|
||||||
|
onClose,
|
||||||
|
game,
|
||||||
|
visible,
|
||||||
|
changePlaytime,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: Readonly<ChangeGamePlaytimeModalProps>) {
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
const [hours, setHours] = useState("");
|
||||||
|
const [minutes, setMinutes] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && game.playTimeInMilliseconds) {
|
||||||
|
const totalMinutes = Math.floor(
|
||||||
|
game.playTimeInMilliseconds / (1000 * 60)
|
||||||
|
);
|
||||||
|
const currentHours = Math.floor(totalMinutes / 60);
|
||||||
|
const currentMinutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
setHours(currentHours.toString());
|
||||||
|
setMinutes(currentMinutes.toString());
|
||||||
|
} else if (visible) {
|
||||||
|
setHours("");
|
||||||
|
setMinutes("");
|
||||||
|
}
|
||||||
|
}, [visible, game.playTimeInMilliseconds]);
|
||||||
|
|
||||||
|
const MAX_TOTAL_HOURS = 10000;
|
||||||
|
|
||||||
|
const currentHours = parseInt(hours) || 0;
|
||||||
|
const currentMinutes = parseInt(minutes) || 0;
|
||||||
|
|
||||||
|
const maxAllowedHours = Math.min(
|
||||||
|
MAX_TOTAL_HOURS,
|
||||||
|
Math.floor(MAX_TOTAL_HOURS - currentMinutes / 60)
|
||||||
|
);
|
||||||
|
const maxAllowedMinutes =
|
||||||
|
currentHours >= MAX_TOTAL_HOURS
|
||||||
|
? 0
|
||||||
|
: Math.min(59, Math.floor((MAX_TOTAL_HOURS - currentHours) * 60));
|
||||||
|
|
||||||
|
const handleChangePlaytime = async () => {
|
||||||
|
const hoursNum = parseInt(hours) || 0;
|
||||||
|
const minutesNum = parseInt(minutes) || 0;
|
||||||
|
const totalSeconds = hoursNum * 3600 + minutesNum * 60;
|
||||||
|
|
||||||
|
if (totalSeconds < 0) return;
|
||||||
|
|
||||||
|
if (hoursNum + minutesNum / 60 > MAX_TOTAL_HOURS) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await changePlaytime(totalSeconds);
|
||||||
|
onSuccess?.(t("update_playtime_success"));
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(t("update_playtime_error"));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHoursChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
|
||||||
|
if (value.length > 1 && value.startsWith("0")) {
|
||||||
|
value = value.replace(/^0+/, "") || "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
const numValue = parseInt(value) || 0;
|
||||||
|
|
||||||
|
if (numValue <= maxAllowedHours) {
|
||||||
|
setHours(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
|
||||||
|
if (value.length > 1 && value.startsWith("0")) {
|
||||||
|
value = value.replace(/^0+/, "") || "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
const numValue = parseInt(value) || 0;
|
||||||
|
|
||||||
|
if (numValue <= maxAllowedMinutes) {
|
||||||
|
setMinutes(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = hours !== "" || minutes !== "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t("update_playtime_title")}
|
||||||
|
description={t("update_playtime_description", {
|
||||||
|
game: game.title,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="change-game-playtime-modal__content">
|
||||||
|
{!game.hasManuallyUpdatedPlaytime && (
|
||||||
|
<div className="change-game-playtime-modal__warning">
|
||||||
|
<AlertIcon size={16} />
|
||||||
|
<span>{t("manual_playtime_warning")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="change-game-playtime-modal__inputs">
|
||||||
|
<TextField
|
||||||
|
label={t("hours")}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max={maxAllowedHours.toString()}
|
||||||
|
value={hours}
|
||||||
|
onChange={handleHoursChange}
|
||||||
|
placeholder="0"
|
||||||
|
theme="dark"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("minutes")}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max={maxAllowedMinutes.toString()}
|
||||||
|
value={minutes}
|
||||||
|
onChange={handleMinutesChange}
|
||||||
|
placeholder="0"
|
||||||
|
theme="dark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="change-game-playtime-modal__actions">
|
||||||
|
<Button onClick={onClose} theme="outline">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleChangePlaytime}
|
||||||
|
theme="primary"
|
||||||
|
disabled={!isValid || isSubmitting}
|
||||||
|
>
|
||||||
|
{t("update_playtime")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user