From 9f4fd0ce61805e5f447c304ebf05c398874bb1e3 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 19 Sep 2025 20:46:53 +0300 Subject: [PATCH] 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 && ( - {game.download?.downloadPath && ( +
- )} + {game.download?.downloadPath && ( + + )} +
- + )}
@@ -493,18 +499,20 @@ export function GameOptionsModal({ {t("remove_from_library")} - + {game.shop !== "custom" && ( + + )} - + {game.shop !== "custom" && ( + + )}