diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 7cdd0c92..bfc353d9 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -27,7 +27,68 @@ "friends": "好友", "favorites": "收藏", "need_help": "需要帮助?", - "playable_button_title": "仅显示现在可以游玩的游戏" + "playable_button_title": "仅显示现在可以游玩的游戏", + "add_custom_game_tooltip": "添加自定义游戏", + "cancel": "取消", + "confirm": "确认", + "custom_game_modal": "添加自定义游戏", + "custom_game_modal_add": "添加游戏", + "custom_game_modal_adding": "正在添加游戏...", + "custom_game_modal_browse": "浏览", + "custom_game_modal_cancel": "取消", + "custom_game_modal_description": "通过选择可执行文件将自定义游戏添加到您的库中", + "custom_game_modal_enter_title": "输入标题", + "custom_game_modal_executable": "可执行文件", + "custom_game_modal_executable_path": "可执行文件路径", + "custom_game_modal_failed": "添加自定义游戏失败", + "custom_game_modal_select_executable": "选择可执行文件", + "custom_game_modal_success": "自定义游戏添加成功", + "custom_game_modal_title": "标题", + "decky_plugin_installation_error": "安装 Decky 插件出错: {{error}}", + "decky_plugin_installation_failed": "Decky 插件安装失败: {{error}}", + "decky_plugin_installed": "Decky 插件 v{{version}} 安装成功", + "decky_plugin_installed_version": "Decky 插件 (v{{version}})", + "edit_game_modal": "自定义资源", + "edit_game_modal_assets": "资源", + "edit_game_modal_browse": "浏览", + "edit_game_modal_cancel": "取消", + "edit_game_modal_description": "自定义游戏资源和详情", + "edit_game_modal_drop_hero_image_here": "拖放主图像到此处", + "edit_game_modal_drop_icon_image_here": "拖放图标到此处", + "edit_game_modal_drop_logo_image_here": "拖放Logo到此处", + "edit_game_modal_drop_to_replace_hero": "拖放以替换主图像", + "edit_game_modal_drop_to_replace_icon": "拖放以替换图标", + "edit_game_modal_drop_to_replace_logo": "拖放以替换Logo", + "edit_game_modal_enter_title": "输入标题", + "edit_game_modal_failed": "资源更新失败", + "edit_game_modal_fill_required": "请填写所有必填项", + "edit_game_modal_hero": "库主图", + "edit_game_modal_hero_preview": "库主图预览", + "edit_game_modal_hero_resolution": "推荐分辨率: 1920x620px", + "edit_game_modal_icon": "图标", + "edit_game_modal_icon_preview": "图标预览", + "edit_game_modal_icon_resolution": "推荐分辨率: 256x256px", + "edit_game_modal_image": "图片", + "edit_game_modal_image_filter": "图片", + "edit_game_modal_image_preview": "图片预览", + "edit_game_modal_logo": "Logo", + "edit_game_modal_logo_preview": "Logo预览", + "edit_game_modal_logo_resolution": "推荐分辨率: 640x360px", + "edit_game_modal_select_hero": "选择库主图", + "edit_game_modal_select_icon": "选择图标", + "edit_game_modal_select_image": "选择图片", + "edit_game_modal_select_logo": "选择Logo", + "edit_game_modal_success": "资源更新成功", + "edit_game_modal_title": "标题", + "edit_game_modal_update": "更新", + "edit_game_modal_updating": "正在更新...", + "install_decky_plugin": "安装 Decky 插件", + "install_decky_plugin_message": "这将下载并安装 Hydra 的 Decky Loader 插件。可能需要提升权限。继续吗?", + "install_decky_plugin_title": "安装 Hydra Decky 插件", + "show_playable_only_tooltip": "仅显示可游玩", + "update_decky_plugin": "更新 Decky 插件", + "update_decky_plugin_message": "有新版本的 Hydra Decky 插件可用。现在要更新吗?", + "update_decky_plugin_title": "更新 Hydra Decky 插件" }, "header": { "search": "搜索游戏", @@ -218,7 +279,93 @@ "reset_achievements_title": "您确定吗?", "save_changes": "保存更改", "unfreeze_backup": "取消固定", - "you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改" + "you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改", + "add_to_favorites": "添加到收藏", + "already_in_library": "已在游戏库中", + "audio": "音频", + "backup_failed": "备份失败", + "be_first_to_review": "成为第一个分享游戏感受的人!", + "caption": "标题", + "create_shortcut_simple": "创建快捷方式", + "currency_country": "zh", + "currency_symbol": "¥", + "delete_review": "删除评价", + "delete_review_modal_cancel_button": "取消", + "delete_review_modal_delete_button": "删除", + "delete_review_modal_description": "此操作无法撤销。", + "delete_review_modal_title": "确定要删除您的评价吗?", + "edit_game_modal_button": "自定义游戏资源", + "failed_remove_files": "文件删除失败", + "failed_remove_from_library": "移出游戏库失败", + "failed_update_favorites": "收藏更新失败", + "files_removed_success": "文件已成功删除", + "filter_by_source": "按来源筛选", + "game_added_to_pinned": "游戏已添加到置顶", + "game_details": "游戏详情", + "game_removed_from_library": "游戏已从库中移除", + "game_removed_from_pinned": "游戏已从置顶移除", + "hide": "隐藏", + "hide_original": "隐藏原文", + "historical_keyshop": "历史密钥商店", + "historical_retail": "历史零售", + "keyshop_price": "密钥商店价格", + "language": "语言", + "leave_a_review": "留下评价", + "load_more_reviews": "加载更多评价", + "loading_more_reviews": "正在加载更多评价...", + "loading_reviews": "正在加载评价...", + "manual_playtime_tooltip": "该游戏时长已手动更新", + "manual_playtime_warning": "您的游戏时长将被标记为手动更新,且无法撤销。", + "maybe_later": "以后再说", + "no_prices_found": "未找到价格信息", + "no_repacks_found": "未找到该游戏的下载来源", + "no_reviews_yet": "暂无评价", + "prices": "价格", + "properties": "属性", + "rating": "评分", + "rating_count": "评分数", + "rating_negative": "差评", + "rating_neutral": "中性", + "rating_positive": "好评", + "rating_stats": "评分统计", + "rating_very_negative": "极差", + "rating_very_positive": "极好", + "remove_from_favorites": "移出收藏", + "remove_review": "移除评价", + "retail_price": "零售价格", + "review_cannot_be_empty": "评价内容不能为空。", + "review_deleted_successfully": "评价已成功删除。", + "review_deletion_failed": "评价删除失败,请重试。", + "review_from_blocked_user": "来自被屏蔽用户的评价", + "review_played_for": "已游玩", + "review_submission_failed": "评价提交失败,请重试。", + "review_submitted_successfully": "评价提交成功!", + "reviews": "评价", + "show": "显示", + "show_less": "收起", + "show_more": "展开", + "show_original": "显示原文", + "show_original_translated_from": "显示原文(由{{language}}翻译)", + "show_translation": "显示翻译", + "sort_highest_score": "最高分", + "sort_lowest_score": "最低分", + "sort_most_voted": "最多投票", + "sort_newest": "最新", + "sort_oldest": "最旧", + "submit_review": "提交", + "submitting": "正在提交...", + "update_game_playtime": "更新游戏时长", + "update_playtime": "更新时长", + "update_playtime_description": "手动更新 {{game}} 的游玩时长", + "update_playtime_error": "游戏时长更新失败", + "update_playtime_success": "游戏时长已成功更新", + "update_playtime_title": "更新游戏时长", + "view_all_prices": "点击查看所有价格", + "vote_failed": "投票失败,请重试。", + "would_you_recommend_this_game": "您想为此游戏留下评价吗?", + "write_review_placeholder": "分享您对本游戏的看法...", + "yes": "是", + "you_seemed_to_enjoy_this_game": "您似乎很喜欢这款游戏" }, "activation": { "title": "激活 Hydra", @@ -394,7 +541,24 @@ "update_email": "更新邮箱", "update_password": "更新密码", "variation": "变体", - "web_store": "网络商店" + "web_store": "网络商店", + "adding": "添加中…", + "autoplay_trailers_on_game_page": "在游戏页面自动播放预告片", + "debrid": "Debrid下载服务", + "debrid_description": "Debrid服务是一种高级不限速下载器,可让您以最快的网速下载托管在各类网盘上的文件,仅受您的网络速度限制。", + "download_source_already_exists": "该下载源URL已存在。", + "download_source_failed": "出错", + "download_source_matched": "已更新", + "download_source_matching": "正在更新", + "download_source_no_information": "暂无信息", + "download_source_pending_matching": "即将更新", + "download_sources_synced_successfully": "所有下载源已同步", + "enable_steam_achievements": "启用Steam成就搜索", + "failed_add_download_source": "添加下载源失败,请重试。", + "hide_to_tray_on_game_start": "启动游戏时隐藏到托盘", + "hydra_cloud": "Hydra Cloud", + "importing": "导入中…", + "removed_all_download_sources": "已移除所有下载源" }, "notifications": { "download_complete": "下载完成", @@ -421,7 +585,8 @@ "game_card": { "no_downloads": "无可用下载选项", "available_one": "可用", - "available_other": "可用" + "available_other": "可用", + "calculating": "正在计算" }, "binary_not_found_modal": { "title": "程序未安装", @@ -515,7 +680,23 @@ "show_achievements_on_profile": "在您的个人资料上显示成就", "show_points_on_profile": "在您的个人资料上显示获得的积分", "stats": "统计", - "top_percentile": "前 {{percentile}}%" + "top_percentile": "前 {{percentile}}%", + "achievements_earned": "已获得成就", + "amount_hours_short": "{{amount}}小时", + "amount_minutes_short": "{{amount}}分钟", + "delete_review": "删除评价", + "game_added_to_pinned": "游戏已添加到置顶", + "game_removed_from_pinned": "游戏已从置顶移除", + "karma": "业力", + "karma_count": "业力值", + "karma_description": "通过评论获得的点赞", + "loading_reviews": "正在加载评价...", + "manual_playtime_tooltip": "该游戏时长已手动更新", + "pinned": "已置顶", + "played_recently": "最近游玩", + "playtime": "游戏时长", + "sort_by": "排序方式:", + "user_reviews": "用户评价" }, "achievement": { "achievement_unlocked": "成就已解锁", diff --git a/src/main/events/themes/copy-theme-achievement-sound.ts b/src/main/events/themes/copy-theme-achievement-sound.ts index 2ec10198..a52e6269 100644 --- a/src/main/events/themes/copy-theme-achievement-sound.ts +++ b/src/main/events/themes/copy-theme-achievement-sound.ts @@ -18,7 +18,7 @@ const copyThemeAchievementSound = async ( throw new Error("Theme not found"); } - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); if (!fs.existsSync(themeDir)) { fs.mkdirSync(themeDir, { recursive: true }); diff --git a/src/main/events/themes/get-theme-sound-data-url.ts b/src/main/events/themes/get-theme-sound-data-url.ts index b9ace306..a93538dd 100644 --- a/src/main/events/themes/get-theme-sound-data-url.ts +++ b/src/main/events/themes/get-theme-sound-data-url.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { getThemeSoundPath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; import fs from "node:fs"; import path from "node:path"; import { logger } from "@main/services"; @@ -9,7 +10,8 @@ const getThemeSoundDataUrl = async ( themeId: string ): Promise => { try { - const soundPath = getThemeSoundPath(themeId); + const theme = await themesSublevel.get(themeId); + const soundPath = getThemeSoundPath(themeId, theme?.name); if (!soundPath || !fs.existsSync(soundPath)) { return null; diff --git a/src/main/events/themes/get-theme-sound-path.ts b/src/main/events/themes/get-theme-sound-path.ts index 37783949..11658c6a 100644 --- a/src/main/events/themes/get-theme-sound-path.ts +++ b/src/main/events/themes/get-theme-sound-path.ts @@ -1,11 +1,13 @@ import { registerEvent } from "../register-event"; import { getThemeSoundPath } from "@main/helpers"; +import { themesSublevel } from "@main/level"; const getThemeSoundPathEvent = async ( _event: Electron.IpcMainInvokeEvent, themeId: string ): Promise => { - return getThemeSoundPath(themeId); + const theme = await themesSublevel.get(themeId); + return getThemeSoundPath(themeId, theme?.name); }; registerEvent("getThemeSoundPath", getThemeSoundPathEvent); diff --git a/src/main/events/themes/import-theme-sound-from-store.ts b/src/main/events/themes/import-theme-sound-from-store.ts index 588db6f5..66da6cb3 100644 --- a/src/main/events/themes/import-theme-sound-from-store.ts +++ b/src/main/events/themes/import-theme-sound-from-store.ts @@ -28,7 +28,7 @@ const importThemeSoundFromStore = async ( timeout: 10000, }); - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); if (!fs.existsSync(themeDir)) { fs.mkdirSync(themeDir, { recursive: true }); @@ -46,6 +46,10 @@ const importThemeSoundFromStore = async ( logger.log(`Successfully imported sound for theme ${themeName}`); return; } catch (error) { + logger.error( + `Failed to import ${format} sound for theme ${themeName}`, + error + ); continue; } } diff --git a/src/main/events/themes/remove-theme-achievement-sound.ts b/src/main/events/themes/remove-theme-achievement-sound.ts index 16500a11..a8603426 100644 --- a/src/main/events/themes/remove-theme-achievement-sound.ts +++ b/src/main/events/themes/remove-theme-achievement-sound.ts @@ -2,6 +2,7 @@ import { registerEvent } from "../register-event"; import fs from "node:fs"; import { getThemePath } from "@main/helpers"; import { themesSublevel } from "@main/level"; +import { THEMES_PATH } from "@main/constants"; import path from "node:path"; const removeThemeAchievementSound = async ( @@ -13,19 +14,27 @@ const removeThemeAchievementSound = async ( throw new Error("Theme not found"); } - const themeDir = getThemePath(themeId); + const themeDir = getThemePath(themeId, theme.name); + const legacyThemeDir = path.join(THEMES_PATH, themeId); - if (!fs.existsSync(themeDir)) { - return; - } - - const formats = ["wav", "mp3", "ogg", "m4a"]; - - for (const format of formats) { - const soundPath = path.join(themeDir, `achievement.${format}`); - if (fs.existsSync(soundPath)) { - await fs.promises.unlink(soundPath); + const removeFromDir = async (dir: string) => { + if (!fs.existsSync(dir)) { + return; } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(dir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + await fs.promises.unlink(soundPath); + } + } + }; + + await removeFromDir(themeDir); + if (themeDir !== legacyThemeDir) { + await removeFromDir(legacyThemeDir); } await themesSublevel.put(themeId, { diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index ae19fbdb..664dbd78 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -33,28 +33,61 @@ export const isPortableVersion = () => { }; export const normalizePath = (str: string) => - path.posix.normalize(str).replace(/\\/g, "/"); + path.posix.normalize(str).replaceAll("\\", "/"); export const addTrailingSlash = (str: string) => str.endsWith("/") ? str : `${str}/`; -export const getThemePath = (themeId: string) => - path.join(THEMES_PATH, themeId); +const sanitizeFolderName = (name: string): string => { + return name + .toLowerCase() + .replaceAll(/[^a-z0-9-_\s]/g, "") + .replaceAll(/\s+/g, "-") + .replaceAll(/-+/g, "-") + .replaceAll(/(^-|-$)/g, ""); +}; -export const getThemeSoundPath = (themeId: string): string | null => { - const themeDir = getThemePath(themeId); +export const getThemePath = (themeId: string, themeName?: string): string => { + if (themeName) { + const sanitizedName = sanitizeFolderName(themeName); + if (sanitizedName) { + return path.join(THEMES_PATH, sanitizedName); + } + } + return path.join(THEMES_PATH, themeId); +}; + +export const getThemeSoundPath = ( + themeId: string, + themeName?: string +): string | null => { + const themeDir = getThemePath(themeId, themeName); + const legacyThemeDir = themeName ? path.join(THEMES_PATH, themeId) : null; + + const checkDir = (dir: string): string | null => { + if (!fs.existsSync(dir)) { + return null; + } + + const formats = ["wav", "mp3", "ogg", "m4a"]; + + for (const format of formats) { + const soundPath = path.join(dir, `achievement.${format}`); + if (fs.existsSync(soundPath)) { + return soundPath; + } + } - if (!fs.existsSync(themeDir)) { return null; + }; + + const soundPath = checkDir(themeDir); + if (soundPath) { + return soundPath; } - const formats = ["wav", "mp3", "ogg", "m4a"]; - - for (const format of formats) { - const soundPath = path.join(themeDir, `achievement.${format}`); - if (fs.existsSync(soundPath)) { - return soundPath; - } + if (legacyThemeDir) { + return checkDir(legacyThemeDir); } return null; diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index 926ba47a..b8ff480c 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -47,7 +47,10 @@ async function getAchievementSoundPath(): Promise { const activeTheme = allThemes.find((theme) => theme.isActive); if (activeTheme?.hasCustomSound) { - const themeSoundPath = getThemeSoundPath(activeTheme.id); + const themeSoundPath = getThemeSoundPath( + activeTheme.id, + activeTheme.name + ); if (themeSoundPath) { return themeSoundPath; } diff --git a/src/main/services/steam-250.ts b/src/main/services/steam-250.ts index 5652b0d3..826e528f 100644 --- a/src/main/services/steam-250.ts +++ b/src/main/services/steam-250.ts @@ -16,7 +16,7 @@ export const requestSteam250 = async (path: string) => { if (!steamGameUrl) return null; return { - title: $title.textContent, + title: $title.getAttribute("data-title") || "", objectId: steamGameUrl.split("/").pop(), } as Steam250Game; }) diff --git a/src/renderer/src/pages/game-details/hero.scss b/src/renderer/src/pages/game-details/hero.scss index 41264fe4..fd071eec 100644 --- a/src/renderer/src/pages/game-details/hero.scss +++ b/src/renderer/src/pages/game-details/hero.scss @@ -231,44 +231,50 @@ $hero-height: 350px; } &__randomizer-button { - padding: calc(globals.$spacing-unit * 1.5); - background-color: rgba(0, 0, 0, 0.6); + position: fixed; + bottom: calc(globals.$spacing-unit * 5); + right: calc(globals.$spacing-unit * 2); + z-index: 100; + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2); + background-color: rgba(255, 255, 255, 0.08); backdrop-filter: blur(20px); border-radius: 8px; transition: all ease 0.2s; cursor: pointer; min-height: 40px; - min-width: 40px; display: flex; align-items: center; justify-content: center; + gap: globals.$spacing-unit; color: globals.$muted-color; border: solid 1px globals.$border-color; - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8); + box-shadow: + 0px 0px 10px 0px rgba(0, 0, 0, 0.8), + 0px 2px 8px 0px rgba(255, 255, 255, 0.1); animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1); + overflow: visible; - &:active { - opacity: 0.9; + &:disabled { + opacity: globals.$disabled-opacity; + cursor: not-allowed; } &:hover { - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(255, 255, 255, 0.12); color: globals.$body-color; } } &__stars-icon-container { - width: 20px; + width: 16px; height: 16px; - display: flex; - align-items: center; - justify-content: center; position: relative; } &__stars-icon { - width: 26px; + width: 70px; position: absolute; - top: -3px; + top: -28px; + left: -27px; } }