Preload images for non-custom games. Added ability to restore images to default if game is non-custom

This commit is contained in:
Moyasee
2025-09-19 21:06:15 +03:00
parent 9f4fd0ce61
commit de70beb01e
7 changed files with 137 additions and 48 deletions

View File

@@ -9,27 +9,27 @@ const getCustomGamesAssetsPath = () => {
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));
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);
.filter((game) => game.shop === "custom" && !game.isDeleted);
const usedPaths = new Set<string>();
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<Set<string>> => {
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);
ipcMain.handle("cleanupUnusedAssets", cleanupUnusedAssets);

View File

@@ -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);
registerEvent("copyCustomGameAsset", copyCustomGameAsset);

View File

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

View File

@@ -130,7 +130,10 @@ declare global {
sourcePath: string,
assetType: "icon" | "logo" | "hero"
) => Promise<string>;
cleanupUnusedAssets: () => Promise<{ deletedCount: number; errors: string[] }>;
cleanupUnusedAssets: () => Promise<{
deletedCount: number;
errors: string[];
}>;
updateGameCustomAssets: (
shop: GameShop,
objectId: string,

View File

@@ -232,6 +232,7 @@ export function GameDetailsContent() {
visible={showEditGameModal}
onClose={() => setShowEditGameModal(false)}
game={game}
shopDetails={shopDetails}
onGameUpdated={handleGameUpdated}
/>
</div>

View File

@@ -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<EditGameModalProps>) {
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<string | null>(null);
const [defaultLogoUrl, setDefaultLogoUrl] = useState<string | null>(null);
const [defaultHeroUrl, setDefaultHeroUrl] = useState<string | null>(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<HTMLInputElement>) => {
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={
<Button
type="button"
theme="outline"
onClick={handleSelectIcon}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
type="button"
theme="outline"
onClick={handleSelectIcon}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
{!isCustomGame(game!) && iconPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultIcon}
disabled={isUpdating}
title="Restore default icon"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
{iconPath && (
{(iconPath || (!isCustomGame(game!) && defaultIconUrl)) && (
<div className="edit-game-modal__image-preview">
<img
src={getIconPreviewUrl()}
@@ -318,19 +372,32 @@ export function EditGameModal({
readOnly
theme="dark"
rightContent={
<Button
type="button"
theme="outline"
onClick={handleSelectLogo}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
type="button"
theme="outline"
onClick={handleSelectLogo}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
{!isCustomGame(game!) && logoPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultLogo}
disabled={isUpdating}
title="Restore default logo"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
{logoPath && (
{(logoPath || (!isCustomGame(game!) && defaultLogoUrl)) && (
<div className="edit-game-modal__image-preview">
<img
src={getLogoPreviewUrl()}
@@ -349,19 +416,32 @@ export function EditGameModal({
readOnly
theme="dark"
rightContent={
<Button
type="button"
theme="outline"
onClick={handleSelectHero}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
type="button"
theme="outline"
onClick={handleSelectHero}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
</Button>
{!isCustomGame(game!) && heroPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultHero}
disabled={isUpdating}
title="Restore default hero image"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
{heroPath && (
{(heroPath || (!isCustomGame(game!) && defaultHeroUrl)) && (
<div className="edit-game-modal__image-preview">
<img
src={getHeroPreviewUrl()}

View File

@@ -528,7 +528,9 @@ export function GameOptionsModal({
}}
theme="danger"
disabled={
isGameDownloading || deleting || !game.download?.downloadPath
isGameDownloading ||
deleting ||
!game.download?.downloadPath
}
>
{t("remove_files")}