mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-20 09:43:57 +00:00
Compare commits
39 Commits
v3.6.3
...
feat/playt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0adea20565 | ||
|
|
f182c7c8e9 | ||
|
|
d9379fbcb9 | ||
|
|
adc4af731e | ||
|
|
af1b3d4535 | ||
|
|
291935a1bc | ||
|
|
665365abdc | ||
|
|
86da92aa3f | ||
|
|
6ff694c078 | ||
|
|
01ac5239dc | ||
|
|
1dc2176813 | ||
|
|
a7ec632a21 | ||
|
|
2b6d8eba78 | ||
|
|
6bc6a10d66 | ||
|
|
51f8b12e13 | ||
|
|
957a6b512e | ||
|
|
8bc1c1c58c | ||
|
|
fbb67af1f6 | ||
|
|
4211f97dd0 | ||
|
|
f569b142f3 | ||
|
|
18e4baec3d | ||
|
|
b7199f4d95 | ||
|
|
2b8cc506df | ||
|
|
e0c5f80b68 | ||
|
|
63374ccd74 | ||
|
|
8eeacf478d | ||
|
|
87a4c27977 | ||
|
|
8177c3fd0c | ||
|
|
d28a5b828f | ||
|
|
68288adef3 | ||
|
|
51894d9924 | ||
|
|
2107261f7b | ||
|
|
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=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.6.3",
|
"version": "3.6.8",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -89,6 +89,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,6 +198,14 @@
|
|||||||
"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",
|
||||||
@@ -379,6 +388,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",
|
||||||
@@ -442,6 +452,7 @@
|
|||||||
"activity": "Recent Activity",
|
"activity": "Recent Activity",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"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",
|
||||||
|
|||||||
@@ -76,6 +76,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}}",
|
||||||
@@ -364,6 +365,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",
|
||||||
|
|||||||
@@ -195,6 +195,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": "Игра удалена из избранного",
|
||||||
@@ -428,6 +432,7 @@
|
|||||||
"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": "Отображаемое имя",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"language_name": "中文",
|
"language_name": "简体中文",
|
||||||
"app": {
|
"app": {
|
||||||
"successfully_signed_in": "已成功登录"
|
"successfully_signed_in": "已成功登录"
|
||||||
},
|
},
|
||||||
@@ -26,7 +26,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 +45,23 @@
|
|||||||
"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": "下一页",
|
"next_page": "下一页",
|
||||||
"previous_page": "上一页"
|
"previous_page": "上一页",
|
||||||
|
"clear_filters": "清除已选的 {{filterCount}} 项",
|
||||||
|
"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 +180,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 +254,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 +321,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 +408,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 +497,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 +506,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 +526,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 +543,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;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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/create-steam-shortcut";
|
import "./library/create-steam-shortcut";
|
||||||
|
|||||||
29
src/main/events/library/change-game-playtime.ts
Normal file
29
src/main/events/library/change-game-playtime.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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;
|
||||||
|
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 favorite status: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("changeGamePlayTime", changeGamePlaytime);
|
||||||
@@ -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] =
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +44,10 @@ 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) {
|
||||||
@@ -61,10 +66,8 @@ 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) {
|
||||||
await 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,6 +6,7 @@ type ProfileGame = {
|
|||||||
id: string;
|
id: string;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
|
hasManuallyUpdatedPlaytime: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
} & ShopAssets;
|
} & ShopAssets;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
iconUrl: game.iconUrl,
|
iconUrl: game.iconUrl,
|
||||||
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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -189,6 +189,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: () =>
|
||||||
|
|||||||
@@ -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:;"
|
||||||
|
|||||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -162,6 +162,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>;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,15 @@
|
|||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__play-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__manual-warning {
|
||||||
|
color: #f59e0b; // Warning amber color
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ 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 +89,22 @@ export function HeroPanelPlaytime() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p
|
||||||
|
className="hero-panel-playtime__play-time"
|
||||||
|
data-tooltip-place="top"
|
||||||
|
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 +119,17 @@ export function HeroPanelPlaytime() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{game.hasManuallyUpdatedPlaytime && (
|
||||||
|
<Tooltip
|
||||||
|
id="manual-playtime-warning"
|
||||||
|
style={{
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
openOnClick={false}
|
||||||
|
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,168 @@
|
|||||||
|
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) {
|
||||||
|
console.log(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={handleChangePlaytime}
|
||||||
|
theme="outline"
|
||||||
|
disabled={!isValid || isSubmitting}
|
||||||
|
>
|
||||||
|
{t("update_playtime")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onClose} theme="primary">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
|||||||
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
import { useDownload, useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
import { RemoveGameFromLibraryModal } from "./remove-from-library-modal";
|
||||||
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
import { ResetAchievementsModal } from "./reset-achievements-modal";
|
||||||
|
import { ChangeGamePlaytimeModal } from "./change-game-playtime-modal";
|
||||||
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
import { FileDirectoryIcon, FileIcon } from "@primer/octicons-react";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
@@ -43,6 +44,7 @@ export function GameOptionsModal({
|
|||||||
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
||||||
const [showResetAchievementsModal, setShowResetAchievementsModal] =
|
const [showResetAchievementsModal, setShowResetAchievementsModal] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [showChangePlaytimeModal, setShowChangePlaytimeModal] = useState(false);
|
||||||
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
|
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
|
||||||
const [automaticCloudSync, setAutomaticCloudSync] = useState(
|
const [automaticCloudSync, setAutomaticCloudSync] = useState(
|
||||||
game.automaticCloudSync ?? false
|
game.automaticCloudSync ?? false
|
||||||
@@ -228,6 +230,20 @@ export function GameOptionsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangePlaytime = async (playtimeInSeconds: number) => {
|
||||||
|
try {
|
||||||
|
await window.electron.changeGamePlayTime(
|
||||||
|
game.shop,
|
||||||
|
game.objectId,
|
||||||
|
playtimeInSeconds
|
||||||
|
);
|
||||||
|
await updateGame();
|
||||||
|
showSuccessToast(t("update_playtime_success"));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(t("update_playtime_error"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleAutomaticCloudSync = async (
|
const handleToggleAutomaticCloudSync = async (
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
) => {
|
) => {
|
||||||
@@ -264,6 +280,13 @@ export function GameOptionsModal({
|
|||||||
game={game}
|
game={game}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ChangeGamePlaytimeModal
|
||||||
|
visible={showChangePlaytimeModal}
|
||||||
|
onClose={() => setShowChangePlaytimeModal(false)}
|
||||||
|
changePlaytime={handleChangePlaytime}
|
||||||
|
game={game}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title={game.title}
|
title={game.title}
|
||||||
@@ -476,6 +499,13 @@ export function GameOptionsModal({
|
|||||||
{t("reset_achievements")}
|
{t("reset_achievements")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowChangePlaytimeModal(true)}
|
||||||
|
theme="danger"
|
||||||
|
>
|
||||||
|
{t("update_game_playtime")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
|
|||||||
@@ -75,7 +75,9 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
&__manual-playtime {
|
||||||
|
color: globals.$warning-color;
|
||||||
|
}
|
||||||
&__stats {
|
&__stats {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import { UserGame } from "@types";
|
|||||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||||
import { useFormat } from "@renderer/hooks";
|
import { useFormat } from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useCallback, useContext } from "react";
|
import { useCallback, useContext, useState } from "react";
|
||||||
import {
|
import {
|
||||||
buildGameAchievementPath,
|
buildGameAchievementPath,
|
||||||
buildGameDetailsPath,
|
buildGameDetailsPath,
|
||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
import { ClockIcon, TrophyIcon, AlertFillIcon } from "@primer/octicons-react";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import "./user-library-game-card.scss";
|
import "./user-library-game-card.scss";
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export function UserLibraryGameCard({
|
|||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [isTooltipHovered, setIsTooltipHovered] = useState(false);
|
||||||
|
|
||||||
const getStatsItemCount = useCallback(() => {
|
const getStatsItemCount = useCallback(() => {
|
||||||
let statsCount = 1;
|
let statsCount = 1;
|
||||||
@@ -83,77 +85,108 @@ export function UserLibraryGameCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<>
|
||||||
onMouseEnter={onMouseEnter}
|
<li
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseEnter={onMouseEnter}
|
||||||
className="user-library-game__wrapper"
|
onMouseLeave={onMouseLeave}
|
||||||
title={game.title}
|
className="user-library-game__wrapper"
|
||||||
>
|
title={isTooltipHovered ? undefined : game.title}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="user-library-game__cover"
|
|
||||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
|
||||||
>
|
>
|
||||||
<div className="user-library-game__overlay">
|
<button
|
||||||
<small className="user-library-game__playtime">
|
type="button"
|
||||||
<ClockIcon size={11} />
|
className="user-library-game__cover"
|
||||||
{formatPlayTime(game.playTimeInSeconds)}
|
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||||
</small>
|
>
|
||||||
|
<div className="user-library-game__overlay">
|
||||||
|
<small
|
||||||
|
className="user-library-game__playtime"
|
||||||
|
data-tooltip-place="top"
|
||||||
|
data-tooltip-content={
|
||||||
|
game.hasManuallyUpdatedPlaytime
|
||||||
|
? t("manual_playtime_tooltip")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
data-tooltip-id={game.objectId}
|
||||||
|
>
|
||||||
|
{game.hasManuallyUpdatedPlaytime ? (
|
||||||
|
<AlertFillIcon
|
||||||
|
size={11}
|
||||||
|
className="user-library-game__manual-playtime"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClockIcon size={11} />
|
||||||
|
)}
|
||||||
|
{formatPlayTime(game.playTimeInSeconds)}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
{userProfile?.hasActiveSubscription &&
|
||||||
|
game.achievementCount > 0 && (
|
||||||
|
<div className="user-library-game__stats">
|
||||||
|
<div className="user-library-game__stats-header">
|
||||||
|
<div className="user-library-game__stats-content">
|
||||||
|
<div
|
||||||
|
className="user-library-game__stats-item"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrophyIcon size={13} />
|
||||||
|
<span>
|
||||||
|
{game.unlockedAchievementCount} /{" "}
|
||||||
|
{game.achievementCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{game.achievementsPointsEarnedSum > 0 && (
|
||||||
|
<div
|
||||||
|
className="user-library-game__stats-item"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HydraIcon width={16} height={16} />
|
||||||
|
{formatAchievementPoints(
|
||||||
|
game.achievementsPointsEarnedSum
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
|
|
||||||
<div className="user-library-game__stats">
|
|
||||||
<div className="user-library-game__stats-header">
|
|
||||||
<div className="user-library-game__stats-content">
|
|
||||||
<div
|
|
||||||
className="user-library-game__stats-item"
|
|
||||||
style={{
|
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrophyIcon size={13} />
|
|
||||||
<span>
|
<span>
|
||||||
{game.unlockedAchievementCount} / {game.achievementCount}
|
{formatDownloadProgress(
|
||||||
|
game.unlockedAchievementCount / game.achievementCount,
|
||||||
|
1
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{game.achievementsPointsEarnedSum > 0 && (
|
<progress
|
||||||
<div
|
max={1}
|
||||||
className="user-library-game__stats-item"
|
value={
|
||||||
style={{
|
game.unlockedAchievementCount / game.achievementCount
|
||||||
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
|
}
|
||||||
}}
|
className="user-library-game__achievements-progress"
|
||||||
>
|
/>
|
||||||
<HydraIcon width={16} height={16} />
|
|
||||||
{formatAchievementPoints(
|
|
||||||
game.achievementsPointsEarnedSum
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span>
|
<img
|
||||||
{formatDownloadProgress(
|
src={game.coverImageUrl}
|
||||||
game.unlockedAchievementCount / game.achievementCount,
|
alt={game.title}
|
||||||
1
|
className="user-library-game__game-image"
|
||||||
)}
|
/>
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
|
<Tooltip
|
||||||
<progress
|
id={game.objectId}
|
||||||
max={1}
|
style={{
|
||||||
value={game.unlockedAchievementCount / game.achievementCount}
|
zIndex: 9999,
|
||||||
className="user-library-game__achievements-progress"
|
}}
|
||||||
/>
|
openOnClick={false}
|
||||||
</div>
|
afterShow={() => setIsTooltipHovered(true)}
|
||||||
)}
|
afterHide={() => setIsTooltipHovered(false)}
|
||||||
</div>
|
/>
|
||||||
|
</>
|
||||||
<img
|
|
||||||
src={game.coverImageUrl}
|
|
||||||
alt={game.title}
|
|
||||||
className="user-library-game__game-image"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,16 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--tooltip {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CheckboxField } from "@renderer/components";
|
|||||||
import { useAppSelector } from "@renderer/hooks";
|
import { useAppSelector } from "@renderer/hooks";
|
||||||
import { settingsContext } from "@renderer/context";
|
import { settingsContext } from "@renderer/context";
|
||||||
import "./settings-behavior.scss";
|
import "./settings-behavior.scss";
|
||||||
|
import { QuestionIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export function SettingsBehavior() {
|
export function SettingsBehavior() {
|
||||||
const userPreferences = useAppSelector(
|
const userPreferences = useAppSelector(
|
||||||
@@ -25,6 +26,7 @@ export function SettingsBehavior() {
|
|||||||
showHiddenAchievementsDescription: false,
|
showHiddenAchievementsDescription: false,
|
||||||
showDownloadSpeedInMegabytes: false,
|
showDownloadSpeedInMegabytes: false,
|
||||||
extractFilesByDefault: true,
|
extractFilesByDefault: true,
|
||||||
|
enableSteamAchievements: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@@ -45,6 +47,8 @@ export function SettingsBehavior() {
|
|||||||
showDownloadSpeedInMegabytes:
|
showDownloadSpeedInMegabytes:
|
||||||
userPreferences.showDownloadSpeedInMegabytes ?? false,
|
userPreferences.showDownloadSpeedInMegabytes ?? false,
|
||||||
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
|
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
|
||||||
|
enableSteamAchievements:
|
||||||
|
userPreferences.enableSteamAchievements ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [userPreferences]);
|
}, [userPreferences]);
|
||||||
@@ -164,6 +168,25 @@ export function SettingsBehavior() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={`settings-behavior__checkbox-container--with-tooltip`}>
|
||||||
|
<CheckboxField
|
||||||
|
label={t("enable_steam_achievements")}
|
||||||
|
checked={form.enableSteamAchievements}
|
||||||
|
onChange={() =>
|
||||||
|
handleChange({
|
||||||
|
enableSteamAchievements: !form.enableSteamAchievements,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<small
|
||||||
|
className="settings-behavior__checkbox-container--tooltip"
|
||||||
|
data-open-article="steam-achievements"
|
||||||
|
>
|
||||||
|
<QuestionIcon size={12} />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export enum Cracker {
|
|||||||
onlineFix = "OnlineFix",
|
onlineFix = "OnlineFix",
|
||||||
goldberg = "Goldberg",
|
goldberg = "Goldberg",
|
||||||
userstats = "user_stats",
|
userstats = "user_stats",
|
||||||
|
Steam = "Steam",
|
||||||
rld = "RLD!",
|
rld = "RLD!",
|
||||||
empress = "EMPRESS",
|
empress = "EMPRESS",
|
||||||
skidrow = "SKIDROW",
|
skidrow = "SKIDROW",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export type UserGame = {
|
|||||||
unlockedAchievementCount: number;
|
unlockedAchievementCount: number;
|
||||||
achievementCount: number;
|
achievementCount: number;
|
||||||
achievementsPointsEarnedSum: number;
|
achievementsPointsEarnedSum: number;
|
||||||
|
hasManuallyUpdatedPlaytime: boolean;
|
||||||
} & ShopAssets;
|
} & ShopAssets;
|
||||||
|
|
||||||
export interface GameRunning {
|
export interface GameRunning {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface Game {
|
|||||||
launchOptions?: string | null;
|
launchOptions?: string | null;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
automaticCloudSync?: boolean;
|
automaticCloudSync?: boolean;
|
||||||
|
hasManuallyUpdatedPlaytime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Download {
|
export interface Download {
|
||||||
@@ -101,6 +102,7 @@ export interface UserPreferences {
|
|||||||
friendStartGameNotificationsEnabled?: boolean;
|
friendStartGameNotificationsEnabled?: boolean;
|
||||||
showDownloadSpeedInMegabytes?: boolean;
|
showDownloadSpeedInMegabytes?: boolean;
|
||||||
extractFilesByDefault?: boolean;
|
extractFilesByDefault?: boolean;
|
||||||
|
enableSteamAchievements?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenState {
|
export interface ScreenState {
|
||||||
|
|||||||
Reference in New Issue
Block a user