mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 08:43:57 +00:00
Feat: Custom Games
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gamesSublevel,
|
||||
gamesShopAssetsSublevel,
|
||||
levelKeys,
|
||||
} from "@main/level";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import { randomUUID } from "crypto";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
|
||||
const addCustomGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
title: string,
|
||||
@@ -21,12 +16,14 @@ const addCustomGameToLibrary = async (
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const existingGames = await gamesSublevel.iterator().all();
|
||||
const existingGame = existingGames.find(([_key, game]) =>
|
||||
game.executablePath === executablePath && !game.isDeleted
|
||||
const existingGame = existingGames.find(
|
||||
([_key, game]) => game.executablePath === executablePath && !game.isDeleted
|
||||
);
|
||||
|
||||
if (existingGame) {
|
||||
throw new Error("A game with this executable path already exists in your library");
|
||||
throw new Error(
|
||||
"A game with this executable path already exists in your library"
|
||||
);
|
||||
}
|
||||
|
||||
const assets = {
|
||||
@@ -65,4 +62,4 @@ const addCustomGameToLibrary = async (
|
||||
return game;
|
||||
};
|
||||
|
||||
registerEvent("addCustomGameToLibrary", addCustomGameToLibrary);
|
||||
registerEvent("addCustomGameToLibrary", addCustomGameToLibrary);
|
||||
|
||||
@@ -30,6 +30,8 @@ const addGameToLibrary = async (
|
||||
game = {
|
||||
title,
|
||||
iconUrl: gameAssets?.iconUrl ?? null,
|
||||
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
|
||||
@@ -13,13 +13,13 @@ const changeGamePlaytime = async (
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
|
||||
if (game.remoteId) {
|
||||
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
|
||||
playTimeInSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
||||
|
||||
@@ -18,11 +18,17 @@ 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,
|
||||
// 确保 gameAssets 中的可能为 null 的字段转换为 undefined
|
||||
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? undefined,
|
||||
libraryImageUrl: gameAssets?.libraryImageUrl ?? undefined,
|
||||
logoImageUrl: gameAssets?.logoImageUrl ?? undefined,
|
||||
logoPosition: gameAssets?.logoPosition ?? undefined,
|
||||
coverImageUrl: gameAssets?.coverImageUrl ?? undefined,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gamesSublevel,
|
||||
gamesShopAssetsSublevel,
|
||||
levelKeys,
|
||||
} from "@main/level";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
const updateCustomGame = async (
|
||||
@@ -16,7 +12,7 @@ const updateCustomGame = async (
|
||||
libraryHeroImageUrl?: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
|
||||
const existingGame = await gamesSublevel.get(gameKey);
|
||||
if (!existingGame) {
|
||||
throw new Error("Game not found");
|
||||
@@ -50,4 +46,4 @@ const updateCustomGame = async (
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
registerEvent("updateCustomGame", updateCustomGame);
|
||||
registerEvent("updateCustomGame", updateCustomGame);
|
||||
|
||||
@@ -53,6 +53,8 @@ const startGameDownload = async (
|
||||
await gamesSublevel.put(gameKey, {
|
||||
title,
|
||||
iconUrl: gameAssets?.iconUrl ?? null,
|
||||
libraryHeroImageUrl: gameAssets?.libraryHeroImageUrl ?? null,
|
||||
logoImageUrl: gameAssets?.logoImageUrl ?? null,
|
||||
objectId,
|
||||
shop,
|
||||
remoteId: null,
|
||||
|
||||
@@ -65,36 +65,61 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
protocol.handle("gradient", (request) => {
|
||||
const gradientCss = decodeURIComponent(request.url.slice("gradient:".length));
|
||||
|
||||
const match = gradientCss.match(/linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/);
|
||||
|
||||
const gradientCss = decodeURIComponent(
|
||||
request.url.slice("gradient:".length)
|
||||
);
|
||||
|
||||
const match = gradientCss.match(
|
||||
/linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/
|
||||
);
|
||||
|
||||
let direction = "45deg";
|
||||
let color1 = '#4a90e2';
|
||||
let color2 = '#7b68ee';
|
||||
|
||||
let color1 = "#4a90e2";
|
||||
let color2 = "#7b68ee";
|
||||
|
||||
if (match) {
|
||||
direction = match[1].trim();
|
||||
color1 = match[2].trim();
|
||||
color2 = match[3].trim();
|
||||
}
|
||||
|
||||
let x1 = "0%", y1 = "0%", x2 = "100%", y2 = "100%";
|
||||
|
||||
|
||||
let x1 = "0%",
|
||||
y1 = "0%",
|
||||
x2 = "100%",
|
||||
y2 = "100%";
|
||||
|
||||
if (direction === "to right") {
|
||||
x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "0%";
|
||||
x1 = "0%";
|
||||
y1 = "0%";
|
||||
x2 = "100%";
|
||||
y2 = "0%";
|
||||
} else if (direction === "to bottom") {
|
||||
x1 = "0%"; y1 = "0%"; x2 = "0%"; y2 = "100%";
|
||||
x1 = "0%";
|
||||
y1 = "0%";
|
||||
x2 = "0%";
|
||||
y2 = "100%";
|
||||
} else if (direction === "45deg") {
|
||||
x1 = "0%"; y1 = "100%"; x2 = "100%"; y2 = "0%";
|
||||
x1 = "0%";
|
||||
y1 = "100%";
|
||||
x2 = "100%";
|
||||
y2 = "0%";
|
||||
} else if (direction === "135deg") {
|
||||
x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "100%";
|
||||
x1 = "0%";
|
||||
y1 = "0%";
|
||||
x2 = "100%";
|
||||
y2 = "100%";
|
||||
} else if (direction === "225deg") {
|
||||
x1 = "100%"; y1 = "0%"; x2 = "0%"; y2 = "100%";
|
||||
x1 = "100%";
|
||||
y1 = "0%";
|
||||
x2 = "0%";
|
||||
y2 = "100%";
|
||||
} else if (direction === "315deg") {
|
||||
x1 = "100%"; y1 = "100%"; x2 = "0%"; y2 = "0%";
|
||||
x1 = "100%";
|
||||
y1 = "100%";
|
||||
x2 = "0%";
|
||||
y2 = "0%";
|
||||
}
|
||||
|
||||
|
||||
const svgContent = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
|
||||
<defs>
|
||||
@@ -106,9 +131,9 @@ app.whenReady().then(async () => {
|
||||
<rect width="100%" height="100%" fill="url(#grad)" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
|
||||
return new Response(svgContent, {
|
||||
headers: { 'Content-Type': 'image/svg+xml' }
|
||||
headers: { "Content-Type": "image/svg+xml" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ export const uploadGamesBatch = async () => {
|
||||
.all()
|
||||
.then((results) => {
|
||||
return results.filter(
|
||||
(game) => !game.isDeleted && game.remoteId === null && game.shop !== "custom"
|
||||
(game) =>
|
||||
!game.isDeleted && game.remoteId === null && game.shop !== "custom"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -135,7 +135,14 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
logoImageUrl?: string,
|
||||
libraryHeroImageUrl?: string
|
||||
) =>
|
||||
ipcRenderer.invoke("addCustomGameToLibrary", title, executablePath, iconUrl, logoImageUrl, libraryHeroImageUrl),
|
||||
ipcRenderer.invoke(
|
||||
"addCustomGameToLibrary",
|
||||
title,
|
||||
executablePath,
|
||||
iconUrl,
|
||||
logoImageUrl,
|
||||
libraryHeroImageUrl
|
||||
),
|
||||
updateCustomGame: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -144,7 +151,15 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
logoImageUrl?: string,
|
||||
libraryHeroImageUrl?: string
|
||||
) =>
|
||||
ipcRenderer.invoke("updateCustomGame", shop, objectId, title, iconUrl, logoImageUrl, libraryHeroImageUrl),
|
||||
ipcRenderer.invoke(
|
||||
"updateCustomGame",
|
||||
shop,
|
||||
objectId,
|
||||
title,
|
||||
iconUrl,
|
||||
logoImageUrl,
|
||||
libraryHeroImageUrl
|
||||
),
|
||||
createGameShortcut: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -493,6 +508,4 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
},
|
||||
closeEditorWindow: (themeId?: string) =>
|
||||
ipcRenderer.invoke("closeEditorWindow", themeId),
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
justify-content: flex-end;
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import { FileDirectoryIcon } from "@primer/octicons-react";
|
||||
|
||||
import { Modal, TextField, Button } from "@renderer/components";
|
||||
import { useLibrary, useToast } from "@renderer/hooks";
|
||||
import { buildGameDetailsPath, generateRandomGradient } from "@renderer/helpers";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
generateRandomGradient,
|
||||
} from "@renderer/helpers";
|
||||
|
||||
import "./sidebar-adding-custom-game-modal.scss";
|
||||
|
||||
@@ -41,7 +44,7 @@ export function SidebarAddingCustomGameModal({
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const selectedPath = filePaths[0];
|
||||
setExecutablePath(selectedPath);
|
||||
|
||||
|
||||
if (!gameName.trim()) {
|
||||
const fileName = selectedPath.split(/[\\/]/).pop() || "";
|
||||
const gameNameFromFile = fileName.replace(/\.[^/.]+$/, "");
|
||||
@@ -54,8 +57,6 @@ export function SidebarAddingCustomGameModal({
|
||||
setGameName(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleAddGame = async () => {
|
||||
if (!gameName.trim() || !executablePath.trim()) {
|
||||
showErrorToast(t("custom_game_modal_fill_required"));
|
||||
@@ -70,7 +71,7 @@ export function SidebarAddingCustomGameModal({
|
||||
const iconUrl = ""; // Don't use gradient for icon
|
||||
const logoImageUrl = ""; // Don't use gradient for logo
|
||||
const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero
|
||||
|
||||
|
||||
const newGame = await window.electron.addCustomGameToLibrary(
|
||||
gameNameForSeed,
|
||||
executablePath,
|
||||
@@ -81,24 +82,22 @@ export function SidebarAddingCustomGameModal({
|
||||
|
||||
showSuccessToast(t("custom_game_modal_success"));
|
||||
updateLibrary();
|
||||
|
||||
|
||||
const gameDetailsPath = buildGameDetailsPath({
|
||||
shop: "custom",
|
||||
objectId: newGame.objectId,
|
||||
title: newGame.title
|
||||
title: newGame.title,
|
||||
});
|
||||
|
||||
|
||||
navigate(gameDetailsPath);
|
||||
|
||||
|
||||
setGameName("");
|
||||
setExecutablePath("");
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to add custom game:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("custom_game_modal_failed")
|
||||
error instanceof Error ? error.message : t("custom_game_modal_failed")
|
||||
);
|
||||
} finally {
|
||||
setIsAdding(false);
|
||||
@@ -115,8 +114,6 @@ export function SidebarAddingCustomGameModal({
|
||||
|
||||
const isFormValid = gameName.trim() && executablePath.trim();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
@@ -153,31 +150,29 @@ export function SidebarAddingCustomGameModal({
|
||||
theme="dark"
|
||||
disabled={isAdding}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="sidebar-adding-custom-game-modal__actions">
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isAdding}
|
||||
>
|
||||
{t("custom_game_modal_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
onClick={handleAddGame}
|
||||
disabled={!isFormValid || isAdding}
|
||||
>
|
||||
{isAdding ? t("custom_game_modal_adding") : t("custom_game_modal_add")}
|
||||
{isAdding
|
||||
? t("custom_game_modal_adding")
|
||||
: t("custom_game_modal_add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { CommentDiscussionIcon, PlayIcon, PlusIcon } from "@primer/octicons-react";
|
||||
import {
|
||||
CommentDiscussionIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { SidebarGameItem } from "./sidebar-game-item";
|
||||
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
|
||||
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
|
||||
@@ -265,7 +269,9 @@ export function Sidebar() {
|
||||
<small className="sidebar__section-title">
|
||||
{t("my_library")}
|
||||
</small>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{ display: "flex", gap: "8px", alignItems: "center" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__add-button"
|
||||
|
||||
@@ -87,9 +87,9 @@ export const removeCustomCss = (target: HTMLElement = document.head) => {
|
||||
|
||||
export const generateRandomGradient = (): string => {
|
||||
// Use a single consistent gradient with softer colors for custom games as placeholder
|
||||
const color1 = '#2c3e50'; // Dark blue-gray
|
||||
const color2 = '#34495e'; // Darker slate
|
||||
|
||||
const color1 = "#2c3e50"; // Dark blue-gray
|
||||
const color2 = "#34495e"; // Darker slate
|
||||
|
||||
// Create SVG data URL that works in img tags
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
|
||||
<defs>
|
||||
@@ -100,7 +100,7 @@ export const generateRandomGradient = (): string => {
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grad)" />
|
||||
</svg>`;
|
||||
|
||||
|
||||
// Return as data URL that works in img tags
|
||||
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
|
||||
};
|
||||
|
||||
@@ -119,7 +119,7 @@ export function GameDetailsContent() {
|
||||
? game?.libraryHeroImageUrl || game?.iconUrl || ""
|
||||
: shopDetails?.assets?.libraryHeroImageUrl || "";
|
||||
const logoImage = isCustomGame
|
||||
? game?.logoImageUrl || "" // Don't use icon as fallback for custom games
|
||||
? game?.logoImageUrl || "" // Don't use icon as fallback for custom games
|
||||
: shopDetails?.assets?.logoImageUrl || "";
|
||||
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,7 @@ $hero-height: 300px;
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
align-items: center;
|
||||
|
||||
|
||||
&--right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -43,4 +43,4 @@
|
||||
justify-content: flex-end;
|
||||
margin-top: globals.$spacing-unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,17 @@ export function EditCustomGameModal({
|
||||
useEffect(() => {
|
||||
if (game && visible) {
|
||||
setGameName(game.title || "");
|
||||
|
||||
const currentIconPath = game.iconUrl?.startsWith("local:")
|
||||
? game.iconUrl.replace("local:", "")
|
||||
|
||||
const currentIconPath = game.iconUrl?.startsWith("local:")
|
||||
? game.iconUrl.replace("local:", "")
|
||||
: "";
|
||||
const currentLogoPath = game.logoImageUrl?.startsWith("local:")
|
||||
? game.logoImageUrl.replace("local:", "")
|
||||
const currentLogoPath = game.logoImageUrl?.startsWith("local:")
|
||||
? game.logoImageUrl.replace("local:", "")
|
||||
: "";
|
||||
const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:")
|
||||
? game.libraryHeroImageUrl.replace("local:", "")
|
||||
const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:")
|
||||
? game.libraryHeroImageUrl.replace("local:", "")
|
||||
: "";
|
||||
|
||||
|
||||
setIconPath(currentIconPath);
|
||||
setLogoPath(currentLogoPath);
|
||||
setHeroPath(currentHeroPath);
|
||||
@@ -114,8 +114,10 @@ export function EditCustomGameModal({
|
||||
// Preserve existing image URLs if not changed
|
||||
const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl;
|
||||
const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl;
|
||||
const libraryHeroImageUrl = heroPath ? `local:${heroPath}` : game.libraryHeroImageUrl;
|
||||
|
||||
const libraryHeroImageUrl = heroPath
|
||||
? `local:${heroPath}`
|
||||
: game.libraryHeroImageUrl;
|
||||
|
||||
const updatedGame = await window.electron.updateCustomGame(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
@@ -131,8 +133,8 @@ export function EditCustomGameModal({
|
||||
} catch (error) {
|
||||
console.error("Failed to update custom game:", error);
|
||||
showErrorToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("edit_custom_game_modal_failed")
|
||||
);
|
||||
} finally {
|
||||
@@ -143,17 +145,17 @@ export function EditCustomGameModal({
|
||||
const handleClose = () => {
|
||||
if (!isUpdating) {
|
||||
setGameName(game?.title || "");
|
||||
|
||||
const currentIconPath = game?.iconUrl?.startsWith("local:")
|
||||
? game.iconUrl.replace("local:", "")
|
||||
|
||||
const currentIconPath = game?.iconUrl?.startsWith("local:")
|
||||
? game.iconUrl.replace("local:", "")
|
||||
: "";
|
||||
const currentLogoPath = game?.logoImageUrl?.startsWith("local:")
|
||||
? game.logoImageUrl.replace("local:", "")
|
||||
const currentLogoPath = game?.logoImageUrl?.startsWith("local:")
|
||||
? game.logoImageUrl.replace("local:", "")
|
||||
: "";
|
||||
const currentHeroPath = game?.libraryHeroImageUrl?.startsWith("local:")
|
||||
? game.libraryHeroImageUrl.replace("local:", "")
|
||||
const currentHeroPath = game?.libraryHeroImageUrl?.startsWith("local:")
|
||||
? game.libraryHeroImageUrl.replace("local:", "")
|
||||
: "";
|
||||
|
||||
|
||||
setIconPath(currentIconPath);
|
||||
setLogoPath(currentLogoPath);
|
||||
setHeroPath(currentHeroPath);
|
||||
@@ -212,7 +214,7 @@ export function EditCustomGameModal({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{iconPath && (
|
||||
<div className="edit-custom-game-modal__image-preview">
|
||||
<img
|
||||
@@ -243,7 +245,7 @@ export function EditCustomGameModal({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{logoPath && (
|
||||
<div className="edit-custom-game-modal__image-preview">
|
||||
<img
|
||||
@@ -274,7 +276,7 @@ export function EditCustomGameModal({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{heroPath && (
|
||||
<div className="edit-custom-game-modal__image-preview">
|
||||
<img
|
||||
@@ -288,24 +290,26 @@ export function EditCustomGameModal({
|
||||
</div>
|
||||
|
||||
<div className="edit-custom-game-modal__actions">
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{t("edit_custom_game_modal_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
<Button
|
||||
type="button"
|
||||
theme="primary"
|
||||
onClick={handleUpdateGame}
|
||||
disabled={!isFormValid || isUpdating}
|
||||
>
|
||||
{isUpdating ? t("edit_custom_game_modal_updating") : t("edit_custom_game_modal_update")}
|
||||
{isUpdating
|
||||
? t("edit_custom_game_modal_updating")
|
||||
: t("edit_custom_game_modal_update")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user