From 7e59e02d033bccc7f83fbe0577e40848760749c8 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 16:18:49 +0300 Subject: [PATCH 01/43] Feat: Custom Games --- src/locales/en/translation.json | 55 +++- src/locales/ru/translation.json | 53 ++- src/main/events/index.ts | 2 + .../library/add-custom-game-to-library.ts | 68 ++++ .../events/library/change-game-playtime.ts | 12 +- src/main/events/library/update-custom-game.ts | 53 +++ src/main/index.ts | 48 +++ .../library-sync/merge-with-remote-games.ts | 2 + .../library-sync/upload-games-batch.ts | 2 +- src/preload/index.ts | 19 ++ src/renderer/src/assets/play-logo.svg | 4 + .../sidebar-adding-custom-game-modal.scss | 52 +++ .../sidebar-adding-custom-game-modal.tsx | 183 +++++++++++ .../components/sidebar/sidebar-game-item.tsx | 12 +- .../src/components/sidebar/sidebar.scss | 20 ++ .../src/components/sidebar/sidebar.tsx | 54 ++- .../game-details/game-details.context.tsx | 6 + src/renderer/src/declaration.d.ts | 15 + src/renderer/src/helpers.ts | 20 ++ .../game-details/game-details-content.tsx | 98 ++++-- .../src/pages/game-details/game-details.scss | 37 +++ .../hero/hero-panel-playtime.scss | 1 + .../game-details/hero/hero-panel-playtime.tsx | 18 +- .../modals/edit-custom-game-modal.scss | 46 +++ .../modals/edit-custom-game-modal.tsx | 311 ++++++++++++++++++ .../src/pages/game-details/modals/index.ts | 1 + src/types/game.types.ts | 2 +- src/types/level.types.ts | 2 + 28 files changed, 1145 insertions(+), 51 deletions(-) create mode 100644 src/main/events/library/add-custom-game-to-library.ts create mode 100644 src/main/events/library/update-custom-game.ts create mode 100644 src/renderer/src/assets/play-logo.svg create mode 100644 src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss create mode 100644 src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx create mode 100644 src/renderer/src/pages/game-details/modals/edit-custom-game-modal.scss create mode 100644 src/renderer/src/pages/game-details/modals/edit-custom-game-modal.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9f8de8f8..86ef567b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -28,7 +28,60 @@ "friends": "Friends", "need_help": "Need help?", "favorites": "Favorites", - "playable_button_title": "Show only games you can play now" + "playable_button_title": "Show only games you can play now", + "add_custom_game_tooltip": "Add Custom Game", + "show_playable_only_tooltip": "Show Playable Only", + "custom_game_modal": "Add Custom Game", + "custom_game_modal_description": "Add a custom game to your library by selecting an executable file", + "custom_game_modal_executable_path": "Executable Path", + "custom_game_modal_select_executable": "Select executable file", + "custom_game_modal_game_name": "Game Name", + "custom_game_modal_enter_name": "Enter game name", + "custom_game_modal_image": "Game Image", + "custom_game_modal_select_image": "Select game image", + "custom_game_modal_image_preview": "Game image preview", + "custom_game_modal_browse": "Browse", + "custom_game_modal_cancel": "Cancel", + "custom_game_modal_add": "Add Game", + "custom_game_modal_adding": "Adding Game...", + "custom_game_modal_fill_required": "Please fill in all required fields", + "custom_game_modal_success": "Custom game added successfully", + "custom_game_modal_failed": "Failed to add custom game", + "custom_game_modal_executable": "Executable", + "custom_game_modal_image_filter": "Image", + "custom_game_modal_icon": "Game Icon", + "custom_game_modal_select_icon": "Select game icon", + "custom_game_modal_icon_preview": "Game icon preview", + "custom_game_modal_logo": "Game Logo", + "custom_game_modal_select_logo": "Select game logo", + "custom_game_modal_logo_preview": "Game logo preview", + "custom_game_modal_hero": "Library Hero Image", + "custom_game_modal_select_hero": "Select library hero image", + "custom_game_modal_hero_preview": "Library hero image preview", + "edit_custom_game_modal": "Edit Custom Game", + "edit_custom_game_modal_description": "Edit your custom game details", + "edit_custom_game_modal_game_name": "Game Name", + "edit_custom_game_modal_enter_name": "Enter game name", + "edit_custom_game_modal_image": "Game Image", + "edit_custom_game_modal_select_image": "Select game image", + "edit_custom_game_modal_browse": "Browse", + "edit_custom_game_modal_image_preview": "Game image preview", + "edit_custom_game_modal_icon": "Game Icon", + "edit_custom_game_modal_select_icon": "Select game icon", + "edit_custom_game_modal_icon_preview": "Game icon preview", + "edit_custom_game_modal_logo": "Game Logo", + "edit_custom_game_modal_select_logo": "Select game logo", + "edit_custom_game_modal_logo_preview": "Game logo preview", + "edit_custom_game_modal_hero": "Library Hero Image", + "edit_custom_game_modal_select_hero": "Select library hero image", + "edit_custom_game_modal_hero_preview": "Library hero image preview", + "edit_custom_game_modal_cancel": "Cancel", + "edit_custom_game_modal_update": "Update Game", + "edit_custom_game_modal_updating": "Updating Game...", + "edit_custom_game_modal_fill_required": "Please fill in all required fields", + "edit_custom_game_modal_success": "Custom game updated successfully", + "edit_custom_game_modal_failed": "Failed to update custom game", + "edit_custom_game_modal_image_filter": "Image" }, "header": { "search": "Search games", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index e576e5d9..ae8f2b9d 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -28,7 +28,58 @@ "friends": "Друзья", "need_help": "Нужна помощь?", "favorites": "Избранное", - "playable_button_title": "Показать только игры, в которые можно играть сейчас" + "playable_button_title": "Показать только игры, в которые можно играть сейчас", + "custom_game_modal": "Добавить пользовательскую игру", + "custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл", + "custom_game_modal_executable_path": "Путь к исполняемому файлу", + "custom_game_modal_select_executable": "Выберите исполняемый файл", + "custom_game_modal_game_name": "Название игры", + "custom_game_modal_enter_name": "Введите название игры", + "custom_game_modal_image": "Изображение игры", + "custom_game_modal_select_image": "Выберите изображение игры", + "custom_game_modal_image_preview": "Предварительный просмотр изображения игры", + "custom_game_modal_browse": "Обзор", + "custom_game_modal_cancel": "Отмена", + "custom_game_modal_add": "Добавить игру", + "custom_game_modal_adding": "Добавление игры...", + "custom_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля", + "custom_game_modal_success": "Пользовательская игра успешно добавлена", + "custom_game_modal_failed": "Не удалось добавить пользовательскую игру", + "custom_game_modal_executable": "Исполняемый файл", + "custom_game_modal_image_filter": "Изображение", + "custom_game_modal_icon": "Иконка игры", + "custom_game_modal_select_icon": "Выберите иконку игры", + "custom_game_modal_icon_preview": "Предпросмотр иконки игры", + "custom_game_modal_logo": "Логотип игры", + "custom_game_modal_select_logo": "Выберите логотип игры", + "custom_game_modal_logo_preview": "Предпросмотр логотипа игры", + "custom_game_modal_hero": "Изображение героя библиотеки", + "custom_game_modal_select_hero": "Выберите изображение героя библиотеки", + "custom_game_modal_hero_preview": "Предпросмотр изображения героя библиотеки", + "edit_custom_game_modal": "Редактировать пользовательскую игру", + "edit_custom_game_modal_description": "Редактируйте детали вашей пользовательской игры", + "edit_custom_game_modal_game_name": "Название игры", + "edit_custom_game_modal_enter_name": "Введите название игры", + "edit_custom_game_modal_image": "Изображение игры", + "edit_custom_game_modal_select_image": "Выберите изображение игры", + "edit_custom_game_modal_browse": "Обзор", + "edit_custom_game_modal_image_preview": "Предпросмотр изображения игры", + "edit_custom_game_modal_icon": "Иконка игры", + "edit_custom_game_modal_select_icon": "Выберите иконку игры", + "edit_custom_game_modal_icon_preview": "Предпросмотр иконки игры", + "edit_custom_game_modal_logo": "Логотип игры", + "edit_custom_game_modal_select_logo": "Выберите логотип игры", + "edit_custom_game_modal_logo_preview": "Предпросмотр логотипа игры", + "edit_custom_game_modal_hero": "Изображение героя библиотеки", + "edit_custom_game_modal_select_hero": "Выберите изображение героя библиотеки", + "edit_custom_game_modal_hero_preview": "Предпросмотр изображения героя библиотеки", + "edit_custom_game_modal_cancel": "Отмена", + "edit_custom_game_modal_update": "Обновить игру", + "edit_custom_game_modal_updating": "Обновление игры...", + "edit_custom_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля", + "edit_custom_game_modal_success": "Пользовательская игра успешно обновлена", + "edit_custom_game_modal_failed": "Не удалось обновить пользовательскую игру", + "edit_custom_game_modal_image_filter": "Изображение" }, "header": { "search": "Поиск", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 9765b517..ea7597ad 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -14,6 +14,8 @@ import "./catalogue/get-developers"; import "./hardware/get-disk-free-space"; import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; +import "./library/add-custom-game-to-library"; +import "./library/update-custom-game"; import "./library/add-game-to-favorites"; import "./library/remove-game-from-favorites"; import "./library/create-game-shortcut"; diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts new file mode 100644 index 00000000..382328a5 --- /dev/null +++ b/src/main/events/library/add-custom-game-to-library.ts @@ -0,0 +1,68 @@ +import { registerEvent } from "../register-event"; +import { + gamesSublevel, + gamesShopAssetsSublevel, + levelKeys, +} from "@main/level"; +import { randomUUID } from "crypto"; +import type { GameShop } from "@types"; + + +const addCustomGameToLibrary = async ( + _event: Electron.IpcMainInvokeEvent, + title: string, + executablePath: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string +) => { + const objectId = randomUUID(); + const shop: GameShop = "custom"; + const gameKey = levelKeys.game(shop, objectId); + + const existingGames = await gamesSublevel.iterator().all(); + const existingGame = existingGames.find(([_key, game]) => + game.executablePath === executablePath && !game.isDeleted + ); + + if (existingGame) { + throw new Error("A game with this executable path already exists in your library"); + } + + const assets = { + objectId, + shop, + title, + iconUrl: iconUrl || null, + libraryHeroImageUrl: libraryHeroImageUrl || "", + libraryImageUrl: iconUrl || "", + logoImageUrl: logoImageUrl || "", + logoPosition: null, + coverImageUrl: iconUrl || "", + }; + await gamesShopAssetsSublevel.put(gameKey, assets); + + const game = { + title, + iconUrl: iconUrl || null, + logoImageUrl: logoImageUrl || null, + libraryHeroImageUrl: libraryHeroImageUrl || null, + objectId, + shop, + remoteId: null, + isDeleted: false, + playTimeInMilliseconds: 0, + lastTimePlayed: null, + executablePath, + launchOptions: null, + favorite: false, + automaticCloudSync: false, + hasManuallyUpdatedPlaytime: false, + }; + + await gamesSublevel.put(gameKey, game); + + return game; +}; + +registerEvent("addCustomGameToLibrary", addCustomGameToLibrary); \ No newline at end of file diff --git a/src/main/events/library/change-game-playtime.ts b/src/main/events/library/change-game-playtime.ts index ff37c33e..20da70ec 100644 --- a/src/main/events/library/change-game-playtime.ts +++ b/src/main/events/library/change-game-playtime.ts @@ -13,16 +13,20 @@ const changeGamePlaytime = async ( const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); if (!game) return; - await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, { - playTimeInSeconds, - }); + + if (game.remoteId) { + await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, { + playTimeInSeconds, + }); + } + await gamesSublevel.put(gameKey, { ...game, playTimeInMilliseconds: playTimeInSeconds * 1000, hasManuallyUpdatedPlaytime: true, }); } catch (error) { - throw new Error(`Failed to update game favorite status: ${error}`); + throw new Error(`Failed to update game playtime: ${error}`); } }; diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts new file mode 100644 index 00000000..4ff858a2 --- /dev/null +++ b/src/main/events/library/update-custom-game.ts @@ -0,0 +1,53 @@ +import { registerEvent } from "../register-event"; +import { + gamesSublevel, + gamesShopAssetsSublevel, + levelKeys, +} from "@main/level"; +import type { GameShop } from "@types"; + +const updateCustomGame = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + title: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string +) => { + const gameKey = levelKeys.game(shop, objectId); + + const existingGame = await gamesSublevel.get(gameKey); + if (!existingGame) { + throw new Error("Game not found"); + } + + const updatedGame = { + ...existingGame, + title, + iconUrl: iconUrl || null, + logoImageUrl: logoImageUrl || null, + libraryHeroImageUrl: libraryHeroImageUrl || null, + }; + + await gamesSublevel.put(gameKey, updatedGame); + + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const updatedAssets = { + ...existingAssets, + title, + iconUrl: iconUrl || null, + libraryHeroImageUrl: libraryHeroImageUrl || "", + libraryImageUrl: iconUrl || "", + logoImageUrl: logoImageUrl || "", + coverImageUrl: iconUrl || "", + }; + + await gamesShopAssetsSublevel.put(gameKey, updatedAssets); + } + + return updatedGame; +}; + +registerEvent("updateCustomGame", updateCustomGame); \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index af197a6b..af9aa676 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -64,6 +64,54 @@ app.whenReady().then(async () => { return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); + protocol.handle("gradient", (request) => { + const gradientCss = decodeURIComponent(request.url.slice("gradient:".length)); + + const match = gradientCss.match(/linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/); + + let direction = "45deg"; + let color1 = '#4a90e2'; + let color2 = '#7b68ee'; + + if (match) { + direction = match[1].trim(); + color1 = match[2].trim(); + color2 = match[3].trim(); + } + + let x1 = "0%", y1 = "0%", x2 = "100%", y2 = "100%"; + + if (direction === "to right") { + x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "0%"; + } else if (direction === "to bottom") { + x1 = "0%"; y1 = "0%"; x2 = "0%"; y2 = "100%"; + } else if (direction === "45deg") { + x1 = "0%"; y1 = "100%"; x2 = "100%"; y2 = "0%"; + } else if (direction === "135deg") { + x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "100%"; + } else if (direction === "225deg") { + x1 = "100%"; y1 = "0%"; x2 = "0%"; y2 = "100%"; + } else if (direction === "315deg") { + x1 = "100%"; y1 = "100%"; x2 = "0%"; y2 = "0%"; + } + + const svgContent = ` + + + + + + + + + + `; + + return new Response(svgContent, { + headers: { 'Content-Type': 'image/svg+xml' } + }); + }); + await loadState(); const language = await db diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index a35414ac..c5bbe144 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -44,6 +44,8 @@ export const mergeWithRemoteGames = async () => { remoteId: game.id, shop: game.shop, iconUrl: game.iconUrl, + libraryHeroImageUrl: game.libraryHeroImageUrl, + logoImageUrl: game.logoImageUrl, lastTimePlayed: game.lastTimePlayed, playTimeInMilliseconds: game.playTimeInMilliseconds, hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime, diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 837fb48a..10d2c8dd 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -11,7 +11,7 @@ export const uploadGamesBatch = async () => { .all() .then((results) => { return results.filter( - (game) => !game.isDeleted && game.remoteId === null + (game) => !game.isDeleted && game.remoteId === null && game.shop !== "custom" ); }); diff --git a/src/preload/index.ts b/src/preload/index.ts index d29417b0..b9ad8d98 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -128,6 +128,23 @@ contextBridge.exposeInMainWorld("electron", { ), addGameToLibrary: (shop: GameShop, objectId: string, title: string) => ipcRenderer.invoke("addGameToLibrary", shop, objectId, title), + addCustomGameToLibrary: ( + title: string, + executablePath: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string + ) => + ipcRenderer.invoke("addCustomGameToLibrary", title, executablePath, iconUrl, logoImageUrl, libraryHeroImageUrl), + updateCustomGame: ( + shop: GameShop, + objectId: string, + title: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string + ) => + ipcRenderer.invoke("updateCustomGame", shop, objectId, title, iconUrl, logoImageUrl, libraryHeroImageUrl), createGameShortcut: ( shop: GameShop, objectId: string, @@ -476,4 +493,6 @@ contextBridge.exposeInMainWorld("electron", { }, closeEditorWindow: (themeId?: string) => ipcRenderer.invoke("closeEditorWindow", themeId), + + }); diff --git a/src/renderer/src/assets/play-logo.svg b/src/renderer/src/assets/play-logo.svg new file mode 100644 index 00000000..51ecaa28 --- /dev/null +++ b/src/renderer/src/assets/play-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss new file mode 100644 index 00000000..48b84da3 --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss @@ -0,0 +1,52 @@ +@use "../../scss/globals.scss"; + +.sidebar-adding-custom-game-modal { + &__container { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 3); + width: 100%; + max-width: 500px; + margin: 0 auto; + text-align: center; + } + + &__form { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + text-align: left; + } + + &__image-section { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + text-align: left; + } + + &__image-preview { + display: flex; + justify-content: center; + align-items: center; + padding: globals.$spacing-unit; + border: 1px solid globals.$border-color; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.05); + } + + &__preview-image { + max-width: 120px; + max-height: 80px; + width: auto; + height: auto; + border-radius: 4px; + object-fit: contain; + } + + &__actions { + display: flex; + justify-content: flex-end; + gap: calc(globals.$spacing-unit * 2); + } +} \ No newline at end of file diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx new file mode 100644 index 00000000..771c5464 --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx @@ -0,0 +1,183 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { FileDirectoryIcon } from "@primer/octicons-react"; + +import { Modal, TextField, Button } from "@renderer/components"; +import { useLibrary, useToast } from "@renderer/hooks"; +import { buildGameDetailsPath, generateRandomGradient } from "@renderer/helpers"; + +import "./sidebar-adding-custom-game-modal.scss"; + +export interface SidebarAddingCustomGameModalProps { + visible: boolean; + onClose: () => void; +} + +export function SidebarAddingCustomGameModal({ + visible, + onClose, +}: SidebarAddingCustomGameModalProps) { + const { t } = useTranslation("sidebar"); + const { updateLibrary } = useLibrary(); + const { showSuccessToast, showErrorToast } = useToast(); + const navigate = useNavigate(); + + const [gameName, setGameName] = useState(""); + const [executablePath, setExecutablePath] = useState(""); + const [isAdding, setIsAdding] = useState(false); + + const handleSelectExecutable = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("custom_game_modal_executable"), + extensions: ["exe", "msi", "app", "deb", "rpm", "dmg"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + const selectedPath = filePaths[0]; + setExecutablePath(selectedPath); + + if (!gameName.trim()) { + const fileName = selectedPath.split(/[\\/]/).pop() || ""; + const gameNameFromFile = fileName.replace(/\.[^/.]+$/, ""); + setGameName(gameNameFromFile); + } + } + }; + + const handleGameNameChange = (event: React.ChangeEvent) => { + setGameName(event.target.value); + }; + + + + const handleAddGame = async () => { + if (!gameName.trim() || !executablePath.trim()) { + showErrorToast(t("custom_game_modal_fill_required")); + return; + } + + setIsAdding(true); + + try { + // Generate gradient URL only for hero image + const gameNameForSeed = gameName.trim(); + const iconUrl = ""; // Don't use gradient for icon + const logoImageUrl = ""; // Don't use gradient for logo + const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero + + const newGame = await window.electron.addCustomGameToLibrary( + gameNameForSeed, + executablePath, + iconUrl, + logoImageUrl, + libraryHeroImageUrl + ); + + showSuccessToast(t("custom_game_modal_success")); + updateLibrary(); + + const gameDetailsPath = buildGameDetailsPath({ + shop: "custom", + objectId: newGame.objectId, + title: newGame.title + }); + + navigate(gameDetailsPath); + + setGameName(""); + setExecutablePath(""); + onClose(); + } catch (error) { + console.error("Failed to add custom game:", error); + showErrorToast( + error instanceof Error + ? error.message + : t("custom_game_modal_failed") + ); + } finally { + setIsAdding(false); + } + }; + + const handleClose = () => { + if (!isAdding) { + setGameName(""); + setExecutablePath(""); + onClose(); + } + }; + + const isFormValid = gameName.trim() && executablePath.trim(); + + + + return ( + +
+
+ + + {t("custom_game_modal_browse")} + + } + /> + + + + + + +
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index 0672f847..4f905c59 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -1,4 +1,5 @@ import SteamLogo from "@renderer/assets/steam-logo.svg?react"; +import PlayLogo from "@renderer/assets/play-logo.svg?react"; import { LibraryGame } from "@types"; import cn from "classnames"; import { useLocation } from "react-router-dom"; @@ -16,6 +17,11 @@ export function SidebarGameItem({ }: Readonly) { const location = useLocation(); + const isCustomGame = game.shop === "custom"; + const sidebarIcon = isCustomGame + ? game.libraryImageUrl || game.iconUrl + : game.iconUrl; + return (
  • handleSidebarGameClick(event, game)} > - {game.iconUrl ? ( + {sidebarIcon ? ( {game.title} + ) : isCustomGame ? ( + ) : ( )} diff --git a/src/renderer/src/components/sidebar/sidebar.scss b/src/renderer/src/components/sidebar/sidebar.scss index 45b3f598..0fec7c30 100644 --- a/src/renderer/src/components/sidebar/sidebar.scss +++ b/src/renderer/src/components/sidebar/sidebar.scss @@ -172,4 +172,24 @@ display: block; } } + + &__add-button { + background: none; + border: none; + color: globals.$muted-color; + cursor: pointer; + padding: 0; + + &:hover { + color: rgba(255, 255, 255, 0.8); + } + + &:active { + color: rgba(255, 255, 255, 0.5); + } + + svg { + display: block; + } + } } diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 42e87e32..a7d946a2 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; +import { Tooltip } from "react-tooltip"; import type { LibraryGame } from "@types"; @@ -21,8 +22,9 @@ import { buildGameDetailsPath } from "@renderer/helpers"; import { SidebarProfile } from "./sidebar-profile"; import { sortBy } from "lodash-es"; import cn from "classnames"; -import { CommentDiscussionIcon, PlayIcon } from "@primer/octicons-react"; +import { CommentDiscussionIcon, PlayIcon, PlusIcon } from "@primer/octicons-react"; import { SidebarGameItem } from "./sidebar-game-item"; +import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal"; import { setFriendRequestCount } from "@renderer/features/user-details-slice"; import { useDispatch } from "react-redux"; @@ -63,11 +65,20 @@ export function Sidebar() { const { showWarningToast } = useToast(); const [showPlayableOnly, setShowPlayableOnly] = useState(false); + const [showAddGameModal, setShowAddGameModal] = useState(false); const handlePlayButtonClick = () => { setShowPlayableOnly(!showPlayableOnly); }; + const handleAddGameButtonClick = () => { + setShowAddGameModal(true); + }; + + const handleCloseAddGameModal = () => { + setShowAddGameModal(false); + }; + useEffect(() => { updateLibrary(); }, [lastPacket?.gameId, updateLibrary]); @@ -254,15 +265,30 @@ export function Sidebar() { {t("my_library")} - +
    + + +
    + + + + + ); } diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index ce2923b2..81abfa13 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -201,6 +201,12 @@ export function GameDetailsContextProvider({ dispatch(setHeaderTitle(gameTitle)); }, [objectId, gameTitle, dispatch]); + useEffect(() => { + if (game?.title && game.shop === "custom") { + dispatch(setHeaderTitle(game.title)); + } + }, [game?.title, game?.shop, dispatch]); + useEffect(() => { const unsubscribe = window.electron.onGamesRunning((gamesIds) => { const updatedIsGameRunning = diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 0744884c..f66bd53e 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -111,6 +111,21 @@ declare global { objectId: string, title: string ) => Promise; + addCustomGameToLibrary: ( + title: string, + executablePath: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string + ) => Promise; + updateCustomGame: ( + shop: GameShop, + objectId: string, + title: string, + iconUrl?: string, + logoImageUrl?: string, + libraryHeroImageUrl?: string + ) => Promise; createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index eb7cebb0..7c45bafd 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -84,3 +84,23 @@ export const injectCustomCss = ( export const removeCustomCss = (target: HTMLElement = document.head) => { target.querySelector("#custom-css")?.remove(); }; + +export const generateRandomGradient = (): string => { + // Use a single consistent gradient with softer colors for custom games as placeholder + const color1 = '#2c3e50'; // Dark blue-gray + const color2 = '#34495e'; // Darker slate + + // Create SVG data URL that works in img tags + const svgContent = ` + + + + + + + + `; + + // Return as data URL that works in img tags + return `data:image/svg+xml;base64,${btoa(svgContent)}`; +}; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 40436614..ba3fb7bd 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,18 +1,20 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { average } from "color.js"; import Color from "color"; +import { PencilIcon } from "@primer/octicons-react"; import { HeroPanel } from "./hero"; import { DescriptionHeader } from "./description-header/description-header"; import { GallerySlider } from "./gallery-slider/gallery-slider"; import { Sidebar } from "./sidebar/sidebar"; +import { EditCustomGameModal } from "./modals"; import { useTranslation } from "react-i18next"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; import { AuthPage } from "@shared"; import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif"; -import { useUserDetails } from "@renderer/hooks"; +import { useUserDetails, useLibrary } from "@renderer/hooks"; import { useSubscription } from "@renderer/hooks/use-subscription"; import "./game-details.scss"; @@ -28,11 +30,13 @@ export function GameDetailsContent() { gameColor, setGameColor, hasNSFWContentBlocked, + updateGame, } = useContext(gameDetailsContext); const { showHydraCloudModal } = useSubscription(); const { userDetails, hasActiveSubscription } = useUserDetails(); + const { updateLibrary } = useLibrary(); const { setShowCloudSyncModal, getGameArtifacts } = useContext(cloudSyncContext); @@ -53,10 +57,15 @@ export function GameDetailsContent() { return document.body.outerHTML; } + if (game?.shop === "custom") { + return ""; + } + return t("no_shop_details"); - }, [shopDetails, t]); + }, [shopDetails, t, game?.shop]); const [backdropOpacity, setBackdropOpacity] = useState(1); + const [showEditCustomGameModal, setShowEditCustomGameModal] = useState(false); const handleHeroLoad = async () => { const output = await average( @@ -92,10 +101,27 @@ export function GameDetailsContent() { setShowCloudSyncModal(true); }; + const handleEditCustomGameClick = () => { + setShowEditCustomGameModal(true); + }; + + const handleGameUpdated = (_updatedGame: any) => { + updateGame(); + updateLibrary(); + }; + useEffect(() => { getGameArtifacts(); }, [getGameArtifacts]); + const isCustomGame = game?.shop === "custom"; + const heroImage = isCustomGame + ? game?.libraryHeroImageUrl || game?.iconUrl || "" + : shopDetails?.assets?.libraryHeroImageUrl || ""; + const logoImage = isCustomGame + ? game?.logoImageUrl || "" // Don't use icon as fallback for custom games + : shopDetails?.assets?.logoImageUrl || ""; + return (
    {game?.title}
    - {game?.title} + {logoImage && ( + {game?.title} + )} - +
    + {game?.shop === "custom" && ( + + )} + + {game?.shop !== "custom" && ( + + )} +
    @@ -160,9 +203,18 @@ export function GameDetailsContent() { /> - + {game?.shop !== "custom" && } + + {game?.shop === "custom" && ( + setShowEditCustomGameModal(false)} + game={game} + onGameUpdated={handleGameUpdated} + /> + )} ); } diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 899d654a..46c0ae51 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -52,6 +52,43 @@ $hero-height: 300px; align-items: flex-end; } + &__hero-buttons { + display: flex; + gap: globals.$spacing-unit; + align-items: center; + + &--right { + margin-left: auto; + } + } + + &__edit-custom-game-button { + padding: calc(globals.$spacing-unit * 1.5); + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(20px); + border-radius: 8px; + transition: all ease 0.2s; + cursor: pointer; + min-height: 40px; + min-width: 40px; + display: flex; + align-items: center; + justify-content: center; + color: globals.$muted-color; + border: solid 1px globals.$border-color; + box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8); + animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1); + + &:active { + opacity: 0.9; + } + + &:hover { + background-color: rgba(0, 0, 0, 0.5); + color: globals.$body-color; + } + } + &__hero-logo-backdrop { width: 100%; height: 100%; diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.scss b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.scss index 00142d59..9429ef21 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.scss +++ b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.scss @@ -17,6 +17,7 @@ display: flex; align-items: center; gap: 8px; + width: fit-content; } &__manual-warning { diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx index 9cd246be..270ed030 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-playtime.tsx @@ -11,7 +11,6 @@ import "./hero-panel-playtime.scss"; export function HeroPanelPlaytime() { const [lastTimePlayed, setLastTimePlayed] = useState(""); - const { game, isGameRunning } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); @@ -89,19 +88,23 @@ export function HeroPanelPlaytime() { return ( <> -

    {game.hasManuallyUpdatedPlaytime && ( - )} @@ -119,7 +122,7 @@ export function HeroPanelPlaytime() { })}

    )} - + {game.hasManuallyUpdatedPlaytime && ( )} diff --git a/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.scss new file mode 100644 index 00000000..6f24d4ef --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.scss @@ -0,0 +1,46 @@ +@use "../../../scss/globals.scss"; + +.edit-custom-game-modal { + &__container { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + } + + &__form { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + } + + &__image-section { + display: flex; + flex-direction: column; + gap: globals.$spacing-unit; + } + + &__image-preview { + display: flex; + justify-content: center; + align-items: center; + padding: globals.$spacing-unit; + border: 1px dashed globals.$border-color; + border-radius: 8px; + background-color: rgba(255, 255, 255, 0.05); + } + + &__preview-image { + max-width: 120px; + max-height: 80px; + border-radius: 8px; + object-fit: cover; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } + + &__actions { + display: flex; + gap: globals.$spacing-unit; + justify-content: flex-end; + margin-top: globals.$spacing-unit; + } +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.tsx new file mode 100644 index 00000000..a489b88c --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/edit-custom-game-modal.tsx @@ -0,0 +1,311 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { ImageIcon } from "@primer/octicons-react"; + +import { Modal, TextField, Button } from "@renderer/components"; +import { useToast } from "@renderer/hooks"; +import type { Game } from "@types"; + +import "./edit-custom-game-modal.scss"; + +export interface EditCustomGameModalProps { + visible: boolean; + onClose: () => void; + game: Game; + onGameUpdated: (updatedGame: Game) => void; +} + +export function EditCustomGameModal({ + visible, + onClose, + game, + onGameUpdated, +}: EditCustomGameModalProps) { + const { t } = useTranslation("sidebar"); + const { showSuccessToast, showErrorToast } = useToast(); + + const [gameName, setGameName] = useState(""); + const [iconPath, setIconPath] = useState(""); + const [logoPath, setLogoPath] = useState(""); + const [heroPath, setHeroPath] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + + useEffect(() => { + if (game && visible) { + setGameName(game.title || ""); + + const currentIconPath = game.iconUrl?.startsWith("local:") + ? game.iconUrl.replace("local:", "") + : ""; + const currentLogoPath = game.logoImageUrl?.startsWith("local:") + ? game.logoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:") + ? game.libraryHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + } + }, [game, visible]); + + const handleGameNameChange = (event: React.ChangeEvent) => { + setGameName(event.target.value); + }; + + const handleSelectIcon = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setIconPath(filePaths[0]); + } + }; + + const handleSelectLogo = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setLogoPath(filePaths[0]); + } + }; + + const handleSelectHero = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setHeroPath(filePaths[0]); + } + }; + + const handleUpdateGame = async () => { + if (!gameName.trim()) { + showErrorToast(t("edit_custom_game_modal_fill_required")); + return; + } + + setIsUpdating(true); + + try { + // Preserve existing image URLs if not changed + const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; + const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl; + const libraryHeroImageUrl = heroPath ? `local:${heroPath}` : game.libraryHeroImageUrl; + + const updatedGame = await window.electron.updateCustomGame( + game.shop, + game.objectId, + gameName.trim(), + iconUrl || undefined, + logoImageUrl || undefined, + libraryHeroImageUrl || undefined + ); + + showSuccessToast(t("edit_custom_game_modal_success")); + onGameUpdated(updatedGame); + onClose(); + } catch (error) { + console.error("Failed to update custom game:", error); + showErrorToast( + error instanceof Error + ? error.message + : t("edit_custom_game_modal_failed") + ); + } finally { + setIsUpdating(false); + } + }; + + const handleClose = () => { + if (!isUpdating) { + setGameName(game?.title || ""); + + const currentIconPath = game?.iconUrl?.startsWith("local:") + ? game.iconUrl.replace("local:", "") + : ""; + const currentLogoPath = game?.logoImageUrl?.startsWith("local:") + ? game.logoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game?.libraryHeroImageUrl?.startsWith("local:") + ? game.libraryHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + onClose(); + } + }; + + const isFormValid = gameName.trim(); + + const getIconPreviewUrl = () => { + return iconPath ? `local:${iconPath}` : null; + }; + + const getLogoPreviewUrl = () => { + return logoPath ? `local:${logoPath}` : null; + }; + + const getHeroPreviewUrl = () => { + return heroPath ? `local:${heroPath}` : null; + }; + + return ( + +
    +
    + + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {iconPath && ( +
    + {t("edit_custom_game_modal_icon_preview")} +
    + )} +
    + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {logoPath && ( +
    + {t("edit_custom_game_modal_logo_preview")} +
    + )} +
    + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {heroPath && ( +
    + {t("edit_custom_game_modal_hero_preview")} +
    + )} +
    +
    + +
    + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/index.ts b/src/renderer/src/pages/game-details/modals/index.ts index e02421b9..7cc779ef 100644 --- a/src/renderer/src/pages/game-details/modals/index.ts +++ b/src/renderer/src/pages/game-details/modals/index.ts @@ -1,3 +1,4 @@ export * from "./repacks-modal"; export * from "./download-settings-modal"; export * from "./game-options-modal"; +export * from "./edit-custom-game-modal"; diff --git a/src/types/game.types.ts b/src/types/game.types.ts index cc19f09c..ed8fb852 100644 --- a/src/types/game.types.ts +++ b/src/types/game.types.ts @@ -1,4 +1,4 @@ -export type GameShop = "steam" | "epic"; +export type GameShop = "steam" | "epic" | "custom"; export type ShortcutLocation = "desktop" | "start_menu"; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index d749e777..dd9ba1dd 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -33,6 +33,8 @@ export interface User { export interface Game { title: string; iconUrl: string | null; + libraryHeroImageUrl: string | null; + logoImageUrl: string | null; playTimeInMilliseconds: number; unsyncedDeltaPlayTimeInMilliseconds?: number; lastTimePlayed: Date | null; From 3409b53268421c8b97fc3360d0e810fba81d5d65 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 16:22:12 +0300 Subject: [PATCH 02/43] Feat: Custom Games --- .../library/add-custom-game-to-library.ts | 17 ++--- .../events/library/add-game-to-library.ts | 2 + .../events/library/change-game-playtime.ts | 4 +- src/main/events/library/get-library.ts | 8 ++- src/main/events/library/update-custom-game.ts | 10 +-- .../events/torrenting/start-game-download.ts | 2 + src/main/index.ts | 63 ++++++++++++------ .../library-sync/upload-games-batch.ts | 3 +- src/preload/index.ts | 21 ++++-- .../sidebar-adding-custom-game-modal.scss | 2 +- .../sidebar-adding-custom-game-modal.tsx | 47 ++++++------- .../src/components/sidebar/sidebar.tsx | 10 ++- src/renderer/src/helpers.ts | 8 +-- .../game-details/game-details-content.tsx | 2 +- .../src/pages/game-details/game-details.scss | 2 +- .../modals/edit-custom-game-modal.scss | 2 +- .../modals/edit-custom-game-modal.tsx | 66 ++++++++++--------- 17 files changed, 158 insertions(+), 111 deletions(-) diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts index 382328a5..47fd3436 100644 --- a/src/main/events/library/add-custom-game-to-library.ts +++ b/src/main/events/library/add-custom-game-to-library.ts @@ -1,13 +1,8 @@ import { registerEvent } from "../register-event"; -import { - gamesSublevel, - gamesShopAssetsSublevel, - levelKeys, -} from "@main/level"; +import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import { randomUUID } from "crypto"; import type { GameShop } from "@types"; - const addCustomGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, title: string, @@ -21,12 +16,14 @@ const addCustomGameToLibrary = async ( const gameKey = levelKeys.game(shop, objectId); const existingGames = await gamesSublevel.iterator().all(); - const existingGame = existingGames.find(([_key, game]) => - game.executablePath === executablePath && !game.isDeleted + const existingGame = existingGames.find( + ([_key, game]) => game.executablePath === executablePath && !game.isDeleted ); if (existingGame) { - throw new Error("A game with this executable path already exists in your library"); + throw new Error( + "A game with this executable path already exists in your library" + ); } const assets = { @@ -65,4 +62,4 @@ const addCustomGameToLibrary = async ( return game; }; -registerEvent("addCustomGameToLibrary", addCustomGameToLibrary); \ No newline at end of file +registerEvent("addCustomGameToLibrary", addCustomGameToLibrary); diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 01495a39..2fa6d20e 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -30,6 +30,8 @@ const addGameToLibrary = async ( game = { title, iconUrl: gameAssets?.iconUrl ?? null, + libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null, + logoImageUrl: gameAssets?.logoImageUrl ?? null, objectId, shop, remoteId: null, diff --git a/src/main/events/library/change-game-playtime.ts b/src/main/events/library/change-game-playtime.ts index 20da70ec..8ad252bd 100644 --- a/src/main/events/library/change-game-playtime.ts +++ b/src/main/events/library/change-game-playtime.ts @@ -13,13 +13,13 @@ const changeGamePlaytime = async ( const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); if (!game) return; - + if (game.remoteId) { await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, { playTimeInSeconds, }); } - + await gamesSublevel.put(gameKey, { ...game, playTimeInMilliseconds: playTimeInSeconds * 1000, diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index ce859908..547045ae 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -18,11 +18,17 @@ const getLibrary = async (): Promise => { const download = await downloadsSublevel.get(key); const gameAssets = await gamesShopAssetsSublevel.get(key); + // 确保返回的对象符合 LibraryGame 类型 return { id: key, ...game, download: download ?? null, - ...gameAssets, + // 确保 gameAssets 中的可能为 null 的字段转换为 undefined + libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? undefined, + libraryImageUrl: gameAssets?.libraryImageUrl ?? undefined, + logoImageUrl: gameAssets?.logoImageUrl ?? undefined, + logoPosition: gameAssets?.logoPosition ?? undefined, + coverImageUrl: gameAssets?.coverImageUrl ?? undefined, }; }) ); diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 4ff858a2..6152c0df 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -1,9 +1,5 @@ import { registerEvent } from "../register-event"; -import { - gamesSublevel, - gamesShopAssetsSublevel, - levelKeys, -} from "@main/level"; +import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop } from "@types"; const updateCustomGame = async ( @@ -16,7 +12,7 @@ const updateCustomGame = async ( libraryHeroImageUrl?: string ) => { const gameKey = levelKeys.game(shop, objectId); - + const existingGame = await gamesSublevel.get(gameKey); if (!existingGame) { throw new Error("Game not found"); @@ -50,4 +46,4 @@ const updateCustomGame = async ( return updatedGame; }; -registerEvent("updateCustomGame", updateCustomGame); \ No newline at end of file +registerEvent("updateCustomGame", updateCustomGame); diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 00281973..8216b519 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -53,6 +53,8 @@ const startGameDownload = async ( await gamesSublevel.put(gameKey, { title, iconUrl: gameAssets?.iconUrl ?? null, + libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null, + logoImageUrl: gameAssets?.logoImageUrl ?? null, objectId, shop, remoteId: null, diff --git a/src/main/index.ts b/src/main/index.ts index af9aa676..ab6980f9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -65,36 +65,61 @@ app.whenReady().then(async () => { }); protocol.handle("gradient", (request) => { - const gradientCss = decodeURIComponent(request.url.slice("gradient:".length)); - - const match = gradientCss.match(/linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/); - + const gradientCss = decodeURIComponent( + request.url.slice("gradient:".length) + ); + + const match = gradientCss.match( + /linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/ + ); + let direction = "45deg"; - let color1 = '#4a90e2'; - let color2 = '#7b68ee'; - + let color1 = "#4a90e2"; + let color2 = "#7b68ee"; + if (match) { direction = match[1].trim(); color1 = match[2].trim(); color2 = match[3].trim(); } - - let x1 = "0%", y1 = "0%", x2 = "100%", y2 = "100%"; - + + let x1 = "0%", + y1 = "0%", + x2 = "100%", + y2 = "100%"; + if (direction === "to right") { - x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "0%"; + x1 = "0%"; + y1 = "0%"; + x2 = "100%"; + y2 = "0%"; } else if (direction === "to bottom") { - x1 = "0%"; y1 = "0%"; x2 = "0%"; y2 = "100%"; + x1 = "0%"; + y1 = "0%"; + x2 = "0%"; + y2 = "100%"; } else if (direction === "45deg") { - x1 = "0%"; y1 = "100%"; x2 = "100%"; y2 = "0%"; + x1 = "0%"; + y1 = "100%"; + x2 = "100%"; + y2 = "0%"; } else if (direction === "135deg") { - x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "100%"; + x1 = "0%"; + y1 = "0%"; + x2 = "100%"; + y2 = "100%"; } else if (direction === "225deg") { - x1 = "100%"; y1 = "0%"; x2 = "0%"; y2 = "100%"; + x1 = "100%"; + y1 = "0%"; + x2 = "0%"; + y2 = "100%"; } else if (direction === "315deg") { - x1 = "100%"; y1 = "100%"; x2 = "0%"; y2 = "0%"; + x1 = "100%"; + y1 = "100%"; + x2 = "0%"; + y2 = "0%"; } - + const svgContent = ` @@ -106,9 +131,9 @@ app.whenReady().then(async () => { `; - + return new Response(svgContent, { - headers: { 'Content-Type': 'image/svg+xml' } + headers: { "Content-Type": "image/svg+xml" }, }); }); diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 10d2c8dd..653b5f40 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -11,7 +11,8 @@ export const uploadGamesBatch = async () => { .all() .then((results) => { return results.filter( - (game) => !game.isDeleted && game.remoteId === null && game.shop !== "custom" + (game) => + !game.isDeleted && game.remoteId === null && game.shop !== "custom" ); }); diff --git a/src/preload/index.ts b/src/preload/index.ts index b9ad8d98..8d297051 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -135,7 +135,14 @@ contextBridge.exposeInMainWorld("electron", { logoImageUrl?: string, libraryHeroImageUrl?: string ) => - ipcRenderer.invoke("addCustomGameToLibrary", title, executablePath, iconUrl, logoImageUrl, libraryHeroImageUrl), + ipcRenderer.invoke( + "addCustomGameToLibrary", + title, + executablePath, + iconUrl, + logoImageUrl, + libraryHeroImageUrl + ), updateCustomGame: ( shop: GameShop, objectId: string, @@ -144,7 +151,15 @@ contextBridge.exposeInMainWorld("electron", { logoImageUrl?: string, libraryHeroImageUrl?: string ) => - ipcRenderer.invoke("updateCustomGame", shop, objectId, title, iconUrl, logoImageUrl, libraryHeroImageUrl), + ipcRenderer.invoke( + "updateCustomGame", + shop, + objectId, + title, + iconUrl, + logoImageUrl, + libraryHeroImageUrl + ), createGameShortcut: ( shop: GameShop, objectId: string, @@ -493,6 +508,4 @@ contextBridge.exposeInMainWorld("electron", { }, closeEditorWindow: (themeId?: string) => ipcRenderer.invoke("closeEditorWindow", themeId), - - }); diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss index 48b84da3..942384fe 100644 --- a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss @@ -49,4 +49,4 @@ justify-content: flex-end; gap: calc(globals.$spacing-unit * 2); } -} \ No newline at end of file +} diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx index 771c5464..4b18a211 100644 --- a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx @@ -5,7 +5,10 @@ import { FileDirectoryIcon } from "@primer/octicons-react"; import { Modal, TextField, Button } from "@renderer/components"; import { useLibrary, useToast } from "@renderer/hooks"; -import { buildGameDetailsPath, generateRandomGradient } from "@renderer/helpers"; +import { + buildGameDetailsPath, + generateRandomGradient, +} from "@renderer/helpers"; import "./sidebar-adding-custom-game-modal.scss"; @@ -41,7 +44,7 @@ export function SidebarAddingCustomGameModal({ if (filePaths && filePaths.length > 0) { const selectedPath = filePaths[0]; setExecutablePath(selectedPath); - + if (!gameName.trim()) { const fileName = selectedPath.split(/[\\/]/).pop() || ""; const gameNameFromFile = fileName.replace(/\.[^/.]+$/, ""); @@ -54,8 +57,6 @@ export function SidebarAddingCustomGameModal({ setGameName(event.target.value); }; - - const handleAddGame = async () => { if (!gameName.trim() || !executablePath.trim()) { showErrorToast(t("custom_game_modal_fill_required")); @@ -70,7 +71,7 @@ export function SidebarAddingCustomGameModal({ const iconUrl = ""; // Don't use gradient for icon const logoImageUrl = ""; // Don't use gradient for logo const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero - + const newGame = await window.electron.addCustomGameToLibrary( gameNameForSeed, executablePath, @@ -81,24 +82,22 @@ export function SidebarAddingCustomGameModal({ showSuccessToast(t("custom_game_modal_success")); updateLibrary(); - + const gameDetailsPath = buildGameDetailsPath({ shop: "custom", objectId: newGame.objectId, - title: newGame.title + title: newGame.title, }); - + navigate(gameDetailsPath); - + setGameName(""); setExecutablePath(""); onClose(); } catch (error) { console.error("Failed to add custom game:", error); showErrorToast( - error instanceof Error - ? error.message - : t("custom_game_modal_failed") + error instanceof Error ? error.message : t("custom_game_modal_failed") ); } finally { setIsAdding(false); @@ -115,8 +114,6 @@ export function SidebarAddingCustomGameModal({ const isFormValid = gameName.trim() && executablePath.trim(); - - return ( - - - -
    - -
    ); -} \ No newline at end of file +} diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index a7d946a2..f77066a2 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -22,7 +22,11 @@ import { buildGameDetailsPath } from "@renderer/helpers"; import { SidebarProfile } from "./sidebar-profile"; import { sortBy } from "lodash-es"; import cn from "classnames"; -import { CommentDiscussionIcon, PlayIcon, PlusIcon } from "@primer/octicons-react"; +import { + CommentDiscussionIcon, + PlayIcon, + PlusIcon, +} from "@primer/octicons-react"; import { SidebarGameItem } from "./sidebar-game-item"; import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal"; import { setFriendRequestCount } from "@renderer/features/user-details-slice"; @@ -265,7 +269,9 @@ export function Sidebar() { {t("my_library")} -
    +
    } /> - + {iconPath && (
    } /> - + {logoPath && (
    } /> - + {heroPath && (
    - -
    ); -} \ No newline at end of file +} From f4e84e46ccb39a77d98d1cbe378193776e7b0298 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 16:58:58 +0300 Subject: [PATCH 03/43] feat: custom game support/game info changing --- .../events/catalogue/save-game-shop-assets.ts | 10 +- src/main/events/index.ts | 1 + .../events/library/add-game-to-library.ts | 12 +- src/main/events/library/get-library.ts | 12 +- .../library/update-game-custom-assets.ts | 45 +++ .../library-sync/merge-with-remote-games.ts | 2 +- src/preload/index.ts | 17 + .../components/sidebar/sidebar-game-item.tsx | 2 +- .../game-details/game-details.context.tsx | 4 +- src/renderer/src/declaration.d.ts | 8 + .../game-details/game-details-content.tsx | 57 ++- .../game-details/modals/edit-game-modal.scss | 47 +++ .../game-details/modals/edit-game-modal.tsx | 367 ++++++++++++++++++ .../src/pages/game-details/modals/index.ts | 1 + src/types/level.types.ts | 3 + 15 files changed, 556 insertions(+), 32 deletions(-) create mode 100644 src/main/events/library/update-game-custom-assets.ts create mode 100644 src/renderer/src/pages/game-details/modals/edit-game-modal.scss create mode 100644 src/renderer/src/pages/game-details/modals/edit-game-modal.tsx diff --git a/src/main/events/catalogue/save-game-shop-assets.ts b/src/main/events/catalogue/save-game-shop-assets.ts index f0ec4343..46aab36a 100644 --- a/src/main/events/catalogue/save-game-shop-assets.ts +++ b/src/main/events/catalogue/save-game-shop-assets.ts @@ -10,7 +10,15 @@ const saveGameShopAssets = async ( ): Promise => { const key = levelKeys.game(shop, objectId); const existingAssets = await gamesShopAssetsSublevel.get(key); - return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets }); + + // Preserve existing title if it differs from the incoming title (indicating it was customized) + const shouldPreserveTitle = existingAssets?.title && existingAssets.title !== assets.title; + + return gamesShopAssetsSublevel.put(key, { + ...existingAssets, + ...assets, + title: shouldPreserveTitle ? existingAssets.title : assets.title + }); }; registerEvent("saveGameShopAssets", saveGameShopAssets); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index ea7597ad..a1acd596 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -16,6 +16,7 @@ import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; import "./library/add-custom-game-to-library"; import "./library/update-custom-game"; +import "./library/update-game-custom-assets"; import "./library/add-game-to-favorites"; import "./library/remove-game-from-favorites"; import "./library/create-game-shortcut"; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 2fa6d20e..4fdeae30 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -43,12 +43,14 @@ const addGameToLibrary = async ( await gamesSublevel.put(gameKey, game); } - await createGame(game).catch(() => {}); + if (game) { + await createGame(game).catch(() => {}); - AchievementWatcherManager.firstSyncWithRemoteIfNeeded( - game.shop, - game.objectId - ); + AchievementWatcherManager.firstSyncWithRemoteIfNeeded( + game.shop, + game.objectId + ); + } }; registerEvent("addGameToLibrary", addGameToLibrary); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 547045ae..ea2971b9 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -18,18 +18,14 @@ const getLibrary = async (): Promise => { const download = await downloadsSublevel.get(key); const gameAssets = await gamesShopAssetsSublevel.get(key); - // 确保返回的对象符合 LibraryGame 类型 return { id: key, ...game, download: download ?? null, - // 确保 gameAssets 中的可能为 null 的字段转换为 undefined - libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? undefined, - libraryImageUrl: gameAssets?.libraryImageUrl ?? undefined, - logoImageUrl: gameAssets?.logoImageUrl ?? undefined, - logoPosition: gameAssets?.logoPosition ?? undefined, - coverImageUrl: gameAssets?.coverImageUrl ?? undefined, - }; + ...gameAssets, + // Ensure compatibility with LibraryGame type + libraryHeroImageUrl: game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl, + } as LibraryGame; }) ); }); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts new file mode 100644 index 00000000..99081a49 --- /dev/null +++ b/src/main/events/library/update-game-custom-assets.ts @@ -0,0 +1,45 @@ +import { registerEvent } from "../register-event"; +import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; +import type { GameShop } from "@types"; + +const updateGameCustomAssets = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null +) => { + const gameKey = levelKeys.game(shop, objectId); + + const existingGame = await gamesSublevel.get(gameKey); + if (!existingGame) { + throw new Error("Game not found"); + } + + const updatedGame = { + ...existingGame, + title, + customIconUrl: customIconUrl !== undefined ? customIconUrl : existingGame.customIconUrl, + customLogoImageUrl: customLogoImageUrl !== undefined ? customLogoImageUrl : existingGame.customLogoImageUrl, + customHeroImageUrl: customHeroImageUrl !== undefined ? customHeroImageUrl : existingGame.customHeroImageUrl, + }; + + await gamesSublevel.put(gameKey, updatedGame); + + // Also update the shop assets for non-custom games + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const updatedAssets = { + ...existingAssets, + title, // Update the title in shop assets as well + }; + + await gamesShopAssetsSublevel.put(gameKey, updatedAssets); + } + + return updatedGame; +}; + +registerEvent("updateGameCustomAssets", updateGameCustomAssets); \ No newline at end of file diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index c5bbe144..3e1398f0 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -57,7 +57,7 @@ export const mergeWithRemoteGames = async () => { await gamesShopAssetsSublevel.put(gameKey, { shop: game.shop, objectId: game.objectId, - title: game.title, + title: localGame?.title || game.title, // Preserve local title if it exists coverImageUrl: game.coverImageUrl, libraryHeroImageUrl: game.libraryHeroImageUrl, libraryImageUrl: game.libraryImageUrl, diff --git a/src/preload/index.ts b/src/preload/index.ts index 8d297051..ca918eee 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -160,6 +160,23 @@ contextBridge.exposeInMainWorld("electron", { logoImageUrl, libraryHeroImageUrl ), + updateGameCustomAssets: ( + shop: GameShop, + objectId: string, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null + ) => + ipcRenderer.invoke( + "updateGameCustomAssets", + shop, + objectId, + title, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ), createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index 4f905c59..108a5536 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -20,7 +20,7 @@ export function SidebarGameItem({ const isCustomGame = game.shop === "custom"; const sidebarIcon = isCustomGame ? game.libraryImageUrl || game.iconUrl - : game.iconUrl; + : game.customIconUrl || game.iconUrl; return (
  • { - if (game?.title && game.shop === "custom") { + if (game?.title) { dispatch(setHeaderTitle(game.title)); } - }, [game?.title, game?.shop, dispatch]); + }, [game?.title, dispatch]); useEffect(() => { const unsubscribe = window.electron.onGamesRunning((gamesIds) => { diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index f66bd53e..5af0d04f 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -126,6 +126,14 @@ declare global { logoImageUrl?: string, libraryHeroImageUrl?: string ) => Promise; + updateGameCustomAssets: ( + shop: GameShop, + objectId: string, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null + ) => Promise; createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 80c95fc4..cf300ceb 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -7,7 +7,7 @@ import { HeroPanel } from "./hero"; import { DescriptionHeader } from "./description-header/description-header"; import { GallerySlider } from "./gallery-slider/gallery-slider"; import { Sidebar } from "./sidebar/sidebar"; -import { EditCustomGameModal } from "./modals"; +import { EditCustomGameModal, EditGameModal } from "./modals"; import { useTranslation } from "react-i18next"; import { cloudSyncContext, gameDetailsContext } from "@renderer/context"; @@ -66,6 +66,7 @@ export function GameDetailsContent() { const [backdropOpacity, setBackdropOpacity] = useState(1); const [showEditCustomGameModal, setShowEditCustomGameModal] = useState(false); + const [showEditGameModal, setShowEditGameModal] = useState(false); const handleHeroLoad = async () => { const output = await average( @@ -105,6 +106,10 @@ export function GameDetailsContent() { setShowEditCustomGameModal(true); }; + const handleEditGameClick = () => { + setShowEditGameModal(true); + }; + const handleGameUpdated = (_updatedGame: any) => { updateGame(); updateLibrary(); @@ -115,12 +120,29 @@ export function GameDetailsContent() { }, [getGameArtifacts]); const isCustomGame = game?.shop === "custom"; + + // Helper function to get image with custom asset priority + const getImageWithCustomPriority = ( + customUrl: string | null | undefined, + originalUrl: string | null | undefined, + fallbackUrl?: string | null | undefined + ) => { + return customUrl || originalUrl || fallbackUrl || ""; + }; + const heroImage = isCustomGame ? game?.libraryHeroImageUrl || game?.iconUrl || "" - : shopDetails?.assets?.libraryHeroImageUrl || ""; + : getImageWithCustomPriority( + game?.customHeroImageUrl, + shopDetails?.assets?.libraryHeroImageUrl + ); + const logoImage = isCustomGame - ? game?.logoImageUrl || "" // Don't use icon as fallback for custom games - : shopDetails?.assets?.logoImageUrl || ""; + ? game?.logoImageUrl || "" + : getImageWithCustomPriority( + game?.customLogoImageUrl, + shopDetails?.assets?.logoImageUrl + ); return (
    - {game?.shop === "custom" && ( - - )} + {game?.shop !== "custom" && (
    ); } diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss new file mode 100644 index 00000000..f37ea44b --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss @@ -0,0 +1,47 @@ +.edit-game-modal__container { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + max-width: 500px; +} + +.edit-game-modal__form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.edit-game-modal__image-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.edit-game-modal__image-preview { + display: flex; + justify-content: center; + align-items: center; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: 8px; + background-color: var(--color-background-secondary); +} + +.edit-game-modal__preview-image { + max-width: 100%; + max-height: 120px; + object-fit: contain; + border-radius: 4px; +} + +.edit-game-modal__actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 8px; +} + +.edit-game-modal__actions button { + min-width: 100px; +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx new file mode 100644 index 00000000..ff8e7c3c --- /dev/null +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -0,0 +1,367 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { ImageIcon } from "@primer/octicons-react"; + +import { Modal, TextField, Button } from "@renderer/components"; +import { useToast } from "@renderer/hooks"; +import type { LibraryGame } from "@types"; + +import "./edit-game-modal.scss"; + +export interface EditGameModalProps { + visible: boolean; + onClose: () => void; + game: LibraryGame | null; + onGameUpdated: (updatedGame: any) => void; +} + +export function EditGameModal({ + visible, + onClose, + game, + onGameUpdated, +}: EditGameModalProps) { + const { t } = useTranslation("sidebar"); + const { showSuccessToast, showErrorToast } = useToast(); + + const [gameName, setGameName] = useState(""); + const [iconPath, setIconPath] = useState(""); + const [logoPath, setLogoPath] = useState(""); + const [heroPath, setHeroPath] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + + useEffect(() => { + if (game && visible) { + setGameName(game.title || ""); + + // For custom games, use existing logic + if (game.shop === "custom") { + const currentIconPath = game.iconUrl?.startsWith("local:") + ? game.iconUrl.replace("local:", "") + : ""; + const currentLogoPath = game.logoImageUrl?.startsWith("local:") + ? game.logoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:") + ? game.libraryHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + } else { + // For non-custom games, use custom asset paths if they exist + const currentIconPath = game.customIconUrl?.startsWith("local:") + ? game.customIconUrl.replace("local:", "") + : ""; + const currentLogoPath = game.customLogoImageUrl?.startsWith("local:") + ? game.customLogoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game.customHeroImageUrl?.startsWith("local:") + ? game.customHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + } + } + }, [game, visible]); + + const handleGameNameChange = (event: React.ChangeEvent) => { + setGameName(event.target.value); + }; + + const handleSelectIcon = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setIconPath(filePaths[0]); + } + }; + + const handleSelectLogo = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setLogoPath(filePaths[0]); + } + }; + + const handleSelectHero = async () => { + const { filePaths } = await window.electron.showOpenDialog({ + properties: ["openFile"], + filters: [ + { + name: t("edit_custom_game_modal_image_filter"), + extensions: ["jpg", "jpeg", "png", "gif", "webp"], + }, + ], + }); + + if (filePaths && filePaths.length > 0) { + setHeroPath(filePaths[0]); + } + }; + + const handleUpdateGame = async () => { + if (!game || !gameName.trim()) { + showErrorToast(t("edit_custom_game_modal_fill_required")); + return; + } + + setIsUpdating(true); + + try { + let updatedGame; + + if (game.shop === "custom") { + // For custom games, use existing logic + const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; + const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl; + const libraryHeroImageUrl = heroPath + ? `local:${heroPath}` + : game.libraryHeroImageUrl; + + updatedGame = await window.electron.updateCustomGame( + game.shop, + game.objectId, + gameName.trim(), + iconUrl || undefined, + logoImageUrl || undefined, + libraryHeroImageUrl || undefined + ); + } else { + // For non-custom games, update custom assets + const customIconUrl = iconPath ? `local:${iconPath}` : null; + const customLogoImageUrl = logoPath ? `local:${logoPath}` : null; + const customHeroImageUrl = heroPath ? `local:${heroPath}` : null; + + updatedGame = await window.electron.updateGameCustomAssets( + game.shop, + game.objectId, + gameName.trim(), + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ); + } + + showSuccessToast(t("edit_custom_game_modal_success")); + onGameUpdated(updatedGame); + onClose(); + } catch (error) { + console.error("Failed to update game:", error); + showErrorToast( + error instanceof Error + ? error.message + : t("edit_custom_game_modal_failed") + ); + } finally { + setIsUpdating(false); + } + }; + + const handleClose = () => { + if (!isUpdating && game) { + setGameName(game.title || ""); + + if (game.shop === "custom") { + const currentIconPath = game.iconUrl?.startsWith("local:") + ? game.iconUrl.replace("local:", "") + : ""; + const currentLogoPath = game.logoImageUrl?.startsWith("local:") + ? game.logoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:") + ? game.libraryHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + } else { + const currentIconPath = game.customIconUrl?.startsWith("local:") + ? game.customIconUrl.replace("local:", "") + : ""; + const currentLogoPath = game.customLogoImageUrl?.startsWith("local:") + ? game.customLogoImageUrl.replace("local:", "") + : ""; + const currentHeroPath = game.customHeroImageUrl?.startsWith("local:") + ? game.customHeroImageUrl.replace("local:", "") + : ""; + + setIconPath(currentIconPath); + setLogoPath(currentLogoPath); + setHeroPath(currentHeroPath); + } + onClose(); + } + }; + + const isFormValid = gameName.trim(); + + const getIconPreviewUrl = () => { + return iconPath ? `local:${iconPath}` : null; + }; + + const getLogoPreviewUrl = () => { + return logoPath ? `local:${logoPath}` : null; + }; + + const getHeroPreviewUrl = () => { + return heroPath ? `local:${heroPath}` : null; + }; + + return ( + +
    +
    + + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {iconPath && ( +
    + {t("edit_custom_game_modal_icon_preview")} +
    + )} +
    + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {logoPath && ( +
    + {t("edit_custom_game_modal_logo_preview")} +
    + )} +
    + +
    + + + {t("edit_custom_game_modal_browse")} + + } + /> + + {heroPath && ( +
    + {t("edit_custom_game_modal_hero_preview")} +
    + )} +
    +
    + +
    + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/src/renderer/src/pages/game-details/modals/index.ts b/src/renderer/src/pages/game-details/modals/index.ts index 7cc779ef..2e09806d 100644 --- a/src/renderer/src/pages/game-details/modals/index.ts +++ b/src/renderer/src/pages/game-details/modals/index.ts @@ -2,3 +2,4 @@ export * from "./repacks-modal"; export * from "./download-settings-modal"; export * from "./game-options-modal"; export * from "./edit-custom-game-modal"; +export * from "./edit-game-modal"; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index dd9ba1dd..bf5429c5 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -35,6 +35,9 @@ export interface Game { iconUrl: string | null; libraryHeroImageUrl: string | null; logoImageUrl: string | null; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; playTimeInMilliseconds: number; unsyncedDeltaPlayTimeInMilliseconds?: number; lastTimePlayed: Date | null; From 672ddff9f8b563e73d534265034e0dbf0814b090 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 17:15:59 +0300 Subject: [PATCH 04/43] security fix --- .../events/catalogue/save-game-shop-assets.ts | 13 +++++++------ src/main/events/library/get-library.ts | 3 ++- .../events/library/update-game-custom-assets.ts | 15 +++++++++++---- src/main/index.ts | 3 ++- .../pages/game-details/game-details-content.tsx | 10 +++++++--- .../game-details/modals/edit-game-modal.scss | 2 +- .../pages/game-details/modals/edit-game-modal.tsx | 2 +- 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/main/events/catalogue/save-game-shop-assets.ts b/src/main/events/catalogue/save-game-shop-assets.ts index 46aab36a..bf5f8b81 100644 --- a/src/main/events/catalogue/save-game-shop-assets.ts +++ b/src/main/events/catalogue/save-game-shop-assets.ts @@ -10,14 +10,15 @@ const saveGameShopAssets = async ( ): Promise => { const key = levelKeys.game(shop, objectId); const existingAssets = await gamesShopAssetsSublevel.get(key); - + // Preserve existing title if it differs from the incoming title (indicating it was customized) - const shouldPreserveTitle = existingAssets?.title && existingAssets.title !== assets.title; - - return gamesShopAssetsSublevel.put(key, { - ...existingAssets, + const shouldPreserveTitle = + existingAssets?.title && existingAssets.title !== assets.title; + + return gamesShopAssetsSublevel.put(key, { + ...existingAssets, ...assets, - title: shouldPreserveTitle ? existingAssets.title : assets.title + title: shouldPreserveTitle ? existingAssets.title : assets.title, }); }; diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index ea2971b9..6314f83d 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -24,7 +24,8 @@ const getLibrary = async (): Promise => { download: download ?? null, ...gameAssets, // Ensure compatibility with LibraryGame type - libraryHeroImageUrl: game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl, + libraryHeroImageUrl: + game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl, } as LibraryGame; }) ); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 99081a49..6049ef5d 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -21,9 +21,16 @@ const updateGameCustomAssets = async ( const updatedGame = { ...existingGame, title, - customIconUrl: customIconUrl !== undefined ? customIconUrl : existingGame.customIconUrl, - customLogoImageUrl: customLogoImageUrl !== undefined ? customLogoImageUrl : existingGame.customLogoImageUrl, - customHeroImageUrl: customHeroImageUrl !== undefined ? customHeroImageUrl : existingGame.customHeroImageUrl, + customIconUrl: + customIconUrl !== undefined ? customIconUrl : existingGame.customIconUrl, + customLogoImageUrl: + customLogoImageUrl !== undefined + ? customLogoImageUrl + : existingGame.customLogoImageUrl, + customHeroImageUrl: + customHeroImageUrl !== undefined + ? customHeroImageUrl + : existingGame.customHeroImageUrl, }; await gamesSublevel.put(gameKey, updatedGame); @@ -42,4 +49,4 @@ const updateGameCustomAssets = async ( return updatedGame; }; -registerEvent("updateGameCustomAssets", updateGameCustomAssets); \ No newline at end of file +registerEvent("updateGameCustomAssets", updateGameCustomAssets); diff --git a/src/main/index.ts b/src/main/index.ts index ab6980f9..9c390176 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -69,8 +69,9 @@ app.whenReady().then(async () => { request.url.slice("gradient:".length) ); + // Fixed regex to prevent ReDoS - removed nested quantifiers and backtracking const match = gradientCss.match( - /linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/ + /^linear-gradient\(([^,()]+),\s*([^,()]+),\s*([^,()]+)\)$/ ); let direction = "45deg"; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index cf300ceb..b8681c5c 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -120,7 +120,7 @@ export function GameDetailsContent() { }, [getGameArtifacts]); const isCustomGame = game?.shop === "custom"; - + // Helper function to get image with custom asset priority const getImageWithCustomPriority = ( customUrl: string | null | undefined, @@ -136,7 +136,7 @@ export function GameDetailsContent() { game?.customHeroImageUrl, shopDetails?.assets?.libraryHeroImageUrl ); - + const logoImage = isCustomGame ? game?.logoImageUrl || "" : getImageWithCustomPriority( @@ -181,7 +181,11 @@ export function GameDetailsContent() { - } - /> - - {iconPath && ( -
    - {t("edit_custom_game_modal_icon_preview")} -
    - )} - - -
    - - - {t("edit_custom_game_modal_browse")} - - } - /> - - {logoPath && ( -
    - {t("edit_custom_game_modal_logo_preview")} -
    - )} -
    - -
    - - - {t("edit_custom_game_modal_browse")} - - } - /> - - {heroPath && ( -
    - {t("edit_custom_game_modal_hero_preview")} -
    - )} -
    - - -
    - - -
    - - - ); -} diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 185da424..1731e03c 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -4,14 +4,14 @@ import { ImageIcon } from "@primer/octicons-react"; import { Modal, TextField, Button } from "@renderer/components"; import { useToast } from "@renderer/hooks"; -import type { LibraryGame } from "@types"; +import type { LibraryGame, Game } from "@types"; import "./edit-game-modal.scss"; export interface EditGameModalProps { visible: boolean; onClose: () => void; - game: LibraryGame | null; + game: LibraryGame | Game | null; onGameUpdated: (updatedGame: any) => void; } @@ -20,7 +20,7 @@ export function EditGameModal({ onClose, game, onGameUpdated, -}: EditGameModalProps) { +}: Readonly) { const { t } = useTranslation("sidebar"); const { showSuccessToast, showErrorToast } = useToast(); @@ -30,13 +30,18 @@ export function EditGameModal({ const [heroPath, setHeroPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); + // Helper function to check if game is a custom game + const isCustomGame = (game: LibraryGame | Game): boolean => { + return game.shop === "custom"; + }; + // Helper function to extract local path from URL const extractLocalPath = (url: string | null | undefined): string => { return url?.startsWith("local:") ? url.replace("local:", "") : ""; }; // Helper function to set asset paths for custom games - const setCustomGameAssets = (game: LibraryGame) => { + const setCustomGameAssets = (game: LibraryGame | Game) => { setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); @@ -53,10 +58,10 @@ export function EditGameModal({ if (game && visible) { setGameName(game.title || ""); - if (game.shop === "custom") { + if (isCustomGame(game)) { setCustomGameAssets(game); } else { - setNonCustomGameAssets(game); + setNonCustomGameAssets(game as LibraryGame); } } }, [game, visible]); @@ -114,7 +119,7 @@ export function EditGameModal({ }; // Helper function to prepare custom game assets - const prepareCustomGameAssets = (game: LibraryGame) => { + const prepareCustomGameAssets = (game: LibraryGame | Game) => { const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl; const libraryHeroImageUrl = heroPath @@ -134,7 +139,7 @@ export function EditGameModal({ }; // Helper function to update custom game - const updateCustomGame = async (game: LibraryGame) => { + const updateCustomGame = async (game: LibraryGame | Game) => { const { iconUrl, logoImageUrl, libraryHeroImageUrl } = prepareCustomGameAssets(game); @@ -173,9 +178,9 @@ export function EditGameModal({ try { const updatedGame = - game.shop === "custom" + isCustomGame(game) ? await updateCustomGame(game) - : await updateNonCustomGame(game); + : await updateNonCustomGame(game as LibraryGame); showSuccessToast(t("edit_custom_game_modal_success")); onGameUpdated(updatedGame); @@ -193,13 +198,13 @@ export function EditGameModal({ }; // Helper function to reset form to initial state - const resetFormToInitialState = (game: LibraryGame) => { + const resetFormToInitialState = (game: LibraryGame | Game) => { setGameName(game.title || ""); - if (game.shop === "custom") { + if (isCustomGame(game)) { setCustomGameAssets(game); } else { - setNonCustomGameAssets(game); + setNonCustomGameAssets(game as LibraryGame); } }; @@ -212,16 +217,16 @@ export function EditGameModal({ const isFormValid = gameName.trim(); - const getIconPreviewUrl = () => { - return iconPath ? `local:${iconPath}` : null; + const getIconPreviewUrl = (): string | undefined => { + return iconPath ? `local:${iconPath}` : undefined; }; - const getLogoPreviewUrl = () => { - return logoPath ? `local:${logoPath}` : null; + const getLogoPreviewUrl = (): string | undefined => { + return logoPath ? `local:${logoPath}` : undefined; }; - const getHeroPreviewUrl = () => { - return heroPath ? `local:${heroPath}` : null; + const getHeroPreviewUrl = (): string | undefined => { + return heroPath ? `local:${heroPath}` : undefined; }; return ( @@ -265,7 +270,7 @@ export function EditGameModal({ {iconPath && (
    {t("edit_custom_game_modal_icon_preview")} @@ -296,7 +301,7 @@ export function EditGameModal({ {logoPath && (
    {t("edit_custom_game_modal_logo_preview")} @@ -327,7 +332,7 @@ export function EditGameModal({ {heroPath && (
    {t("edit_custom_game_modal_hero_preview")} diff --git a/src/renderer/src/pages/game-details/modals/index.ts b/src/renderer/src/pages/game-details/modals/index.ts index 2e09806d..724e0003 100644 --- a/src/renderer/src/pages/game-details/modals/index.ts +++ b/src/renderer/src/pages/game-details/modals/index.ts @@ -1,5 +1,4 @@ export * from "./repacks-modal"; export * from "./download-settings-modal"; export * from "./game-options-modal"; -export * from "./edit-custom-game-modal"; export * from "./edit-game-modal"; From 9ce1c40b2192f9a6203b51b1432ee6004c52fddb Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 19:48:05 +0300 Subject: [PATCH 10/43] redirect to home page when removing custom game from library --- src/renderer/src/pages/game-details/game-details.tsx | 1 + .../src/pages/game-details/modals/edit-game-modal.tsx | 7 +++---- .../src/pages/game-details/modals/game-options-modal.tsx | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index b966e6e7..f0778494 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -178,6 +178,7 @@ export default function GameDetails() { onClose={() => { setShowGameOptionsModal(false); }} + onNavigateHome={() => navigate("/")} /> )} diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 1731e03c..e33d59b2 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -177,10 +177,9 @@ export function EditGameModal({ setIsUpdating(true); try { - const updatedGame = - isCustomGame(game) - ? await updateCustomGame(game) - : await updateNonCustomGame(game as LibraryGame); + const updatedGame = isCustomGame(game) + ? await updateCustomGame(game) + : await updateNonCustomGame(game as LibraryGame); showSuccessToast(t("edit_custom_game_modal_success")); onGameUpdated(updatedGame); diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 9c20acce..34f2690d 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -18,12 +18,14 @@ export interface GameOptionsModalProps { visible: boolean; game: LibraryGame; onClose: () => void; + onNavigateHome?: () => void; } export function GameOptionsModal({ visible, game, onClose, + onNavigateHome, }: Readonly) { const { t } = useTranslation("game_details"); @@ -90,6 +92,11 @@ export function GameOptionsModal({ await removeGameFromLibrary(game.shop, game.objectId); updateGame(); onClose(); + + // Redirect to home page if it's a custom game + if (game.shop === "custom" && onNavigateHome) { + onNavigateHome(); + } }; const handleChangeExecutableLocation = async () => { From bf9e3de0b5f7c28564e169af316bdf6a863ddffb Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 20:04:09 +0300 Subject: [PATCH 11/43] fixed background color in hero arent being properly used if game is custom --- .../src/pages/game-details/game-details-content.tsx | 11 ++++++++++- .../pages/game-details/modals/game-options-modal.tsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 4597263b..9c2b5bd8 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -68,8 +68,17 @@ export function GameDetailsContent() { const [showEditGameModal, setShowEditGameModal] = useState(false); const handleHeroLoad = async () => { + // Use the same logic as heroImage to get the correct URL for both custom and non-custom games + const isCustomGame = game?.shop === "custom"; + const heroImageUrl = isCustomGame + ? game?.libraryHeroImageUrl || game?.iconUrl || "" + : getImageWithCustomPriority( + game?.customHeroImageUrl, + shopDetails?.assets?.libraryHeroImageUrl + ); + const output = await average( - shopDetails?.assets?.libraryHeroImageUrl ?? "", + heroImageUrl, { amount: 1, format: "hex", diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 34f2690d..69d610d9 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -92,7 +92,7 @@ export function GameOptionsModal({ await removeGameFromLibrary(game.shop, game.objectId); updateGame(); onClose(); - + // Redirect to home page if it's a custom game if (game.shop === "custom" && onNavigateHome) { onNavigateHome(); From 9f4fd0ce61805e5f447c304ebf05c398874bb1e3 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 20:46:53 +0300 Subject: [PATCH 12/43] added proper image saving for custom games + edited game settings to hide buttons if game is custom --- src/main/events/index.ts | 2 + .../events/library/cleanup-unused-assets.ts | 73 +++++++++ .../events/library/copy-custom-game-asset.ts | 42 ++++++ src/preload/index.ts | 5 + src/renderer/src/declaration.d.ts | 5 + .../game-details/game-details-content.tsx | 11 +- .../game-details/modals/edit-game-modal.tsx | 39 ++++- .../modals/game-options-modal.tsx | 140 ++++++++++-------- 8 files changed, 242 insertions(+), 75 deletions(-) create mode 100644 src/main/events/library/cleanup-unused-assets.ts create mode 100644 src/main/events/library/copy-custom-game-asset.ts diff --git a/src/main/events/index.ts b/src/main/events/index.ts index a1acd596..deab53a6 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -39,7 +39,9 @@ import "./library/reset-game-achievements"; import "./library/change-game-playtime"; import "./library/toggle-automatic-cloud-sync"; import "./library/get-default-wine-prefix-selection-path"; +import "./library/cleanup-unused-assets"; import "./library/create-steam-shortcut"; +import "./library/copy-custom-game-asset"; import "./misc/open-checkout"; import "./misc/open-external"; import "./misc/show-open-dialog"; diff --git a/src/main/events/library/cleanup-unused-assets.ts b/src/main/events/library/cleanup-unused-assets.ts new file mode 100644 index 00000000..9cde548d --- /dev/null +++ b/src/main/events/library/cleanup-unused-assets.ts @@ -0,0 +1,73 @@ +import { ipcMain } from "electron"; +import fs from "fs"; +import path from "path"; +import { ASSETS_PATH } from "@main/constants"; + +const getCustomGamesAssetsPath = () => { + return path.join(ASSETS_PATH, "custom-games"); +}; + +const getAllCustomGameAssets = async (): Promise => { + const assetsPath = getCustomGamesAssetsPath(); + + if (!fs.existsSync(assetsPath)) { + return []; + } + + const files = await fs.promises.readdir(assetsPath); + return files.map(file => path.join(assetsPath, file)); +}; + +const getUsedAssetPaths = async (): Promise> => { + // Get all custom games from the level database + const { gamesSublevel } = await import("@main/level"); + const allGames = await gamesSublevel.iterator().all(); + + const customGames = allGames + .map(([_key, game]) => game) + .filter(game => game.shop === "custom" && !game.isDeleted); + + const usedPaths = new Set(); + + customGames.forEach(game => { + // Extract file paths from local URLs + if (game.iconUrl?.startsWith("local:")) { + usedPaths.add(game.iconUrl.replace("local:", "")); + } + if (game.logoImageUrl?.startsWith("local:")) { + usedPaths.add(game.logoImageUrl.replace("local:", "")); + } + if (game.libraryHeroImageUrl?.startsWith("local:")) { + usedPaths.add(game.libraryHeroImageUrl.replace("local:", "")); + } + }); + + return usedPaths; +}; + +export const cleanupUnusedAssets = async (): Promise<{ deletedCount: number; errors: string[] }> => { + try { + const allAssets = await getAllCustomGameAssets(); + const usedAssets = await getUsedAssetPaths(); + + const errors: string[] = []; + let deletedCount = 0; + + for (const assetPath of allAssets) { + if (!usedAssets.has(assetPath)) { + try { + await fs.promises.unlink(assetPath); + deletedCount++; + } catch (error) { + errors.push(`Failed to delete ${assetPath}: ${error}`); + } + } + } + + return { deletedCount, errors }; + } catch (error) { + throw new Error(`Failed to cleanup unused assets: ${error}`); + } +}; + +ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets); \ No newline at end of file diff --git a/src/main/events/library/copy-custom-game-asset.ts b/src/main/events/library/copy-custom-game-asset.ts new file mode 100644 index 00000000..dfb7bf47 --- /dev/null +++ b/src/main/events/library/copy-custom-game-asset.ts @@ -0,0 +1,42 @@ +import { registerEvent } from "../register-event"; +import fs from "node:fs"; +import path from "node:path"; +import { randomUUID } from "crypto"; +import { ASSETS_PATH } from "@main/constants"; + +const copyCustomGameAsset = async ( + _event: Electron.IpcMainInvokeEvent, + sourcePath: string, + assetType: "icon" | "logo" | "hero" +): Promise => { + if (!sourcePath || !fs.existsSync(sourcePath)) { + throw new Error("Source file does not exist"); + } + + // Ensure assets directory exists + if (!fs.existsSync(ASSETS_PATH)) { + fs.mkdirSync(ASSETS_PATH, { recursive: true }); + } + + // Create custom games assets subdirectory + const customGamesAssetsPath = path.join(ASSETS_PATH, "custom-games"); + if (!fs.existsSync(customGamesAssetsPath)) { + fs.mkdirSync(customGamesAssetsPath, { recursive: true }); + } + + // Get file extension + const fileExtension = path.extname(sourcePath); + + // Generate unique filename + const uniqueId = randomUUID(); + const fileName = `${assetType}-${uniqueId}${fileExtension}`; + const destinationPath = path.join(customGamesAssetsPath, fileName); + + // Copy the file + await fs.promises.copyFile(sourcePath, destinationPath); + + // Return the local URL format + return `local:${destinationPath}`; +}; + +registerEvent("copyCustomGameAsset", copyCustomGameAsset); \ No newline at end of file diff --git a/src/preload/index.ts b/src/preload/index.ts index ca918eee..3bcd4524 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -143,6 +143,11 @@ contextBridge.exposeInMainWorld("electron", { logoImageUrl, libraryHeroImageUrl ), + copyCustomGameAsset: ( + sourcePath: string, + assetType: "icon" | "logo" | "hero" + ) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType), + cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"), updateCustomGame: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 5af0d04f..0515f1f0 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -126,6 +126,11 @@ declare global { logoImageUrl?: string, libraryHeroImageUrl?: string ) => Promise; + copyCustomGameAsset: ( + sourcePath: string, + assetType: "icon" | "logo" | "hero" + ) => Promise; + cleanupUnusedAssets: () => Promise<{ deletedCount: number; errors: string[] }>; updateGameCustomAssets: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 9c2b5bd8..22f56630 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -77,13 +77,10 @@ export function GameDetailsContent() { shopDetails?.assets?.libraryHeroImageUrl ); - const output = await average( - heroImageUrl, - { - amount: 1, - format: "hex", - } - ); + const output = await average(heroImageUrl, { + amount: 1, + format: "hex", + }); const backgroundColor = output ? new Color(output).darken(0.7).toString() diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index e33d59b2..b922a231 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -82,7 +82,18 @@ export function EditGameModal({ }); if (filePaths && filePaths.length > 0) { - setIconPath(filePaths[0]); + try { + // Copy the asset to the app's assets folder + const copiedAssetUrl = await window.electron.copyCustomGameAsset( + filePaths[0], + "icon" + ); + setIconPath(copiedAssetUrl.replace("local:", "")); + } catch (error) { + console.error("Failed to copy icon asset:", error); + // Fallback to original behavior + setIconPath(filePaths[0]); + } } }; @@ -98,7 +109,18 @@ export function EditGameModal({ }); if (filePaths && filePaths.length > 0) { - setLogoPath(filePaths[0]); + try { + // Copy the asset to the app's assets folder + const copiedAssetUrl = await window.electron.copyCustomGameAsset( + filePaths[0], + "logo" + ); + setLogoPath(copiedAssetUrl.replace("local:", "")); + } catch (error) { + console.error("Failed to copy logo asset:", error); + // Fallback to original behavior + setLogoPath(filePaths[0]); + } } }; @@ -114,7 +136,18 @@ export function EditGameModal({ }); if (filePaths && filePaths.length > 0) { - setHeroPath(filePaths[0]); + try { + // Copy the asset to the app's assets folder + const copiedAssetUrl = await window.electron.copyCustomGameAsset( + filePaths[0], + "hero" + ); + setHeroPath(copiedAssetUrl.replace("local:", "")); + } catch (error) { + console.error("Failed to copy hero asset:", error); + // Fallback to original behavior + setHeroPath(filePaths[0]); + } } }; diff --git a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx index 69d610d9..ccbce05d 100644 --- a/src/renderer/src/pages/game-details/modals/game-options-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/game-options-modal.tsx @@ -353,14 +353,16 @@ export function GameOptionsModal({ > {t("create_shortcut")} - + {game.shop !== "custom" && ( + + )} {shouldShowCreateStartMenuShortcut && (
    - - {t("enable_automatic_cloud_sync")} - - Hydra Cloud - -
    - } - checked={automaticCloudSync} - disabled={!hasActiveSubscription || !game.executablePath} - onChange={handleToggleAutomaticCloudSync} - /> + {game.shop !== "custom" && ( + + {t("enable_automatic_cloud_sync")} + + Hydra Cloud + + + } + checked={automaticCloudSync} + disabled={!hasActiveSubscription || !game.executablePath} + onChange={handleToggleAutomaticCloudSync} + /> + )} {shouldShowWinePrefixConfiguration && (
    @@ -448,33 +452,35 @@ export function GameOptionsModal({ />
    -
    -
    -

    {t("downloads_section_title")}

    -

    - {t("downloads_section_description")} -

    -
    + {game.shop !== "custom" && ( +
    +
    +

    {t("downloads_section_title")}

    +

    + {t("downloads_section_description")} +

    +
    -
    - - {game.download?.downloadPath && ( +
    - )} + {game.download?.downloadPath && ( + + )} +
    -
    + )}
    @@ -493,18 +499,20 @@ export function GameOptionsModal({ {t("remove_from_library")} - + {game.shop !== "custom" && ( + + )} - + {game.shop !== "custom" && ( + + )}
    From de70beb01e138e0974abea2f9c861477e2674c2a Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 21:06:15 +0300 Subject: [PATCH 13/43] Preload images for non-custom games. Added ability to restore images to default if game is non-custom --- .../events/library/cleanup-unused-assets.ts | 19 ++- .../events/library/copy-custom-game-asset.ts | 4 +- .../library/update-game-custom-assets.ts | 6 +- src/renderer/src/declaration.d.ts | 5 +- .../game-details/game-details-content.tsx | 1 + .../game-details/modals/edit-game-modal.tsx | 146 ++++++++++++++---- .../modals/game-options-modal.tsx | 4 +- 7 files changed, 137 insertions(+), 48 deletions(-) diff --git a/src/main/events/library/cleanup-unused-assets.ts b/src/main/events/library/cleanup-unused-assets.ts index 9cde548d..22490c07 100644 --- a/src/main/events/library/cleanup-unused-assets.ts +++ b/src/main/events/library/cleanup-unused-assets.ts @@ -9,27 +9,27 @@ const getCustomGamesAssetsPath = () => { const getAllCustomGameAssets = async (): Promise => { const assetsPath = getCustomGamesAssetsPath(); - + if (!fs.existsSync(assetsPath)) { return []; } const files = await fs.promises.readdir(assetsPath); - return files.map(file => path.join(assetsPath, file)); + return files.map((file) => path.join(assetsPath, file)); }; const getUsedAssetPaths = async (): Promise> => { // Get all custom games from the level database const { gamesSublevel } = await import("@main/level"); const allGames = await gamesSublevel.iterator().all(); - + const customGames = allGames .map(([_key, game]) => game) - .filter(game => game.shop === "custom" && !game.isDeleted); + .filter((game) => game.shop === "custom" && !game.isDeleted); const usedPaths = new Set(); - customGames.forEach(game => { + customGames.forEach((game) => { // Extract file paths from local URLs if (game.iconUrl?.startsWith("local:")) { usedPaths.add(game.iconUrl.replace("local:", "")); @@ -45,11 +45,14 @@ const getUsedAssetPaths = async (): Promise> => { return usedPaths; }; -export const cleanupUnusedAssets = async (): Promise<{ deletedCount: number; errors: string[] }> => { +export const cleanupUnusedAssets = async (): Promise<{ + deletedCount: number; + errors: string[]; +}> => { try { const allAssets = await getAllCustomGameAssets(); const usedAssets = await getUsedAssetPaths(); - + const errors: string[] = []; let deletedCount = 0; @@ -70,4 +73,4 @@ export const cleanupUnusedAssets = async (): Promise<{ deletedCount: number; err } }; -ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets); \ No newline at end of file +ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets); diff --git a/src/main/events/library/copy-custom-game-asset.ts b/src/main/events/library/copy-custom-game-asset.ts index dfb7bf47..07c3d6f7 100644 --- a/src/main/events/library/copy-custom-game-asset.ts +++ b/src/main/events/library/copy-custom-game-asset.ts @@ -26,7 +26,7 @@ const copyCustomGameAsset = async ( // Get file extension const fileExtension = path.extname(sourcePath); - + // Generate unique filename const uniqueId = randomUUID(); const fileName = `${assetType}-${uniqueId}${fileExtension}`; @@ -39,4 +39,4 @@ const copyCustomGameAsset = async ( return `local:${destinationPath}`; }; -registerEvent("copyCustomGameAsset", copyCustomGameAsset); \ No newline at end of file +registerEvent("copyCustomGameAsset", copyCustomGameAsset); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 6e504e92..74446fb0 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -21,9 +21,9 @@ const updateGameCustomAssets = async ( const updatedGame = { ...existingGame, title, - customIconUrl: customIconUrl ?? existingGame.customIconUrl, - customLogoImageUrl: customLogoImageUrl ?? existingGame.customLogoImageUrl, - customHeroImageUrl: customHeroImageUrl ?? existingGame.customHeroImageUrl, + customIconUrl: customIconUrl !== undefined ? customIconUrl : existingGame.customIconUrl, + customLogoImageUrl: customLogoImageUrl !== undefined ? customLogoImageUrl : existingGame.customLogoImageUrl, + customHeroImageUrl: customHeroImageUrl !== undefined ? customHeroImageUrl : existingGame.customHeroImageUrl, }; await gamesSublevel.put(gameKey, updatedGame); diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 0515f1f0..45bdb20d 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -130,7 +130,10 @@ declare global { sourcePath: string, assetType: "icon" | "logo" | "hero" ) => Promise; - cleanupUnusedAssets: () => Promise<{ deletedCount: number; errors: string[] }>; + cleanupUnusedAssets: () => Promise<{ + deletedCount: number; + errors: string[]; + }>; updateGameCustomAssets: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 22f56630..6f3c2ee2 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -232,6 +232,7 @@ export function GameDetailsContent() { visible={showEditGameModal} onClose={() => setShowEditGameModal(false)} game={game} + shopDetails={shopDetails} onGameUpdated={handleGameUpdated} /> diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index b922a231..45653649 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { ImageIcon } from "@primer/octicons-react"; +import { ImageIcon, ReplyIcon } from "@primer/octicons-react"; import { Modal, TextField, Button } from "@renderer/components"; import { useToast } from "@renderer/hooks"; -import type { LibraryGame, Game } from "@types"; +import type { LibraryGame, Game, ShopDetailsWithAssets } from "@types"; import "./edit-game-modal.scss"; @@ -12,6 +12,7 @@ export interface EditGameModalProps { visible: boolean; onClose: () => void; game: LibraryGame | Game | null; + shopDetails?: ShopDetailsWithAssets | null; onGameUpdated: (updatedGame: any) => void; } @@ -19,6 +20,7 @@ export function EditGameModal({ visible, onClose, game, + shopDetails, onGameUpdated, }: Readonly) { const { t } = useTranslation("sidebar"); @@ -29,6 +31,11 @@ export function EditGameModal({ const [logoPath, setLogoPath] = useState(""); const [heroPath, setHeroPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); + + // Store default image URLs for non-custom games + const [defaultIconUrl, setDefaultIconUrl] = useState(null); + const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); + const [defaultHeroUrl, setDefaultHeroUrl] = useState(null); // Helper function to check if game is a custom game const isCustomGame = (game: LibraryGame | Game): boolean => { @@ -52,6 +59,11 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); + + // Store default URLs for restore functionality from shopDetails.assets + setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); + setDefaultLogoUrl(shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null); + setDefaultHeroUrl(shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null); }; useEffect(() => { @@ -64,7 +76,7 @@ export function EditGameModal({ setNonCustomGameAssets(game as LibraryGame); } } - }, [game, visible]); + }, [game, visible, shopDetails]); const handleGameNameChange = (event: React.ChangeEvent) => { setGameName(event.target.value); @@ -151,6 +163,19 @@ export function EditGameModal({ } }; + // Helper functions to restore default images for non-custom games + const handleRestoreDefaultIcon = () => { + setIconPath(""); + }; + + const handleRestoreDefaultLogo = () => { + setLogoPath(""); + }; + + const handleRestoreDefaultHero = () => { + setHeroPath(""); + }; + // Helper function to prepare custom game assets const prepareCustomGameAssets = (game: LibraryGame | Game) => { const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; @@ -235,6 +260,10 @@ export function EditGameModal({ if (isCustomGame(game)) { setCustomGameAssets(game); + // Clear default URLs for custom games + setDefaultIconUrl(null); + setDefaultLogoUrl(null); + setDefaultHeroUrl(null); } else { setNonCustomGameAssets(game as LibraryGame); } @@ -250,14 +279,26 @@ export function EditGameModal({ const isFormValid = gameName.trim(); const getIconPreviewUrl = (): string | undefined => { + if (!isCustomGame(game!)) { + // For non-custom games, show custom image if set, otherwise show default + return iconPath ? `local:${iconPath}` : defaultIconUrl || undefined; + } return iconPath ? `local:${iconPath}` : undefined; }; const getLogoPreviewUrl = (): string | undefined => { + if (!isCustomGame(game!)) { + // For non-custom games, show custom image if set, otherwise show default + return logoPath ? `local:${logoPath}` : defaultLogoUrl || undefined; + } return logoPath ? `local:${logoPath}` : undefined; }; const getHeroPreviewUrl = (): string | undefined => { + if (!isCustomGame(game!)) { + // For non-custom games, show custom image if set, otherwise show default + return heroPath ? `local:${heroPath}` : defaultHeroUrl || undefined; + } return heroPath ? `local:${heroPath}` : undefined; }; @@ -287,19 +328,32 @@ export function EditGameModal({ readOnly theme="dark" rightContent={ - +
    + + {!isCustomGame(game!) && iconPath && ( + + )} +
    } /> - {iconPath && ( + {(iconPath || (!isCustomGame(game!) && defaultIconUrl)) && (
    - - {t("edit_custom_game_modal_browse")} - +
    + + {!isCustomGame(game!) && logoPath && ( + + )} +
    } /> - {logoPath && ( + {(logoPath || (!isCustomGame(game!) && defaultLogoUrl)) && (
    - - {t("edit_custom_game_modal_browse")} - +
    + + {!isCustomGame(game!) && heroPath && ( + + )} +
    } /> - {heroPath && ( + {(heroPath || (!isCustomGame(game!) && defaultHeroUrl)) && (
    {t("remove_files")} From 607bc6407c9e2bb054de3cedd6fe588a05554797 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 21:16:46 +0300 Subject: [PATCH 14/43] fixed assertions + use ?? operators --- .../library/update-game-custom-assets.ts | 6 +-- .../game-details/modals/edit-game-modal.tsx | 40 +++++++++++-------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 74446fb0..866cd60e 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -21,9 +21,9 @@ const updateGameCustomAssets = async ( const updatedGame = { ...existingGame, title, - customIconUrl: customIconUrl !== undefined ? customIconUrl : existingGame.customIconUrl, - customLogoImageUrl: customLogoImageUrl !== undefined ? customLogoImageUrl : existingGame.customLogoImageUrl, - customHeroImageUrl: customHeroImageUrl !== undefined ? customHeroImageUrl : existingGame.customHeroImageUrl, + ...(customIconUrl !== undefined && { customIconUrl }), + ...(customLogoImageUrl !== undefined && { customLogoImageUrl }), + ...(customHeroImageUrl !== undefined && { customHeroImageUrl }), }; await gamesSublevel.put(gameKey, updatedGame); diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 45653649..ea34223c 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -31,7 +31,7 @@ export function EditGameModal({ const [logoPath, setLogoPath] = useState(""); const [heroPath, setHeroPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); - + // Store default image URLs for non-custom games const [defaultIconUrl, setDefaultIconUrl] = useState(null); const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); @@ -59,11 +59,17 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); - + // Store default URLs for restore functionality from shopDetails.assets setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); - setDefaultLogoUrl(shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null); - setDefaultHeroUrl(shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null); + setDefaultLogoUrl( + shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null + ); + setDefaultHeroUrl( + shopDetails?.assets?.libraryHeroImageUrl || + game.libraryHeroImageUrl || + null + ); }; useEffect(() => { @@ -235,7 +241,7 @@ export function EditGameModal({ setIsUpdating(true); try { - const updatedGame = isCustomGame(game) + const updatedGame = game && isCustomGame(game) ? await updateCustomGame(game) : await updateNonCustomGame(game as LibraryGame); @@ -279,7 +285,7 @@ export function EditGameModal({ const isFormValid = gameName.trim(); const getIconPreviewUrl = (): string | undefined => { - if (!isCustomGame(game!)) { + if (game && !isCustomGame(game)) { // For non-custom games, show custom image if set, otherwise show default return iconPath ? `local:${iconPath}` : defaultIconUrl || undefined; } @@ -287,7 +293,7 @@ export function EditGameModal({ }; const getLogoPreviewUrl = (): string | undefined => { - if (!isCustomGame(game!)) { + if (game && !isCustomGame(game)) { // For non-custom games, show custom image if set, otherwise show default return logoPath ? `local:${logoPath}` : defaultLogoUrl || undefined; } @@ -295,7 +301,7 @@ export function EditGameModal({ }; const getHeroPreviewUrl = (): string | undefined => { - if (!isCustomGame(game!)) { + if (game && !isCustomGame(game)) { // For non-custom games, show custom image if set, otherwise show default return heroPath ? `local:${heroPath}` : defaultHeroUrl || undefined; } @@ -328,7 +334,7 @@ export function EditGameModal({ readOnly theme="dark" rightContent={ -
    +
    - {!isCustomGame(game!) && iconPath && ( + {game && !isCustomGame(game) && iconPath && ( - {!isCustomGame(game!) && logoPath && ( + {game && !isCustomGame(game) && logoPath && ( - {!isCustomGame(game!) && heroPath && ( + {game && !isCustomGame(game) && heroPath && ( {game && !isCustomGame(game) && iconPath && ( {game && !isCustomGame(game) && logoPath && ( {game && !isCustomGame(game) && heroPath && (
    From de4119988c3f3d54405d54f271d6c08baa4b5ea6 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 22:30:16 +0300 Subject: [PATCH 16/43] avoiding non-native interactive elements + extracted ternary operation --- src/main/events/misc/delete-temp-file.ts | 2 +- src/main/events/misc/save-temp-file.ts | 11 +- .../game-details/game-details-content.tsx | 52 +++--- .../game-details/modals/edit-game-modal.scss | 22 ++- .../game-details/modals/edit-game-modal.tsx | 176 ++++++++++++------ 5 files changed, 170 insertions(+), 93 deletions(-) diff --git a/src/main/events/misc/delete-temp-file.ts b/src/main/events/misc/delete-temp-file.ts index b26dd975..7ca88fa1 100644 --- a/src/main/events/misc/delete-temp-file.ts +++ b/src/main/events/misc/delete-temp-file.ts @@ -15,4 +15,4 @@ const deleteTempFile = async ( } }; -registerEvent("deleteTempFile", deleteTempFile); \ No newline at end of file +registerEvent("deleteTempFile", deleteTempFile); diff --git a/src/main/events/misc/save-temp-file.ts b/src/main/events/misc/save-temp-file.ts index c9776430..8f253bf2 100644 --- a/src/main/events/misc/save-temp-file.ts +++ b/src/main/events/misc/save-temp-file.ts @@ -10,15 +10,18 @@ const saveTempFile = async ( ): Promise => { try { const tempDir = app.getPath("temp"); - const tempFilePath = path.join(tempDir, `hydra-temp-${Date.now()}-${fileName}`); - + const tempFilePath = path.join( + tempDir, + `hydra-temp-${Date.now()}-${fileName}` + ); + // Write the file data to temp directory fs.writeFileSync(tempFilePath, fileData); - + return tempFilePath; } catch (error) { throw new Error(`Failed to save temp file: ${error}`); } }; -registerEvent("saveTempFile", saveTempFile); \ No newline at end of file +registerEvent("saveTempFile", saveTempFile); diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index f4619514..4ef45b6f 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -145,6 +145,34 @@ export function GameDetailsContent() { shopDetails?.assets?.logoImageUrl ); + const renderGameLogo = () => { + if (isCustomGame) { + // For custom games, show logo image if available, otherwise show game title as text + if (logoImage) { + return ( + {game?.title} + ); + } else { + return ( +
    {game?.title}
    + ); + } + } else { + // For non-custom games, show logo image if available + return logoImage ? ( + {game?.title} + ) : null; + } + }; + return (
    - {isCustomGame ? ( - // For custom games, show logo image if available, otherwise show game title as text - logoImage ? ( - {game?.title} - ) : ( -
    - {game?.title} -
    - ) - ) : ( - // For non-custom games, show logo image if available - logoImage && ( - {game?.title} - ) - )} + {renderGameLogo()}
    {(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && ( -
    handleDragEnter(e, 'icon')} + onDragEnter={(e) => handleDragEnter(e, "icon")} onDragLeave={handleDragLeave} onDrop={handleIconDrop} + onClick={handleSelectIcon} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelectIcon(); + } + }} > {t("edit_game_modal_icon_preview")} - {dragOverTarget === 'icon' && ( + {dragOverTarget === "icon" && (
    Drop to replace icon
    @@ -521,15 +549,27 @@ export function EditGameModal({
    )} - {(!iconPath && !(game && !isCustomGame(game) && defaultIconUrl)) && ( -
    handleDragEnter(e, 'icon')} + onDragEnter={(e) => handleDragEnter(e, "icon")} onDragLeave={handleDragLeave} onDrop={handleIconDrop} + onClick={handleSelectIcon} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelectIcon(); + } + }} >
    @@ -576,21 +616,33 @@ export function EditGameModal({
    {(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && ( -
    handleDragEnter(e, 'logo')} + onDragEnter={(e) => handleDragEnter(e, "logo")} onDragLeave={handleDragLeave} onDrop={handleLogoDrop} + onClick={handleSelectLogo} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelectLogo(); + } + }} > {t("edit_game_modal_logo_preview")} - {dragOverTarget === 'logo' && ( + {dragOverTarget === "logo" && (
    Drop to replace logo
    @@ -598,13 +650,15 @@ export function EditGameModal({
    )} - {(!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl)) && ( -
    handleDragEnter(e, 'logo')} + onDragEnter={(e) => handleDragEnter(e, "logo")} onDragLeave={handleDragLeave} onDrop={handleLogoDrop} > @@ -653,12 +707,14 @@ export function EditGameModal({
    {(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && ( -
    handleDragEnter(e, 'hero')} + onDragEnter={(e) => handleDragEnter(e, "hero")} onDragLeave={handleDragLeave} onDrop={handleHeroDrop} > @@ -667,7 +723,7 @@ export function EditGameModal({ alt={t("edit_game_modal_hero_preview")} className="edit-game-modal__preview-image" /> - {dragOverTarget === 'hero' && ( + {dragOverTarget === "hero" && (
    Drop to replace hero image
    @@ -675,13 +731,15 @@ export function EditGameModal({
    )} - {(!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl)) && ( -
    handleDragEnter(e, 'hero')} + onDragEnter={(e) => handleDragEnter(e, "hero")} onDragLeave={handleDragLeave} onDrop={handleHeroDrop} > From 393a04d7c39ffa231e05db09cdd00882d8c45c4e Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 22:39:08 +0300 Subject: [PATCH 17/43] Complete fix for non-native interactive elements --- .../game-details/modals/edit-game-modal.scss | 15 +++++ .../game-details/modals/edit-game-modal.tsx | 60 ++++++++----------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss index facb3fc4..bb6d5918 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss @@ -53,6 +53,21 @@ transform 0.2s ease; position: relative; + /* Reset button styles when used as button element */ + &[type="button"] { + font: inherit; + color: inherit; + text-align: inherit; + text-decoration: none; + outline: none; + cursor: pointer; + + &:focus { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + } + } + &:hover { border-color: var(--color-primary); } diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 66418ab4..26d33a95 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -515,9 +515,8 @@ export function EditGameModal({
    {(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && ( -
    { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleSelectIcon(); - } - }} > Drop to replace icon
    )} -
    + )} {!iconPath && !(game && !isCustomGame(game) && defaultIconUrl) && ( -
    { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleSelectIcon(); - } - }} >
    Drop icon image here
    -
    + )}
    @@ -616,9 +602,8 @@ export function EditGameModal({
    {(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && ( -
    { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleSelectLogo(); - } - }} > Drop to replace logo
    )} -
    + )} {!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl) && ( -
    handleDragEnter(e, "logo")} onDragLeave={handleDragLeave} onDrop={handleLogoDrop} + onClick={handleSelectLogo} >
    Drop logo image here
    -
    + )}
    @@ -707,7 +689,9 @@ export function EditGameModal({
    {(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && ( -
    handleDragEnter(e, "hero")} onDragLeave={handleDragLeave} onDrop={handleHeroDrop} + onClick={handleSelectHero} > Drop to replace hero image
    )} - + )} {!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl) && ( -
    handleDragEnter(e, "hero")} onDragLeave={handleDragLeave} onDrop={handleHeroDrop} + onClick={handleSelectHero} >
    Drop hero image here
    -
    + )} From eeed34adcbc7678838a884a428e3db77185cb96c Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 20 Sep 2025 09:21:28 +0300 Subject: [PATCH 18/43] lint failing fix --- .../game-details/modals/edit-game-modal.scss | 2 +- .../game-details/modals/edit-game-modal.tsx | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss index bb6d5918..3a5d8943 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss @@ -61,7 +61,7 @@ text-decoration: none; outline: none; cursor: pointer; - + &:focus { outline: 2px solid var(--color-primary); outline-offset: 2px; diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 26d33a95..614d458b 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { ImageIcon, ReplyIcon } from "@primer/octicons-react"; @@ -13,7 +13,7 @@ export interface EditGameModalProps { onClose: () => void; game: LibraryGame | Game | null; shopDetails?: ShopDetailsWithAssets | null; - onGameUpdated: (updatedGame: any) => void; + onGameUpdated: (updatedGame: LibraryGame | Game) => void; } export function EditGameModal({ @@ -48,29 +48,32 @@ export function EditGameModal({ }; // Helper function to set asset paths for custom games - const setCustomGameAssets = (game: LibraryGame | Game) => { + const setCustomGameAssets = useCallback((game: LibraryGame | Game) => { setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); - }; + }, []); // Helper function to set asset paths for non-custom games - const setNonCustomGameAssets = (game: LibraryGame) => { - setIconPath(extractLocalPath(game.customIconUrl)); - setLogoPath(extractLocalPath(game.customLogoImageUrl)); - setHeroPath(extractLocalPath(game.customHeroImageUrl)); + const setNonCustomGameAssets = useCallback( + (game: LibraryGame) => { + setIconPath(extractLocalPath(game.customIconUrl)); + setLogoPath(extractLocalPath(game.customLogoImageUrl)); + setHeroPath(extractLocalPath(game.customHeroImageUrl)); - // Store default URLs for restore functionality from shopDetails.assets - setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); - setDefaultLogoUrl( - shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null - ); - setDefaultHeroUrl( - shopDetails?.assets?.libraryHeroImageUrl || - game.libraryHeroImageUrl || - null - ); - }; + // Store default URLs for restore functionality from shopDetails.assets + setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); + setDefaultLogoUrl( + shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null + ); + setDefaultHeroUrl( + shopDetails?.assets?.libraryHeroImageUrl || + game.libraryHeroImageUrl || + null + ); + }, + [shopDetails] + ); useEffect(() => { if (game && visible) { @@ -82,7 +85,7 @@ export function EditGameModal({ setNonCustomGameAssets(game as LibraryGame); } } - }, [game, visible, shopDetails]); + }, [game, visible, shopDetails, setCustomGameAssets, setNonCustomGameAssets]); const handleGameNameChange = (event: React.ChangeEvent) => { setGameName(event.target.value); @@ -233,8 +236,13 @@ export function EditGameModal({ let filePath: string; // Try to get the path from the file object (Electron specific) - if ("path" in file && typeof (file as any).path === "string") { - filePath = (file as any).path; + // In Electron, File objects have a path property + interface ElectronFile extends File { + path?: string; + } + + if ("path" in file && typeof (file as ElectronFile).path === "string") { + filePath = (file as ElectronFile).path!; } else { // Fallback: create a temporary file from the file data const arrayBuffer = await file.arrayBuffer(); From 889f3bb77311d4d9907473d20870ca8f2b307b62 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 28 Sep 2025 17:40:18 +0300 Subject: [PATCH 19/43] conflicts fix --- .../game-details/game-details-content.tsx | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index eeeb67c7..46dfcd01 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,6 +1,4 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { average } from "color.js"; -import Color from "color"; import { PencilIcon } from "@primer/octicons-react"; import { HeroPanel } from "./hero"; @@ -23,15 +21,8 @@ export function GameDetailsContent() { const { t } = useTranslation("game_details"); - const { - objectId, - shopDetails, - game, - gameColor, - setGameColor, - hasNSFWContentBlocked, - updateGame, - } = useContext(gameDetailsContext); + const { objectId, shopDetails, game, hasNSFWContentBlocked, updateGame } = + useContext(gameDetailsContext); const { showHydraCloudModal } = useSubscription(); @@ -67,28 +58,6 @@ export function GameDetailsContent() { const [backdropOpacity, setBackdropOpacity] = useState(1); const [showEditGameModal, setShowEditGameModal] = useState(false); - const handleHeroLoad = async () => { - // Use the same logic as heroImage to get the correct URL for both custom and non-custom games - const isCustomGame = game?.shop === "custom"; - const heroImageUrl = isCustomGame - ? game?.libraryHeroImageUrl || game?.iconUrl || "" - : getImageWithCustomPriority( - game?.customHeroImageUrl, - shopDetails?.assets?.libraryHeroImageUrl - ); - - const output = await average(heroImageUrl, { - amount: 1, - format: "hex", - }); - - const backgroundColor = output - ? new Color(output).darken(0.7).toString() - : ""; - - setGameColor(backgroundColor); - }; - useEffect(() => { setBackdropOpacity(1); }, [objectId]); From d78631a7f409fc37f4efc7e5a8c4c172243b8f35 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 28 Sep 2025 20:08:03 +0300 Subject: [PATCH 20/43] Fix: hide pin button if game is custom --- src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 307de108..d8d98583 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -229,7 +229,7 @@ export function HeroPanelActions() { {game.favorite ? : } - {userDetails && ( + {userDetails && game.shop !== "custom" && ( - diff --git a/src/renderer/src/pages/game-details/description-header/description-header.scss b/src/renderer/src/pages/game-details/description-header/description-header.scss index a6faaee3..920e8068 100644 --- a/src/renderer/src/pages/game-details/description-header/description-header.scss +++ b/src/renderer/src/pages/game-details/description-header/description-header.scss @@ -1,13 +1,17 @@ @use "../../../scss/globals.scss"; .description-header { - width: 100%; - padding: calc(globals.$spacing-unit * 2); + width: calc(100% - calc(globals.$spacing-unit * 2)); + margin: calc(globals.$spacing-unit * 1) auto; + padding: calc(globals.$spacing-unit * 1.5); display: flex; justify-content: space-between; align-items: center; background-color: globals.$background-color; height: 72px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.03); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); &__info { display: flex; diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss index c7932a34..f66da32b 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss @@ -2,7 +2,7 @@ .gallery-slider { &__container { - padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 1); width: 100%; display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index a5566a96..786a8d30 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -43,12 +43,16 @@ $hero-height: 300px; } &__hero-content { - padding: calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 1.5); height: 100%; width: 100%; display: flex; justify-content: space-between; align-items: flex-end; + + @media (min-width: 768px) { + padding: calc(globals.$spacing-unit * 2); + } } &__hero-buttons { @@ -116,14 +120,22 @@ $hero-height: 300px; } &__game-logo { - width: 300px; + width: 200px; align-self: flex-end; + + @media (min-width: 768px) { + width: 250px; + } + + @media (min-width: 1024px) { + width: 300px; + } } &__game-logo-text { - width: 300px; + width: 200px; align-self: flex-end; - font-size: 2.5rem; + font-size: 1.8rem; font-weight: bold; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); @@ -132,6 +144,16 @@ $hero-height: 300px; word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; + + @media (min-width: 768px) { + width: 250px; + font-size: 2.2rem; + } + + @media (min-width: 1024px) { + width: 300px; + font-size: 2.5rem; + } } &__hero-image-skeleton { @@ -173,11 +195,19 @@ $hero-height: 300px; user-select: text; line-height: 22px; font-size: globals.$body-font-size; - padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5); width: 100%; margin-left: auto; margin-right: auto; + @media (min-width: 768px) { + padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); + } + + @media (min-width: 1024px) { + padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + } + @media (min-width: 1280px) { width: 60%; } @@ -206,11 +236,19 @@ $hero-height: 300px; display: flex; flex-direction: column; gap: globals.$spacing-unit; - padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5); width: 100%; margin-left: auto; margin-right: auto; + @media (min-width: 768px) { + padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); + } + + @media (min-width: 1024px) { + padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + } + @media (min-width: 1280px) { width: 60%; line-height: 22px; diff --git a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx index 7355461a..6d5ef135 100644 --- a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx @@ -149,17 +149,17 @@ export function ChangeGamePlaytimeModal({
    + + - -
    diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss index 3a5d8943..5400df07 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss @@ -12,6 +12,28 @@ gap: 16px; } +.edit-game-modal__asset-selector { + margin-bottom: 8px; +} + +.edit-game-modal__asset-tabs { + display: flex; + gap: 8px; + margin-bottom: 4px; + + button { + flex: 1; + min-width: 0; + } +} + +.edit-game-modal__asset-label { + font-size: 14px; + font-weight: 500; + color: var(--color-text); + margin-bottom: 4px; +} + .edit-game-modal__image-section { display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 614d458b..04a27779 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { ImageIcon, ReplyIcon } from "@primer/octicons-react"; +import { ImageIcon, XIcon } from "@primer/octicons-react"; import { Modal, TextField, Button } from "@renderer/components"; import { useToast } from "@renderer/hooks"; @@ -16,6 +16,8 @@ export interface EditGameModalProps { onGameUpdated: (updatedGame: LibraryGame | Game) => void; } +type AssetType = "icon" | "logo" | "hero"; + export function EditGameModal({ visible, onClose, @@ -31,37 +33,32 @@ export function EditGameModal({ const [logoPath, setLogoPath] = useState(""); const [heroPath, setHeroPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); + const [selectedAssetType, setSelectedAssetType] = useState("icon"); - // Store default image URLs for non-custom games const [defaultIconUrl, setDefaultIconUrl] = useState(null); const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); const [defaultHeroUrl, setDefaultHeroUrl] = useState(null); - // Helper function to check if game is a custom game const isCustomGame = (game: LibraryGame | Game): boolean => { return game.shop === "custom"; }; - // Helper function to extract local path from URL const extractLocalPath = (url: string | null | undefined): string => { return url?.startsWith("local:") ? url.replace("local:", "") : ""; }; - // Helper function to set asset paths for custom games const setCustomGameAssets = useCallback((game: LibraryGame | Game) => { setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); }, []); - // Helper function to set asset paths for non-custom games const setNonCustomGameAssets = useCallback( (game: LibraryGame) => { setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); - // Store default URLs for restore functionality from shopDetails.assets setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); setDefaultLogoUrl( shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null @@ -91,7 +88,47 @@ export function EditGameModal({ setGameName(event.target.value); }; - const handleSelectIcon = async () => { + const handleAssetTypeChange = (assetType: AssetType) => { + setSelectedAssetType(assetType); + }; + + const getAssetPath = (assetType: AssetType): string => { + switch (assetType) { + case "icon": + return iconPath; + case "logo": + return logoPath; + case "hero": + return heroPath; + } + }; + + const setAssetPath = (assetType: AssetType, path: string): void => { + switch (assetType) { + case "icon": + setIconPath(path); + break; + case "logo": + setLogoPath(path); + break; + case "hero": + setHeroPath(path); + break; + } + }; + + const getDefaultUrl = (assetType: AssetType): string | null => { + switch (assetType) { + case "icon": + return defaultIconUrl; + case "logo": + return defaultLogoUrl; + case "hero": + return defaultHeroUrl; + } + }; + + const handleSelectAsset = async (assetType: AssetType) => { const { filePaths } = await window.electron.showOpenDialog({ properties: ["openFile"], filters: [ @@ -104,91 +141,24 @@ export function EditGameModal({ if (filePaths && filePaths.length > 0) { try { - // Copy the asset to the app's assets folder const copiedAssetUrl = await window.electron.copyCustomGameAsset( filePaths[0], - "icon" + assetType ); - setIconPath(copiedAssetUrl.replace("local:", "")); + setAssetPath(assetType, copiedAssetUrl.replace("local:", "")); } catch (error) { - console.error("Failed to copy icon asset:", error); - // Fallback to original behavior - setIconPath(filePaths[0]); + console.error(`Failed to copy ${assetType} asset:`, error); + setAssetPath(assetType, filePaths[0]); } } }; - const handleSelectLogo = async () => { - const { filePaths } = await window.electron.showOpenDialog({ - properties: ["openFile"], - filters: [ - { - name: t("edit_game_modal_image_filter"), - extensions: ["jpg", "jpeg", "png", "gif", "webp"], - }, - ], - }); - - if (filePaths && filePaths.length > 0) { - try { - // Copy the asset to the app's assets folder - const copiedAssetUrl = await window.electron.copyCustomGameAsset( - filePaths[0], - "logo" - ); - setLogoPath(copiedAssetUrl.replace("local:", "")); - } catch (error) { - console.error("Failed to copy logo asset:", error); - // Fallback to original behavior - setLogoPath(filePaths[0]); - } - } + const handleRestoreDefault = (assetType: AssetType) => { + setAssetPath(assetType, ""); }; - const handleSelectHero = async () => { - const { filePaths } = await window.electron.showOpenDialog({ - properties: ["openFile"], - filters: [ - { - name: t("edit_game_modal_image_filter"), - extensions: ["jpg", "jpeg", "png", "gif", "webp"], - }, - ], - }); - - if (filePaths && filePaths.length > 0) { - try { - // Copy the asset to the app's assets folder - const copiedAssetUrl = await window.electron.copyCustomGameAsset( - filePaths[0], - "hero" - ); - setHeroPath(copiedAssetUrl.replace("local:", "")); - } catch (error) { - console.error("Failed to copy hero asset:", error); - // Fallback to original behavior - setHeroPath(filePaths[0]); - } - } - }; - - // Helper functions to restore default images for non-custom games - const handleRestoreDefaultIcon = () => { - setIconPath(""); - }; - - const handleRestoreDefaultLogo = () => { - setLogoPath(""); - }; - - const handleRestoreDefaultHero = () => { - setHeroPath(""); - }; - - // Drag and drop state const [dragOverTarget, setDragOverTarget] = useState(null); - // Drag and drop handlers const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -203,7 +173,6 @@ export function EditGameModal({ const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - // Only clear drag state if we're leaving the drop zone entirely if (!e.currentTarget.contains(e.relatedTarget as Node)) { setDragOverTarget(null); } @@ -220,10 +189,7 @@ export function EditGameModal({ return validTypes.includes(file.type); }; - const processDroppedFile = async ( - file: File, - assetType: "icon" | "logo" | "hero" - ) => { + const processDroppedFile = async (file: File, assetType: AssetType) => { setDragOverTarget(null); if (!validateImageFile(file)) { @@ -232,11 +198,8 @@ export function EditGameModal({ } try { - // In Electron, we need to get the file path differently let filePath: string; - // Try to get the path from the file object (Electron specific) - // In Electron, File objects have a path property interface ElectronFile extends File { path?: string; } @@ -244,11 +207,9 @@ export function EditGameModal({ if ("path" in file && typeof (file as ElectronFile).path === "string") { filePath = (file as ElectronFile).path!; } else { - // Fallback: create a temporary file from the file data const arrayBuffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); - // Use a temporary file approach const tempFileName = `temp_${Date.now()}_${file.name}`; const tempPath = await window.electron.saveTempFile?.( tempFileName, @@ -264,31 +225,18 @@ export function EditGameModal({ filePath = tempPath; } - // Copy the asset to the app's assets folder using the file path const copiedAssetUrl = await window.electron.copyCustomGameAsset( filePath, assetType ); const assetPath = copiedAssetUrl.replace("local:", ""); - - switch (assetType) { - case "icon": - setIconPath(assetPath); - break; - case "logo": - setLogoPath(assetPath); - break; - case "hero": - setHeroPath(assetPath); - break; - } + setAssetPath(assetType, assetPath); showSuccessToast( `${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!` ); - // Clean up temporary file if we created one if (!("path" in file) && filePath) { try { await window.electron.deleteTempFile?.(filePath); @@ -304,7 +252,7 @@ export function EditGameModal({ } }; - const handleIconDrop = async (e: React.DragEvent) => { + const handleAssetDrop = async (e: React.DragEvent, assetType: AssetType) => { e.preventDefault(); e.stopPropagation(); setDragOverTarget(null); @@ -313,33 +261,7 @@ export function EditGameModal({ const files = Array.from(e.dataTransfer.files); if (files.length > 0) { - await processDroppedFile(files[0], "icon"); - } - }; - - const handleLogoDrop = async (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setDragOverTarget(null); - - if (isUpdating) return; - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - await processDroppedFile(files[0], "logo"); - } - }; - - const handleHeroDrop = async (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setDragOverTarget(null); - - if (isUpdating) return; - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - await processDroppedFile(files[0], "hero"); + await processDroppedFile(files[0], assetType); } }; @@ -444,28 +366,111 @@ export function EditGameModal({ const isFormValid = gameName.trim(); - const getIconPreviewUrl = (): string | undefined => { + const getPreviewUrl = (assetType: AssetType): string | undefined => { + const assetPath = getAssetPath(assetType); + const defaultUrl = getDefaultUrl(assetType); + if (game && !isCustomGame(game)) { - // For non-custom games, show custom image if set, otherwise show default - return iconPath ? `local:${iconPath}` : defaultIconUrl || undefined; + return assetPath ? `local:${assetPath}` : defaultUrl || undefined; } - return iconPath ? `local:${iconPath}` : undefined; + return assetPath ? `local:${assetPath}` : undefined; }; - const getLogoPreviewUrl = (): string | undefined => { - if (game && !isCustomGame(game)) { - // For non-custom games, show custom image if set, otherwise show default - return logoPath ? `local:${logoPath}` : defaultLogoUrl || undefined; - } - return logoPath ? `local:${logoPath}` : undefined; - }; + const renderImageSection = (assetType: AssetType) => { + const assetPath = getAssetPath(assetType); + const defaultUrl = getDefaultUrl(assetType); + const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl); + const isDragOver = dragOverTarget === assetType; - const getHeroPreviewUrl = (): string | undefined => { - if (game && !isCustomGame(game)) { - // For non-custom games, show custom image if set, otherwise show default - return heroPath ? `local:${heroPath}` : defaultHeroUrl || undefined; - } - return heroPath ? `local:${heroPath}` : undefined; + const getTranslationKey = (suffix: string) => + `edit_game_modal_${assetType}${suffix}`; + const getResolutionKey = () => `edit_game_modal_${assetType}_resolution`; + + return ( +
    + + + {game && !isCustomGame(game) && assetPath && ( + + )} +
    + } + /> +
    + {t(getResolutionKey())} +
    + + {hasImage && ( + + )} + + {!hasImage && ( + + )} + + ); }; return ( @@ -486,266 +491,39 @@ export function EditGameModal({ disabled={isUpdating} /> -
    - - - {game && !isCustomGame(game) && iconPath && ( - - )} -
    - } - /> -
    - {t("edit_game_modal_icon_resolution")} +
    +
    + {t("edit_game_modal_assets")}
    - - {(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && ( - - )} - - {!iconPath && !(game && !isCustomGame(game) && defaultIconUrl) && ( - - )} + {t("edit_game_modal_logo")} + + +
    -
    - - - {game && !isCustomGame(game) && logoPath && ( - - )} -
    - } - /> -
    - {t("edit_game_modal_logo_resolution")} -
    - - {(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && ( - - )} - - {!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl) && ( - - )} - - -
    - - - {game && !isCustomGame(game) && heroPath && ( - - )} -
    - } - /> -
    - {t("edit_game_modal_hero_resolution")} -
    - - {(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && ( - - )} - - {!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl) && ( - - )} - + {renderImageSection(selectedAssetType)}
    diff --git a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx index 85cea8cd..eb421ec7 100644 --- a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx @@ -31,12 +31,12 @@ export function RemoveGameFromLibraryModal({ onClose={onClose} >
    - -
    diff --git a/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx b/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx index fc71e2d0..b6eb38a2 100644 --- a/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx @@ -36,12 +36,12 @@ export function ResetAchievementsModal({ })} >
    - -
    diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx index be46e56a..9439d273 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx @@ -38,12 +38,12 @@ export const DeleteAllThemesModal = ({ onClose={onClose} >
    - -
    diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx index 5783ce01..c1a5a1e0 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx @@ -41,12 +41,12 @@ export const DeleteThemeModal = ({ onClose={onClose} >
    - -
    diff --git a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx index db4abe8c..601e9568 100644 --- a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx @@ -77,12 +77,12 @@ export const ImportThemeModal = ({ onClose={onClose} >
    - -
    From 8f30f8a4ad09f20d761e4de9a93a7520dc3e128f Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 28 Sep 2025 20:17:02 +0300 Subject: [PATCH 22/43] fix: fixed edit game assets button not using translation --- src/locales/en/translation.json | 1 + src/renderer/src/pages/game-details/game-details-content.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b1a9c61b..fbb7d479 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -272,6 +272,7 @@ "backup_unfrozen": "Backup unpinned", "backup_freeze_failed": "Failed to freeze backup", "backup_freeze_failed_description": "You must leave at least one free slot for automatic backups", + "edit_game_modal_button": "Customize game assets", "game_details": "Game Details", "currency_symbol": "$", "currency_country": "us", diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 46dfcd01..347e5a1c 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -172,7 +172,7 @@ export function GameDetailsContent() { type="button" className="game-details__edit-custom-game-button" onClick={handleEditGameClick} - title={t("edit_custom_game")} + title={t("edit_game_modal_button")} > From dfd640ebdac8d2c203781f6b070c7fe857eabfee Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 00:14:47 +0300 Subject: [PATCH 23/43] feat: add revert title for non-custom games --- .../game-details/modals/edit-game-modal.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 04a27779..7bfc48fa 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -157,6 +157,24 @@ export function EditGameModal({ setAssetPath(assetType, ""); }; + const getOriginalTitle = (): string => { + if (!game) return ""; + + // For non-custom games, the original title is from shopDetails assets + return shopDetails?.assets?.title || game.title || ""; + }; + + const handleRestoreDefaultTitle = () => { + const originalTitle = getOriginalTitle(); + setGameName(originalTitle); + }; + + const isTitleChanged = (): boolean => { + if (!game || isCustomGame(game)) return false; + const originalTitle = getOriginalTitle(); + return gameName.trim() !== originalTitle.trim(); + }; + const [dragOverTarget, setDragOverTarget] = useState(null); const handleDragOver = (e: React.DragEvent) => { @@ -489,6 +507,19 @@ export function EditGameModal({ onChange={handleGameNameChange} theme="dark" disabled={isUpdating} + rightContent={ + isTitleChanged() && ( + + ) + } />
    From 046f6d388a577878d299a093dd26a5fcdb190867 Mon Sep 17 00:00:00 2001 From: Lianela | Kyatto <140931995+Lianela@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:33:33 -0700 Subject: [PATCH 24/43] updated spanish translation i guess being offline for a while was kinda refreshing lol --- src/locales/es/translation.json | 680 +++++++++++++++++++------------- 1 file changed, 396 insertions(+), 284 deletions(-) diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 7f54925a..6f0fc9f1 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -1,34 +1,76 @@ { "language_name": "Español", "app": { - "successfully_signed_in": "Sesión iniciada exitosamente" + "successfully_signed_in": "Iniciaste sesión exitosamente" }, "home": { - "featured": "Destacado", "surprise_me": "¡Sorpréndeme!", - "no_results": "Sin resultados encontrados", - "start_typing": "Empieza a escribir para buscar...", - "hot": "Popular Ahora", + "no_results": "No se encontraron resultados", + "start_typing": "Empezá a escribir para buscar...", + "hot": "Tendencias", "weekly": "📅 Mejores juegos de la semana", - "achievements": "🏆 Juegos para completar" + "achievements": "🏆 Juegos para platinar" }, "sidebar": { "catalogue": "Catálogo", "downloads": "Descargas", "settings": "Ajustes", - "my_library": "Mi biblioteca", + "my_library": "Mi Librería", "downloading_metadata": "{{title}} (Descargando metadatos…)", "paused": "{{title}} (Pausado)", "downloading": "{{title}} ({{percentage}} - Descargando…)", - "filter": "Buscar en la biblioteca", + "filter": "Filtrar Librería", "home": "Inicio", "queued": "{{title}} (En cola)", "game_has_no_executable": "El juego no tiene un ejecutable seleccionado", - "sign_in": "Iniciar sesión", + "sign_in": "Iniciar Sesión", "friends": "Amigos", - "need_help": "¿Necesitas ayuda?", + "need_help": "¿Necesitás ayuda?", "favorites": "Favoritos", - "playable_button_title": "Mostrar solo juegos que puedes jugar ahora" + "playable_button_title": "Solo mostrar juegos que podés jugar en este momento", + "add_custom_game_tooltip": "Añadir juego personalizado", + "show_playable_only_tooltip": "Mostrar Solo Jugable", + "custom_game_modal": "Añadir juego personalizado", + "custom_game_modal_description": "Añadí un juego personalizado a tu librería seleccionando el ejecutable", + "custom_game_modal_executable_path": "Ruta del Ejecutable", + "custom_game_modal_select_executable": "Seleccionar archivo ejecutable", + "custom_game_modal_title": "Título", + "custom_game_modal_enter_title": "Ingresá el título", + "custom_game_modal_browse": "Buscar", + "custom_game_modal_cancel": "Cancelar", + "custom_game_modal_add": "Añadir juego", + "custom_game_modal_adding": "Añadiendo juego...", + "custom_game_modal_success": "Juego personalizado añadido exitosamente", + "custom_game_modal_failed": "Error al añadir juego personalizado", + "custom_game_modal_executable": "Ejecutable", + "edit_game_modal": "Personalizar recursos", + "edit_game_modal_description": "Personaliza los recursos y detalles del juego", + "edit_game_modal_title": "Título", + "edit_game_modal_enter_title": "Ingresá el título", + "edit_game_modal_image": "Imagen", + "edit_game_modal_select_image": "Seleccionar imagen", + "edit_game_modal_browse": "Navegar", + "edit_game_modal_image_preview": "Vista previa de imagen", + "edit_game_modal_icon": "Ícono", + "edit_game_modal_select_icon": "Seleccionar ícono", + "edit_game_modal_icon_preview": "Vista previa de ícono", + "edit_game_modal_logo": "Logo", + "edit_game_modal_select_logo": "Seleccionar logo", + "edit_game_modal_logo_preview": "Vista previa del logo", + "edit_game_modal_hero": "Library Hero", + "edit_game_modal_select_hero": "Seleccionar una imagen de Library Hero", + "edit_game_modal_hero_preview": "Vista previa de library hero", + "edit_game_modal_cancel": "Cancelar", + "edit_game_modal_update": "Actualizar", + "edit_game_modal_updating": "Actualizando...", + "edit_game_modal_fill_required": "Por favor rellená todos los espacios requeridos", + "edit_game_modal_success": "Recursos actualizados exitosamente", + "edit_game_modal_failed": "Error al actualizar los recursos", + "edit_game_modal_image_filter": "Imagen", + "edit_game_modal_icon_resolution": "Resolución recomendada: 256x256px", + "edit_game_modal_logo_resolution": "Resolución recomendada: 640x360px", + "edit_game_modal_hero_resolution": "Resolución recomendada: 1920x620px", + "edit_game_modal_assets": "Recursos" }, "header": { "search": "Buscar juegos", @@ -37,348 +79,409 @@ "downloads": "Descargas", "search_results": "Resultados de búsqueda", "settings": "Ajustes", - "version_available_install": "Versión {{version}} disponible. Presiona acá para descargar y reinstalar.", - "version_available_download": "Versión {{version}} disponible. Presiona aquí para descargar." + "version_available_install": "Versión {{version}} disponible. Presiona acá para reiniciar e instalar.", + "version_available_download": "Versión {{version}} disponible. Presiona acá para descargar." }, "bottom_panel": { "no_downloads_in_progress": "Sin descargas en progreso", "downloading_metadata": "Descargando metadatos de {{title}}…", - "downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}", - "calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…", - "installation_complete": "Instalación completada", - "installation_complete_message": "Common redistributables instalados exitosamente", + "downloading": "Descargando {{title}}… ({{percentage}} completado) - Restante {{eta}} - {{speed}}", + "calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Comprobando tiempo restante…", + "checking_files": "Revisando archivos de {{title}}… ({{percentage}} completado)", "installing_common_redist": "{{log}}…", - "checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)" + "installation_complete": "Instalación completada", + "installation_complete_message": "Common redistributables instalados correctamente" }, "catalogue": { "search": "Filtrar…", "developers": "Desarrolladores", "genres": "Géneros", - "tags": "Marcadores", + "tags": "Etiquetas", "publishers": "Editores", - "download_sources": "Fuentes de descarga", + "download_sources": "Descargando fuentes", "result_count": "{{resultCount}} resultados", - "filter_count": "{{filterCount}} disponibles", + "filter_count": "{{filterCount}} disponible", "clear_filters": "Limpiar {{filterCount}} seleccionados" }, "game_details": { - "open_download_options": "Ver opciones de descargas", - "automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados", - "download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.", - "download_options_zero": "No hay opciones de descargas disponibles", + "open_download_options": "Abrir opciones de descargas", + "download_options_zero": "Sin opciones de descargas", "download_options_one": "{{count}} opción de descarga", "download_options_other": "{{count}} opciones de descargas", - "updated_at": "Actualizado el: {{updated_at}}", + "updated_at": "Actualizado el {{updated_at}}", "install": "Instalar", - "resume": "Continuar", - "pause": "Pausa", + "resume": "Resumir", + "pause": "Pausar", "cancel": "Cancelar", - "remove": "Eliminar", - "space_left_on_disk": "{{space}} restantes en el disco", - "eta": "Tiempo restante: {{eta}}", + "remove": "Remover", + "space_left_on_disk": "{{space}} restante en el disco", + "eta": "Conclusión {{eta}}", "calculating_eta": "Calculando tiempo restante…", "downloading_metadata": "Descargando metadatos…", - "filter": "Buscar repacks", + "filter": "Filtrar repacks", "requirements": "Requisitos del Sistema", "minimum": "Mínimos", "recommended": "Recomendados", - "paused": "Pausado", - "release_date": "Fecha de lanzamiento: {{date}}", - "publisher": "Publicado por: {{publisher}}", + "paused": "En Pausa", + "release_date": "Lanzado el {{date}}", + "publisher": "Públicado por {{publisher}}", "hours": "horas", "minutes": "minutos", "amount_hours": "{{amount}} horas", "amount_minutes": "{{amount}} minutos", - "accuracy": "{{accuracy}}% precisión", - "add_to_library": "Agregar a la biblioteca", - "remove_from_library": "Eliminar de la biblioteca", - "no_downloads": "No hay descargas disponibles", - "play_time": "Has jugado {{amount}}", - "last_time_played": "Jugado por última vez: {{period}}", - "not_played_yet": "Aún no has jugado a {{title}}", + "accuracy": "{{accuracy}}% completista", + "add_to_library": "Añadir a la librería", + "already_in_library": "Ya está en la librería", + "remove_from_library": "Eliminar de la librería", + "no_downloads": "Sin descargas disponibles", + "play_time": "Jugado por {{amount}}", + "last_time_played": "Última vez jugado {{period}}", + "not_played_yet": "No has jugado a {{title}} todavía", "next_suggestion": "Siguiente sugerencia", "play": "Jugar", "deleting": "Eliminando instalador…", "close": "Cerrar", "playing_now": "Jugando ahora", "change": "Cambiar", - "repacks_modal_description": "Selecciona el repack que quieres descargar", - "select_folder_hint": "Para cambiar la carpeta predeterminada, ve a <0>Ajustes", + "repacks_modal_description": "Elegí el repack que querés descargar", + "select_folder_hint": "Si querés cambiar la carpeta por defecto, andá a <0>Ajustes", "download_now": "Descargar ahora", "no_shop_details": "No se pudieron obtener detalles de la tienda.", "download_options": "Opciones de descarga", "download_path": "Ruta de descarga", "previous_screenshot": "Anterior captura", "next_screenshot": "Siguiente captura", - "screenshot": "Captura {{number}}", - "open_screenshot": "Abrir captura {{number}}", - "download_settings": "Ajustes de descarga", - "downloader": "Método de descarga", + "screenshot": "Captura número {{number}}", + "open_screenshot": "Abrir captura número {{number}}", + "download_settings": "Descargar ajustes", + "downloader": "Descargador", "select_executable": "Seleccionar", - "no_executable_selected": "No se seleccionó un ejecutable", + "no_executable_selected": "Sin ejecutable seleccionado", "open_folder": "Abrir carpeta", "open_download_location": "Ver archivos descargados", - "create_shortcut": "Crear acceso directo en el escritorio", - "remove_files": "Eliminar archivos", + "create_shortcut": "Crear atajo en el escritorio", + "clear": "Limpiar", + "remove_files": "Remover archivos", "remove_from_library_title": "¿Estás seguro?", - "remove_from_library_description": "Esto eliminará {{game}} de tu biblioteca", + "remove_from_library_description": "Esto va eliminará {{game}} de tu librería", "options": "Opciones", "executable_section_title": "Ejecutable", - "executable_section_description": "Ruta del archivo que se ejecutará cuando se presione \"Jugar\"", + "executable_section_description": "Ruta del archivo que se ejecutará cuando presiones \"Jugar\"", "downloads_section_title": "Descargas", - "downloads_section_description": "Buscar actualizaciones u otras versiones de este juego", - "danger_zone_section_title": "Opciones Avanzadas", - "danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)", + "downloads_section_description": "Revisar actualizaciones u otras versiones del juego", + "danger_zone_section_title": "Zona de Peligro", + "danger_zone_section_description": "Remover este juego de tu librería o los archivos descargados por Hydra", "download_in_progress": "Descarga en progreso", "download_paused": "Descarga pausada", + "last_downloaded_option": "Última opción de descarga", "create_steam_shortcut": "Crear atajo de Steam", - "last_downloaded_option": "Última opción descargada", "create_shortcut_success": "Atajo creado con éxito", - "you_might_need_to_restart_steam": "Es posible que necesites reiniciar Steam para ver los cambios", - "create_shortcut_error": "Error al crear un atajo", - "nsfw_content_title": "Este juego contiene contenido inapropiado.", - "nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?", + "you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios", + "create_shortcut_error": "Error al crear atajo", + "nsfw_content_title": "Este juego tiene contenido inapropiado", + "nsfw_content_description": "{{title}} tiene contenido no apto para todas las edades. ¿Querés continuar igualmente?", "allow_nsfw_content": "Continuar", - "refuse_nsfw_content": "No, gracias", + "refuse_nsfw_content": "Regresar", "stats": "Estadísticas", "download_count": "Descargas", - "player_count": "Jugadores activos", - "download_error": "Esta opción de descarga no está disponible.", + "player_count": "Jugadores activos", + "download_error": "Esta opción de descarga no está disponible", "download": "Descargar", - "executable_path_in_use": "El ejecutable se encuentra en uso por \"{{game}}\"", + "executable_path_in_use": "El ejecutable ya se está usando por \"{{game}}\"", "warning": "Advertencia:", - "hydra_needs_to_remain_open": "Para esta descarga, Hydra necesita mantenerse abierta hasta que concluya. En caso de que Hydra se cierre antes de que concluya, podrías perder todo el progreso.", + "hydra_needs_to_remain_open": "para esta descarga, Hydra necesita estar abierta hasta que termine. Si se cierra antes de completar, perderás todo el progreso.", "achievements": "Logros", "achievements_count": "Logros {{unlockedCount}}/{{achievementsCount}}", "cloud_save": "Guardado en la nube", - "cloud_save_description": "Guarda tu progreso en la nube y continúa jugando en cualquier dispositivo", - "backups": "Copias de Seguridad", + "cloud_save_description": "Guardá tu progreso en la nube y jugá en cualquier dispositivo", + "backups": "Copia de seguridad", "install_backup": "Instalar", "delete_backup": "Eliminar", - "create_backup": "Nueva Copia de Seguridad", - "last_backup_date": "Última copia de seguridad el {{date}}", - "no_backup_preview": "No se encontraron datos de guardados para este juego", + "create_backup": "Nueva copia de seguridad", + "last_backup_date": "Última copia de seguridad {{date}}", + "no_backup_preview": "No se han encotrado puntos de guardado para este juego", "restoring_backup": "Restaurando copia de seguridad ({{progress}} completado)…", "uploading_backup": "Subiendo copia de seguridad…", - "no_backups": "No has creado ninguna copia de seguridad para este juego aún", + "no_backups": "No has creado ninguna copia de seguridad para este juego todavía", "backup_uploaded": "Copia de seguridad subida", "backup_deleted": "Copia de seguridad eliminada", "backup_restored": "Copia de seguridad restaurada", "see_all_achievements": "Ver todos los logros", - "sign_in_to_see_achievements": "Inicia sesión para ver los logros", + "sign_in_to_see_achievements": "Iniciá sesión para ver los logros", "mapping_method_automatic": "Automático", "mapping_method_manual": "Manual", - "mapping_method_label": "Método de mapeo", - "files_automatically_mapped": "Archivos mapeados automáticamente", + "mapping_method_label": "Método de mapeado", + "files_automatically_mapped": "Archivos automáticamente mapeados", "no_backups_created": "Sin copias de seguridad creadas para este juego", - "manage_files": "Gestionar archivos", - "loading_save_preview": "Buscando datos de guardados de juegos…", + "manage_files": "Administrar archivos", + "loading_save_preview": "Buscando por guardado de juegos…", "wine_prefix": "Prefijo de Wine", - "wine_prefix_description": "El prefijo de Wine usado para ejecutar este juego", + "wine_prefix_description": "El prefijo de Wine usado para este juego", + "launch_options": "Opciones para iniciar", + "launch_options_description": "Los usuarios avanzados pueden ingresar sus modificaciones para el inicio de sus juegos (característica experimental)", + "launch_options_placeholder": "Sin parámetro específicado", "no_download_option_info": "Sin información disponible", - "backup_deletion_failed": "La eliminación de la copia de seguridad falló", - "max_number_of_artifacts_reached": "Número máximo de copias de seguridad de este juego alcanzadas", - "achievements_not_sync": "Tus logros no están sincronizados", - "manage_files_description": "Gestiona los archivos que serán respaldados y restaurados", + "backup_deletion_failed": "Error al eliminar copia de seguridad", + "max_number_of_artifacts_reached": "Máximo de copias de seguridad alcanzadas para este juego", + "achievements_not_sync": "Revisá como sincronizar tus logros'", + "manage_files_description": "Elegí que archivos se guardarán y restaurarán de la copia de seguridad", "select_folder": "Seleccionar carpeta", "backup_from": "Copia de seguridad de {{date}}", "automatic_backup_from": "Copia de seguridad automática de {{date}}", - "enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube", - "custom_backup_location_set": "Se configuró la carpeta de copia de seguridad", - "clear": "Limpiar", - "no_directory_selected": "No se seleccionó un directorio", - "launch_options": "Opciones de Inicio", - "launch_options_description": "Los usuarios avanzados pueden introducir sus propias modificaciones de opciones de inicio (característica experimental)", - "launch_options_placeholder": "Sin parámetro específicado", - "no_write_permission": "No se puede descargar en este directorio. Presiona aquí para aprender más.", + "enable_automatic_cloud_sync": "Habilitar sincronización con la nube", + "custom_backup_location_set": "Ubicación de copia de seguridad personalizada", + "no_directory_selected": "Sin directorio seleccionado", + "no_write_permission": "No se puede descargar en este directorio. Presioná acá para más información.", "reset_achievements": "Reiniciar logros", - "reset_achievements_description": "Esto reiniciará todos los logros de {{game}}", - "reset_achievements_title": "¿Estás seguro?", - "reset_achievements_success": "Logros reiniciados exitosamente", - "reset_achievements_error": "Se produjo un error al reiniciar los logros", - "download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.", - "download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.", - "download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.", - "download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y aún no se puede verificar el estado de la descarga.", + "reset_achievements_description": "Esto va a reiniciar todos los logros para {{game}}", + "reset_achievements_title": "¿Querés continuar?", + "reset_achievements_success": "Logros reiniciados éxitosamente", + "reset_achievements_error": "Error al reiniciar logros", + "download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de GoFile. Esperá a que se reinice.", + "download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nuevas descargas. Revisá los ajustes de tu cuenta y probá de nuevo.", + "download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y no está disponible el estado de descarga de sondeo todavía.", + "update_playtime_title": "Actualizar tiempo de juego", + "update_playtime_description": "Actualizar manualmente el tiempo de juego para {{game}}", + "update_playtime": "Actualizar tiempo de juego", + "update_playtime_success": "Tiempo de juego actualizado éxitosamente", + "update_playtime_error": "Error al actualizar el tiempo de juego", + "update_game_playtime": "Actualizar tu tiempo de juego", + "manual_playtime_warning": "Tus horas de juego se marcarán como actualizadas manualmente, y esto no se puede deshacer.", + "manual_playtime_tooltip": "Este tiempo de juego se ha actualizad manualmente", + "download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y no está disponible el estado de descarga de sondeo todavía.", + "download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.", + "game_removed_from_favorites": "Juego eliminado de favoritos", "game_added_to_favorites": "Juego añadido a favoritos", - "game_removed_from_favorites": "Juego removido de favoritos", - "invalid_wine_prefix_path": "Ruta de prefijo de Wine inválida", - "invalid_wine_prefix_path_description": "La ruta del prefijo Wine es inválida. Por favor, checa la ruta y vuelve a intentarlo.", - "missing_wine_prefix": "Se requiere el prefijo Wine para crear una copia de seguridad en Linux" + "game_removed_from_pinned": "Juego removido de fijados", + "game_added_to_pinned": "Juego añadido a fijados", + "automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados", + "create_start_menu_shortcut": "Crear un atajo en el Menú de Inicio", + "invalid_wine_prefix_path": "Ruta inválida del prefijo de Wine", + "invalid_wine_prefix_path_description": "La ruta al prefijo de Wine es inválida. Por favor revisá la ruta y probá de nuevo.", + "missing_wine_prefix": "EL prefijo de Wine es requerido para hacer una copia en Linux", + "artifact_renamed": "Copia de seguridad renombrada éxitosamente", + "rename_artifact": "Renombrar copia de seguridad", + "rename_artifact_description": "Renombrar copia de seguridad con un nombre más descriptivo", + "artifact_name_label": "Nombre de la copia de seguridad", + "artifact_name_placeholder": "Introducí un nombre para la copia de seguridad", + "save_changes": "Guardar cambios", + "required_field": "Este campo es requerido", + "max_length_field": "Este campo debe tener menos de {{length}} carácteres", + "freeze_backup": "Fíjalo así no se re-escríbira por copias de seguridad automáticas", + "unfreeze_backup": "Dejar de fijar", + "backup_frozen": "Copia de seguridad fijada", + "backup_unfrozen": "Copia de seguridad desfijada", + "backup_freeze_failed": "Error al congelar tu copia de seguridad", + "backup_freeze_failed_description": "Tenés que tener mínimo un espacio para copias de seguridad automáticas", + "edit_game_modal_button": "Personalizar recursos de juego", + "game_details": "Detalles del juego", + "currency_symbol": "$", + "currency_country": "us", + "prices": "Precios", + "no_prices_found": "No se encontraron precios", + "view_all_prices": "Presioná acá para ver todos los precios", + "retail_price": "Precio recomendado", + "keyshop_price": "Precio de tiendas de terceros", + "historical_retail": "Precio de tiendas", + "historical_keyshop": "Precio de tiendas de terceros", + "language": "Idioma", + "caption": "Subtítulo", + "audio": "Audio" }, "activation": { "title": "Activar Hydra", - "installation_id": "ID de la Instalación:", - "enter_activation_code": "Introduce tu código de activación", - "message": "Si no sabes donde obtener el código, no deberías de tener esto.", + "installation_id": "ID de Instalación:", + "enter_activation_code": "Introducí tu código de activación", + "message": "Si no sabes donde preguntar por esto, entonces no tenés que tener esto.", "activate": "Activar", "loading": "Cargando…" }, "downloads": { "resume": "Resumir", - "pause": "Pausa", - "eta": "Finalizando en {{eta}}", - "paused": "En Pausa", + "pause": "Pausar", + "eta": "Tiempo de finalizción {{eta}}", + "paused": "Pausado", "verifying": "Verificando…", "completed": "Completado", "removed": "No descargado", "cancel": "Cancelar", - "filter": "Buscar juegos descargados", - "remove": "Eliminar", + "filter": "Filtrar juegos descargados", + "remove": "Remover", "downloading_metadata": "Descargando metadatos…", - "deleting": "Eliminando instalador…", - "delete": "Eliminar instalador", - "delete_modal_title": "¿Estás seguro?", - "delete_modal_description": "Esto eliminará todos los archivos de la instalación del repack del juego de tu computadora. (Si ya instalaste el juego, puedes eliminar esto, no afectará al juego)", + "deleting": "Eliminado instalador…", + "delete": "Remover instalador", + "delete_modal_title": "¿Querés continuar?", + "delete_modal_description": "Esto eliminará todos los archivos del instalador de tu computadora", "install": "Instalar", "download_in_progress": "En progreso", "queued_downloads": "Descargas en cola", "downloads_completed": "Completado", "queued": "En cola", - "no_downloads_title": "Esto está tan... vacío", - "no_downloads_description": "No has descargado nada con Hydra... aún, ¡pero nunca es tarde para comenzar!.", - "checking_files": "Verificando archivos…", - "seeding": "Seeding", - "stop_seeding": "Detener seeding", - "resume_seeding": "Continuar seeding", + "no_downloads_title": "Esto está... tan, ¿vacío?", + "no_downloads_description": "No has descargado nada con Hydra, pero nunca es tarde para comenzar.", + "checking_files": "Revisando archivos…", + "seeding": "Sembrando", + "stop_seeding": "Dejar de sembrar", + "resume_seeding": "Continuar sembrando", + "options": "Administrar", "extract": "Extraer archivos", - "extracting": "Extrayendo archivos…", - "options": "Gestionar" + "extracting": "Extrayendo archivos…" }, "settings": { "downloads_path": "Ruta de descarga", - "common_redist": "Common redistributables", - "common_redist_description": "Las Common redistributables son requeridos para ejecutar algunos juegos. Es recomendado instalarlos para evitar problemas.", - "create_real_debrid_account": "Presiona acá si no tienes una cuenta de Real-Debrid aún", - "create_torbox_account": "Presiona acá si no tienes una cuenta de TorBox aún", - "install_common_redist": "Instalar", - "installing_common_redist": "Instalando…", - "show_download_speed_in_megabytes": "Mostrar velocidad de descargar en megabytes por segundo", - "change": "Cambiar", + "change": "Actualizar", "notifications": "Notificaciones", - "enable_download_notifications": "Cuando se completa una descarga", - "enable_repack_list_notifications": "Cuando se añade un repack nuevo", - "real_debrid_api_token_label": "Token API de Real-Debrid", - "quit_app_instead_hiding": "Salir de Hydra en vez de minimizar en la bandeja del sistema", - "launch_with_system": "Iniciar Hydra al inicio del sistema", + "enable_download_notifications": "Cuando una descarga se completa", + "enable_repack_list_notifications": "Cuando un nuevo repack se añade", + "real_debrid_api_token_label": "Real-Debrid API token", + "quit_app_instead_hiding": "No ocultar Hydra cuando se cierra", + "launch_with_system": "Iniciar Hydra con el sistema", "general": "General", - "behavior": "Otros", - "download_sources": "Fuentes de descarga", + "behavior": "Comportamiento", + "download_sources": "Fuentes de descargas", "language": "Idioma", - "api_token": "Token API", - "enable_real_debrid": "Activar Real-Debrid", - "real_debrid_description": "Real-Debrid es una forma de descargar sin restricciones archivos instantáneamente con la máxima velocidad de tu internet.", - "debrid_invalid_token": "Token de API inválido", - "debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí", - "real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid", + "api_token": "API Token", + "enable_real_debrid": "Habilitar Real-Debrid", + "real_debrid_description": "Real-Debrid es un descargador que te permite descargar archivos más rápidos, solo límitado por la velocidad de tu internet.", + "debrid_invalid_token": "Token API inválido", + "debrid_api_token_hint": "Podés obtener la el token de tu API <0>acá", + "real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratis. Por favor suscribíte a Real-Debrid", "debrid_linked_message": "Cuenta \"{{username}}\" vinculada", "save_changes": "Guardar cambios", - "changes_saved": "Ajustes guardados exitosamente", - "download_sources_description": "Hydra buscará los enlaces de descarga de estas fuentes. La URL de origen debe ser un enlace directo a un archivo .json que contenga los enlaces de descarga", + "changes_saved": "Cambios guardados éxitosamente", + "download_sources_description": "Hydra va a recoger los links de descarga de cada fuente. La URL de origen debe ser un enlace .json que contenga los enlaces de descarga.", "validate_download_source": "Validar", - "remove_download_source": "Eliminar", - "add_download_source": "Añadir fuente de descarga", - "download_count_zero": "No hay descargas en la lista", - "download_count_one": "{{countFormatted}} descarga en la lista", - "download_count_other": "{{countFormatted}} descargas en la lista", - "download_source_url": "Descargar URL de origen", - "add_download_source_description": "Introduce la URL con el archivo .json", - "download_source_up_to_date": "Al día", + "remove_download_source": "Remover", + "add_download_source": "Añadir fuente", + "download_count_zero": "Sin opciones de descarga", + "download_count_one": "{{countFormatted}} opción de descarga", + "download_count_other": "{{countFormatted}} opciones de descarga", + "download_source_url": "Descargar fuente URL", + "add_download_source_description": "Introducí la URL del archivo .json", + "download_source_up_to_date": "Actualizado", "download_source_errored": "Error", "sync_download_sources": "Sincronizar fuentes", "removed_download_source": "Fuente de descarga eliminada", + "removed_download_sources": "Fuente de descarga eliminadas", "cancel_button_confirmation_delete_all_sources": "No", - "confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo", - "description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga", + "confirm_button_confirmation_delete_all_sources": "Si, eliminar todo", "title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga", - "removed_download_sources": "Fuentes de descarga eliminadas", - "button_delete_all_sources": "Eliminar todas las fuentes de descarga", - "added_download_source": "Fuente de descarga añadida", - "download_sources_synced": "Todas las fuentes de descargas están actualizadas.", - "insert_valid_json_url": "Introduce una URL JSON válida", - "found_download_option_zero": "No se encontró una opción de descarga", - "found_download_option_one": "Se encontró {{countFormatted}} opción de descarga", - "found_download_option_other": "Se encontraron {{countFormatted}} opciones de descarga", + "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", + "button_delete_all_sources": "Eliminar todo", + "added_download_source": "Añadir fuente de descarga", + "download_sources_synced": "Todas las fuentes de descarga están sincronizadas", + "insert_valid_json_url": "Introducí una URL de json válida", + "found_download_option_zero": "Sin opciones de descargas encontrada", + "found_download_option_one": "Encontrada {{countFormatted}} fuente de descarga", + "found_download_option_other": "Encontradas {{countFormatted}} opciones de descargas", "import": "Importar", "public": "Público", "private": "Privado", - "friends_only": "Solo amigos", + "friends_only": "Sólo amigos", "privacy": "Privacidad", "profile_visibility": "Visibilidad del perfil", - "profile_visibility_description": "Elige quién puede ver tu perfil y biblioteca", - "required_field": "Este campo es obligatorio", - "source_already_exists": "Esta fuente ya ha sido agregada.", - "must_be_valid_url": "La fuente debe ser una URL válida.", + "profile_visibility_description": "Elegí quién puede ver tú perfil y biblioteca", + "required_field": "Este campo es requerido", + "source_already_exists": "Esta fuente ya está añadida", + "must_be_valid_url": "La fuente debe ser una URL válida", "blocked_users": "Usuarios bloqueados", - "user_unblocked": "El usuario ha sido desbloqueado", - "enable_achievement_notifications": "Cuando un logro se desbloquea", + "user_unblocked": "Has desbloqueado a este usuario", + "enable_achievement_notifications": "Cuando desbloqueás un logro", "launch_minimized": "Iniciar Hydra minimizado", - "disable_nsfw_alert": "Desactivar alerta NSFW", - "seed_after_download_complete": "Realizar seeding después de que se completa la descarga", - "show_hidden_achievement_description": "Ocultar descripción de logros ocultos antes de desbloquearlos", + "disable_nsfw_alert": "Deshabilitar alerta de NSFW", + "seed_after_download_complete": "Sembrar después de completar una descarga", + "show_hidden_achievement_description": "Mostrar logros ocultos antes de desbloquearlos", "account": "Cuenta", - "account_data_updated_successfully": "Datos de la cuenta actualizados", - "bill_sent_until": "Tú próxima factura se enviará el {{date}}", - "current_email": "Correo actual:", - "manage_subscription": "Gestionar suscripción", - "no_email_account": "No has configurado un correo aún", - "no_subscription": "Disfruta Hydra de la mejor manera", - "no_users_blocked": "No tienes usuarios bloqueados", - "renew_subscription": "Renovar Hydra Cloud", - "subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}", - "subscription_expired_at": "Tú suscripción expiró el {{date}}", - "subscription_renew_cancelled": "Está desactivada la renovación automática", - "subscription_renews_on": "Tú suscripción se renueva el {{date}}", + "no_users_blocked": "No has bloqueado a ningún usuario", + "subscription_active_until": "Tu Hydra Cloud está activo hasta {{date}}", + "manage_subscription": "Administrar suscripción", "update_email": "Actualizar correo", - "update_password": "Actualizar contraseña", - "appearance": "Apariencia", - "become_subscriber": "Sé Hydra Cloud", - "cancel": "Cancelar", - "clear_themes": "Limpiar", - "create_theme": "Crear", - "create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra", - "create_theme_modal_title": "Crear tema personalizado", - "delete_all_themes": "Eliminar todos los temas", - "delete_all_themes_description": "Esto eliminará todos tus temas personalizados", - "delete_theme": "Eliminar tema", - "delete_theme_description": "Esto eliminará el tema {{theme}}", - "edit_theme": "Editar tema", + "update_password": "Cambiar contraseña", + "current_email": "Correo actual:", + "no_email_account": "No tenés ningún correo vinculado aún", + "account_data_updated_successfully": "Datos de la cuenta actualizados correctamente", + "renew_subscription": "Renovar Hydra Cloud", + "subscription_expired_at": "Tu suscripción expiró el {{date}}", + "no_subscription": "Disfrutá Hydra de la mejor forma", + "become_subscriber": "Sé parte de Hydra Cloud", + "subscription_renew_cancelled": "Renovación automática desactivada", + "subscription_renews_on": "Tu suscripción se renueva el {{date}}", + "bill_sent_until": "Tu próxima factura se enviará este día", + "no_themes": "Parece que no tenés ningún tema aún, pero no te preocupés, presiona acá para hacer tu primera obra maestra.", "editor_tab_code": "Código", "editor_tab_info": "Info", "editor_tab_save": "Guardar", - "enable_torbox": "Habilitar TorBox", - "error_importing_theme": "Error al importar el tema", + "web_store": "Tienda Web", + "clear_themes": "Limpiar", + "create_theme": "Crear", + "create_theme_modal_title": "Crear tema personalizado", + "create_theme_modal_description": "Crear un nuevo tema para personalizar el estilo de Hydra", + "theme_name": "Nombre", + "insert_theme_name": "Introducí un nombre del tema", + "set_theme": "Usar tema", + "unset_theme": "Dejar de usar tema", + "delete_theme": "Eliminar tema", + "edit_theme": "Editar tema", + "delete_all_themes": "Eliminar todos los temas", + "delete_all_themes_description": "Esto va a eliminar todos los temas personalizados", + "delete_theme_description": "Esto va a eliminar el tema {{theme}}", + "cancel": "Cancelar", + "appearance": "Apariencia", + "enable_torbox": "Activar TorBox", + "torbox_description": "TorBox es un servicio premium de seedbox que incluso rivaliza los mejores servidores.", + "torbox_account_linked": "Cuenta de TorBox vinculada", + "create_real_debrid_account": "Presioná acá si todavía no tenés una cuenta de Real-Debrid", + "create_torbox_account": "Presioná acá si todavía no tenés una cuenta de TorBox", + "real_debrid_account_linked": "Cuenta de Real-Debrid vinculada", + "name_min_length": "El nombre del tema debe tener mínimo 3 carácteres", "import_theme": "Importar tema", "import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas", - "insert_theme_name": "Introducí el nombre del tema", - "name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo", - "no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.", - "real_debrid_account_linked": "Cuenta de Real-Debrid vinculada", - "set_theme": "Establecer tema", - "theme_imported": "Tema importado exitosamente", - "theme_name": "Nombre", - "torbox_account_linked": "Cuenta de TorBox vinculada", - "torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.", - "unset_theme": "Desactivar tema", - "web_store": "Tienda Web", - "enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad", - "enable_auto_install": "Descargar actualizaciones automáticamente" + "error_importing_theme": "Error al importar el tema", + "theme_imported": "Tema importado correctamente", + "enable_friend_request_notifications": "Cuando recibís una solicitud de amistad", + "enable_auto_install": "Descargar actualizaciones automáticamente", + "common_redist": "Common redistributables", + "common_redist_description": "Common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.", + "install_common_redist": "Instalar", + "installing_common_redist": "Instalando…", + "show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo", + "extract_files_by_default": "Extraer archivos por defecto después de descargar", + "enable_steam_achievements": "Habilitar búsqueda de logros de Steam", + "achievement_custom_notification_position": "Posición de notificación de logros", + "top-left": "Superior Izquierda", + "top-center": "Superior Centro", + "top-right": "Superior Derecha", + "bottom-left": "Inferior Izquierda", + "bottom-center": "Inferior Centro", + "bottom-right": "Inferior Derecha", + "enable_achievement_custom_notifications": "Habilitar notificación personalizada de logros", + "alignment": "Centrado", + "variation": "Variación", + "default": "Defecto", + "rare": "Raro", + "platinum": "Platino", + "hidden": "Oculto", + "test_notification": "Probar notificación", + "notification_preview": "Probar notificación de logro", + "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego" }, "notifications": { "download_complete": "Descarga completada", + "game_ready_to_install": "{{title}} está listo para instalar", + "repack_list_updated": "Lista de repacks actualizadas", + "repack_count_one": "{{count}} repack añadido", + "repack_count_other": "{{count}} repacks añadidos", + "new_update_available": "Versión {{version}} disponible", + "restart_to_install_update": "Reiniciá Hydra para instalar la actualización", + "notification_achievement_unlocked_title": "Logro desbloqueado para {{game}}", + "notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados", + "new_friend_request_description": "{{displayName}} te envió una solicitud de amistad", + "new_friend_request_title": "Nueva solicitud de amistad", "extraction_complete": "Extracción completada", "game_extracted": "{{title}} extraído exitosamente", - "game_ready_to_install": "{{title}} está listo para instalarse", - "repack_list_updated": "Lista de repacks actualizadas", - "repack_count_one": "{{count}} repack ha sido añadido", - "repack_count_other": "{{count}} repacks añadidos", - "new_update_available": "Version {{version}} disponible", - "restart_to_install_update": "Reinicia Hydra para instalar la actualización", - "notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}", - "notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados", - "new_friend_request_title": "Nueva solicitud de amistad", - "new_friend_request_description": "{{displayName}} te envió una solicitud de amistad", - "friend_started_playing_game": "{{displayName}} está jugando" + "friend_started_playing_game": "{{displayName}} empezó a jugar un juego", + "test_achievement_notification_title": "Esto es una notificación de prueba", + "test_achievement_notification_description": "Piola, ¿verdad?" }, "system_tray": { "open": "Abrir Hydra", @@ -387,15 +490,15 @@ "game_card": { "available_one": "Disponible", "available_other": "Disponibles", - "no_downloads": "No hay descargas disponibles" + "no_downloads": "Sin descargas disponibles" }, "binary_not_found_modal": { "title": "Programas no instalados", - "description": "Los ejecutables de Wine o Lutris no se encontraron en tu sistema", - "instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad" + "description": "Ejecutables de Wine o Lutris executables no encontrados en tu sistema", + "instructions": "Comprobá la forma correcta de instalar cualquiera de ellos en tu distribución de Linux para que el juego pueda ejecutarse con normalidad" }, "modal": { - "close": "Botón de cierre" + "close": "Botón de cerrar" }, "forms": { "toggle_password_visibility": "Cambiar visibilidad de contraseña" @@ -403,111 +506,120 @@ "user_profile": { "amount_hours": "{{amount}} horas", "amount_minutes": "{{amount}} minutos", - "last_time_played": "Última vez jugado: {{period}}", + "last_time_played": "Jugado por última vez el {{period}}", "activity": "Actividad reciente", - "library": "Biblioteca", - "total_play_time": "Has jugado", - "no_recent_activity_title": "Que raro, no hay nada por acá...", - "no_recent_activity_description": "No has jugado ningún juego recientemente, ¡vamos a cambiar eso ahora!", - "display_name": "Nombre en pantalla", + "library": "Librería", + "pinned": "Fijado", + "total_play_time": "Total de tiempo de juego", + "achievements_earned": "Logros conseguidos", + "played_recently": "Jugado recientemente", + "playtime": "Tiempo de juego", + "manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente", + "no_recent_activity_title": "Hmmm… nada por acá", + "no_recent_activity_description": "No has jugado nada recientemente. ¡Te toca cambiar eso!", + "display_name": "Nombre a mostar", "saving": "Guardando", - "save": "Guardar", + "save": "Guardado", "edit_profile": "Editar perfil", "saved_successfully": "Guardado exitosamente", - "try_again": "Por favor, intenta de nuevo", - "sign_out_modal_title": "¿Estás seguro?", + "try_again": "Por favor, intentá de nuevo", + "sign_out_modal_title": "¿Querés continuar?", "cancel": "Cancelar", - "successfully_signed_out": "Sesión cerrada exitosamente", + "successfully_signed_out": "Cerraste sesión exitosamente", "sign_out": "Cerrar sesión", - "playing_for": "Llevas jugando {{amount}}", - "sign_out_modal_text": "Tu biblioteca se ha vinculado con tu cuenta. Cuando cierres sesión, tú biblioteca ya no será visible y cualquier progreso no se guardará. ¿Continuar con el cierre de sesión?", - "add_friends": "Añadir amigos", + "playing_for": "Jugando por {{amount}}", + "sign_out_modal_text": "Tu librería está vinculada con esta cuenta. Cuando cerrés sesión, tu librería ya no será visible, y cualquier progreso no se guardará. ¿Querés continuar con el cierre de sesión?", + "add_friends": "Añadir amistades", "add": "Añadir", - "friend_code": "Código de amigo", + "friend_code": "Código de amistad", "see_profile": "Ver perfil", "sending": "Enviando", "friend_request_sent": "Solicitud de amistad enviada", - "friends": "Amigos", - "friends_list": "Lista de amigos", + "friends": "Amistades", + "friends_list": "Lista de amistades", "user_not_found": "Usuario no encontrado", "block_user": "Bloquear usuario", - "add_friend": "Añadir amigo", + "add_friend": "Añadir amistad", "request_sent": "Solicitud enviada", "request_received": "Solicitud recibida", "accept_request": "Aceptar solicitud", "ignore_request": "Ignorar solicitud", "cancel_request": "Cancelar solicitud", - "undo_friendship": "Eliminar amistad", + "undo_friendship": "Deshacer amistad", "request_accepted": "Solicitud aceptada", "user_blocked_successfully": "Usuario bloqueado exitosamente", "user_block_modal_text": "Esto va a bloquear a {{displayName}}", "blocked_users": "Usuarios bloqueados", "unblock": "Desbloquear", - "no_friends_added": "Todavía no tienes amigos añadidos", + "no_friends_added": "No tenés amistades añadidas", "pending": "Pendiente", - "no_pending_invites": "No tienes invitaciones pendientes", - "no_blocked_users": "No has bloqueado a ningún usuario", - "friend_code_copied": "Código de amigo copiado", - "undo_friendship_modal_text": "Esto deshará tu amistad con {{displayName}}", - "privacy_hint": "Para ajustar quién puede ver esto, ve a <0>Configuración.", + "no_pending_invites": "No tenés invitaciones pendientes", + "no_blocked_users": "No has bloqueado a nadie", + "friend_code_copied": "Código de amistad copiado", + "undo_friendship_modal_text": "Esto va a deshacer tu amistad con {{displayName}}", + "privacy_hint": "Para cambiar quién puede ver esto, andá a <0>Ajustes", "locked_profile": "Este perfil es privado", - "image_process_failure": "Error al procesar la imagen", - "required_field": "Este campo es obligatorio", - "displayname_min_length": "El nombre a mostrar debe tener al menos 3 caracteres", - "displayname_max_length": "El nombre a mostrar debe tener como máximo 50 caracteres", + "image_process_failure": "Errpr al procesar la imagen", + "required_field": "Este campo es requerido", + "displayname_min_length": "El nombre en pantalla debe tener mínimo 3 caracteres", + "displayname_max_length": "El nombre en pantalla debe tener máximo 50 caracteres", "report_profile": "Reportar este perfil", - "report_reason": "¿Cual es el motivo del reporte?", + "report_reason": "¿Porque estás reportando este perfil?", "report_description": "Información adicional", "report_description_placeholder": "Información adicional", "report": "Reportar", "report_reason_hate": "Discursos de odio", "report_reason_sexual_content": "Contenido sexual", "report_reason_violence": "Violencia", - "report_reason_spam": "Spam / Contenido no deseado", - "report_reason_other": "Otro", + "report_reason_spam": "Spam", + "report_reason_other": "Otros", "profile_reported": "Perfil reportado", - "your_friend_code": "Tu código de amigo:", - "upload_banner": "Subir un banner", + "your_friend_code": "Tu código de amistad:", + "upload_banner": "Subir banner", "uploading_banner": "Subiendo banner…", "background_image_updated": "Imagen de fondo actualizada", - "playing": "Jugando {{game}}", - "achievements": "logros", - "achievements_unlocked": "Logros desbloqueados", - "earned_points": "Puntos Obtenidos", - "show_achievements_on_profile": "Mostrar tus logros en tu perfil", - "show_points_on_profile": "Mostrar tus puntos obtenidos en tu perfil", - "games": "Juegos", - "ranking_updated_weekly": "El Ranking se actualiza semanalmente", "stats": "Estadísticas", - "top_percentile": "Top {{percentile}}%" + "achievements": "logros", + "games": "Juegos", + "top_percentile": "Top {{percentile}}%", + "ranking_updated_weekly": "El ranking se actualiza semanalmente", + "playing": "Jugando {{game}}", + "achievements_unlocked": "Logros desbloqueados", + "earned_points": "Puntos obtenidos", + "show_achievements_on_profile": "Mostrá tus logros en tu perfil", + "show_points_on_profile": "Mostrá los puntos obtenidos en tu perfil", + "error_adding_friend": "No se pudo enviar la solicitud de amistad. Por favor revisá el código", + "friend_code_length_error": "El código de amistad debe tener mínimo 8 caracteres", + "game_removed_from_pinned": "Juego removido de fijados", + "game_added_to_pinned": "Juego añadido a fijados" }, "achievement": { "achievement_unlocked": "Logro desbloqueado", "user_achievements": "Logros de {{displayName}}", - "your_achievements": "Tus Logros", + "your_achievements": "Tus logros", "unlocked_at": "Desbloqueado el: {{date}}", - "subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido", - "new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos", + "subscription_needed": "Se requiere una suscripción a Hydra Cloud para ver esto", + "new_achievements_unlocked": "Desbloqueaste {{achievementCount}} nuevos logros de {{gameCount}} juegos", "achievement_progress": "{{unlockedCount}}/{{totalCount}} logros", - "achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}", + "achievements_unlocked_for_game": "Desbloqueaste {{achievementCount}} nuevos logros para {{gameTitle}}", "hidden_achievement_tooltip": "Este es un logro oculto", - "achievement_earn_points": "Obtén {{points}} puntos con este logro", + "achievement_earn_points": "Conseguí {{points}} puntos por este logro", "earned_points": "Puntos obtenidos:", "available_points": "Puntos disponibles:", - "how_to_earn_achievements_points": "¿Cómo obtener puntos de logros?" + "how_to_earn_achievements_points": "¿Como conseguir puntos por logros?" }, "hydra_cloud": { "subscription_tour_title": "Suscripción Hydra Cloud", - "debrid_description": "Descargas hasta x4 más rápidas con Nimbus", - "subscribe_now": "Suscribirse ahora", + "subscribe_now": "suscribíte ahora", "cloud_saving": "Guardado en la nube", - "cloud_achievements": "Guarda tus logros en la nube", - "animated_profile_picture": "Fotos de perfil animadas", + "cloud_achievements": "Guardá tus logros en la nube", + "animated_profile_picture": "Foto de perfil animada", "premium_support": "Soporte Premium", - "show_and_compare_achievements": "Muestra y compara tus logros con otros usuarios", - "animated_profile_banner": "Fondo de perfil animado", + "show_and_compare_achievements": "Mostrá y compará tus logros con otros usuarios", + "animated_profile_banner": "Banner de perfil animado", "hydra_cloud": "Hydra Cloud", - "hydra_cloud_feature_found": "¡Has descubierto una característica de Hydra Cloud!", - "learn_more": "Aprender más" + "hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!", + "learn_more": "Descubrir más", + "debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus" } } From f3b4898e9ce75ae653a21d67b3c453023c457beb Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 14:51:08 +0300 Subject: [PATCH 25/43] feat: proper cleanup of unused assets --- .../library/remove-game-from-library.ts | 53 +++++++++++++++++-- src/main/events/library/update-custom-game.ts | 38 +++++++++++++ .../library/update-game-custom-assets.ts | 29 ++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 6a33ffaf..4868d588 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,6 +1,6 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { gamesSublevel, levelKeys } from "@main/level"; +import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop } from "@types"; const removeGameFromLibrary = async ( @@ -12,15 +12,62 @@ const removeGameFromLibrary = async ( const game = await gamesSublevel.get(gameKey); if (game) { - await gamesSublevel.put(gameKey, { + // Collect asset paths that need to be cleaned up before marking as deleted + const assetPathsToDelete: string[] = []; + + const assetUrls = game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; + + assetUrls.forEach(url => { + if (url?.startsWith("local:")) { + assetPathsToDelete.push(url.replace("local:", "")); + } + }); + + + const updatedGame = { ...game, isDeleted: true, executablePath: null, - }); + ...(game.shop !== "custom" && { + customIconUrl: null, + customLogoImageUrl: null, + customHeroImageUrl: null, + }), + }; + + await gamesSublevel.put(gameKey, updatedGame); + + + if (game.shop !== "custom") { + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const resetAssets = { + ...existingAssets, + title: existingAssets.title, + }; + await gamesShopAssetsSublevel.put(gameKey, resetAssets); + } + } if (game?.remoteId) { HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } + + + if (assetPathsToDelete.length > 0) { + const fs = await import("fs"); + for (const assetPath of assetPathsToDelete) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete asset ${assetPath}:`, error); + } + } + } } }; diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 6152c0df..168e0050 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -18,6 +18,30 @@ const updateCustomGame = async ( throw new Error("Game not found"); } + // Collect old asset paths that will be replaced + const oldAssetPaths: string[] = []; + + if (existingGame.iconUrl && iconUrl && existingGame.iconUrl !== iconUrl && existingGame.iconUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); + } + if (existingGame.iconUrl && !iconUrl && existingGame.iconUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); + } + + if (existingGame.logoImageUrl && logoImageUrl && existingGame.logoImageUrl !== logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); + } + if (existingGame.logoImageUrl && !logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); + } + + if (existingGame.libraryHeroImageUrl && libraryHeroImageUrl && existingGame.libraryHeroImageUrl !== libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); + } + if (existingGame.libraryHeroImageUrl && !libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { + oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); + } + const updatedGame = { ...existingGame, title, @@ -43,6 +67,20 @@ const updateCustomGame = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } + // Manually delete specific old asset files instead of running full cleanup + if (oldAssetPaths.length > 0) { + const fs = await import("fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete old asset ${assetPath}:`, error); + } + } + } + return updatedGame; }; diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 866cd60e..f8206904 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -18,6 +18,21 @@ const updateGameCustomAssets = async ( throw new Error("Game not found"); } + // Collect old custom asset paths that will be replaced + const oldAssetPaths: string[] = []; + + const assetPairs = [ + { existing: existingGame.customIconUrl, new: customIconUrl }, + { existing: existingGame.customLogoImageUrl, new: customLogoImageUrl }, + { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl } + ]; + + assetPairs.forEach(({ existing, new: newUrl }) => { + if (existing && newUrl !== undefined && existing !== newUrl && existing.startsWith("local:")) { + oldAssetPaths.push(existing.replace("local:", "")); + } + }); + const updatedGame = { ...existingGame, title, @@ -39,6 +54,20 @@ const updateGameCustomAssets = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } + // Manually delete specific old custom asset files instead of running full cleanup + if (oldAssetPaths.length > 0) { + const fs = await import("fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete old custom asset ${assetPath}:`, error); + } + } + } + return updatedGame; }; From 2bed7c0b37c83ff031ebe7b374e77e85458d9ed1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 14:55:23 +0300 Subject: [PATCH 26/43] fix: cleaned comments and simplified function --- .../library/remove-game-from-library.ts | 22 +++++++------ src/main/events/library/update-custom-game.ts | 31 ++++++------------- .../library/update-game-custom-assets.ts | 18 ++++++----- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 4868d588..c4c8be9d 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -14,18 +14,22 @@ const removeGameFromLibrary = async ( if (game) { // Collect asset paths that need to be cleaned up before marking as deleted const assetPathsToDelete: string[] = []; - - const assetUrls = game.shop === "custom" - ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; - - assetUrls.forEach(url => { + + const assetUrls = + game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [ + game.customIconUrl, + game.customLogoImageUrl, + game.customHeroImageUrl, + ]; + + assetUrls.forEach((url) => { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } }); - const updatedGame = { ...game, isDeleted: true, @@ -39,13 +43,12 @@ const removeGameFromLibrary = async ( await gamesSublevel.put(gameKey, updatedGame); - if (game.shop !== "custom") { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const resetAssets = { ...existingAssets, - title: existingAssets.title, + title: existingAssets.title, }; await gamesShopAssetsSublevel.put(gameKey, resetAssets); } @@ -55,7 +58,6 @@ const removeGameFromLibrary = async ( HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } - if (assetPathsToDelete.length > 0) { const fs = await import("fs"); for (const assetPath of assetPathsToDelete) { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 168e0050..141e251e 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -18,29 +18,19 @@ const updateCustomGame = async ( throw new Error("Game not found"); } - // Collect old asset paths that will be replaced const oldAssetPaths: string[] = []; - if (existingGame.iconUrl && iconUrl && existingGame.iconUrl !== iconUrl && existingGame.iconUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); - } - if (existingGame.iconUrl && !iconUrl && existingGame.iconUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.iconUrl.replace("local:", "")); - } + const assetPairs = [ + { existing: existingGame.iconUrl, new: iconUrl }, + { existing: existingGame.logoImageUrl, new: logoImageUrl }, + { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl } + ]; - if (existingGame.logoImageUrl && logoImageUrl && existingGame.logoImageUrl !== logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); - } - if (existingGame.logoImageUrl && !logoImageUrl && existingGame.logoImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.logoImageUrl.replace("local:", "")); - } - - if (existingGame.libraryHeroImageUrl && libraryHeroImageUrl && existingGame.libraryHeroImageUrl !== libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); - } - if (existingGame.libraryHeroImageUrl && !libraryHeroImageUrl && existingGame.libraryHeroImageUrl.startsWith("local:")) { - oldAssetPaths.push(existingGame.libraryHeroImageUrl.replace("local:", "")); - } + assetPairs.forEach(({ existing, new: newUrl }) => { + if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) { + oldAssetPaths.push(existing.replace("local:", "")); + } + }); const updatedGame = { ...existingGame, @@ -67,7 +57,6 @@ const updateCustomGame = async ( await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } - // Manually delete specific old asset files instead of running full cleanup if (oldAssetPaths.length > 0) { const fs = await import("fs"); for (const assetPath of oldAssetPaths) { diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index f8206904..392a0923 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -18,17 +18,21 @@ const updateGameCustomAssets = async ( throw new Error("Game not found"); } - // Collect old custom asset paths that will be replaced const oldAssetPaths: string[] = []; - + const assetPairs = [ { existing: existingGame.customIconUrl, new: customIconUrl }, { existing: existingGame.customLogoImageUrl, new: customLogoImageUrl }, - { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl } + { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl }, ]; - + assetPairs.forEach(({ existing, new: newUrl }) => { - if (existing && newUrl !== undefined && existing !== newUrl && existing.startsWith("local:")) { + if ( + existing && + newUrl !== undefined && + existing !== newUrl && + existing.startsWith("local:") + ) { oldAssetPaths.push(existing.replace("local:", "")); } }); @@ -43,18 +47,16 @@ const updateGameCustomAssets = async ( await gamesSublevel.put(gameKey, updatedGame); - // Also update the shop assets for non-custom games const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { ...existingAssets, - title, // Update the title in shop assets as well + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } - // Manually delete specific old custom asset files instead of running full cleanup if (oldAssetPaths.length > 0) { const fs = await import("fs"); for (const assetPath of oldAssetPaths) { From 3e93a14deb066a3a674a3f2879fc39fbdd45acae Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 19:53:52 +0300 Subject: [PATCH 27/43] Fix: display actual image path in edit game modal --- .../library/update-game-custom-assets.ts | 2 +- .../game-details/modals/edit-game-modal.tsx | 60 ++++++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 392a0923..1b75e0c4 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -51,7 +51,7 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 7bfc48fa..0948a749 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { ImageIcon, XIcon } from "@primer/octicons-react"; @@ -32,6 +32,9 @@ export function EditGameModal({ const [iconPath, setIconPath] = useState(""); const [logoPath, setLogoPath] = useState(""); const [heroPath, setHeroPath] = useState(""); + const [iconDisplayPath, setIconDisplayPath] = useState(""); + const [logoDisplayPath, setLogoDisplayPath] = useState(""); + const [heroDisplayPath, setHeroDisplayPath] = useState(""); const [isUpdating, setIsUpdating] = useState(false); const [selectedAssetType, setSelectedAssetType] = useState("icon"); @@ -51,6 +54,10 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); + // For existing assets, show the asset path as display path since we don't have the original + setIconDisplayPath(extractLocalPath(game.iconUrl)); + setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); + setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); }, []); const setNonCustomGameAssets = useCallback( @@ -58,6 +65,10 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); + // For existing assets, show the asset path as display path since we don't have the original + setIconDisplayPath(extractLocalPath(game.customIconUrl)); + setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); + setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); setDefaultLogoUrl( @@ -103,6 +114,17 @@ export function EditGameModal({ } }; + const getAssetDisplayPath = (assetType: AssetType): string => { + switch (assetType) { + case "icon": + return iconDisplayPath; + case "logo": + return logoDisplayPath; + case "hero": + return heroDisplayPath; + } + }; + const setAssetPath = (assetType: AssetType, path: string): void => { switch (assetType) { case "icon": @@ -117,6 +139,20 @@ export function EditGameModal({ } }; + const setAssetDisplayPath = (assetType: AssetType, path: string): void => { + switch (assetType) { + case "icon": + setIconDisplayPath(path); + break; + case "logo": + setLogoDisplayPath(path); + break; + case "hero": + setHeroDisplayPath(path); + break; + } + }; + const getDefaultUrl = (assetType: AssetType): string | null => { switch (assetType) { case "icon": @@ -140,21 +176,25 @@ export function EditGameModal({ }); if (filePaths && filePaths.length > 0) { + const originalPath = filePaths[0]; try { const copiedAssetUrl = await window.electron.copyCustomGameAsset( - filePaths[0], + originalPath, assetType ); setAssetPath(assetType, copiedAssetUrl.replace("local:", "")); + setAssetDisplayPath(assetType, originalPath); } catch (error) { console.error(`Failed to copy ${assetType} asset:`, error); - setAssetPath(assetType, filePaths[0]); + setAssetPath(assetType, originalPath); + setAssetDisplayPath(assetType, originalPath); } } }; const handleRestoreDefault = (assetType: AssetType) => { setAssetPath(assetType, ""); + setAssetDisplayPath(assetType, ""); }; const getOriginalTitle = (): string => { @@ -169,11 +209,11 @@ export function EditGameModal({ setGameName(originalTitle); }; - const isTitleChanged = (): boolean => { + const isTitleChanged = useMemo((): boolean => { if (!game || isCustomGame(game)) return false; const originalTitle = getOriginalTitle(); return gameName.trim() !== originalTitle.trim(); - }; + }, [game, gameName, shopDetails]); const [dragOverTarget, setDragOverTarget] = useState(null); @@ -250,6 +290,7 @@ export function EditGameModal({ const assetPath = copiedAssetUrl.replace("local:", ""); setAssetPath(assetType, assetPath); + setAssetDisplayPath(assetType, filePath); showSuccessToast( `${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!` @@ -361,7 +402,7 @@ export function EditGameModal({ }; // Helper function to reset form to initial state - const resetFormToInitialState = (game: LibraryGame | Game) => { + const resetFormToInitialState = useCallback((game: LibraryGame | Game) => { setGameName(game.title || ""); if (isCustomGame(game)) { @@ -373,7 +414,7 @@ export function EditGameModal({ } else { setNonCustomGameAssets(game as LibraryGame); } - }; + }, [setCustomGameAssets, setNonCustomGameAssets]); const handleClose = () => { if (!isUpdating && game) { @@ -396,6 +437,7 @@ export function EditGameModal({ const renderImageSection = (assetType: AssetType) => { const assetPath = getAssetPath(assetType); + const assetDisplayPath = getAssetDisplayPath(assetType); const defaultUrl = getDefaultUrl(assetType); const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl); const isDragOver = dragOverTarget === assetType; @@ -408,7 +450,7 @@ export function EditGameModal({
    Date: Mon, 29 Sep 2025 20:09:04 +0300 Subject: [PATCH 28/43] fix: using for...of instead of forEach --- src/main/events/library/remove-game-from-library.ts | 4 ++-- src/main/events/library/update-custom-game.ts | 10 +++++----- src/main/events/library/update-game-custom-assets.ts | 6 +++--- .../src/pages/game-details/modals/edit-game-modal.tsx | 2 -- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index c4c8be9d..92539650 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -24,11 +24,11 @@ const removeGameFromLibrary = async ( game.customHeroImageUrl, ]; - assetUrls.forEach((url) => { + for (const url of assetUrls) { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } - }); + } const updatedGame = { ...game, diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 141e251e..39f3551b 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -19,18 +19,18 @@ const updateCustomGame = async ( } const oldAssetPaths: string[] = []; - + const assetPairs = [ { existing: existingGame.iconUrl, new: iconUrl }, { existing: existingGame.logoImageUrl, new: logoImageUrl }, - { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl } + { existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl }, ]; - - assetPairs.forEach(({ existing, new: newUrl }) => { + + for (const { existing, new: newUrl } of assetPairs) { if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) { oldAssetPaths.push(existing.replace("local:", "")); } - }); + } const updatedGame = { ...existingGame, diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 1b75e0c4..166b2641 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -26,7 +26,7 @@ const updateGameCustomAssets = async ( { existing: existingGame.customHeroImageUrl, new: customHeroImageUrl }, ]; - assetPairs.forEach(({ existing, new: newUrl }) => { + for (const { existing, new: newUrl } of assetPairs) { if ( existing && newUrl !== undefined && @@ -35,7 +35,7 @@ const updateGameCustomAssets = async ( ) { oldAssetPaths.push(existing.replace("local:", "")); } - }); + } const updatedGame = { ...existingGame, @@ -51,7 +51,7 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 0948a749..5e8e2311 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -54,7 +54,6 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.iconUrl)); setLogoPath(extractLocalPath(game.logoImageUrl)); setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); - // For existing assets, show the asset path as display path since we don't have the original setIconDisplayPath(extractLocalPath(game.iconUrl)); setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); @@ -65,7 +64,6 @@ export function EditGameModal({ setIconPath(extractLocalPath(game.customIconUrl)); setLogoPath(extractLocalPath(game.customLogoImageUrl)); setHeroPath(extractLocalPath(game.customHeroImageUrl)); - // For existing assets, show the asset path as display path since we don't have the original setIconDisplayPath(extractLocalPath(game.customIconUrl)); setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); From a87e04a366eb12985d1fa1282d634cd95cf74c05 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:14:36 +0300 Subject: [PATCH 29/43] Fix: using node:fs instead of fs --- .../library/remove-game-from-library.ts | 2 +- src/main/events/library/update-custom-game.ts | 2 +- .../library/update-game-custom-assets.ts | 4 +-- .../game-details/modals/edit-game-modal.tsx | 27 ++++++++++--------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 92539650..a9c0d272 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -59,7 +59,7 @@ const removeGameFromLibrary = async ( } if (assetPathsToDelete.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of assetPathsToDelete) { try { if (fs.existsSync(assetPath)) { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 39f3551b..47641a6e 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -58,7 +58,7 @@ const updateCustomGame = async ( } if (oldAssetPaths.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 166b2641..4e86bbc0 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -51,14 +51,14 @@ const updateGameCustomAssets = async ( if (existingAssets) { const updatedAssets = { ...existingAssets, - title, + title, }; await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } if (oldAssetPaths.length > 0) { - const fs = await import("fs"); + const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 5e8e2311..3979d051 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -400,19 +400,22 @@ export function EditGameModal({ }; // Helper function to reset form to initial state - const resetFormToInitialState = useCallback((game: LibraryGame | Game) => { - setGameName(game.title || ""); + const resetFormToInitialState = useCallback( + (game: LibraryGame | Game) => { + setGameName(game.title || ""); - if (isCustomGame(game)) { - setCustomGameAssets(game); - // Clear default URLs for custom games - setDefaultIconUrl(null); - setDefaultLogoUrl(null); - setDefaultHeroUrl(null); - } else { - setNonCustomGameAssets(game as LibraryGame); - } - }, [setCustomGameAssets, setNonCustomGameAssets]); + if (isCustomGame(game)) { + setCustomGameAssets(game); + // Clear default URLs for custom games + setDefaultIconUrl(null); + setDefaultLogoUrl(null); + setDefaultHeroUrl(null); + } else { + setNonCustomGameAssets(game as LibraryGame); + } + }, + [setCustomGameAssets, setNonCustomGameAssets] + ); const handleClose = () => { if (!isUpdating && game) { From 96d6b90356c7612210c5358937d872f6d320bdca Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:20:58 +0300 Subject: [PATCH 30/43] Fix: Refactoring functions to reduce complexity --- .../library/remove-game-from-library.ts | 133 ++++++++++-------- .../library/update-game-custom-assets.ts | 90 ++++++++---- 2 files changed, 141 insertions(+), 82 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index a9c0d272..438aa39a 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,7 +1,69 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; -import type { GameShop } from "@types"; +import type { GameShop, Game } from "@types"; + +const collectAssetPathsToDelete = (game: Game): string[] => { + const assetPathsToDelete: string[] = []; + + const assetUrls = + game.shop === "custom" + ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] + : [ + game.customIconUrl, + game.customLogoImageUrl, + game.customHeroImageUrl, + ]; + + for (const url of assetUrls) { + if (url?.startsWith("local:")) { + assetPathsToDelete.push(url.replace("local:", "")); + } + } + + return assetPathsToDelete; +}; + +const updateGameAsDeleted = async (game: Game, gameKey: string): Promise => { + const updatedGame = { + ...game, + isDeleted: true, + executablePath: null, + ...(game.shop !== "custom" && { + customIconUrl: null, + customLogoImageUrl: null, + customHeroImageUrl: null, + }), + }; + + await gamesSublevel.put(gameKey, updatedGame); +}; + +const resetShopAssets = async (gameKey: string): Promise => { + const existingAssets = await gamesShopAssetsSublevel.get(gameKey); + if (existingAssets) { + const resetAssets = { + ...existingAssets, + title: existingAssets.title, + }; + await gamesShopAssetsSublevel.put(gameKey, resetAssets); + } +}; + +const deleteAssetFiles = async (assetPathsToDelete: string[]): Promise => { + if (assetPathsToDelete.length === 0) return; + + const fs = await import("node:fs"); + for (const assetPath of assetPathsToDelete) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); + } + } catch (error) { + console.warn(`Failed to delete asset ${assetPath}:`, error); + } + } +}; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -11,66 +73,21 @@ const removeGameFromLibrary = async ( const gameKey = levelKeys.game(shop, objectId); const game = await gamesSublevel.get(gameKey); - if (game) { - // Collect asset paths that need to be cleaned up before marking as deleted - const assetPathsToDelete: string[] = []; + if (!game) return; - const assetUrls = - game.shop === "custom" - ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [ - game.customIconUrl, - game.customLogoImageUrl, - game.customHeroImageUrl, - ]; + const assetPathsToDelete = collectAssetPathsToDelete(game); + + await updateGameAsDeleted(game, gameKey); - for (const url of assetUrls) { - if (url?.startsWith("local:")) { - assetPathsToDelete.push(url.replace("local:", "")); - } - } - - const updatedGame = { - ...game, - isDeleted: true, - executablePath: null, - ...(game.shop !== "custom" && { - customIconUrl: null, - customLogoImageUrl: null, - customHeroImageUrl: null, - }), - }; - - await gamesSublevel.put(gameKey, updatedGame); - - if (game.shop !== "custom") { - const existingAssets = await gamesShopAssetsSublevel.get(gameKey); - if (existingAssets) { - const resetAssets = { - ...existingAssets, - title: existingAssets.title, - }; - await gamesShopAssetsSublevel.put(gameKey, resetAssets); - } - } - - if (game?.remoteId) { - HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); - } - - if (assetPathsToDelete.length > 0) { - const fs = await import("node:fs"); - for (const assetPath of assetPathsToDelete) { - try { - if (fs.existsSync(assetPath)) { - await fs.promises.unlink(assetPath); - } - } catch (error) { - console.warn(`Failed to delete asset ${assetPath}:`, error); - } - } - } + if (game.shop !== "custom") { + await resetShopAssets(gameKey); } + + if (game?.remoteId) { + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); + } + + await deleteAssetFiles(assetPathsToDelete); }; registerEvent("removeGameFromLibrary", removeGameFromLibrary); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 4e86bbc0..2138af3a 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -1,23 +1,13 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; -import type { GameShop } from "@types"; +import type { GameShop, Game } from "@types"; -const updateGameCustomAssets = async ( - _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string, - title: string, +const collectOldAssetPaths = ( + existingGame: Game, customIconUrl?: string | null, customLogoImageUrl?: string | null, customHeroImageUrl?: string | null -) => { - const gameKey = levelKeys.game(shop, objectId); - - const existingGame = await gamesSublevel.get(gameKey); - if (!existingGame) { - throw new Error("Game not found"); - } - +): string[] => { const oldAssetPaths: string[] = []; const assetPairs = [ @@ -37,6 +27,17 @@ const updateGameCustomAssets = async ( } } + return oldAssetPaths; +}; + +const updateGameData = async ( + gameKey: string, + existingGame: Game, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null +): Promise => { const updatedGame = { ...existingGame, title, @@ -46,29 +47,70 @@ const updateGameCustomAssets = async ( }; await gamesSublevel.put(gameKey, updatedGame); + return updatedGame; +}; +const updateShopAssets = async (gameKey: string, title: string): Promise => { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { ...existingAssets, title, }; - await gamesShopAssetsSublevel.put(gameKey, updatedAssets); } +}; - if (oldAssetPaths.length > 0) { - const fs = await import("node:fs"); - for (const assetPath of oldAssetPaths) { - try { - if (fs.existsSync(assetPath)) { - await fs.promises.unlink(assetPath); - } - } catch (error) { - console.warn(`Failed to delete old custom asset ${assetPath}:`, error); +const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { + if (oldAssetPaths.length === 0) return; + + const fs = await import("node:fs"); + for (const assetPath of oldAssetPaths) { + try { + if (fs.existsSync(assetPath)) { + await fs.promises.unlink(assetPath); } + } catch (error) { + console.warn(`Failed to delete old custom asset ${assetPath}:`, error); } } +}; + +const updateGameCustomAssets = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string, + title: string, + customIconUrl?: string | null, + customLogoImageUrl?: string | null, + customHeroImageUrl?: string | null +) => { + const gameKey = levelKeys.game(shop, objectId); + + const existingGame = await gamesSublevel.get(gameKey); + if (!existingGame) { + throw new Error("Game not found"); + } + + const oldAssetPaths = collectOldAssetPaths( + existingGame, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ); + + const updatedGame = await updateGameData( + gameKey, + existingGame, + title, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl + ); + + await updateShopAssets(gameKey, title); + + await deleteOldAssetFiles(oldAssetPaths); return updatedGame; }; From 7e22344f77a0cab9bb597d89b50c17ff5b50c4f0 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:36:05 +0300 Subject: [PATCH 31/43] Fix: fixed fs import and started using logger.warn --- .../library/remove-game-from-library.ts | 28 ++++++++++--------- src/main/events/library/update-custom-game.ts | 5 ++-- .../library/update-game-custom-assets.ts | 5 ++-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 438aa39a..b7033147 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -2,29 +2,30 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const collectAssetPathsToDelete = (game: Game): string[] => { const assetPathsToDelete: string[] = []; - + const assetUrls = game.shop === "custom" ? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl] - : [ - game.customIconUrl, - game.customLogoImageUrl, - game.customHeroImageUrl, - ]; + : [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl]; for (const url of assetUrls) { if (url?.startsWith("local:")) { assetPathsToDelete.push(url.replace("local:", "")); } } - + return assetPathsToDelete; }; -const updateGameAsDeleted = async (game: Game, gameKey: string): Promise => { +const updateGameAsDeleted = async ( + game: Game, + gameKey: string +): Promise => { const updatedGame = { ...game, isDeleted: true, @@ -50,17 +51,18 @@ const resetShopAssets = async (gameKey: string): Promise => { } }; -const deleteAssetFiles = async (assetPathsToDelete: string[]): Promise => { +const deleteAssetFiles = async ( + assetPathsToDelete: string[] +): Promise => { if (assetPathsToDelete.length === 0) return; - - const fs = await import("node:fs"); + for (const assetPath of assetPathsToDelete) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete asset ${assetPath}:`, error); + logger.warn(`Failed to delete asset ${assetPath}:`, error); } } }; @@ -76,7 +78,7 @@ const removeGameFromLibrary = async ( if (!game) return; const assetPathsToDelete = collectAssetPathsToDelete(game); - + await updateGameAsDeleted(game, gameKey); if (game.shop !== "custom") { diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 47641a6e..82d7f45f 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -1,6 +1,8 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const updateCustomGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -58,14 +60,13 @@ const updateCustomGame = async ( } if (oldAssetPaths.length > 0) { - const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete old asset ${assetPath}:`, error); + logger.warn(`Failed to delete old asset ${assetPath}:`, error); } } } diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 2138af3a..1813e8a8 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -1,6 +1,8 @@ import { registerEvent } from "../register-event"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; +import fs from "node:fs"; +import { logger } from "@main/services"; const collectOldAssetPaths = ( existingGame: Game, @@ -64,14 +66,13 @@ const updateShopAssets = async (gameKey: string, title: string): Promise = const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { if (oldAssetPaths.length === 0) return; - const fs = await import("node:fs"); for (const assetPath of oldAssetPaths) { try { if (fs.existsSync(assetPath)) { await fs.promises.unlink(assetPath); } } catch (error) { - console.warn(`Failed to delete old custom asset ${assetPath}:`, error); + logger.warn(`Failed to delete old custom asset ${assetPath}:`, error); } } }; From a39f9ebb70abab4e598d820b9f793c64ef73d2e5 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 20:39:58 +0300 Subject: [PATCH 32/43] fix: multiple imports --- src/main/events/library/remove-game-from-library.ts | 3 +-- src/main/events/library/update-game-custom-assets.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index b7033147..fbb60ab2 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,9 +1,8 @@ import { registerEvent } from "../register-event"; -import { HydraApi } from "@main/services"; +import { HydraApi, logger } from "@main/services"; import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level"; import type { GameShop, Game } from "@types"; import fs from "node:fs"; -import { logger } from "@main/services"; const collectAssetPathsToDelete = (game: Game): string[] => { const assetPathsToDelete: string[] = []; diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 1813e8a8..4bd4e517 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -52,7 +52,10 @@ const updateGameData = async ( return updatedGame; }; -const updateShopAssets = async (gameKey: string, title: string): Promise => { +const updateShopAssets = async ( + gameKey: string, + title: string +): Promise => { const existingAssets = await gamesShopAssetsSublevel.get(gameKey); if (existingAssets) { const updatedAssets = { From bd053a1635fae69049688b80d6d04c702cfe6e57 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Mon, 29 Sep 2025 22:04:01 +0300 Subject: [PATCH 33/43] fix: favorite and pin button overlapping playtime --- src/locales/en/translation.json | 2 + src/locales/ru/translation.json | 2 + .../user-library-game-card.scss | 2 +- .../user-library-game-card.tsx | 56 ++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ce8b4de1..93fd5b0a 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -506,6 +506,8 @@ "user_profile": { "amount_hours": "{{amount}} hours", "amount_minutes": "{{amount}} minutes", + "amount_hours_short": "{{amount}}h", + "amount_minutes_short": "{{amount}}m", "last_time_played": "Last played {{period}}", "activity": "Recent Activity", "library": "Library", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 58235989..c92d7902 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -486,6 +486,8 @@ "user_profile": { "amount_hours": "{{amount}} часов", "amount_minutes": "{{amount}} минут", + "amount_hours_short": "{{amount}}ч", + "amount_minutes_short": "{{amount}}м", "last_time_played": "Последняя игра {{period}}", "activity": "Недавняя активность", "library": "Библиотека", diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index f072fdd5..e40061de 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -86,7 +86,7 @@ top: 8px; right: 8px; display: flex; - gap: 6px; + gap: 4px; z-index: 2; } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index 860c6758..d30dbf8a 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -2,7 +2,7 @@ import { UserGame } from "@types"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useFormat, useToast } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useContext, useState } from "react"; +import { useCallback, useContext, useState, useEffect, useRef } from "react"; import { buildGameAchievementPath, buildGameDetailsPath, @@ -43,6 +43,9 @@ export function UserLibraryGameCard({ const navigate = useNavigate(); const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isPinning, setIsPinning] = useState(false); + const [useShortFormat, setUseShortFormat] = useState(false); + const cardRef = useRef(null); + const playtimeRef = useRef(null); const getStatsItemCount = useCallback(() => { let statsCount = 1; @@ -94,6 +97,50 @@ export function UserLibraryGameCard({ [numberFormatter, t] ); + const formatPlayTimeShort = useCallback( + (playTimeInSeconds = 0) => { + const minutes = playTimeInSeconds / 60; + + if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { + return t("amount_minutes_short", { + amount: minutes.toFixed(0), + }); + } + + const hours = minutes / 60; + return t("amount_hours_short", { amount: Math.floor(hours) }); + }, + [t] + ); + + const checkForOverlap = useCallback(() => { + if (!cardRef.current || !playtimeRef.current) return; + + const cardWidth = cardRef.current.offsetWidth; + const hasButtons = game.isFavorite || isMe; + + if (hasButtons && cardWidth < 180) { + setUseShortFormat(true); + } else { + setUseShortFormat(false); + } + }, [game.isFavorite, isMe]); + + useEffect(() => { + checkForOverlap(); + + const handleResize = () => { + checkForOverlap(); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [checkForOverlap]); + + useEffect(() => { + checkForOverlap(); + }, [game.isFavorite, isMe, checkForOverlap]); + const toggleGamePinned = async () => { setIsPinning(true); @@ -119,6 +166,7 @@ export function UserLibraryGameCard({ return ( <>
  • )} )} - {formatPlayTime(game.playTimeInSeconds)} + {useShortFormat + ? formatPlayTimeShort(game.playTimeInSeconds) + : formatPlayTime(game.playTimeInSeconds) + } {userProfile?.hasActiveSubscription && From 9689c19863c1ddfc1fce0f05767c98c68d3e5d61 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 00:52:46 +0300 Subject: [PATCH 34/43] fix: state fix and remake checking for overlap --- .../game-details/modals/edit-game-modal.tsx | 144 +++++++----------- .../user-library-game-card.scss | 20 +++ .../user-library-game-card.tsx | 73 ++------- 3 files changed, 93 insertions(+), 144 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 3979d051..2413cb9e 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -29,19 +29,24 @@ export function EditGameModal({ const { showSuccessToast, showErrorToast } = useToast(); const [gameName, setGameName] = useState(""); - const [iconPath, setIconPath] = useState(""); - const [logoPath, setLogoPath] = useState(""); - const [heroPath, setHeroPath] = useState(""); - const [iconDisplayPath, setIconDisplayPath] = useState(""); - const [logoDisplayPath, setLogoDisplayPath] = useState(""); - const [heroDisplayPath, setHeroDisplayPath] = useState(""); + const [assetPaths, setAssetPaths] = useState({ + icon: "", + logo: "", + hero: "", + }); + const [assetDisplayPaths, setAssetDisplayPaths] = useState({ + icon: "", + logo: "", + hero: "", + }); + const [defaultUrls, setDefaultUrls] = useState({ + icon: null as string | null, + logo: null as string | null, + hero: null as string | null, + }); const [isUpdating, setIsUpdating] = useState(false); const [selectedAssetType, setSelectedAssetType] = useState("icon"); - const [defaultIconUrl, setDefaultIconUrl] = useState(null); - const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); - const [defaultHeroUrl, setDefaultHeroUrl] = useState(null); - const isCustomGame = (game: LibraryGame | Game): boolean => { return game.shop === "custom"; }; @@ -51,32 +56,36 @@ export function EditGameModal({ }; const setCustomGameAssets = useCallback((game: LibraryGame | Game) => { - setIconPath(extractLocalPath(game.iconUrl)); - setLogoPath(extractLocalPath(game.logoImageUrl)); - setHeroPath(extractLocalPath(game.libraryHeroImageUrl)); - setIconDisplayPath(extractLocalPath(game.iconUrl)); - setLogoDisplayPath(extractLocalPath(game.logoImageUrl)); - setHeroDisplayPath(extractLocalPath(game.libraryHeroImageUrl)); + setAssetPaths({ + icon: extractLocalPath(game.iconUrl), + logo: extractLocalPath(game.logoImageUrl), + hero: extractLocalPath(game.libraryHeroImageUrl), + }); + setAssetDisplayPaths({ + icon: extractLocalPath(game.iconUrl), + logo: extractLocalPath(game.logoImageUrl), + hero: extractLocalPath(game.libraryHeroImageUrl), + }); }, []); const setNonCustomGameAssets = useCallback( (game: LibraryGame) => { - setIconPath(extractLocalPath(game.customIconUrl)); - setLogoPath(extractLocalPath(game.customLogoImageUrl)); - setHeroPath(extractLocalPath(game.customHeroImageUrl)); - setIconDisplayPath(extractLocalPath(game.customIconUrl)); - setLogoDisplayPath(extractLocalPath(game.customLogoImageUrl)); - setHeroDisplayPath(extractLocalPath(game.customHeroImageUrl)); + setAssetPaths({ + icon: extractLocalPath(game.customIconUrl), + logo: extractLocalPath(game.customLogoImageUrl), + hero: extractLocalPath(game.customHeroImageUrl), + }); + setAssetDisplayPaths({ + icon: extractLocalPath(game.customIconUrl), + logo: extractLocalPath(game.customLogoImageUrl), + hero: extractLocalPath(game.customHeroImageUrl), + }); - setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null); - setDefaultLogoUrl( - shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null - ); - setDefaultHeroUrl( - shopDetails?.assets?.libraryHeroImageUrl || - game.libraryHeroImageUrl || - null - ); + setDefaultUrls({ + icon: shopDetails?.assets?.iconUrl || game.iconUrl || null, + logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null, + hero: shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null, + }); }, [shopDetails] ); @@ -102,64 +111,23 @@ export function EditGameModal({ }; const getAssetPath = (assetType: AssetType): string => { - switch (assetType) { - case "icon": - return iconPath; - case "logo": - return logoPath; - case "hero": - return heroPath; - } + return assetPaths[assetType]; }; const getAssetDisplayPath = (assetType: AssetType): string => { - switch (assetType) { - case "icon": - return iconDisplayPath; - case "logo": - return logoDisplayPath; - case "hero": - return heroDisplayPath; - } + return assetDisplayPaths[assetType]; }; const setAssetPath = (assetType: AssetType, path: string): void => { - switch (assetType) { - case "icon": - setIconPath(path); - break; - case "logo": - setLogoPath(path); - break; - case "hero": - setHeroPath(path); - break; - } + setAssetPaths(prev => ({ ...prev, [assetType]: path })); }; const setAssetDisplayPath = (assetType: AssetType, path: string): void => { - switch (assetType) { - case "icon": - setIconDisplayPath(path); - break; - case "logo": - setLogoDisplayPath(path); - break; - case "hero": - setHeroDisplayPath(path); - break; - } + setAssetDisplayPaths(prev => ({ ...prev, [assetType]: path })); }; const getDefaultUrl = (assetType: AssetType): string | null => { - switch (assetType) { - case "icon": - return defaultIconUrl; - case "logo": - return defaultLogoUrl; - case "hero": - return defaultHeroUrl; - } + return defaultUrls[assetType]; }; const handleSelectAsset = async (assetType: AssetType) => { @@ -324,10 +292,10 @@ export function EditGameModal({ // Helper function to prepare custom game assets const prepareCustomGameAssets = (game: LibraryGame | Game) => { - const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; - const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl; - const libraryHeroImageUrl = heroPath - ? `local:${heroPath}` + const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl; + const logoImageUrl = assetPaths.logo ? `local:${assetPaths.logo}` : game.logoImageUrl; + const libraryHeroImageUrl = assetPaths.hero + ? `local:${assetPaths.hero}` : game.libraryHeroImageUrl; return { iconUrl, logoImageUrl, libraryHeroImageUrl }; @@ -336,9 +304,9 @@ export function EditGameModal({ // Helper function to prepare non-custom game assets const prepareNonCustomGameAssets = () => { return { - customIconUrl: iconPath ? `local:${iconPath}` : null, - customLogoImageUrl: logoPath ? `local:${logoPath}` : null, - customHeroImageUrl: heroPath ? `local:${heroPath}` : null, + customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null, + customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null, + customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null, }; }; @@ -407,9 +375,11 @@ export function EditGameModal({ if (isCustomGame(game)) { setCustomGameAssets(game); // Clear default URLs for custom games - setDefaultIconUrl(null); - setDefaultLogoUrl(null); - setDefaultHeroUrl(null); + setDefaultUrls({ + icon: null, + logo: null, + hero: null, + }); } else { setNonCustomGameAssets(game as LibraryGame); } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index e40061de..dccd9dd1 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -8,6 +8,7 @@ display: flex; transition: all ease 0.2s; cursor: grab; + container-type: inline-size; &:hover { transform: scale(1.05); @@ -160,6 +161,25 @@ transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } + + &-long { + display: inline; + } + + &-short { + display: none; + } + + // When the card is narrow (less than 180px), show short format + @container (max-width: 180px) { + &-long { + display: none; + } + + &-short { + display: inline; + } + } } &__manual-playtime { color: globals.$warning-color; diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index d30dbf8a..ec7736e0 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -2,7 +2,7 @@ import { UserGame } from "@types"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import { useFormat, useToast } from "@renderer/hooks"; import { useNavigate } from "react-router-dom"; -import { useCallback, useContext, useState, useEffect, useRef } from "react"; +import { useCallback, useContext, useState } from "react"; import { buildGameAchievementPath, buildGameDetailsPath, @@ -43,9 +43,7 @@ export function UserLibraryGameCard({ const navigate = useNavigate(); const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isPinning, setIsPinning] = useState(false); - const [useShortFormat, setUseShortFormat] = useState(false); - const cardRef = useRef(null); - const playtimeRef = useRef(null); + const getStatsItemCount = useCallback(() => { let statsCount = 1; @@ -82,64 +80,25 @@ export function UserLibraryGameCard({ }; const formatPlayTime = useCallback( - (playTimeInSeconds = 0) => { + (playTimeInSeconds = 0, isShort = false) => { const minutes = playTimeInSeconds / 60; if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t("amount_minutes", { + return t(isShort ? "amount_minutes_short" : "amount_minutes", { amount: minutes.toFixed(0), }); } const hours = minutes / 60; - return t("amount_hours", { amount: numberFormatter.format(hours) }); + const hoursKey = isShort ? "amount_hours_short" : "amount_hours"; + const hoursAmount = isShort ? Math.floor(hours) : numberFormatter.format(hours); + + return t(hoursKey, { amount: hoursAmount }); }, [numberFormatter, t] ); - const formatPlayTimeShort = useCallback( - (playTimeInSeconds = 0) => { - const minutes = playTimeInSeconds / 60; - if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) { - return t("amount_minutes_short", { - amount: minutes.toFixed(0), - }); - } - - const hours = minutes / 60; - return t("amount_hours_short", { amount: Math.floor(hours) }); - }, - [t] - ); - - const checkForOverlap = useCallback(() => { - if (!cardRef.current || !playtimeRef.current) return; - - const cardWidth = cardRef.current.offsetWidth; - const hasButtons = game.isFavorite || isMe; - - if (hasButtons && cardWidth < 180) { - setUseShortFormat(true); - } else { - setUseShortFormat(false); - } - }, [game.isFavorite, isMe]); - - useEffect(() => { - checkForOverlap(); - - const handleResize = () => { - checkForOverlap(); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [checkForOverlap]); - - useEffect(() => { - checkForOverlap(); - }, [game.isFavorite, isMe, checkForOverlap]); const toggleGamePinned = async () => { setIsPinning(true); @@ -166,7 +125,6 @@ export function UserLibraryGameCard({ return ( <>
  • )} - )} - {useShortFormat - ? formatPlayTimeShort(game.playTimeInSeconds) - : formatPlayTime(game.playTimeInSeconds) - } - + + {formatPlayTime(game.playTimeInSeconds)} + + + {formatPlayTime(game.playTimeInSeconds, true)} + + {userProfile?.hasActiveSubscription && game.achievementCount > 0 && ( From 0f3d6ef76f36e4b10f8d9dd4060d4f3bcf6d8320 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 01:18:44 +0300 Subject: [PATCH 35/43] fix: state fix and remake checking for overlap --- .../pages/game-details/modals/edit-game-modal.tsx | 13 +++++++++---- .../profile-content/user-library-game-card.tsx | 11 +++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 2413cb9e..e55772c6 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -84,7 +84,10 @@ export function EditGameModal({ setDefaultUrls({ icon: shopDetails?.assets?.iconUrl || game.iconUrl || null, logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null, - hero: shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null, + hero: + shopDetails?.assets?.libraryHeroImageUrl || + game.libraryHeroImageUrl || + null, }); }, [shopDetails] @@ -119,11 +122,11 @@ export function EditGameModal({ }; const setAssetPath = (assetType: AssetType, path: string): void => { - setAssetPaths(prev => ({ ...prev, [assetType]: path })); + setAssetPaths((prev) => ({ ...prev, [assetType]: path })); }; const setAssetDisplayPath = (assetType: AssetType, path: string): void => { - setAssetDisplayPaths(prev => ({ ...prev, [assetType]: path })); + setAssetDisplayPaths((prev) => ({ ...prev, [assetType]: path })); }; const getDefaultUrl = (assetType: AssetType): string | null => { @@ -293,7 +296,9 @@ export function EditGameModal({ // Helper function to prepare custom game assets const prepareCustomGameAssets = (game: LibraryGame | Game) => { const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl; - const logoImageUrl = assetPaths.logo ? `local:${assetPaths.logo}` : game.logoImageUrl; + const logoImageUrl = assetPaths.logo + ? `local:${assetPaths.logo}` + : game.logoImageUrl; const libraryHeroImageUrl = assetPaths.hero ? `local:${assetPaths.hero}` : game.libraryHeroImageUrl; diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index ec7736e0..251a3bc7 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -44,7 +44,6 @@ export function UserLibraryGameCard({ const [isTooltipHovered, setIsTooltipHovered] = useState(false); const [isPinning, setIsPinning] = useState(false); - const getStatsItemCount = useCallback(() => { let statsCount = 1; if (game.achievementsPointsEarnedSum > 0) statsCount++; @@ -91,15 +90,15 @@ export function UserLibraryGameCard({ const hours = minutes / 60; const hoursKey = isShort ? "amount_hours_short" : "amount_hours"; - const hoursAmount = isShort ? Math.floor(hours) : numberFormatter.format(hours); - + const hoursAmount = isShort + ? Math.floor(hours) + : numberFormatter.format(hours); + return t(hoursKey, { amount: hoursAmount }); }, [numberFormatter, t] ); - - const toggleGamePinned = async () => { setIsPinning(true); @@ -162,7 +161,7 @@ export function UserLibraryGameCard({ )} )} -
    Date: Tue, 30 Sep 2025 01:43:13 +0300 Subject: [PATCH 36/43] fix: playtime font-size increased --- .../pages/profile/profile-content/user-library-game-card.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index dccd9dd1..a19961fd 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -164,10 +164,12 @@ &-long { display: inline; + font-size: 12px; } &-short { display: none; + font-size: 12px; } // When the card is narrow (less than 180px), show short format From 959bed746b75f74217ee9fc052427f742a4d0f40 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 02:09:19 +0300 Subject: [PATCH 37/43] fix: original path to image not showing in modal after updating game asset --- src/main/events/library/update-custom-game.ts | 8 ++++- .../library/update-game-custom-assets.ts | 18 +++++++++-- src/preload/index.ts | 20 +++++++++--- src/renderer/src/declaration.d.ts | 10 ++++-- .../game-details/modals/edit-game-modal.tsx | 32 +++++++++++++++++-- src/types/level.types.ts | 6 ++++ 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 82d7f45f..62473e54 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -11,7 +11,10 @@ const updateCustomGame = async ( title: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + originalIconPath?: string, + originalLogoPath?: string, + originalHeroPath?: string ) => { const gameKey = levelKeys.game(shop, objectId); @@ -40,6 +43,9 @@ const updateCustomGame = async ( iconUrl: iconUrl || null, logoImageUrl: logoImageUrl || null, libraryHeroImageUrl: libraryHeroImageUrl || null, + originalIconPath: originalIconPath || existingGame.originalIconPath || null, + originalLogoPath: originalLogoPath || existingGame.originalLogoPath || null, + originalHeroPath: originalHeroPath || existingGame.originalHeroPath || null, }; await gamesSublevel.put(gameKey, updatedGame); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 4bd4e517..8cfc79f0 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -38,7 +38,10 @@ const updateGameData = async ( title: string, customIconUrl?: string | null, customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null + customHeroImageUrl?: string | null, + customOriginalIconPath?: string | null, + customOriginalLogoPath?: string | null, + customOriginalHeroPath?: string | null ): Promise => { const updatedGame = { ...existingGame, @@ -46,6 +49,9 @@ const updateGameData = async ( ...(customIconUrl !== undefined && { customIconUrl }), ...(customLogoImageUrl !== undefined && { customLogoImageUrl }), ...(customHeroImageUrl !== undefined && { customHeroImageUrl }), + ...(customOriginalIconPath !== undefined && { customOriginalIconPath }), + ...(customOriginalLogoPath !== undefined && { customOriginalLogoPath }), + ...(customOriginalHeroPath !== undefined && { customOriginalHeroPath }), }; await gamesSublevel.put(gameKey, updatedGame); @@ -87,7 +93,10 @@ const updateGameCustomAssets = async ( title: string, customIconUrl?: string | null, customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null + customHeroImageUrl?: string | null, + customOriginalIconPath?: string | null, + customOriginalLogoPath?: string | null, + customOriginalHeroPath?: string | null ) => { const gameKey = levelKeys.game(shop, objectId); @@ -109,7 +118,10 @@ const updateGameCustomAssets = async ( title, customIconUrl, customLogoImageUrl, - customHeroImageUrl + customHeroImageUrl, + customOriginalIconPath, + customOriginalLogoPath, + customOriginalHeroPath ); await updateShopAssets(gameKey, title); diff --git a/src/preload/index.ts b/src/preload/index.ts index e536f8c7..e4a29c90 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -158,7 +158,10 @@ contextBridge.exposeInMainWorld("electron", { title: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + originalIconPath?: string, + originalLogoPath?: string, + originalHeroPath?: string ) => ipcRenderer.invoke( "updateCustomGame", @@ -167,7 +170,10 @@ contextBridge.exposeInMainWorld("electron", { title, iconUrl, logoImageUrl, - libraryHeroImageUrl + libraryHeroImageUrl, + originalIconPath, + originalLogoPath, + originalHeroPath ), updateGameCustomAssets: ( shop: GameShop, @@ -175,7 +181,10 @@ contextBridge.exposeInMainWorld("electron", { title: string, customIconUrl?: string | null, customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null + customHeroImageUrl?: string | null, + customOriginalIconPath?: string | null, + customOriginalLogoPath?: string | null, + customOriginalHeroPath?: string | null ) => ipcRenderer.invoke( "updateGameCustomAssets", @@ -184,7 +193,10 @@ contextBridge.exposeInMainWorld("electron", { title, customIconUrl, customLogoImageUrl, - customHeroImageUrl + customHeroImageUrl, + customOriginalIconPath, + customOriginalLogoPath, + customOriginalHeroPath ), createGameShortcut: ( shop: GameShop, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 81d18940..9477edb5 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -125,7 +125,10 @@ declare global { title: string, iconUrl?: string, logoImageUrl?: string, - libraryHeroImageUrl?: string + libraryHeroImageUrl?: string, + originalIconPath?: string, + originalLogoPath?: string, + originalHeroPath?: string ) => Promise; copyCustomGameAsset: ( sourcePath: string, @@ -141,7 +144,10 @@ declare global { title: string, customIconUrl?: string | null, customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null + customHeroImageUrl?: string | null, + customOriginalIconPath?: string | null, + customOriginalLogoPath?: string | null, + customOriginalHeroPath?: string | null ) => Promise; createGameShortcut: ( shop: GameShop, diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index e55772c6..37801c6f 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -39,6 +39,11 @@ export function EditGameModal({ logo: "", hero: "", }); + const [originalAssetPaths, setOriginalAssetPaths] = useState({ + icon: "", + logo: "", + hero: "", + }); const [defaultUrls, setDefaultUrls] = useState({ icon: null as string | null, logo: null as string | null, @@ -66,6 +71,11 @@ export function EditGameModal({ logo: extractLocalPath(game.logoImageUrl), hero: extractLocalPath(game.libraryHeroImageUrl), }); + setOriginalAssetPaths({ + icon: (game as any).originalIconPath || extractLocalPath(game.iconUrl), + logo: (game as any).originalLogoPath || extractLocalPath(game.logoImageUrl), + hero: (game as any).originalHeroPath || extractLocalPath(game.libraryHeroImageUrl), + }); }, []); const setNonCustomGameAssets = useCallback( @@ -80,6 +90,11 @@ export function EditGameModal({ logo: extractLocalPath(game.customLogoImageUrl), hero: extractLocalPath(game.customHeroImageUrl), }); + setOriginalAssetPaths({ + icon: (game as any).customOriginalIconPath || extractLocalPath(game.customIconUrl), + logo: (game as any).customOriginalLogoPath || extractLocalPath(game.customLogoImageUrl), + hero: (game as any).customOriginalHeroPath || extractLocalPath(game.customHeroImageUrl), + }); setDefaultUrls({ icon: shopDetails?.assets?.iconUrl || game.iconUrl || null, @@ -118,7 +133,8 @@ export function EditGameModal({ }; const getAssetDisplayPath = (assetType: AssetType): string => { - return assetDisplayPaths[assetType]; + // Use original path if available, otherwise fall back to display path + return originalAssetPaths[assetType] || assetDisplayPaths[assetType]; }; const setAssetPath = (assetType: AssetType, path: string): void => { @@ -153,10 +169,13 @@ export function EditGameModal({ ); setAssetPath(assetType, copiedAssetUrl.replace("local:", "")); setAssetDisplayPath(assetType, originalPath); + // Store the original path for display purposes + setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: originalPath })); } catch (error) { console.error(`Failed to copy ${assetType} asset:`, error); setAssetPath(assetType, originalPath); setAssetDisplayPath(assetType, originalPath); + setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: originalPath })); } } }; @@ -164,6 +183,7 @@ export function EditGameModal({ const handleRestoreDefault = (assetType: AssetType) => { setAssetPath(assetType, ""); setAssetDisplayPath(assetType, ""); + setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" })); }; const getOriginalTitle = (): string => { @@ -326,7 +346,10 @@ export function EditGameModal({ gameName.trim(), iconUrl || undefined, logoImageUrl || undefined, - libraryHeroImageUrl || undefined + libraryHeroImageUrl || undefined, + originalAssetPaths.icon || undefined, + originalAssetPaths.logo || undefined, + originalAssetPaths.hero || undefined ); }; @@ -341,7 +364,10 @@ export function EditGameModal({ gameName.trim(), customIconUrl, customLogoImageUrl, - customHeroImageUrl + customHeroImageUrl, + originalAssetPaths.icon || undefined, + originalAssetPaths.logo || undefined, + originalAssetPaths.hero || undefined ); }; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 73fce370..8a6c56a0 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -38,6 +38,12 @@ export interface Game { customIconUrl?: string | null; customLogoImageUrl?: string | null; customHeroImageUrl?: string | null; + originalIconPath?: string | null; + originalLogoPath?: string | null; + originalHeroPath?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; playTimeInMilliseconds: number; unsyncedDeltaPlayTimeInMilliseconds?: number; lastTimePlayed: Date | null; From ceb236c40c0bae6e3d97ef0f844d54d9f40baa13 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 02:17:17 +0300 Subject: [PATCH 38/43] fix: async error function had too many arguments --- src/main/events/library/update-custom-game.ts | 33 ++++++--- .../library/update-game-custom-assets.ts | 72 +++++++++++++------ src/preload/index.ts | 68 ++++++------------ src/renderer/src/declaration.d.ts | 44 ++++++------ .../game-details/modals/edit-game-modal.tsx | 67 ++++++++++------- 5 files changed, 160 insertions(+), 124 deletions(-) diff --git a/src/main/events/library/update-custom-game.ts b/src/main/events/library/update-custom-game.ts index 62473e54..8129fc57 100644 --- a/src/main/events/library/update-custom-game.ts +++ b/src/main/events/library/update-custom-game.ts @@ -4,18 +4,33 @@ import type { GameShop } from "@types"; import fs from "node:fs"; import { logger } from "@main/services"; +interface UpdateCustomGameParams { + shop: GameShop; + objectId: string; + title: string; + iconUrl?: string; + logoImageUrl?: string; + libraryHeroImageUrl?: string; + originalIconPath?: string; + originalLogoPath?: string; + originalHeroPath?: string; +} + const updateCustomGame = async ( _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string, - title: string, - iconUrl?: string, - logoImageUrl?: string, - libraryHeroImageUrl?: string, - originalIconPath?: string, - originalLogoPath?: string, - originalHeroPath?: string + params: UpdateCustomGameParams ) => { + const { + shop, + objectId, + title, + iconUrl, + logoImageUrl, + libraryHeroImageUrl, + originalIconPath, + originalLogoPath, + originalHeroPath, + } = params; const gameKey = levelKeys.game(shop, objectId); const existingGame = await gamesSublevel.get(gameKey); diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 8cfc79f0..57b14775 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -32,17 +32,32 @@ const collectOldAssetPaths = ( return oldAssetPaths; }; +interface UpdateGameDataParams { + gameKey: string; + existingGame: Game; + title: string; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; +} + const updateGameData = async ( - gameKey: string, - existingGame: Game, - title: string, - customIconUrl?: string | null, - customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null, - customOriginalIconPath?: string | null, - customOriginalLogoPath?: string | null, - customOriginalHeroPath?: string | null + params: UpdateGameDataParams ): Promise => { + const { + gameKey, + existingGame, + title, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl, + customOriginalIconPath, + customOriginalLogoPath, + customOriginalHeroPath, + } = params; const updatedGame = { ...existingGame, title, @@ -86,18 +101,33 @@ const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise => { } }; +interface UpdateGameCustomAssetsParams { + shop: GameShop; + objectId: string; + title: string; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; +} + const updateGameCustomAssets = async ( _event: Electron.IpcMainInvokeEvent, - shop: GameShop, - objectId: string, - title: string, - customIconUrl?: string | null, - customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null, - customOriginalIconPath?: string | null, - customOriginalLogoPath?: string | null, - customOriginalHeroPath?: string | null + params: UpdateGameCustomAssetsParams ) => { + const { + shop, + objectId, + title, + customIconUrl, + customLogoImageUrl, + customHeroImageUrl, + customOriginalIconPath, + customOriginalLogoPath, + customOriginalHeroPath, + } = params; const gameKey = levelKeys.game(shop, objectId); const existingGame = await gamesSublevel.get(gameKey); @@ -112,7 +142,7 @@ const updateGameCustomAssets = async ( customHeroImageUrl ); - const updatedGame = await updateGameData( + const updatedGame = await updateGameData({ gameKey, existingGame, title, @@ -121,8 +151,8 @@ const updateGameCustomAssets = async ( customHeroImageUrl, customOriginalIconPath, customOriginalLogoPath, - customOriginalHeroPath - ); + customOriginalHeroPath, + }); await updateShopAssets(gameKey, title); diff --git a/src/preload/index.ts b/src/preload/index.ts index e4a29c90..b92ba137 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -152,52 +152,28 @@ contextBridge.exposeInMainWorld("electron", { deleteTempFile: (filePath: string) => ipcRenderer.invoke("deleteTempFile", filePath), cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"), - updateCustomGame: ( - shop: GameShop, - objectId: string, - title: string, - iconUrl?: string, - logoImageUrl?: string, - libraryHeroImageUrl?: string, - originalIconPath?: string, - originalLogoPath?: string, - originalHeroPath?: string - ) => - ipcRenderer.invoke( - "updateCustomGame", - shop, - objectId, - title, - iconUrl, - logoImageUrl, - libraryHeroImageUrl, - originalIconPath, - originalLogoPath, - originalHeroPath - ), - updateGameCustomAssets: ( - shop: GameShop, - objectId: string, - title: string, - customIconUrl?: string | null, - customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null, - customOriginalIconPath?: string | null, - customOriginalLogoPath?: string | null, - customOriginalHeroPath?: string | null - ) => - ipcRenderer.invoke( - "updateGameCustomAssets", - shop, - objectId, - title, - customIconUrl, - customLogoImageUrl, - customHeroImageUrl, - customOriginalIconPath, - customOriginalLogoPath, - customOriginalHeroPath - ), + updateCustomGame: (params: { + shop: GameShop; + objectId: string; + title: string; + iconUrl?: string; + logoImageUrl?: string; + libraryHeroImageUrl?: string; + originalIconPath?: string; + originalLogoPath?: string; + originalHeroPath?: string; + }) => ipcRenderer.invoke("updateCustomGame", params), + updateGameCustomAssets: (params: { + shop: GameShop; + objectId: string; + title: string; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; + }) => ipcRenderer.invoke("updateGameCustomAssets", params), createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 9477edb5..e6277888 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -119,17 +119,17 @@ declare global { logoImageUrl?: string, libraryHeroImageUrl?: string ) => Promise; - updateCustomGame: ( - shop: GameShop, - objectId: string, - title: string, - iconUrl?: string, - logoImageUrl?: string, - libraryHeroImageUrl?: string, - originalIconPath?: string, - originalLogoPath?: string, - originalHeroPath?: string - ) => Promise; + updateCustomGame: (params: { + shop: GameShop; + objectId: string; + title: string; + iconUrl?: string; + logoImageUrl?: string; + libraryHeroImageUrl?: string; + originalIconPath?: string; + originalLogoPath?: string; + originalHeroPath?: string; + }) => Promise; copyCustomGameAsset: ( sourcePath: string, assetType: "icon" | "logo" | "hero" @@ -138,17 +138,17 @@ declare global { deletedCount: number; errors: string[]; }>; - updateGameCustomAssets: ( - shop: GameShop, - objectId: string, - title: string, - customIconUrl?: string | null, - customLogoImageUrl?: string | null, - customHeroImageUrl?: string | null, - customOriginalIconPath?: string | null, - customOriginalLogoPath?: string | null, - customOriginalHeroPath?: string | null - ) => Promise; + updateGameCustomAssets: (params: { + shop: GameShop; + objectId: string; + title: string; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; + }) => Promise; createGameShortcut: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 37801c6f..0f6df95d 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -73,8 +73,11 @@ export function EditGameModal({ }); setOriginalAssetPaths({ icon: (game as any).originalIconPath || extractLocalPath(game.iconUrl), - logo: (game as any).originalLogoPath || extractLocalPath(game.logoImageUrl), - hero: (game as any).originalHeroPath || extractLocalPath(game.libraryHeroImageUrl), + logo: + (game as any).originalLogoPath || extractLocalPath(game.logoImageUrl), + hero: + (game as any).originalHeroPath || + extractLocalPath(game.libraryHeroImageUrl), }); }, []); @@ -91,9 +94,15 @@ export function EditGameModal({ hero: extractLocalPath(game.customHeroImageUrl), }); setOriginalAssetPaths({ - icon: (game as any).customOriginalIconPath || extractLocalPath(game.customIconUrl), - logo: (game as any).customOriginalLogoPath || extractLocalPath(game.customLogoImageUrl), - hero: (game as any).customOriginalHeroPath || extractLocalPath(game.customHeroImageUrl), + icon: + (game as any).customOriginalIconPath || + extractLocalPath(game.customIconUrl), + logo: + (game as any).customOriginalLogoPath || + extractLocalPath(game.customLogoImageUrl), + hero: + (game as any).customOriginalHeroPath || + extractLocalPath(game.customHeroImageUrl), }); setDefaultUrls({ @@ -170,12 +179,18 @@ export function EditGameModal({ setAssetPath(assetType, copiedAssetUrl.replace("local:", "")); setAssetDisplayPath(assetType, originalPath); // Store the original path for display purposes - setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: originalPath })); + setOriginalAssetPaths((prev) => ({ + ...prev, + [assetType]: originalPath, + })); } catch (error) { console.error(`Failed to copy ${assetType} asset:`, error); setAssetPath(assetType, originalPath); setAssetDisplayPath(assetType, originalPath); - setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: originalPath })); + setOriginalAssetPaths((prev) => ({ + ...prev, + [assetType]: originalPath, + })); } } }; @@ -340,17 +355,17 @@ export function EditGameModal({ const { iconUrl, logoImageUrl, libraryHeroImageUrl } = prepareCustomGameAssets(game); - return window.electron.updateCustomGame( - game.shop, - game.objectId, - gameName.trim(), - iconUrl || undefined, - logoImageUrl || undefined, - libraryHeroImageUrl || undefined, - originalAssetPaths.icon || undefined, - originalAssetPaths.logo || undefined, - originalAssetPaths.hero || undefined - ); + return window.electron.updateCustomGame({ + shop: game.shop, + objectId: game.objectId, + title: gameName.trim(), + iconUrl: iconUrl || undefined, + logoImageUrl: logoImageUrl || undefined, + libraryHeroImageUrl: libraryHeroImageUrl || undefined, + originalIconPath: originalAssetPaths.icon || undefined, + originalLogoPath: originalAssetPaths.logo || undefined, + originalHeroPath: originalAssetPaths.hero || undefined, + }); }; // Helper function to update non-custom game @@ -358,17 +373,17 @@ export function EditGameModal({ const { customIconUrl, customLogoImageUrl, customHeroImageUrl } = prepareNonCustomGameAssets(); - return window.electron.updateGameCustomAssets( - game.shop, - game.objectId, - gameName.trim(), + return window.electron.updateGameCustomAssets({ + shop: game.shop, + objectId: game.objectId, + title: gameName.trim(), customIconUrl, customLogoImageUrl, customHeroImageUrl, - originalAssetPaths.icon || undefined, - originalAssetPaths.logo || undefined, - originalAssetPaths.hero || undefined - ); + customOriginalIconPath: originalAssetPaths.icon || undefined, + customOriginalLogoPath: originalAssetPaths.logo || undefined, + customOriginalHeroPath: originalAssetPaths.hero || undefined, + }); }; const handleUpdateGame = async () => { From e5646240abdbb08a6972c49078afdfc8ca45c679 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 02:18:22 +0300 Subject: [PATCH 39/43] fix: async error function had too many arguments --- .../library/update-game-custom-assets.ts | 4 +--- src/preload/index.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/events/library/update-game-custom-assets.ts b/src/main/events/library/update-game-custom-assets.ts index 57b14775..1f912901 100644 --- a/src/main/events/library/update-game-custom-assets.ts +++ b/src/main/events/library/update-game-custom-assets.ts @@ -44,9 +44,7 @@ interface UpdateGameDataParams { customOriginalHeroPath?: string | null; } -const updateGameData = async ( - params: UpdateGameDataParams -): Promise => { +const updateGameData = async (params: UpdateGameDataParams): Promise => { const { gameKey, existingGame, diff --git a/src/preload/index.ts b/src/preload/index.ts index b92ba137..17c1225f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -164,16 +164,16 @@ contextBridge.exposeInMainWorld("electron", { originalHeroPath?: string; }) => ipcRenderer.invoke("updateCustomGame", params), updateGameCustomAssets: (params: { - shop: GameShop; - objectId: string; - title: string; - customIconUrl?: string | null; - customLogoImageUrl?: string | null; - customHeroImageUrl?: string | null; - customOriginalIconPath?: string | null; - customOriginalLogoPath?: string | null; - customOriginalHeroPath?: string | null; - }) => ipcRenderer.invoke("updateGameCustomAssets", params), + shop: GameShop; + objectId: string; + title: string; + customIconUrl?: string | null; + customLogoImageUrl?: string | null; + customHeroImageUrl?: string | null; + customOriginalIconPath?: string | null; + customOriginalLogoPath?: string | null; + customOriginalHeroPath?: string | null; + }) => ipcRenderer.invoke("updateGameCustomAssets", params), createGameShortcut: ( shop: GameShop, objectId: string, From de4b039d105332fad4bc36c58d1e4dd323675f56 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 30 Sep 2025 02:46:58 +0300 Subject: [PATCH 40/43] Fix: deleted favorite icon from profile game card and fixed the media for showing short format of hours --- .../user-library-game-card.scss | 24 +---------- .../user-library-game-card.tsx | 40 ++++++++----------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss index a19961fd..5d0d7f2c 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss @@ -91,28 +91,6 @@ z-index: 2; } - &__favorite-icon { - color: rgba(255, 255, 255, 0.8); - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border: solid 1px rgba(255, 255, 255, 0.15); - border-radius: 50%; - padding: 6px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - transition: all ease 0.2s; - - &:hover { - background: rgba(0, 0, 0, 0.5); - border-color: rgba(255, 255, 255, 0.25); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); - } - } - &__pin-button { color: rgba(255, 255, 255, 0.8); background: rgba(0, 0, 0, 0.4); @@ -173,7 +151,7 @@ } // When the card is narrow (less than 180px), show short format - @container (max-width: 180px) { + @container (max-width: 140px) { &-long { display: none; } diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx index 251a3bc7..eac0912d 100644 --- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx @@ -13,7 +13,6 @@ import { ClockIcon, TrophyIcon, AlertFillIcon, - HeartFillIcon, PinIcon, PinSlashIcon, } from "@primer/octicons-react"; @@ -135,30 +134,23 @@ export function UserLibraryGameCard({ onClick={() => navigate(buildUserGameDetailsPath(game))} >
    - {(game.isFavorite || isMe) && ( + {isMe && (
    - {game.isFavorite && ( -
    - -
    - )} - {isMe && ( - - )} +
    )}
    Date: Mon, 29 Sep 2025 22:15:53 +0100 Subject: [PATCH 41/43] feat: sync with main --- .../profile-content/profile-content.tsx | 154 +++--------------- 1 file changed, 27 insertions(+), 127 deletions(-) diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 8de16d3d..41b11ba3 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -5,7 +5,6 @@ import { useAppDispatch, useFormat } from "@renderer/hooks"; import { setHeaderTitle } from "@renderer/features"; import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { UserGame } from "@types"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; @@ -17,8 +16,6 @@ import { useSectionCollapse } from "@renderer/hooks/use-section-collapse"; import { motion, AnimatePresence } from "framer-motion"; import { sectionVariants, - gameCardVariants, - gameGridVariants, chevronVariants, GAME_STATS_ANIMATION_DURATION_IN_MS, } from "./profile-animations"; @@ -38,8 +35,6 @@ export function ProfileContent() { const [statsIndex, setStatsIndex] = useState(0); const [isAnimationRunning, setIsAnimationRunning] = useState(true); const [sortBy, setSortBy] = useState("playedRecently"); - const [prevLibraryGames, setPrevLibraryGames] = useState([]); - const [prevPinnedGames, setPrevPinnedGames] = useState([]); const statsAnimation = useRef(-1); const { toggleSection, isPinnedCollapsed } = useSectionCollapse(); @@ -92,27 +87,6 @@ export function ProfileContent() { const { numberFormatter } = useFormat(); - const gamesHaveChanged = ( - current: UserGame[], - previous: UserGame[] - ): boolean => { - if (current.length !== previous.length) return true; - return current.some( - (game, index) => game.objectId !== previous[index]?.objectId - ); - }; - - const shouldAnimateLibrary = gamesHaveChanged(libraryGames, prevLibraryGames); - const shouldAnimatePinned = gamesHaveChanged(pinnedGames, prevPinnedGames); - - useEffect(() => { - setPrevLibraryGames(libraryGames); - }, [libraryGames]); - - useEffect(() => { - setPrevPinnedGames(pinnedGames); - }, [pinnedGames]); - const usersAreFriends = useMemo(() => { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); @@ -192,57 +166,21 @@ export function ProfileContent() { exit="collapsed" layout > - - {shouldAnimatePinned ? ( - - {pinnedGames?.map((game, index) => ( - - - - ))} - - ) : ( - pinnedGames?.map((game) => ( -
  • - -
  • - )) - )} - +
      + {pinnedGames?.map((game) => ( +
    • + +
    • + ))} +
    )} @@ -262,54 +200,18 @@ export function ProfileContent() { - - {shouldAnimateLibrary ? ( - - {libraryGames?.map((game, index) => ( - - - - ))} - - ) : ( - libraryGames?.map((game) => ( -
  • - -
  • - )) - )} -
    +
      + {libraryGames?.map((game) => ( +
    • + +
    • + ))} +
    )} @@ -338,8 +240,6 @@ export function ProfileContent() { pinnedGames, isPinnedCollapsed, toggleSection, - shouldAnimateLibrary, - shouldAnimatePinned, sortBy, ]); From 26dfb6db8edd4eb07b604fda0df6ec7b03f24472 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Tue, 30 Sep 2025 01:07:45 +0100 Subject: [PATCH 42/43] feat: new style for sidebar on game page --- src/locales/ar/translation.json | 1 - src/locales/be/translation.json | 7 +-- src/locales/bg/translation.json | 1 - src/locales/ca/translation.json | 7 +-- src/locales/cs/translation.json | 1 - src/locales/da/translation.json | 7 +-- src/locales/de/translation.json | 5 +-- src/locales/et/translation.json | 6 +-- src/locales/fa/translation.json | 7 +-- src/locales/fr/translation.json | 1 - src/locales/hu/translation.json | 7 +-- src/locales/id/translation.json | 7 +-- src/locales/it/translation.json | 7 +-- src/locales/kk/translation.json | 7 +-- src/locales/ko/translation.json | 7 +-- src/locales/nb/translation.json | 7 +-- src/locales/nl/translation.json | 7 +-- src/locales/pl/translation.json | 7 +-- src/locales/pt-BR/translation.json | 43 +++++++++++++++++-- src/locales/pt-PT/translation.json | 10 +---- src/locales/ro/translation.json | 7 +-- src/locales/ru/translation.json | 4 +- src/locales/sv/translation.json | 1 - src/locales/tr/translation.json | 1 - src/locales/uk/translation.json | 1 - src/locales/uz/translation.json | 1 - src/locales/zh/translation.json | 3 -- .../description-header.scss | 6 +-- .../gallery-slider/gallery-slider.scss | 41 ++++++++++++++++++ .../game-details/game-details-content.tsx | 23 ++++++++++ .../src/pages/game-details/game-details.scss | 30 ++++++++++--- .../sidebar-section/sidebar-section.scss | 6 +++ .../pages/game-details/sidebar/sidebar.scss | 7 ++- src/renderer/src/pages/home/home.scss | 9 ++++ src/renderer/src/pages/home/home.tsx | 2 +- 35 files changed, 169 insertions(+), 125 deletions(-) diff --git a/src/locales/ar/translation.json b/src/locales/ar/translation.json index 782d6b51..034d0cbd 100644 --- a/src/locales/ar/translation.json +++ b/src/locales/ar/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "تم تسجيل الدخول بنجاح" }, "home": { - "featured": "مميز", "surprise_me": "مفاجئني", "no_results": "لم يتم العثور على نتائج", "start_typing": "ابدأ بالكتابة للبحث...", diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json index c9d49626..8d67e693 100644 --- a/src/locales/be/translation.json +++ b/src/locales/be/translation.json @@ -1,7 +1,6 @@ { "language_name": "беларуская мова", "home": { - "featured": "Рэкамэндаванае", "surprise_me": "Здзіві мяне", "no_results": "Няма вынікаў" }, @@ -17,7 +16,6 @@ "home": "Галоўная", "favorites": "Улюбленыя" }, - "header": { "search": "Пошук", "home": "Галоўная", @@ -31,10 +29,7 @@ "downloading_metadata": "Сцягванне мэтаданых {{title}}…", "downloading": "Сцягванне {{title}}… ({{percentage}} скончана) - Канчатак {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Наступная старонка", - "previous_page": "Папярэдняя старонка" - }, + "catalogue": {}, "game_details": { "open_download_options": "Адкрыць варыянты сцягвання", "download_options_zero": "Няма варыянтаў сцягвання", diff --git a/src/locales/bg/translation.json b/src/locales/bg/translation.json index 458b9e36..3e289700 100644 --- a/src/locales/bg/translation.json +++ b/src/locales/bg/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Успешно влизане" }, "home": { - "featured": "Препоръчани", "surprise_me": "Изненадай ме", "no_results": "Няма намерени резултати", "start_typing": "Започнете да пишете за търсене...", diff --git a/src/locales/ca/translation.json b/src/locales/ca/translation.json index aa69001f..96eb67e2 100644 --- a/src/locales/ca/translation.json +++ b/src/locales/ca/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Has entrat correctament" }, "home": { - "featured": "Destacats", "surprise_me": "Sorprèn-me", "no_results": "No s'ha trobat res" }, @@ -25,7 +24,6 @@ }, "header": { "search": "Cerca jocs", - "home": "Inici", "catalogue": "Catàleg", "downloads": "Baixades", @@ -41,10 +39,7 @@ "calculating_eta": "Descarregant {{title}}… ({{percentage}} completat) - Calculant el temps restant…", "checking_files": "Comprovant els fitxers de {{title}}… ({{percentage}} completat)" }, - "catalogue": { - "next_page": "Pàgina següent", - "previous_page": "Pàgina anterior" - }, + "catalogue": {}, "game_details": { "open_download_options": "Obre les opcions de baixada", "download_options_zero": "No hi ha opcions de baixada", diff --git a/src/locales/cs/translation.json b/src/locales/cs/translation.json index 9b501b54..6bcc8944 100644 --- a/src/locales/cs/translation.json +++ b/src/locales/cs/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Úspěšně přihlášen" }, "home": { - "featured": "Doporučené", "surprise_me": "Překvap mě", "no_results": "Výsledek nenalezen", "start_typing": "Začni psát pro vyhledávání...", diff --git a/src/locales/da/translation.json b/src/locales/da/translation.json index 618f085c..21a92f72 100644 --- a/src/locales/da/translation.json +++ b/src/locales/da/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Loggede ind successfuldt" }, "home": { - "featured": "Anbefalet", "surprise_me": "Overrask mig", "no_results": "Ingen resultater fundet", "start_typing": "Begynd at skrive for at søge...", @@ -29,7 +28,6 @@ }, "header": { "search": "Søg efter spil", - "home": "Hjem", "catalogue": "Katalog", "downloads": "Downloads", @@ -45,10 +43,7 @@ "calculating_eta": "Downloader {{title}}… ({{percentage}} færdig) - Udregner resterende tid…", "checking_files": "Checker {{title}} filer… ({{percentage}} færdig)" }, - "catalogue": { - "next_page": "Næste side", - "previous_page": "Forrige side" - }, + "catalogue": {}, "game_details": { "open_download_options": "Åben download muligheder", "download_options_zero": "Ingen download mulighed", diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 5101f459..fb285ee0 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Erfolgreich angemeldet" }, "home": { - "featured": "Empfohlen", "surprise_me": "Überrasche mich", "no_results": "Keine Ergebnisse gefunden", "start_typing": "Tippe, um zu suchen...", @@ -59,9 +58,7 @@ "download_sources": "Download-Quellen", "result_count": "{{resultCount}} Ergebnisse", "filter_count": "{{filterCount}} verfügbar", - "clear_filters": "{{filterCount}} ausgewählte löschen", - "next_page": "Nächste Seite", - "previous_page": "Vorherige Seite" + "clear_filters": "{{filterCount}} ausgewählte löschen" }, "game_details": { "open_download_options": "Download-Optionen öffnen", diff --git a/src/locales/et/translation.json b/src/locales/et/translation.json index 119e1aab..c5566eeb 100644 --- a/src/locales/et/translation.json +++ b/src/locales/et/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Edukalt sisse logitud" }, "home": { - "featured": "Esile toodud", "surprise_me": "Üllata mind", "no_results": "Tulemusi ei leitud", "start_typing": "Alusta otsimiseks kirjutamist...", @@ -45,10 +44,7 @@ "calculating_eta": "{{title}} allalaadimine… ({{percentage}} valmis) - Järelejäänud aja arvutamine…", "checking_files": "{{title}} failide kontrollimine… ({{percentage}} valmis)" }, - "catalogue": { - "next_page": "Järgmine leht", - "previous_page": "Eelmine leht" - }, + "catalogue": {}, "game_details": { "open_download_options": "Ava allalaadimise valikud", "download_options_zero": "Allalaadimise valikuid pole", diff --git a/src/locales/fa/translation.json b/src/locales/fa/translation.json index be18263a..69a49b79 100644 --- a/src/locales/fa/translation.json +++ b/src/locales/fa/translation.json @@ -1,7 +1,6 @@ { "language_name": "فارسی", "home": { - "featured": "پیشنهادی", "surprise_me": "سوپرایزم کن", "no_results": "اتمام‌ای پیدا نشد" }, @@ -17,7 +16,6 @@ "home": "خانه", "favorites": "علاقه‌مندی‌ها" }, - "header": { "search": "جستجوی بازی‌ها", "home": "خانه", @@ -31,10 +29,7 @@ "downloading_metadata": "درحال دانلود متادیتاهای {{title}}…", "downloading": "در حال دانلود {{title}}… ({{percentage}} تکمیل شده) - اتمام {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "صفحه‌ی بعدی", - "previous_page": "صفحه‌ی قبلی" - }, + "catalogue": {}, "game_details": { "open_download_options": "بازکردن آپشن‌های دانلود", "download_options_zero": "هیچ آپشن دانلودی وجود ندارد", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 1c129a64..8fc07722 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Connecté avec succès" }, "home": { - "featured": "En vedette", "surprise_me": "Surprenez-moi", "no_results": "Aucun résultat trouvé", "start_typing": "Commencez à taper pour rechercher...", diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 0cea87b0..efed5e2d 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -1,7 +1,6 @@ { "language_name": "Magyar", "home": { - "featured": "Featured", "surprise_me": "Lepj meg", "no_results": "Nem található" }, @@ -19,7 +18,6 @@ }, "header": { "search": "Keresés", - "home": "Főoldal", "catalogue": "Katalógus", "downloads": "Letöltések", @@ -31,10 +29,7 @@ "downloading_metadata": "{{title}} metaadatainak letöltése…", "downloading": "{{title}} letöltése… ({{percentage}} kész) - Befejezés {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Következő olda", - "previous_page": "Előző olda" - }, + "catalogue": {}, "game_details": { "open_download_options": "Letöltési lehetőségek", "download_options_zero": "Nincs letöltési lehetőség", diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json index 4fa347fc..fc72fc51 100644 --- a/src/locales/id/translation.json +++ b/src/locales/id/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Berhasil masuk" }, "home": { - "featured": "Unggulan", "surprise_me": "Kejutkan saya", "no_results": "Tidak ada hasil ditemukan" }, @@ -25,7 +24,6 @@ }, "header": { "search": "Cari game", - "home": "Beranda", "catalogue": "Katalog", "downloads": "Unduhan", @@ -41,10 +39,7 @@ "calculating_eta": "Mengunduh {{title}}… ({{percentage}} selesai) - Menghitung waktu yang tersisa…", "checking_files": "Memeriksa file {{title}}… ({{percentage}} selesai)" }, - "catalogue": { - "next_page": "Halaman Berikutnya", - "previous_page": "Halaman Sebelumnya" - }, + "catalogue": {}, "game_details": { "open_download_options": "Buka opsi unduhan", "download_options_zero": "Tidak ada opsi unduhan", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index b23d1244..ac37ffe9 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1,7 +1,6 @@ { "language_name": "Italiano", "home": { - "featured": "In primo piano", "surprise_me": "Sorprendimi", "no_results": "Nessun risultato trovato" }, @@ -20,7 +19,6 @@ }, "header": { "search": "Cerca", - "home": "Home", "catalogue": "Catalogo", "downloads": "Download", @@ -32,10 +30,7 @@ "downloading_metadata": "Scaricamento metadati di {{title}}…", "downloading": "Download di {{title}}… ({{percentage}} completato) - Conclusione {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Pagina successiva", - "previous_page": "Pagina precedente" - }, + "catalogue": {}, "game_details": { "open_download_options": "Apri opzioni di download", "download_options_zero": "Nessuna opzione di download", diff --git a/src/locales/kk/translation.json b/src/locales/kk/translation.json index bfb009a7..48fb8181 100644 --- a/src/locales/kk/translation.json +++ b/src/locales/kk/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Сәтті кіру" }, "home": { - "featured": "Ұсынылған", "surprise_me": "Таңқалдыр", "no_results": "Ештеңе табылмады" }, @@ -23,7 +22,6 @@ "sign_in": "Кіру", "favorites": "Таңдаулылар" }, - "header": { "search": "Іздеу", "home": "Басты бет", @@ -40,10 +38,7 @@ "downloading": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Аяқтау {{eta}} - {{speed}}", "calculating_eta": "Жүктеу {{title}}… ({{percentage}} аяқталды) - Қалған уақытты есептеу…" }, - "catalogue": { - "next_page": "Келесі бет", - "previous_page": "Алдыңғы бет" - }, + "catalogue": {}, "game_details": { "open_download_options": "Жүктеу нұсқаларын ашу", "download_options_zero": "Жүктеу нұсқалары жоқ", diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 9ec389b1..a9b9c0e5 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -1,7 +1,6 @@ { "language_name": "한국어", "home": { - "featured": "추천", "surprise_me": "무작위 추천", "no_results": "결과 없음" }, @@ -17,7 +16,6 @@ "home": "홈", "favorites": "즐겨찾기" }, - "header": { "search": "게임 검색하기", "home": "홈", @@ -31,10 +29,7 @@ "downloading_metadata": "{{title}}의 메타데이터를 다운로드 중…", "downloading": "{{title}}의 파일들을 다운로드 중… ({{percentage}} 완료) - 완료까지 {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "다음 페이지", - "previous_page": "이전 페이지" - }, + "catalogue": {}, "game_details": { "open_download_options": "다운로드 선택지 열기", "download_options_zero": "다운로드 선택지 없음", diff --git a/src/locales/nb/translation.json b/src/locales/nb/translation.json index 8898ec7b..95bda8fe 100644 --- a/src/locales/nb/translation.json +++ b/src/locales/nb/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Logget inn vellykket" }, "home": { - "featured": "Anbefalinger", "surprise_me": "Overrask meg", "no_results": "Ingen resultater fundet", "start_typing": "Begynn å skrive for å søke...", @@ -29,7 +28,6 @@ }, "header": { "search": "Søk efter spill", - "home": "Hjem", "catalogue": "Katalog", "downloads": "Nedlastinger", @@ -45,10 +43,7 @@ "calculating_eta": "Laster ned {{title}}… ({{percentage}} ferdig) - Regner ut resterende tid…", "checking_files": "Sjekker {{title}} filer… ({{percentage}} ferdig)" }, - "catalogue": { - "next_page": "Neste side", - "previous_page": "Forrige side" - }, + "catalogue": {}, "game_details": { "open_download_options": "Åpne nedlastingsmuligheter", "download_options_zero": "Ingen nedlastingsmulighet", diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index 72d20c74..baa6df6e 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -1,7 +1,6 @@ { "language_name": "Nederlands", "home": { - "featured": "Uitgelicht", "surprise_me": "Verrasing", "no_results": "Geen resultaten gevonden" }, @@ -19,7 +18,6 @@ }, "header": { "search": "Zoek spellen", - "home": "Home", "catalogue": "Bibliotheek", "downloads": "Downloads", @@ -31,10 +29,7 @@ "downloading_metadata": "Downloading {{title}} metadata…", "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Volgende Pagina", - "previous_page": "Vorige Pagina" - }, + "catalogue": {}, "game_details": { "open_download_options": "Open download Instellingen", "download_options_zero": "Geen download Instellingen", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index 86751b0e..2e0d1696 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -1,7 +1,6 @@ { "language_name": "Polski", "home": { - "featured": "Wyróżnione", "surprise_me": "Zaskocz mnie", "no_results": "Nie znaleziono wyników" }, @@ -20,7 +19,6 @@ }, "header": { "search": "Szukaj", - "home": "Główna", "catalogue": "Katalog", "downloads": "Pobrane", @@ -32,10 +30,7 @@ "downloading_metadata": "Pobieranie {{title}} metadata…", "downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}" }, - "catalogue": { - "next_page": "Następna strona", - "previous_page": "Poprzednia strona" - }, + "catalogue": {}, "game_details": { "open_download_options": "Otwórz opcje pobierania", "download_options_zero": "Brak opcji pobierania", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 7f7f8cc1..b88c38a5 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -26,7 +26,22 @@ "sign_in": "Login", "friends": "Amigos", "need_help": "Precisa de ajuda?", - "favorites": "Favoritos" + "favorites": "Favoritos", + "add_custom_game_tooltip": "Adicionar jogo personalizado", + "custom_game_modal": "Adicionar jogo personalizado", + "edit_game_modal_title": "Título", + "playable_button_title": "", + "custom_game_modal_add": "Adicionar Jogo", + "custom_game_modal_adding": "Adicionando...", + "custom_game_modal_browse": "Buscar", + "custom_game_modal_cancel": "Cancelar", + "edit_game_modal_assets": "Imagens", + "edit_game_modal_icon": "Ícone", + "edit_game_modal_browse": "Buscar", + "edit_game_modal_cancel": "Cancelar", + "edit_game_modal_enter_title": "Insira o título", + "edit_game_modal_logo": "Logo", + "edit_game_modal": "Personalizar detalhes" }, "header": { "search": "Buscar jogos", @@ -219,7 +234,18 @@ "historical_keyshop": "Preço histórico em keyshops", "language": "Idioma", "caption": "Legenda", - "audio": "Áudio" + "audio": "Áudio", + "edit_game_modal_button": "Alterar detalhes do jogo", + "game_added_to_pinned": "Jogo adicionado aos fixados", + "game_removed_from_pinned": "Jogo removido dos fixados", + "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente", + "manual_playtime_warning": "As suas horas de jogo serão marcadas como atualizadas manualmente. Esta ação não pode ser desfeita.", + "missing_wine_prefix": "Um prefixo Wine é necessário para criar um backup no Linux", + "update_game_playtime": "Modificar tempo de jogo", + "update_playtime": "Modificar tempo de jogo", + "update_playtime_description": "Atualizar manualmente o tempo de jogo de {{game}}", + "update_playtime_error": "Falha ao atualizar tempo de jogo", + "update_playtime_title": "Atualizar tempo de jogo" }, "activation": { "title": "Ativação", @@ -394,7 +420,8 @@ "hidden": "Oculta", "test_notification": "Testar notificação", "notification_preview": "Prévia da Notificação de Conquistas", - "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo" + "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo", + "editor_tab_code": "Código" }, "notifications": { "download_complete": "Download concluído", @@ -523,7 +550,15 @@ "show_achievements_on_profile": "Exiba suas conquistas no perfil", "show_points_on_profile": "Exiba seus pontos ganhos no perfil", "error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido", - "friend_code_length_error": "Código de amigo deve ter 8 caracteres" + "friend_code_length_error": "Código de amigo deve ter 8 caracteres", + "top_percentile": "Top {{percentile}}%", + "playtime": "Tempo de jogo", + "played_recently": "Jogado recentemente", + "pinned": "Fixado", + "amount_minutes_short": "{{amount}}h", + "amount_hours_short": "{{amount}}h", + "game_added_to_pinned": "Jogo adicionado aos fixados", + "achievements_earned": "Conquistas recebidas" }, "achievement": { "achievement_unlocked": "Conquista desbloqueada", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 6c32b35b..654e94ec 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Sessão iniciada com sucesso" }, "home": { - "featured": "Destaques", "hot": "Populares", "weekly": "📅 Mais descarregados esta semana", "achievements": "🏆 Para completar", @@ -26,7 +25,8 @@ "game_has_no_executable": "O jogo não tem um executável selecionado", "sign_in": "Iniciar sessão", "friends": "Amigos", - "favorites": "Favoritos" + "favorites": "Favoritos", + "edit_game_modal_cancel": "Cancelar" }, "header": { "search": "Procurar jogos", @@ -247,9 +247,6 @@ "download_count_zero": "Sem downloads na lista", "download_count_one": "{{countFormatted}} download na lista", "download_count_other": "{{countFormatted}} downloads na lista", - "download_options_zero": "Sem downloads disponíveis", - "download_options_one": "{{countFormatted}} download disponível", - "download_options_other": "{{countFormatted}} downloads disponíveis", "download_source_url": "URL da fonte", "add_download_source_description": "Insere o URL que contém o ficheiro .json", "download_source_up_to_date": "Sincronizada", @@ -359,8 +356,6 @@ "instructions": "Verifica a forma correta de instalar algum deles na tua distribuição Linux, para garantir a execução normal do jogo" }, "catalogue": { - "next_page": "Página seguinte", - "previous_page": "Página anterior", "search": "Filtrar…", "developers": "Desenvolvedores", "genres": "Géneros", @@ -427,7 +422,6 @@ "friend_code_copied": "Código de amigo copiado", "undo_friendship_modal_text": "Isto vai remover a tua amizade com {{displayName}}", "privacy_hint": "Para controlar quem pode ver o teu perfil, acede às <0>Definições", - "profile_locked": "Este perfil é privado", "image_process_failure": "Falha ao processar a imagem", "required_field": "Este campo é obrigatório", "displayname_min_length": "O nome de apresentação deve ter pelo menos 3 caracteres", diff --git a/src/locales/ro/translation.json b/src/locales/ro/translation.json index c5a81881..8ed6fd39 100644 --- a/src/locales/ro/translation.json +++ b/src/locales/ro/translation.json @@ -1,7 +1,6 @@ { "language_name": "Română", "home": { - "featured": "Recomandate", "surprise_me": "Surprinde-mă", "no_results": "Niciun rezultat găsit" }, @@ -19,7 +18,6 @@ }, "header": { "search": "Caută jocuri", - "home": "Acasă", "catalogue": "Catalog", "downloads": "Descărcări", @@ -32,10 +30,7 @@ "downloading": "Se descarcă {{title}}... ({{percentage}} complet) - Concluzie {{eta}} - {{speed}}", "calculating_eta": "Se descarcă {{title}}... ({{percentage}} complet) - Calculare timp rămas..." }, - "catalogue": { - "next_page": "Pagina următoare", - "previous_page": "Pagina anterioară" - }, + "catalogue": {}, "game_details": { "open_download_options": "Deschide opțiunile de descărcare", "download_options_zero": "Nicio opțiune de descărcare", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index c92d7902..03413554 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -4,14 +4,12 @@ "successfully_signed_in": "Успешный вход" }, "home": { - "featured": "Рекомендации", "surprise_me": "Удиви меня", "no_results": "Ничего не найдено", "hot": "Сейчас популярно", "start_typing": "Начинаю вводить текст...", "weekly": "📅 Лучшие игры недели", - "achievements": "🏆 Игры с достижениями", - "already_in_library": "Уже в библиотеке" + "achievements": "🏆 Игры с достижениями" }, "sidebar": { "catalogue": "Каталог", diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json index 0972effa..901e4ca7 100644 --- a/src/locales/sv/translation.json +++ b/src/locales/sv/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Inloggningen lyckades" }, "home": { - "featured": "Utvalt", "surprise_me": "Överraska mig", "no_results": "Inga resultat hittades", "start_typing": "Börja skriva för att söka...", diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json index c3fe2081..e8e1cb2b 100644 --- a/src/locales/tr/translation.json +++ b/src/locales/tr/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Başarıyla giriş yapıldı" }, "home": { - "featured": "Öne Çıkanlar", "surprise_me": "Beni Şaşırt", "no_results": "Sonuç bulunamadı", "start_typing": "Aramak için yazmaya başlayın...", diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json index 48a3972d..26aa8aae 100644 --- a/src/locales/uk/translation.json +++ b/src/locales/uk/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Успішний вхід в систему" }, "home": { - "featured": "Рекомендоване", "surprise_me": "Здивуй мене", "no_results": "Результатів не знайдено", "start_typing": "Почніть набирати текст для пошуку...", diff --git a/src/locales/uz/translation.json b/src/locales/uz/translation.json index d20a9677..24e508af 100644 --- a/src/locales/uz/translation.json +++ b/src/locales/uz/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "Kirish muvaffaqiyatli amalga oshirildi" }, "home": { - "featured": "Tavsiya etilgan", "surprise_me": "Hayratda qoldir", "no_results": "Natijalar topilmadi", "hot": "Eng mashhur", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 0323d991..7cdd0c92 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -4,7 +4,6 @@ "successfully_signed_in": "已成功登录" }, "home": { - "featured": "特色推荐", "surprise_me": "向我推荐", "no_results": "没有找到结果", "start_typing": "键入以开始搜素...", @@ -51,8 +50,6 @@ "installing_common_redist": "{{log}}…" }, "catalogue": { - "next_page": "下一页", - "previous_page": "上一页", "clear_filters": "清除已选的 {{filterCount}} 项", "developers": "开发商", "download_sources": "下载源", diff --git a/src/renderer/src/pages/game-details/description-header/description-header.scss b/src/renderer/src/pages/game-details/description-header/description-header.scss index 920e8068..8ca78eaa 100644 --- a/src/renderer/src/pages/game-details/description-header/description-header.scss +++ b/src/renderer/src/pages/game-details/description-header/description-header.scss @@ -2,7 +2,7 @@ .description-header { width: calc(100% - calc(globals.$spacing-unit * 2)); - margin: calc(globals.$spacing-unit * 1) auto; + margin: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 1); padding: calc(globals.$spacing-unit * 1.5); display: flex; justify-content: space-between; @@ -10,8 +10,8 @@ background-color: globals.$background-color; height: 72px; border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.03); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); &__info { display: flex; diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss index f66da32b..9483b50e 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss @@ -7,6 +7,15 @@ display: flex; flex-direction: column; align-items: center; + max-height: 80vh; + + @media (min-width: 1024px) { + max-height: 70vh; + } + + @media (min-width: 1280px) { + max-height: 60vh; + } } &__viewport { @@ -16,8 +25,19 @@ overflow: hidden; border-radius: 8px; + @media (min-width: 1024px) { + width: 80%; + max-height: 400px; + } + @media (min-width: 1280px) { width: 60%; + max-height: 500px; + } + + @media (min-width: 1536px) { + width: 50%; + max-height: 600px; } } @@ -52,10 +72,18 @@ overflow-y: hidden; gap: calc(globals.$spacing-unit / 2); + @media (min-width: 1024px) { + width: 80%; + } + @media (min-width: 1280px) { width: 60%; } + @media (min-width: 1536px) { + width: 50%; + } + &::-webkit-scrollbar-thumb { width: 20%; } @@ -79,6 +107,19 @@ border: solid 1px globals.$border-color; overflow: hidden; position: relative; + aspect-ratio: 16/9; + + @media (min-width: 1024px) { + width: 15%; + } + + @media (min-width: 1280px) { + width: 12%; + } + + @media (min-width: 1536px) { + width: 10%; + } &:hover { opacity: 0.8; diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 347e5a1c..4e9ecf14 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -43,6 +43,29 @@ export function GameDetailsContent() { const $images = Array.from(document.querySelectorAll("img")); $images.forEach(($image) => { $image.loading = "lazy"; + // Remove any inline width/height styles that might cause overflow + $image.removeAttribute("width"); + $image.removeAttribute("height"); + $image.removeAttribute("style"); + // Set max-width to prevent overflow + $image.style.maxWidth = "100%"; + $image.style.width = "auto"; + $image.style.height = "auto"; + $image.style.boxSizing = "border-box"; + }); + + // Handle videos the same way + const $videos = Array.from(document.querySelectorAll("video")); + $videos.forEach(($video) => { + // Remove any inline width/height styles that might cause overflow + $video.removeAttribute("width"); + $video.removeAttribute("height"); + $video.removeAttribute("style"); + // Set max-width to prevent overflow + $video.style.maxWidth = "100%"; + $video.style.width = "auto"; + $video.style.height = "auto"; + $video.style.boxSizing = "border-box"; }); return document.body.outerHTML; diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 786a8d30..6b02dde5 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -186,9 +186,10 @@ $hero-height: 300px; &__description-content { width: 100%; - height: 100%; + min-height: 100%; min-width: 0; flex: 1; + overflow-x: hidden; } &__description { @@ -199,6 +200,8 @@ $hero-height: 300px; width: 100%; margin-left: auto; margin-right: auto; + overflow-x: auto; + min-height: auto; @media (min-width: 768px) { padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); @@ -206,20 +209,30 @@ $hero-height: 300px; @media (min-width: 1024px) { padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + width: 80%; } @media (min-width: 1280px) { width: 60%; } - img { + @media (min-width: 1536px) { + width: 50%; + } + + img, + video { border-radius: 5px; margin-top: globals.$spacing-unit; margin-bottom: calc(globals.$spacing-unit * 3); - display: block; - width: 100%; - height: auto; - object-fit: cover; + display: block !important; + max-width: 100% !important; + width: auto !important; + height: auto !important; + object-fit: contain !important; + box-sizing: border-box !important; + word-wrap: break-word; + overflow-wrap: break-word; } a { @@ -247,12 +260,17 @@ $hero-height: 300px; @media (min-width: 1024px) { padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2); + width: 80%; } @media (min-width: 1280px) { width: 60%; line-height: 22px; } + + @media (min-width: 1536px) { + width: 50%; + } } &__randomizer-button { diff --git a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss index 8674b044..f86db399 100644 --- a/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss +++ b/src/renderer/src/pages/game-details/sidebar-section/sidebar-section.scss @@ -1,6 +1,12 @@ @use "../../../scss/globals.scss"; .sidebar-section { + background-color: globals.$dark-background-color; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow: hidden; + &__button { padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); display: flex; diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index d1c54f84..4d212440 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -1,11 +1,14 @@ @use "../../../scss/globals.scss"; .content-sidebar { - border-left: solid 1px globals.$border-color; - background-color: globals.$dark-background-color; + background-color: transparent; height: 100%; flex-shrink: 0; width: 280px; + padding: calc(globals.$spacing-unit * 1); + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 1.5); @media (min-width: 1024px) { width: 320px; diff --git a/src/renderer/src/pages/home/home.scss b/src/renderer/src/pages/home/home.scss index 497f074e..478e96a1 100644 --- a/src/renderer/src/pages/home/home.scss +++ b/src/renderer/src/pages/home/home.scss @@ -76,6 +76,15 @@ width: 24px; height: 24px; position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + &__title-flame-icon { + width: 32px; + height: 32px; + object-fit: contain; } &__title { diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx index e2f66283..0b762882 100644 --- a/src/renderer/src/pages/home/home.tsx +++ b/src/renderer/src/pages/home/home.tsx @@ -158,7 +158,7 @@ export default function Home() { Flame animation )} From 0df5486fecb7563ec0015df157c1597b4d39328b Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Tue, 30 Sep 2025 03:59:08 +0100 Subject: [PATCH 43/43] fix: fixing how long to beat broken logic --- .../game-details/sidebar/how-long-to-beat-section.tsx | 2 +- src/renderer/src/pages/game-details/sidebar/sidebar.scss | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx index 61c90389..9a29f150 100644 --- a/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx +++ b/src/renderer/src/pages/game-details/sidebar/how-long-to-beat-section.tsx @@ -25,7 +25,7 @@ export function HowLongToBeatSection({ return `${value} ${t(durationTranslation[unit])}`; }; - if (!howLongToBeatData && !isLoading) return null; + if (!howLongToBeatData || !isLoading) return null; return ( diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.scss b/src/renderer/src/pages/game-details/sidebar/sidebar.scss index 4d212440..d1c54f84 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.scss +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.scss @@ -1,14 +1,11 @@ @use "../../../scss/globals.scss"; .content-sidebar { - background-color: transparent; + border-left: solid 1px globals.$border-color; + background-color: globals.$dark-background-color; height: 100%; flex-shrink: 0; width: 280px; - padding: calc(globals.$spacing-unit * 1); - display: flex; - flex-direction: column; - gap: calc(globals.$spacing-unit * 1.5); @media (min-width: 1024px) { width: 320px;