mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-17 16:33:55 +00:00
feat: custom game support/game info changing
This commit is contained in:
@@ -10,7 +10,15 @@ const saveGameShopAssets = async (
|
||||
): Promise<void> => {
|
||||
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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,18 +18,14 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||
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;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
45
src/main/events/library/update-game-custom-assets.ts
Normal file
45
src/main/events/library/update-game-custom-assets.ts
Normal file
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<li
|
||||
|
||||
@@ -202,10 +202,10 @@ export function GameDetailsContextProvider({
|
||||
}, [objectId, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
|
||||
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@@ -126,6 +126,14 @@ declare global {
|
||||
logoImageUrl?: string,
|
||||
libraryHeroImageUrl?: string
|
||||
) => Promise<Game>;
|
||||
updateGameCustomAssets: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
title: string,
|
||||
customIconUrl?: string | null,
|
||||
customLogoImageUrl?: string | null,
|
||||
customHeroImageUrl?: string | null
|
||||
) => Promise<Game>;
|
||||
createGameShortcut: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -156,16 +178,14 @@ export function GameDetailsContent() {
|
||||
)}
|
||||
|
||||
<div className="game-details__hero-buttons game-details__hero-buttons--right">
|
||||
{game?.shop === "custom" && (
|
||||
<button
|
||||
type="button"
|
||||
className="game-details__edit-custom-game-button"
|
||||
onClick={handleEditCustomGameClick}
|
||||
title={t("edit_custom_game")}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="game-details__edit-custom-game-button"
|
||||
onClick={game?.shop === "custom" ? handleEditCustomGameClick : handleEditGameClick}
|
||||
title={t("edit_custom_game")}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</button>
|
||||
|
||||
{game?.shop !== "custom" && (
|
||||
<button
|
||||
@@ -215,6 +235,15 @@ export function GameDetailsContent() {
|
||||
onGameUpdated={handleGameUpdated}
|
||||
/>
|
||||
)}
|
||||
|
||||
{game?.shop !== "custom" && (
|
||||
<EditGameModal
|
||||
visible={showEditGameModal}
|
||||
onClose={() => setShowEditGameModal(false)}
|
||||
game={game}
|
||||
onGameUpdated={handleGameUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
367
src/renderer/src/pages/game-details/modals/edit-game-modal.tsx
Normal file
367
src/renderer/src/pages/game-details/modals/edit-game-modal.tsx
Normal file
@@ -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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("edit_custom_game_modal")}
|
||||
description={t("edit_custom_game_modal_description")}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="edit-game-modal__container">
|
||||
<div className="edit-game-modal__form">
|
||||
<TextField
|
||||
label={t("edit_custom_game_modal_game_name")}
|
||||
placeholder={t("edit_custom_game_modal_enter_name")}
|
||||
value={gameName}
|
||||
onChange={handleGameNameChange}
|
||||
theme="dark"
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
<div className="edit-game-modal__image-section">
|
||||
<TextField
|
||||
label={t("edit_custom_game_modal_icon")}
|
||||
placeholder={t("edit_custom_game_modal_select_icon")}
|
||||
value={iconPath}
|
||||
readOnly
|
||||
theme="dark"
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleSelectIcon}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<ImageIcon />
|
||||
{t("edit_custom_game_modal_browse")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{iconPath && (
|
||||
<div className="edit-game-modal__image-preview">
|
||||
<img
|
||||
src={getIconPreviewUrl()!}
|
||||
alt={t("edit_custom_game_modal_icon_preview")}
|
||||
className="edit-game-modal__preview-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-game-modal__image-section">
|
||||
<TextField
|
||||
label={t("edit_custom_game_modal_logo")}
|
||||
placeholder={t("edit_custom_game_modal_select_logo")}
|
||||
value={logoPath}
|
||||
readOnly
|
||||
theme="dark"
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleSelectLogo}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<ImageIcon />
|
||||
{t("edit_custom_game_modal_browse")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{logoPath && (
|
||||
<div className="edit-game-modal__image-preview">
|
||||
<img
|
||||
src={getLogoPreviewUrl()!}
|
||||
alt={t("edit_custom_game_modal_logo_preview")}
|
||||
className="edit-game-modal__preview-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="edit-game-modal__image-section">
|
||||
<TextField
|
||||
label={t("edit_custom_game_modal_hero")}
|
||||
placeholder={t("edit_custom_game_modal_select_hero")}
|
||||
value={heroPath}
|
||||
readOnly
|
||||
theme="dark"
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleSelectHero}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<ImageIcon />
|
||||
{t("edit_custom_game_modal_browse")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{heroPath && (
|
||||
<div className="edit-game-modal__image-preview">
|
||||
<img
|
||||
src={getHeroPreviewUrl()!}
|
||||
alt={t("edit_custom_game_modal_hero_preview")}
|
||||
className="edit-game-modal__preview-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="edit-game-modal__actions">
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{t("edit_custom_game_modal_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
onClick={handleUpdateGame}
|
||||
disabled={!isFormValid || isUpdating}
|
||||
>
|
||||
{isUpdating
|
||||
? t("edit_custom_game_modal_updating")
|
||||
: t("edit_custom_game_modal_update")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user