mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc1c1c58c | ||
|
|
fbb67af1f6 | ||
|
|
4211f97dd0 | ||
|
|
f569b142f3 | ||
|
|
18e4baec3d | ||
|
|
b7199f4d95 | ||
|
|
2b8cc506df | ||
|
|
e0c5f80b68 | ||
|
|
63374ccd74 | ||
|
|
8eeacf478d | ||
|
|
87a4c27977 | ||
|
|
8177c3fd0c | ||
|
|
d28a5b828f | ||
|
|
68288adef3 | ||
|
|
51894d9924 | ||
|
|
2107261f7b | ||
|
|
6a94c3c812 | ||
|
|
c7ecd541d5 | ||
|
|
2dc700188d | ||
|
|
07ac13f4cc | ||
|
|
de605ecd02 | ||
|
|
81654d7688 | ||
|
|
14a8336695 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.6.3",
|
||||
"version": "3.6.5",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"accuracy": "{{accuracy}}% accuracy",
|
||||
"add_to_library": "Add to library",
|
||||
"already_in_library": "Already in library",
|
||||
"remove_from_library": "Remove from library",
|
||||
"no_downloads": "No downloads available",
|
||||
"play_time": "Played for {{amount}}",
|
||||
@@ -379,6 +380,7 @@
|
||||
"installing_common_redist": "Installing…",
|
||||
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
|
||||
"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",
|
||||
"top-left": "Top left",
|
||||
"top-center": "Top center",
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% de precisão",
|
||||
"add_to_library": "Adicionar à biblioteca",
|
||||
"already_in_library": "Já está na biblioteca",
|
||||
"remove_from_library": "Remover da biblioteca",
|
||||
"no_downloads": "Nenhum download disponível",
|
||||
"play_time": "Jogou por {{amount}}",
|
||||
@@ -364,6 +365,7 @@
|
||||
"installing_common_redist": "Instalando…",
|
||||
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
|
||||
"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",
|
||||
"top-left": "Superior esquerdo",
|
||||
"top-center": "Superior central",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"language_name": "中文",
|
||||
"language_name": "简体中文",
|
||||
"app": {
|
||||
"successfully_signed_in": "已成功登录"
|
||||
},
|
||||
@@ -26,7 +26,9 @@
|
||||
"game_has_no_executable": "未选择游戏的可执行文件",
|
||||
"sign_in": "登入",
|
||||
"friends": "好友",
|
||||
"favorites": "收藏"
|
||||
"favorites": "收藏",
|
||||
"need_help": "需要帮助?",
|
||||
"playable_button_title": "仅显示现在可以游玩的游戏"
|
||||
},
|
||||
"header": {
|
||||
"search": "搜索游戏",
|
||||
@@ -43,11 +45,23 @@
|
||||
"downloading_metadata": "正在下载{{title}}的元数据…",
|
||||
"downloading": "正在下载{{title}}… ({{percentage}}完成) - 剩余时间{{eta}} - 速度{{speed}}",
|
||||
"calculating_eta": "正在下载 {{title}}… (已完成{{percentage}}.) - 正在计算剩余时间...",
|
||||
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)"
|
||||
"checking_files": "正在校验 {{title}} 的文件... ({{percentage}} 已完成)",
|
||||
"installation_complete": "安装完成",
|
||||
"installation_complete_message": "通用可再发行组件安装成功",
|
||||
"installing_common_redist": "{{log}}…"
|
||||
},
|
||||
"catalogue": {
|
||||
"next_page": "下一页",
|
||||
"previous_page": "上一页"
|
||||
"previous_page": "上一页",
|
||||
"clear_filters": "清除已选的 {{filterCount}} 项",
|
||||
"developers": "开发商",
|
||||
"download_sources": "下载源",
|
||||
"filter_count": "{{filterCount}} 项可用",
|
||||
"genres": "类型",
|
||||
"publishers": "发行商",
|
||||
"result_count": "{{resultCount}} 个结果",
|
||||
"search": "筛选…",
|
||||
"tags": "标签"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "打开下载菜单",
|
||||
@@ -166,7 +180,48 @@
|
||||
"manage_files_description": "管理哪些文件要备份和恢复",
|
||||
"select_folder": "选择文件夹",
|
||||
"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": {
|
||||
"title": "激活 Hydra",
|
||||
@@ -199,7 +254,13 @@
|
||||
"queued": "下载列表",
|
||||
"no_downloads_title": "空空如也",
|
||||
"no_downloads_description": "你还未使用Hydra下载任何游戏,但什么时候开始,都为时不晚。",
|
||||
"checking_files": "正在校验文件…"
|
||||
"checking_files": "正在校验文件…",
|
||||
"extract": "解压文件",
|
||||
"extracting": "正在解压文件…",
|
||||
"options": "管理",
|
||||
"resume_seeding": "恢复做种",
|
||||
"seeding": "做种中",
|
||||
"stop_seeding": "停止做种"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "下载路径",
|
||||
@@ -260,7 +321,83 @@
|
||||
"must_be_valid_url": "来源必须是有效的 URL",
|
||||
"blocked_users": "已屏蔽用户",
|
||||
"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": {
|
||||
"download_complete": "下载完成",
|
||||
@@ -271,14 +408,23 @@
|
||||
"new_update_available": "版本 {{version}} 可用",
|
||||
"restart_to_install_update": "重启 Hydra 以安装更新",
|
||||
"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": {
|
||||
"open": "打开 Hydra",
|
||||
"quit": "退出"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "无可用下载选项"
|
||||
"no_downloads": "无可用下载选项",
|
||||
"available_one": "可用",
|
||||
"available_other": "可用"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "程序未安装",
|
||||
@@ -351,7 +497,7 @@
|
||||
"report_description": "额外信息",
|
||||
"report_description_placeholder": "额外信息",
|
||||
"report": "举报",
|
||||
"report_reason_hate": "Hate speech",
|
||||
"report_reason_hate": "仇恨言论",
|
||||
"report_reason_sexual_content": "色情内容",
|
||||
"report_reason_violence": "暴力",
|
||||
"report_reason_spam": "骚扰",
|
||||
@@ -360,7 +506,19 @@
|
||||
"your_friend_code": "你的好友代码:",
|
||||
"upload_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_unlocked": "成就已解锁",
|
||||
@@ -368,7 +526,14 @@
|
||||
"your_achievements": "你的成就",
|
||||
"unlocked_at": "解锁于: {{date}}",
|
||||
"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": {
|
||||
"subscription_tour_title": "Hydra 云订阅",
|
||||
@@ -378,6 +543,10 @@
|
||||
"animated_profile_picture": "动画头像",
|
||||
"premium_support": "高级技术支持",
|
||||
"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 MAIN_LOOP_INTERVAL = 1500;
|
||||
export const MAIN_LOOP_INTERVAL = 2000;
|
||||
|
||||
@@ -94,7 +94,7 @@ const createSteamShortcut = async (
|
||||
|
||||
if (!steamUserIds.length) {
|
||||
logger.error("No Steam user ID found");
|
||||
return;
|
||||
throw new Error("No Steam user ID found");
|
||||
}
|
||||
|
||||
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
|
||||
|
||||
@@ -80,7 +80,7 @@ const getUnlockedAchievementsEvent = async (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
): Promise<UserAchievement[]> => {
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||
return getUnlockedAchievements(objectId, shop, false);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mergeAchievements } from "./merge-achievements";
|
||||
import fs, { readdirSync } from "node:fs";
|
||||
import {
|
||||
findAchievementFileInExecutableDirectory,
|
||||
findAchievementFileInSteamPath,
|
||||
findAchievementFiles,
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
@@ -43,6 +44,10 @@ const watchAchievementsWindows = async () => {
|
||||
gameAchievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
|
||||
gameAchievementFiles.push(
|
||||
...(await findAchievementFileInSteamPath(game))
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
@@ -61,10 +66,8 @@ const watchAchievementsWithWine = async () => {
|
||||
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
for (const file of gameAchievementFiles) {
|
||||
await compareFile(game, file);
|
||||
@@ -167,15 +170,14 @@ export class AchievementWatcherManager {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
if (this.alreadySyncedGames.get(gameKey)) return;
|
||||
|
||||
this.alreadySyncedGames.set(gameKey, true);
|
||||
|
||||
const game = await gamesSublevel.get(gameKey).catch(() => null);
|
||||
if (!game || game.isDeleted) return;
|
||||
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
gameAchievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
@@ -190,8 +192,6 @@ export class AchievementWatcherManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.alreadySyncedGames.set(gameKey, true);
|
||||
|
||||
const newAchievements = await mergeAchievements(
|
||||
game,
|
||||
unlockedAchievements,
|
||||
@@ -259,7 +259,7 @@ export class AchievementWatcherManager {
|
||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
games.map(async (game) => {
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
@@ -270,6 +270,10 @@ export class AchievementWatcherManager {
|
||||
achievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
|
||||
achievementFiles.push(
|
||||
...(await findAchievementFileInSteamPath(game))
|
||||
);
|
||||
}
|
||||
|
||||
return { game, achievementFiles };
|
||||
@@ -284,12 +288,10 @@ export class AchievementWatcherManager {
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
games.map(async (game) => {
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
achievementFiles.push(...achievementFileInsideDirectory);
|
||||
achievementFiles.push(...(await findAchievementFileInSteamPath(game)));
|
||||
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { Game, AchievementFile } from "@types";
|
||||
import type { Game, AchievementFile, UserPreferences } from "@types";
|
||||
import { Cracker } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { SystemPath } from "../system-path";
|
||||
import { getSteamLocation, getSteamUsersIds } from "../steam";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const getAppDataPath = () => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ export const parseAchievementFile = (
|
||||
return processRazor1911(filePath);
|
||||
}
|
||||
|
||||
if (type === Cracker.Steam) {
|
||||
const parsed = jsonParse(filePath);
|
||||
return processSteamCacheAchievement(parsed);
|
||||
}
|
||||
|
||||
achievementsLogger.log(
|
||||
`Unprocessed ${type} achievements found on ${filePath}`
|
||||
);
|
||||
@@ -234,6 +239,35 @@ const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
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 newUnlockedAchievements: UnlockedAchievement[] = [];
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CloudSync } from "./cloud-sync";
|
||||
import { logger } from "./logger";
|
||||
import path from "path";
|
||||
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||
import { MAIN_LOOP_INTERVAL } from "@main/constants";
|
||||
|
||||
export const gamesPlaytime = new Map<
|
||||
string,
|
||||
@@ -25,7 +26,7 @@ interface GameExecutables {
|
||||
[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;
|
||||
|
||||
const platform = process.platform;
|
||||
|
||||
@@ -42,6 +42,10 @@ export const getSteamLocation = async () => {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
reject(new Error("SteamPath not found in registry"));
|
||||
}
|
||||
|
||||
resolve(value.value);
|
||||
});
|
||||
});
|
||||
@@ -78,7 +82,11 @@ export const getSteamAppDetails = async (
|
||||
};
|
||||
|
||||
export const getSteamUsersIds = async () => {
|
||||
const userDataPath = await getSteamLocation();
|
||||
const userDataPath = await getSteamLocation().catch(() => null);
|
||||
|
||||
if (!userDataPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
|
||||
withFileTypes: true,
|
||||
|
||||
@@ -582,7 +582,7 @@ export class WindowManager {
|
||||
tray.popUpContextMenu(contextMenu);
|
||||
};
|
||||
|
||||
tray.setToolTip("Hydra");
|
||||
tray.setToolTip("Hydra Launcher");
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
await updateSystemTray();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hydra</title>
|
||||
<title>Hydra Launcher</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' 'unsafe-inline' * data: local:;"
|
||||
|
||||
@@ -16,6 +16,25 @@
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Badge } from "@renderer/components";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { useAppSelector, useRepacks } from "@renderer/hooks";
|
||||
import { useMemo } from "react";
|
||||
import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import "./game-item.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 {
|
||||
game: CatalogueSearchResult;
|
||||
@@ -16,7 +17,9 @@ export interface GameItemProps {
|
||||
export function GameItem({ game }: GameItemProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const { i18n, t } = useTranslation("game_details");
|
||||
|
||||
const language = i18n.language.split("-")[0];
|
||||
|
||||
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
|
||||
|
||||
@@ -24,7 +27,41 @@ export function GameItem({ game }: GameItemProps) {
|
||||
|
||||
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(() => {
|
||||
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
|
||||
@@ -36,7 +73,11 @@ export function GameItem({ game }: GameItemProps) {
|
||||
(steamGenre) => steamGenre === genre
|
||||
);
|
||||
|
||||
if (index && steamGenres[language] && steamGenres[language][index]) {
|
||||
if (
|
||||
index !== undefined &&
|
||||
steamGenres[language] &&
|
||||
steamGenres[language][index]
|
||||
) {
|
||||
return steamGenres[language][index];
|
||||
}
|
||||
|
||||
@@ -81,6 +122,23 @@ export function GameItem({ game }: GameItemProps) {
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,5 +9,16 @@
|
||||
opacity: 1;
|
||||
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 { settingsContext } from "@renderer/context";
|
||||
import "./settings-behavior.scss";
|
||||
import { QuestionIcon } from "@primer/octicons-react";
|
||||
|
||||
export function SettingsBehavior() {
|
||||
const userPreferences = useAppSelector(
|
||||
@@ -25,6 +26,7 @@ export function SettingsBehavior() {
|
||||
showHiddenAchievementsDescription: false,
|
||||
showDownloadSpeedInMegabytes: false,
|
||||
extractFilesByDefault: true,
|
||||
enableSteamAchievements: false,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
@@ -45,6 +47,8 @@ export function SettingsBehavior() {
|
||||
showDownloadSpeedInMegabytes:
|
||||
userPreferences.showDownloadSpeedInMegabytes ?? false,
|
||||
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
|
||||
enableSteamAchievements:
|
||||
userPreferences.enableSteamAchievements ?? false,
|
||||
});
|
||||
}
|
||||
}, [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",
|
||||
goldberg = "Goldberg",
|
||||
userstats = "user_stats",
|
||||
Steam = "Steam",
|
||||
rld = "RLD!",
|
||||
empress = "EMPRESS",
|
||||
skidrow = "SKIDROW",
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface UserPreferences {
|
||||
friendStartGameNotificationsEnabled?: boolean;
|
||||
showDownloadSpeedInMegabytes?: boolean;
|
||||
extractFilesByDefault?: boolean;
|
||||
enableSteamAchievements?: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenState {
|
||||
|
||||
Reference in New Issue
Block a user