mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
added proper image saving for custom games + edited game settings to hide buttons if game is custom
This commit is contained in:
@@ -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";
|
||||
|
||||
73
src/main/events/library/cleanup-unused-assets.ts
Normal file
73
src/main/events/library/cleanup-unused-assets.ts
Normal 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);
|
||||
42
src/main/events/library/copy-custom-game-asset.ts
Normal file
42
src/main/events/library/copy-custom-game-asset.ts
Normal 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);
|
||||
@@ -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,
|
||||
|
||||
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user