added proper image saving for custom games + edited game settings to hide buttons if game is custom

This commit is contained in:
Moyasee
2025-09-19 20:46:53 +03:00
parent bf9e3de0b5
commit 9f4fd0ce61
8 changed files with 242 additions and 75 deletions

View File

@@ -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";

View File

@@ -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<string[]> => {
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<Set<string>> => {
// 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<string>();
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);

View File

@@ -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<string> => {
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);

View File

@@ -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,

View File

@@ -126,6 +126,11 @@ declare global {
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => Promise<Game>;
copyCustomGameAsset: (
sourcePath: string,
assetType: "icon" | "logo" | "hero"
) => Promise<string>;
cleanupUnusedAssets: () => Promise<{ deletedCount: number; errors: string[] }>;
updateGameCustomAssets: (
shop: GameShop,
objectId: string,

View File

@@ -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()

View File

@@ -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]);
}
}
};

View File

@@ -353,14 +353,16 @@ export function GameOptionsModal({
>
{t("create_shortcut")}
</Button>
<Button
onClick={handleCreateSteamShortcut}
theme="outline"
disabled={creatingSteamShortcut}
>
<SteamLogo />
{t("create_steam_shortcut")}
</Button>
{game.shop !== "custom" && (
<Button
onClick={handleCreateSteamShortcut}
theme="outline"
disabled={creatingSteamShortcut}
>
<SteamLogo />
{t("create_steam_shortcut")}
</Button>
)}
{shouldShowCreateStartMenuShortcut && (
<Button
onClick={() => handleCreateShortcut("start_menu")}
@@ -374,19 +376,21 @@ export function GameOptionsModal({
</div>
</div>
<CheckboxField
label={
<div className="game-options-modal__cloud-sync-label">
{t("enable_automatic_cloud_sync")}
<span className="game-options-modal__cloud-sync-hydra-cloud">
Hydra Cloud
</span>
</div>
}
checked={automaticCloudSync}
disabled={!hasActiveSubscription || !game.executablePath}
onChange={handleToggleAutomaticCloudSync}
/>
{game.shop !== "custom" && (
<CheckboxField
label={
<div className="game-options-modal__cloud-sync-label">
{t("enable_automatic_cloud_sync")}
<span className="game-options-modal__cloud-sync-hydra-cloud">
Hydra Cloud
</span>
</div>
}
checked={automaticCloudSync}
disabled={!hasActiveSubscription || !game.executablePath}
onChange={handleToggleAutomaticCloudSync}
/>
)}
{shouldShowWinePrefixConfiguration && (
<div className="game-options-modal__wine-prefix">
@@ -448,33 +452,35 @@ export function GameOptionsModal({
/>
</div>
<div className="game-options-modal__downloads">
<div className="game-options-modal__header">
<h2>{t("downloads_section_title")}</h2>
<h4 className="game-options-modal__header-description">
{t("downloads_section_description")}
</h4>
</div>
{game.shop !== "custom" && (
<div className="game-options-modal__downloads">
<div className="game-options-modal__header">
<h2>{t("downloads_section_title")}</h2>
<h4 className="game-options-modal__header-description">
{t("downloads_section_description")}
</h4>
</div>
<div className="game-options-modal__row">
<Button
onClick={() => setShowRepacksModal(true)}
theme="outline"
disabled={deleting || isGameDownloading || !repacks.length}
>
{t("open_download_options")}
</Button>
{game.download?.downloadPath && (
<div className="game-options-modal__row">
<Button
onClick={handleOpenDownloadFolder}
onClick={() => setShowRepacksModal(true)}
theme="outline"
disabled={deleting}
disabled={deleting || isGameDownloading || !repacks.length}
>
{t("open_download_location")}
{t("open_download_options")}
</Button>
)}
{game.download?.downloadPath && (
<Button
onClick={handleOpenDownloadFolder}
theme="outline"
disabled={deleting}
>
{t("open_download_location")}
</Button>
)}
</div>
</div>
</div>
)}
<div className="game-options-modal__danger-zone">
<div className="game-options-modal__header">
@@ -493,18 +499,20 @@ export function GameOptionsModal({
{t("remove_from_library")}
</Button>
<Button
onClick={() => setShowResetAchievementsModal(true)}
theme="danger"
disabled={
deleting ||
isDeletingAchievements ||
!hasAchievements ||
!userDetails
}
>
{t("reset_achievements")}
</Button>
{game.shop !== "custom" && (
<Button
onClick={() => setShowResetAchievementsModal(true)}
theme="danger"
disabled={
deleting ||
isDeletingAchievements ||
!hasAchievements ||
!userDetails
}
>
{t("reset_achievements")}
</Button>
)}
<Button
onClick={() => setShowChangePlaytimeModal(true)}
@@ -513,17 +521,19 @@ export function GameOptionsModal({
{t("update_game_playtime")}
</Button>
<Button
onClick={() => {
setShowDeleteModal(true);
}}
theme="danger"
disabled={
isGameDownloading || deleting || !game.download?.downloadPath
}
>
{t("remove_files")}
</Button>
{game.shop !== "custom" && (
<Button
onClick={() => {
setShowDeleteModal(true);
}}
theme="danger"
disabled={
isGameDownloading || deleting || !game.download?.downloadPath
}
>
{t("remove_files")}
</Button>
)}
</div>
</div>
</div>