mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 09:13:57 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba2ac1eb93 | ||
|
|
726d99a5c7 | ||
|
|
402e5df9ac | ||
|
|
0ee55b7fd5 | ||
|
|
8286390d9f | ||
|
|
6e00fb8e13 | ||
|
|
9f5d8cadda | ||
|
|
9060d435cf | ||
|
|
d8e7fca224 | ||
|
|
3bef2633fd | ||
|
|
db2688f3a7 | ||
|
|
4e282921ef | ||
|
|
b22b998c29 | ||
|
|
423693040b | ||
|
|
c098d8ffcf | ||
|
|
e1904b853e | ||
|
|
520eb91a55 | ||
|
|
16eaf4261a | ||
|
|
ec289fe4c7 | ||
|
|
1f000ab2b2 | ||
|
|
d6bd0ec221 | ||
|
|
a9f8d1b42c | ||
|
|
f5d5aa39dc | ||
|
|
83e662f633 | ||
|
|
c7b924bf2f | ||
|
|
e0d69ccf9d | ||
|
|
aa4e71076d | ||
|
|
eccff27739 | ||
|
|
601b3b0a00 | ||
|
|
1ceea5d5a3 | ||
|
|
9c99724e7d | ||
|
|
55e0f42702 | ||
|
|
b355930eaf | ||
|
|
57b47951a1 | ||
|
|
4780640ed0 | ||
|
|
8b92c8fdd9 | ||
|
|
acd98d0aad | ||
|
|
27239f848e | ||
|
|
c4d0a8eb94 | ||
|
|
60b12b2435 | ||
|
|
1c9515516f | ||
|
|
58a89372ab | ||
|
|
90841bf5e4 | ||
|
|
5564644378 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ out
|
|||||||
ludusavi/
|
ludusavi/
|
||||||
hydra-python-rpc/
|
hydra-python-rpc/
|
||||||
aria2/
|
aria2/
|
||||||
|
.python-version
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
3.9.20
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
!macro customUnInstall
|
!macro customUnInstall
|
||||||
${ifNot} ${isUpdated}
|
${ifNot} ${isUpdated}
|
||||||
RMDir /r "$APPDATA\${APP_PACKAGE_NAME}"
|
|
||||||
RMDir /r "$APPDATA\hydra"
|
|
||||||
RMDir /r "$LOCALAPPDATA\hydralauncher-updater"
|
RMDir /r "$LOCALAPPDATA\hydralauncher-updater"
|
||||||
${endIf}
|
${endIf}
|
||||||
!macroend
|
!macroend
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||||
|
|
||||||
<h1 align="center">Hydra Launcher</h1>
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
[](README.da.md)
|
[](README.da.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/actions)
|
[](https://github.com/hydralauncher/hydra/actions)
|
||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
[](README.pl.md)
|
[](README.pl.md)
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||||
|
|
||||||
<h1 align="center">Hydra Launcher</h1>
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,21 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/actions)
|
[](https://github.com/hydralauncher/hydra/actions)
|
||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](./README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](./README.md)
|
[](../README.md)
|
||||||
[](./README.ru.md)
|
[](README.ru.md)
|
||||||
[](./README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](./README.be.md)
|
[](README.be.md)
|
||||||
[](./README.es.md)
|
[](README.es.md)
|
||||||
[](./README.fr.md)
|
[](README.fr.md)
|
||||||
[](./README.de.md)
|
[](README.de.md)
|
||||||
[](./README.it.md)
|
[](README.it.md)
|
||||||
[](./README.cs.md)
|
[](README.cs.md)
|
||||||
[](./README.da.md)
|
[](README.da.md)
|
||||||
[](./README.nb.md)
|
[](README.nb.md)
|
||||||
[](./README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||||
|
|
||||||
<h1 align="center">Hydra Launcher</h1>
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||||
|
|
||||||
<h1 align="center">Hydra Launcher</h1>
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[<img src="./resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
|
||||||
|
|
||||||
<h1 align="center">Hydra Launcher</h1>
|
<h1 align="center">Hydra Launcher</h1>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
[](https://github.com/hydralauncher/hydra/releases)
|
[](https://github.com/hydralauncher/hydra/releases)
|
||||||
|
|
||||||
[](README.pt-BR.md)
|
[](README.pt-BR.md)
|
||||||
[](README.md)
|
[](../README.md)
|
||||||
[](README.ru.md)
|
[](README.ru.md)
|
||||||
[](README.uk-UA.md)
|
[](README.uk-UA.md)
|
||||||
[](README.be.md)
|
[](README.be.md)
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
[](README.nb.md)
|
[](README.nb.md)
|
||||||
[](README.et.md)
|
[](README.et.md)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -46,10 +46,20 @@
|
|||||||
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
|
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
|
||||||
},
|
},
|
||||||
"catalogue": {
|
"catalogue": {
|
||||||
"next_page": "Следваща страница",
|
"search": "Филтър…",
|
||||||
"previous_page": "Предишна страница"
|
"developers": "Разработчици",
|
||||||
|
"genres": "Жанрове",
|
||||||
|
"tags": "Тагове",
|
||||||
|
"publishers": "Издатели",
|
||||||
|
"download_sources": "Източници за изтегляне",
|
||||||
|
"result_count": "{{resultCount}} резултати",
|
||||||
|
"filter_count": "{{filterCount}} налични",
|
||||||
|
"clear_filters": "Изчисти {{filterCount}} избрани"
|
||||||
},
|
},
|
||||||
"game_details": {
|
"game_details": {
|
||||||
|
"launch_options": "Опции за стартиране",
|
||||||
|
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране",
|
||||||
|
"launch_options_placeholder": "Няма зададен параметър",
|
||||||
"open_download_options": "Варианти за изтегляне",
|
"open_download_options": "Варианти за изтегляне",
|
||||||
"download_options_zero": "Няма варианти за изтегляне",
|
"download_options_zero": "Няма варианти за изтегляне",
|
||||||
"download_options_one": "{{count}} варианти за изтегляне",
|
"download_options_one": "{{count}} варианти за изтегляне",
|
||||||
@@ -177,6 +187,10 @@
|
|||||||
"loading": "Зареждане…"
|
"loading": "Зареждане…"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
|
"seeding": "Сийдване",
|
||||||
|
"stop_seeding": "Спри сийдването",
|
||||||
|
"resume_seeding": "Продължи сийдването",
|
||||||
|
"options": "Управление",
|
||||||
"resume": "Продължи",
|
"resume": "Продължи",
|
||||||
"pause": "Пауза",
|
"pause": "Пауза",
|
||||||
"eta": "Conclusion {{eta}}",
|
"eta": "Conclusion {{eta}}",
|
||||||
@@ -202,6 +216,8 @@
|
|||||||
"checking_files": "Проверка на файлове…"
|
"checking_files": "Проверка на файлове…"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
|
||||||
|
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
|
||||||
"downloads_path": "Инсталационен път",
|
"downloads_path": "Инсталационен път",
|
||||||
"change": "Актуализиране",
|
"change": "Актуализиране",
|
||||||
"notifications": "Известия",
|
"notifications": "Известия",
|
||||||
@@ -210,7 +226,7 @@
|
|||||||
"real_debrid_api_token_label": "Real-Debrid API токен",
|
"real_debrid_api_token_label": "Real-Debrid API токен",
|
||||||
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
|
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
|
||||||
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
|
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
|
||||||
"general": "Общ",
|
"general": "Общи",
|
||||||
"behavior": "Поведение",
|
"behavior": "Поведение",
|
||||||
"download_sources": "Източници за изтегляне",
|
"download_sources": "Източници за изтегляне",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
@@ -288,6 +304,16 @@
|
|||||||
"toggle_password_visibility": "Превключване на видимостта на паролата"
|
"toggle_password_visibility": "Превключване на видимостта на паролата"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
|
"stats": "Статистики",
|
||||||
|
"achievements": "Постижения",
|
||||||
|
"games": "Игри",
|
||||||
|
"top_percentile": "Топ {{percentile}}%",
|
||||||
|
"ranking_updated_weekly": "Класацията се актуализира седмично",
|
||||||
|
"playing": "Играе {{game}}",
|
||||||
|
"achievements_unlocked": "Отключени постижения",
|
||||||
|
"earned_points": "Спечелени точки",
|
||||||
|
"show_achievements_on_profile": "Показвай своите постижения в профила",
|
||||||
|
"show_points_on_profile": "Показвай спечелените точки в профила",
|
||||||
"amount_hours": "{{amount}} часове",
|
"amount_hours": "{{amount}} часове",
|
||||||
"amount_minutes": "{{amount}} минути",
|
"amount_minutes": "{{amount}} минути",
|
||||||
"last_time_played": "Последно играно {{period}}",
|
"last_time_played": "Последно играно {{period}}",
|
||||||
@@ -359,6 +385,11 @@
|
|||||||
"background_image_updated": "Обновено фоново изображение"
|
"background_image_updated": "Обновено фоново изображение"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
|
"hidden_achievement_tooltip": "Това е скрито постижение",
|
||||||
|
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
|
||||||
|
"earned_points": "Спечелени точки:",
|
||||||
|
"available_points": "Налични точки:",
|
||||||
|
"how_to_earn_achievements_points": "Как да спечелиш точки за постижения?",
|
||||||
"achievement_unlocked": "Постижението е отключено",
|
"achievement_unlocked": "Постижението е отключено",
|
||||||
"user_achievements": "Постиженията на {{displayName}} ",
|
"user_achievements": "Постиженията на {{displayName}} ",
|
||||||
"your_achievements": "Вашите Постижения",
|
"your_achievements": "Вашите Постижения",
|
||||||
@@ -369,6 +400,9 @@
|
|||||||
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
|
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"hydra_cloud": {
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
|
||||||
|
"learn_more": "Научете повече",
|
||||||
"subscription_tour_title": "Hydra Cloud Абонамент",
|
"subscription_tour_title": "Hydra Cloud Абонамент",
|
||||||
"subscribe_now": "Абонирай се сега",
|
"subscribe_now": "Абонирай се сега",
|
||||||
"cloud_saving": "Запазване в облака",
|
"cloud_saving": "Запазване в облака",
|
||||||
|
|||||||
@@ -167,6 +167,9 @@
|
|||||||
"loading_save_preview": "Searching for save games…",
|
"loading_save_preview": "Searching for save games…",
|
||||||
"wine_prefix": "Wine Prefix",
|
"wine_prefix": "Wine Prefix",
|
||||||
"wine_prefix_description": "The Wine prefix used to run this game",
|
"wine_prefix_description": "The Wine prefix used to run this game",
|
||||||
|
"launch_options": "Launch Options",
|
||||||
|
"launch_options_description": "Advanced users may choose to enter modifications to their launch options",
|
||||||
|
"launch_options_placeholder": "No parameter specified",
|
||||||
"no_download_option_info": "No information available",
|
"no_download_option_info": "No information available",
|
||||||
"backup_deletion_failed": "Failed to delete backup",
|
"backup_deletion_failed": "Failed to delete backup",
|
||||||
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"developers": "Desarrolladores",
|
"developers": "Desarrolladores",
|
||||||
"genres": "Géneros",
|
"genres": "Géneros",
|
||||||
"tags": "Marcadores",
|
"tags": "Marcadores",
|
||||||
"publishers": "Distribuidoras",
|
"publishers": "Editores",
|
||||||
"download_sources": "Fuentes de descarga",
|
"download_sources": "Fuentes de descarga",
|
||||||
"result_count": "{{resultCount}} resultados",
|
"result_count": "{{resultCount}} resultados",
|
||||||
"filter_count": "{{filterCount}} disponibles",
|
"filter_count": "{{filterCount}} disponibles",
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"backup_from": "Copia de seguridad de {{date}}",
|
||||||
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"no_directory_selected": "No se seleccionó un directório"
|
"no_directory_selected": "No se seleccionó un directorio"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
@@ -208,7 +208,11 @@
|
|||||||
"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... aún, ¡pero nunca es tarde para comenzar!.",
|
||||||
"checking_files": "Verificando archivos…"
|
"checking_files": "Verificando archivos…",
|
||||||
|
"seeding": "Seeding",
|
||||||
|
"stop_seeding": "Detener seeding",
|
||||||
|
"resume_seeding": "Continuar seeding",
|
||||||
|
"options": "Gestionar"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Ruta de descarga",
|
"downloads_path": "Ruta de descarga",
|
||||||
@@ -265,7 +269,9 @@
|
|||||||
"user_unblocked": "El usuario ha sido desbloqueado",
|
"user_unblocked": "El usuario ha sido desbloqueado",
|
||||||
"enable_achievement_notifications": "Cuando un logro se desbloquea",
|
"enable_achievement_notifications": "Cuando un logro se desbloquea",
|
||||||
"launch_minimized": "Iniciar Hydra minimizado",
|
"launch_minimized": "Iniciar Hydra minimizado",
|
||||||
"disable_nsfw_alert": "Desactivar alerta NSFW"
|
"disable_nsfw_alert": "Desactivar alerta NSFW",
|
||||||
|
"seed_after_download_complete": "Realizar seeding después de que se completa la descarga",
|
||||||
|
"show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
@@ -366,7 +372,16 @@
|
|||||||
"upload_banner": "Subir un banner",
|
"upload_banner": "Subir un 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}}"
|
"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",
|
||||||
|
"top_percentile": "Top {{percentile}}%"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Logro desbloqueado",
|
"achievement_unlocked": "Logro desbloqueado",
|
||||||
@@ -376,7 +391,12 @@
|
|||||||
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
|
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
|
||||||
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos",
|
"new_achievements_unlocked": "Desbloqueados {{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": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}",
|
||||||
|
"hidden_achievement_tooltip": "Este es un logro oculto",
|
||||||
|
"achievement_earn_points": "Obtén {{points}} puntos con este logro",
|
||||||
|
"earned_points": "Puntos obtenidos:",
|
||||||
|
"available_points": "Puntos disponibles:",
|
||||||
|
"how_to_earn_achievements_points": "¿Cómo obtener puntos de logros?"
|
||||||
},
|
},
|
||||||
"hydra_cloud": {
|
"hydra_cloud": {
|
||||||
"subscription_tour_title": "Suscripción Hydra Cloud",
|
"subscription_tour_title": "Suscripción Hydra Cloud",
|
||||||
@@ -386,6 +406,9 @@
|
|||||||
"animated_profile_picture": "Fotos de perfil animadas",
|
"animated_profile_picture": "Fotos de perfil animadas",
|
||||||
"premium_support": "Soporte Premium",
|
"premium_support": "Soporte Premium",
|
||||||
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
|
"show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios",
|
||||||
"animated_profile_banner": "Fondo de perfil animado"
|
"animated_profile_banner": "Fondo de perfil animado",
|
||||||
|
"hydra_cloud": "Hydra Cloud",
|
||||||
|
"hydra_cloud_feature_found": "¡Has descubierto una característica de Hydra Cloud!",
|
||||||
|
"learn_more": "Aprender más"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,9 @@
|
|||||||
"loading_save_preview": "Buscando por arquivos de salvamento…",
|
"loading_save_preview": "Buscando por arquivos de salvamento…",
|
||||||
"wine_prefix": "Prefixo Wine",
|
"wine_prefix": "Prefixo Wine",
|
||||||
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
|
||||||
|
"launch_options": "Opções de Inicialização",
|
||||||
|
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo",
|
||||||
|
"launch_options_placeholder": "Nenhum parâmetro informado",
|
||||||
"no_download_option_info": "Sem informações disponíveis",
|
"no_download_option_info": "Sem informações disponíveis",
|
||||||
"backup_deletion_failed": "Falha ao apagar backup",
|
"backup_deletion_failed": "Falha ao apagar backup",
|
||||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export class Game {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
launchOptions: string | null;
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
winePrefixPath: string | null;
|
winePrefixPath: string | null;
|
||||||
|
|
||||||
|
|||||||
9
src/main/events/helpers/parse-launch-options.ts
Normal file
9
src/main/events/helpers/parse-launch-options.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const parseLaunchOptions = (params: string | null): string[] => {
|
||||||
|
if (params == null || params == "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsSplit = params.split(" ");
|
||||||
|
|
||||||
|
return paramsSplit;
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@ import "./library/open-game-executable-path";
|
|||||||
import "./library/open-game-installer";
|
import "./library/open-game-installer";
|
||||||
import "./library/open-game-installer-path";
|
import "./library/open-game-installer-path";
|
||||||
import "./library/update-executable-path";
|
import "./library/update-executable-path";
|
||||||
|
import "./library/update-launch-options";
|
||||||
import "./library/verify-executable-path";
|
import "./library/verify-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
|
|||||||
@@ -2,18 +2,31 @@ import { gameRepository } from "@main/repository";
|
|||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number,
|
gameId: number,
|
||||||
executablePath: string
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
) => {
|
) => {
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
|
await gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{ executablePath: parsedPath, launchOptions }
|
||||||
|
);
|
||||||
|
|
||||||
shell.openPath(parsedPath);
|
if (process.platform === "linux" || process.platform === "darwin") {
|
||||||
|
shell.openPath(parsedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
|||||||
19
src/main/events/library/update-launch-options.ts
Normal file
19
src/main/events/library/update-launch-options.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const updateLaunchOptions = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => {
|
||||||
|
return gameRepository.update(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
||||||
@@ -16,6 +16,7 @@ import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disab
|
|||||||
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
||||||
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
||||||
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
||||||
|
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
AddShouldSeedColumn,
|
AddShouldSeedColumn,
|
||||||
AddSeedAfterDownloadColumn,
|
AddSeedAfterDownloadColumn,
|
||||||
AddHiddenAchievementDescriptionColumn,
|
AddHiddenAchievementDescriptionColumn,
|
||||||
|
AddLaunchOptionsColumnToGame,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddLaunchOptionsColumnToGame: HydraMigration = {
|
||||||
|
name: "AddLaunchOptionsColumnToGame",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.string("launchOptions").nullable();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.dropColumn("launchOptions");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -244,7 +244,7 @@ export class DownloadManager {
|
|||||||
private static async getDownloadPayload(game: Game) {
|
private static async getDownloadPayload(game: Game) {
|
||||||
switch (game.downloader) {
|
switch (game.downloader) {
|
||||||
case Downloader.Gofile: {
|
case Downloader.Gofile: {
|
||||||
const id = game!.uri!.split("/").pop();
|
const id = game.uri!.split("/").pop();
|
||||||
|
|
||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
@@ -258,7 +258,7 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.PixelDrain: {
|
case Downloader.PixelDrain: {
|
||||||
const id = game!.uri!.split("/").pop();
|
const id = game.uri!.split("/").pop();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const gamesPlaytime = new Map<
|
|||||||
interface ExecutableInfo {
|
interface ExecutableInfo {
|
||||||
name: string;
|
name: string;
|
||||||
os: string;
|
os: string;
|
||||||
|
exe: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GameExecutables {
|
interface GameExecutables {
|
||||||
@@ -30,47 +31,65 @@ interface GameExecutables {
|
|||||||
const TICKS_TO_UPDATE_API = 120;
|
const TICKS_TO_UPDATE_API = 120;
|
||||||
let currentTick = 1;
|
let currentTick = 1;
|
||||||
|
|
||||||
const gameExecutables = (
|
const isWindowsPlatform = process.platform === "win32";
|
||||||
await axios
|
const isLinuxPlatform = process.platform === "linux";
|
||||||
.get(
|
|
||||||
import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL +
|
const getGameExecutables = async () => {
|
||||||
"/game-executables.json"
|
const gameExecutables = (
|
||||||
)
|
await axios
|
||||||
.catch(() => {
|
.get(
|
||||||
return { data: {} };
|
import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL +
|
||||||
})
|
"/game-executables.json"
|
||||||
).data as GameExecutables;
|
)
|
||||||
|
.catch(() => {
|
||||||
|
return { data: {} };
|
||||||
|
})
|
||||||
|
).data as GameExecutables;
|
||||||
|
|
||||||
|
Object.keys(gameExecutables).forEach((key) => {
|
||||||
|
gameExecutables[key] = gameExecutables[key]
|
||||||
|
.filter((executable) => {
|
||||||
|
if (isWindowsPlatform) {
|
||||||
|
return executable.os === "win32";
|
||||||
|
} else if (isLinuxPlatform) {
|
||||||
|
return executable.os === "linux" || executable.os === "win32";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.map((executable) => {
|
||||||
|
return {
|
||||||
|
name: isWindowsPlatform
|
||||||
|
? executable.name.replace(/\//g, "\\")
|
||||||
|
: executable.name,
|
||||||
|
os: executable.os,
|
||||||
|
exe: executable.name.slice(executable.name.lastIndexOf("/") + 1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return gameExecutables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gameExecutables = await getGameExecutables();
|
||||||
|
|
||||||
const findGamePathByProcess = (
|
const findGamePathByProcess = (
|
||||||
processMap: Map<string, Set<string>>,
|
processMap: Map<string, Set<string>>,
|
||||||
gameId: string
|
gameId: string
|
||||||
) => {
|
) => {
|
||||||
const executables = gameExecutables[gameId].filter((info) => {
|
const executables = gameExecutables[gameId];
|
||||||
if (process.platform === "linux" && info.os === "linux") return true;
|
|
||||||
return info.os === "win32";
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const executable of executables) {
|
for (const executable of executables) {
|
||||||
const exe = executable.name.slice(executable.name.lastIndexOf("/") + 1);
|
const pathSet = processMap.get(executable.exe);
|
||||||
|
|
||||||
if (!exe) continue;
|
|
||||||
|
|
||||||
const pathSet = processMap.get(exe);
|
|
||||||
|
|
||||||
if (pathSet) {
|
if (pathSet) {
|
||||||
const executableName =
|
|
||||||
process.platform === "win32"
|
|
||||||
? executable.name.replace(/\//g, "\\")
|
|
||||||
: executable.name;
|
|
||||||
|
|
||||||
pathSet.forEach((path) => {
|
pathSet.forEach((path) => {
|
||||||
if (path.toLowerCase().endsWith(executableName)) {
|
if (path.toLowerCase().endsWith(executable.name)) {
|
||||||
gameRepository.update(
|
gameRepository.update(
|
||||||
{ objectID: gameId, shop: "steam" },
|
{ objectID: gameId, shop: "steam" },
|
||||||
{ executablePath: path }
|
{ executablePath: path }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (isLinuxPlatform) {
|
||||||
exec(commands.findWineDir, (err, out) => {
|
exec(commands.findWineDir, (err, out) => {
|
||||||
if (err) return;
|
if (err) return;
|
||||||
|
|
||||||
@@ -105,7 +124,7 @@ const getSystemProcessMap = async () => {
|
|||||||
map.set(key, currentSet.add(value));
|
map.set(key, currentSet.add(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (isLinuxPlatform) {
|
||||||
await new Promise((res) => {
|
await new Promise((res) => {
|
||||||
exec(commands.findWineExecutables, (err, out) => {
|
exec(commands.findWineExecutables, (err, out) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -152,7 +171,6 @@ export const watchProcesses = async () => {
|
|||||||
|
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const executablePath = game.executablePath;
|
const executablePath = game.executablePath;
|
||||||
|
|
||||||
if (!executablePath) {
|
if (!executablePath) {
|
||||||
if (gameExecutables[game.objectID]) {
|
if (gameExecutables[game.objectID]) {
|
||||||
findGamePathByProcess(processMap, game.objectID);
|
findGamePathByProcess(processMap, game.objectID);
|
||||||
@@ -161,10 +179,7 @@ export const watchProcesses = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executable = executablePath
|
const executable = executablePath
|
||||||
.slice(
|
.slice(executablePath.lastIndexOf(isWindowsPlatform ? "\\" : "/") + 1)
|
||||||
executablePath.lastIndexOf(process.platform === "win32" ? "\\" : "/") +
|
|
||||||
1
|
|
||||||
)
|
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
const hasProcess = processMap.get(executable)?.has(executablePath);
|
const hasProcess = processMap.get(executable)?.has(executablePath);
|
||||||
|
|||||||
@@ -277,14 +277,9 @@ export class WindowManager {
|
|||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
await updateSystemTray();
|
await updateSystemTray();
|
||||||
|
|
||||||
tray.addListener("click", () => {
|
tray.addListener("double-click", () => {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
if (
|
this.mainWindow.show();
|
||||||
WindowManager.mainWindow?.isMinimized() ||
|
|
||||||
!WindowManager.mainWindow?.isVisible()
|
|
||||||
) {
|
|
||||||
WindowManager.mainWindow?.show();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.createMainWindow();
|
this.createMainWindow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("createGameShortcut", id),
|
ipcRenderer.invoke("createGameShortcut", id),
|
||||||
updateExecutablePath: (id: number, executablePath: string | null) =>
|
updateExecutablePath: (id: number, executablePath: string | null) =>
|
||||||
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||||
|
updateLaunchOptions: (id: number, launchOptions: string | null) =>
|
||||||
|
ipcRenderer.invoke("updateLaunchOptions", id, launchOptions),
|
||||||
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
|
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
|
||||||
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
|
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
|
||||||
verifyExecutablePathInUse: (executablePath: string) =>
|
verifyExecutablePathInUse: (executablePath: string) =>
|
||||||
@@ -115,8 +117,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
||||||
openGameExecutablePath: (gameId: number) =>
|
openGameExecutablePath: (gameId: number) =>
|
||||||
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
||||||
openGame: (gameId: number, executablePath: string) =>
|
openGame: (
|
||||||
ipcRenderer.invoke("openGame", gameId, executablePath),
|
gameId: number,
|
||||||
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => ipcRenderer.invoke("openGame", gameId, executablePath, launchOptions),
|
||||||
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
|
||||||
removeGameFromLibrary: (gameId: number) =>
|
removeGameFromLibrary: (gameId: number) =>
|
||||||
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
ipcRenderer.invoke("removeGameFromLibrary", gameId),
|
||||||
|
|||||||
@@ -154,7 +154,11 @@ export function Sidebar() {
|
|||||||
|
|
||||||
if (event.detail === 2) {
|
if (event.detail === 2) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
window.electron.openGame(game.id, game.executablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
game.executablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showWarningToast(t("game_has_no_executable"));
|
showWarningToast(t("game_has_no_executable"));
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
@@ -93,6 +93,10 @@ declare global {
|
|||||||
id: number,
|
id: number,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
updateLaunchOptions: (
|
||||||
|
id: number,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => Promise<void>;
|
||||||
selectGameWinePrefix: (
|
selectGameWinePrefix: (
|
||||||
id: number,
|
id: number,
|
||||||
winePrefixPath: string | null
|
winePrefixPath: string | null
|
||||||
@@ -102,7 +106,11 @@ declare global {
|
|||||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||||
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||||
openGameExecutablePath: (gameId: number) => Promise<void>;
|
openGameExecutablePath: (gameId: number) => Promise<void>;
|
||||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
openGame: (
|
||||||
|
gameId: number,
|
||||||
|
executablePath: string,
|
||||||
|
launchOptions: string | null
|
||||||
|
) => Promise<void>;
|
||||||
closeGame: (gameId: number) => Promise<boolean>;
|
closeGame: (gameId: number) => Promise<boolean>;
|
||||||
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
removeGameFromLibrary: (gameId: number) => Promise<void>;
|
||||||
removeGame: (gameId: number) => Promise<void>;
|
removeGame: (gameId: number) => Promise<void>;
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import type { GameShop } from "@types";
|
|||||||
|
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
|
|
||||||
export const formatDownloadProgress = (progress?: number) => {
|
export const formatDownloadProgress = (
|
||||||
|
progress?: number,
|
||||||
|
fractionDigits?: number
|
||||||
|
) => {
|
||||||
if (!progress) return "0%";
|
if (!progress) return "0%";
|
||||||
const progressPercentage = progress * 100;
|
const progressPercentage = progress * 100;
|
||||||
|
|
||||||
if (Number(progressPercentage.toFixed(2)) % 1 === 0)
|
if (Number(progressPercentage.toFixed(fractionDigits ?? 2)) % 1 === 0)
|
||||||
return `${Math.floor(progressPercentage)}%`;
|
return `${Math.floor(progressPercentage)}%`;
|
||||||
|
|
||||||
return `${progressPercentage.toFixed(2)}%`;
|
return `${progressPercentage.toFixed(fractionDigits ?? 2)}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSteamLanguage = (language: string) => {
|
export const getSteamLanguage = (language: string) => {
|
||||||
|
|||||||
@@ -55,13 +55,21 @@ export function HeroPanelActions() {
|
|||||||
const openGame = async () => {
|
const openGame = async () => {
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.executablePath) {
|
if (game.executablePath) {
|
||||||
window.electron.openGame(game.id, game.executablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
game.executablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameExecutablePath = await selectGameExecutable();
|
const gameExecutablePath = await selectGameExecutable();
|
||||||
if (gameExecutablePath)
|
if (gameExecutablePath)
|
||||||
window.electron.openGame(game.id, gameExecutablePath);
|
window.electron.openGame(
|
||||||
|
game.id,
|
||||||
|
gameExecutablePath,
|
||||||
|
game.launchOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext, useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
@@ -8,6 +8,7 @@ import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
|||||||
import { useDownload, useToast } from "@renderer/hooks";
|
import { useDownload, useToast } from "@renderer/hooks";
|
||||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
|
||||||
export interface GameOptionsModalProps {
|
export interface GameOptionsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -29,6 +30,7 @@ export function GameOptionsModal({
|
|||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||||
|
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
removeGameInstaller,
|
removeGameInstaller,
|
||||||
@@ -44,6 +46,13 @@ export function GameOptionsModal({
|
|||||||
const isGameDownloading =
|
const isGameDownloading =
|
||||||
game.status === "active" && lastPacket?.game.id === game.id;
|
game.status === "active" && lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
|
const debounceUpdateLaunchOptions = useRef(
|
||||||
|
debounce(async (value: string) => {
|
||||||
|
await window.electron.updateLaunchOptions(game.id, value);
|
||||||
|
updateGame();
|
||||||
|
}, 1000)
|
||||||
|
).current;
|
||||||
|
|
||||||
const handleRemoveGameFromLibrary = async () => {
|
const handleRemoveGameFromLibrary = async () => {
|
||||||
if (isGameDownloading) {
|
if (isGameDownloading) {
|
||||||
await cancelDownload(game.id);
|
await cancelDownload(game.id);
|
||||||
@@ -116,9 +125,25 @@ export function GameOptionsModal({
|
|||||||
updateGame();
|
updateGame();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeLaunchOptions = async (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
setLaunchOptions(value);
|
||||||
|
debounceUpdateLaunchOptions(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearLaunchOptions = async () => {
|
||||||
|
setLaunchOptions("");
|
||||||
|
|
||||||
|
window.electron.updateLaunchOptions(game.id, null).then(updateGame);
|
||||||
|
};
|
||||||
|
|
||||||
const shouldShowWinePrefixConfiguration =
|
const shouldShowWinePrefixConfiguration =
|
||||||
window.electron.platform === "linux";
|
window.electron.platform === "linux";
|
||||||
|
|
||||||
|
const shouldShowLaunchOptionsConfiguration =
|
||||||
|
window.electron.platform === "win32";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGameModal
|
<DeleteGameModal
|
||||||
@@ -226,6 +251,28 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShowLaunchOptionsConfiguration && (
|
||||||
|
<div className={styles.gameOptionHeader}>
|
||||||
|
<h2>{t("launch_options")}</h2>
|
||||||
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
{t("launch_options_description")}
|
||||||
|
</h4>
|
||||||
|
<TextField
|
||||||
|
value={launchOptions}
|
||||||
|
theme="dark"
|
||||||
|
placeholder={t("launch_options_placeholder")}
|
||||||
|
onChange={handleChangeLaunchOptions}
|
||||||
|
rightContent={
|
||||||
|
game.launchOptions && (
|
||||||
|
<Button onClick={handleClearLaunchOptions} theme="outline">
|
||||||
|
{t("clear")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.gameOptionHeader}>
|
<div className={styles.gameOptionHeader}>
|
||||||
<h2>{t("downloads_secion_title")}</h2>
|
<h2>{t("downloads_secion_title")}</h2>
|
||||||
<h4 className={styles.gameOptionHeaderDescription}>
|
<h4 className={styles.gameOptionHeaderDescription}>
|
||||||
|
|||||||
@@ -228,3 +228,11 @@ export const link = style({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const gameCardStats = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
transition: "transform 0.5s ease-in-out",
|
||||||
|
flexShrink: "0",
|
||||||
|
flexGrow: "0",
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { useCallback, useContext, useEffect, useMemo } from "react";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
import { useAppDispatch, useFormat } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
import * as styles from "./profile-content.css";
|
||||||
import { ClockIcon, TelescopeIcon, TrophyIcon } from "@primer/octicons-react";
|
import { TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
import { ReportProfile } from "../report-profile/report-profile";
|
import { ReportProfile } from "../report-profile/report-profile";
|
||||||
import { FriendsBox } from "./friends-box";
|
import { FriendsBox } from "./friends-box";
|
||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import type { UserGame } from "@types";
|
|
||||||
import {
|
|
||||||
buildGameAchievementPath,
|
|
||||||
buildGameDetailsPath,
|
|
||||||
formatDownloadProgress,
|
|
||||||
} from "@renderer/helpers";
|
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
|
||||||
import { UserStatsBox } from "./user-stats-box";
|
import { UserStatsBox } from "./user-stats-box";
|
||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import { UserLibraryGameCard } from "./user-library-game-card";
|
||||||
|
|
||||||
|
const GAME_STATS_ANIMATION_DURATION_IN_MS = 3500;
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
const { userProfile, isMe, userStats } = useContext(userProfileContext);
|
||||||
|
const [statsIndex, setStatsIndex] = useState(0);
|
||||||
|
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
|
||||||
|
const statsAnimation = useRef(-1);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -39,6 +35,35 @@ export function ProfileContent() {
|
|||||||
}
|
}
|
||||||
}, [userProfile, dispatch]);
|
}, [userProfile, dispatch]);
|
||||||
|
|
||||||
|
const handleOnMouseEnterGameCard = () => {
|
||||||
|
setIsAnimationRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnMouseLeaveGameCard = () => {
|
||||||
|
setIsAnimationRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let zero = performance.now();
|
||||||
|
if (!isAnimationRunning) return;
|
||||||
|
|
||||||
|
statsAnimation.current = requestAnimationFrame(
|
||||||
|
function animateGameStats(time) {
|
||||||
|
if (time - zero <= GAME_STATS_ANIMATION_DURATION_IN_MS) {
|
||||||
|
statsAnimation.current = requestAnimationFrame(animateGameStats);
|
||||||
|
} else {
|
||||||
|
setStatsIndex((index) => index + 1);
|
||||||
|
zero = performance.now();
|
||||||
|
statsAnimation.current = requestAnimationFrame(animateGameStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(statsAnimation.current);
|
||||||
|
};
|
||||||
|
}, [setStatsIndex, isAnimationRunning]);
|
||||||
|
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -47,42 +72,6 @@ export function ProfileContent() {
|
|||||||
return userProfile?.relation?.status === "ACCEPTED";
|
return userProfile?.relation?.status === "ACCEPTED";
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const buildUserGameDetailsPath = useCallback(
|
|
||||||
(game: UserGame) => {
|
|
||||||
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
|
||||||
return buildGameDetailsPath({
|
|
||||||
...game,
|
|
||||||
objectId: game.objectId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userParams = userProfile
|
|
||||||
? {
|
|
||||||
userId: userProfile.id,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return buildGameAchievementPath({ ...game }, userParams);
|
|
||||||
},
|
|
||||||
[userProfile]
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatPlayTime = useCallback(
|
|
||||||
(playTimeInSeconds = 0) => {
|
|
||||||
const minutes = playTimeInSeconds / 60;
|
|
||||||
|
|
||||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
|
||||||
return t("amount_minutes", {
|
|
||||||
amount: minutes.toFixed(0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = minutes / 60;
|
|
||||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
|
||||||
},
|
|
||||||
[numberFormatter, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
||||||
@@ -129,137 +118,13 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
<ul className={styles.gamesGrid}>
|
<ul className={styles.gamesGrid}>
|
||||||
{userProfile?.libraryGames?.map((game) => (
|
{userProfile?.libraryGames?.map((game) => (
|
||||||
<li
|
<UserLibraryGameCard
|
||||||
|
game={game}
|
||||||
key={game.objectId}
|
key={game.objectId}
|
||||||
style={{
|
statIndex={statsIndex}
|
||||||
borderRadius: 4,
|
onMouseEnter={handleOnMouseEnterGameCard}
|
||||||
overflow: "hidden",
|
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||||
position: "relative",
|
/>
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
title={game.title}
|
|
||||||
className={styles.game}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
className={styles.gameCover}
|
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
background:
|
|
||||||
"linear-gradient(0deg, rgba(0, 0, 0, 0.75) 25%, transparent 100%)",
|
|
||||||
padding: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<small
|
|
||||||
style={{
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
color: vars.color.muted,
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
borderRadius: 4,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 4,
|
|
||||||
padding: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ClockIcon size={11} />
|
|
||||||
{formatPlayTime(game.playTimeInSeconds)}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
{userProfile.hasActiveSubscription &&
|
|
||||||
game.achievementCount > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "#fff",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "start",
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HydraIcon width={16} height={16} />
|
|
||||||
{numberFormatter.format(
|
|
||||||
game.achievementsPointsEarnedSum
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: 8,
|
|
||||||
color: vars.color.muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
|
||||||
<span>
|
|
||||||
{game.unlockedAchievementCount} /{" "}
|
|
||||||
{game.achievementCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{formatDownloadProgress(
|
|
||||||
game.unlockedAchievementCount /
|
|
||||||
game.achievementCount
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<progress
|
|
||||||
max={1}
|
|
||||||
value={
|
|
||||||
game.unlockedAchievementCount /
|
|
||||||
game.achievementCount
|
|
||||||
}
|
|
||||||
className={styles.achievementsProgressBar}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img
|
|
||||||
src={steamUrlBuilder.cover(game.objectId)}
|
|
||||||
alt={game.title}
|
|
||||||
style={{
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: 4,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
minWidth: "100%",
|
|
||||||
minHeight: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
@@ -271,7 +136,6 @@ export function ProfileContent() {
|
|||||||
<UserStatsBox />
|
<UserStatsBox />
|
||||||
<RecentGamesBox />
|
<RecentGamesBox />
|
||||||
<FriendsBox />
|
<FriendsBox />
|
||||||
|
|
||||||
<ReportProfile />
|
<ReportProfile />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -284,9 +148,8 @@ export function ProfileContent() {
|
|||||||
userStats,
|
userStats,
|
||||||
numberFormatter,
|
numberFormatter,
|
||||||
t,
|
t,
|
||||||
buildUserGameDetailsPath,
|
|
||||||
formatPlayTime,
|
|
||||||
navigate,
|
navigate,
|
||||||
|
statsIndex,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { UserGame } from "@types";
|
||||||
|
import * as styles from "./profile-content.css";
|
||||||
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
|
import { useFormat } from "@renderer/hooks";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useCallback, useContext } from "react";
|
||||||
|
import {
|
||||||
|
buildGameAchievementPath,
|
||||||
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
import { userProfileContext } from "@renderer/context";
|
||||||
|
import { vars } from "@renderer/theme.css";
|
||||||
|
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
|
interface UserLibraryGameCardProps {
|
||||||
|
game: UserGame;
|
||||||
|
statIndex: number;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
onMouseLeave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserLibraryGameCard({
|
||||||
|
game,
|
||||||
|
statIndex,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
}: UserLibraryGameCardProps) {
|
||||||
|
const { userProfile } = useContext(userProfileContext);
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
const { numberFormatter } = useFormat();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const getStatsItemCount = useCallback(() => {
|
||||||
|
let statsCount = 1;
|
||||||
|
if (game.achievementsPointsEarnedSum > 0) statsCount++;
|
||||||
|
return statsCount;
|
||||||
|
}, [game]);
|
||||||
|
|
||||||
|
const buildUserGameDetailsPath = useCallback(
|
||||||
|
(game: UserGame) => {
|
||||||
|
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
||||||
|
return buildGameDetailsPath({
|
||||||
|
...game,
|
||||||
|
objectId: game.objectId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userParams = userProfile
|
||||||
|
? {
|
||||||
|
userId: userProfile.id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return buildGameAchievementPath({ ...game }, userParams);
|
||||||
|
},
|
||||||
|
[userProfile]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatAchievementPoints = (number: number) => {
|
||||||
|
if (number < 100_000) return numberFormatter.format(number);
|
||||||
|
|
||||||
|
if (number < 1_000_000) return `${(number / 1000).toFixed(1)}K`;
|
||||||
|
|
||||||
|
return `${(number / 1_000_000).toFixed(1)}M`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPlayTime = useCallback(
|
||||||
|
(playTimeInSeconds = 0) => {
|
||||||
|
const minutes = playTimeInSeconds / 60;
|
||||||
|
|
||||||
|
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||||
|
return t("amount_minutes", {
|
||||||
|
amount: minutes.toFixed(0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = minutes / 60;
|
||||||
|
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||||
|
},
|
||||||
|
[numberFormatter, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
style={{
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
title={game.title}
|
||||||
|
className={styles.game}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
className={styles.gameCover}
|
||||||
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
background:
|
||||||
|
"linear-gradient(0deg, rgba(0, 0, 0, 0.70) 20%, transparent 100%)",
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
style={{
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
color: vars.color.muted,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
borderRadius: 4,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClockIcon size={11} />
|
||||||
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 8,
|
||||||
|
color: vars.color.muted,
|
||||||
|
overflow: "hidden",
|
||||||
|
height: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.gameCardStats}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{game.unlockedAchievementCount} / {game.achievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{game.achievementsPointsEarnedSum > 0 && (
|
||||||
|
<div
|
||||||
|
className={styles.gameCardStats}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 5,
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HydraIcon width={16} height={16} />
|
||||||
|
{formatAchievementPoints(
|
||||||
|
game.achievementsPointsEarnedSum
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{formatDownloadProgress(
|
||||||
|
game.unlockedAchievementCount / game.achievementCount,
|
||||||
|
1
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<progress
|
||||||
|
max={1}
|
||||||
|
value={game.unlockedAchievementCount / game.achievementCount}
|
||||||
|
className={styles.achievementsProgressBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.cover(game.objectId)}
|
||||||
|
alt={game.title}
|
||||||
|
style={{
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: 4,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
minWidth: "100%",
|
||||||
|
minHeight: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ export interface Game {
|
|||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
winePrefixPath: string | null;
|
winePrefixPath: string | null;
|
||||||
executablePath: string | null;
|
executablePath: string | null;
|
||||||
|
launchOptions: string | null;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user