feat: improving visuals on assets

This commit is contained in:
Chubby Granny Chaser
2025-09-28 18:13:08 +01:00
parent 889f3bb773
commit dceb3a7509
13 changed files with 286 additions and 443 deletions

View File

@@ -57,7 +57,7 @@
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Select logo",
"edit_game_modal_logo_preview": "Logo preview",
"edit_game_modal_hero": "Library Hero Image",
"edit_game_modal_hero": "Library Hero",
"edit_game_modal_select_hero": "Select library hero image",
"edit_game_modal_hero_preview": "Library hero image preview",
"edit_game_modal_cancel": "Cancel",
@@ -69,7 +69,8 @@
"edit_game_modal_image_filter": "Image",
"edit_game_modal_icon_resolution": "Recommended resolution: 256x256px",
"edit_game_modal_logo_resolution": "Recommended resolution: 640x360px",
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px"
"edit_game_modal_hero_resolution": "Recommended resolution: 1920x620px",
"edit_game_modal_assets": "Assets"
},
"header": {
"search": "Search games",

View File

@@ -28,12 +28,12 @@ export function DeleteGameModal({
onClose={onClose}
>
<div className="delete-game-modal__actions">
<Button onClick={handleDeleteGame} theme="outline">
{t("delete")}
<Button onClick={onClose} theme="outline">
{t("cancel")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
<Button onClick={handleDeleteGame} theme="primary">
{t("delete")}
</Button>
</div>
</Modal>

View File

@@ -1,13 +1,17 @@
@use "../../../scss/globals.scss";
.description-header {
width: 100%;
padding: calc(globals.$spacing-unit * 2);
width: calc(100% - calc(globals.$spacing-unit * 2));
margin: calc(globals.$spacing-unit * 1) auto;
padding: calc(globals.$spacing-unit * 1.5);
display: flex;
justify-content: space-between;
align-items: center;
background-color: globals.$background-color;
height: 72px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.03);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
&__info {
display: flex;

View File

@@ -2,7 +2,7 @@
.gallery-slider {
&__container {
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 1);
width: 100%;
display: flex;
flex-direction: column;

View File

@@ -43,12 +43,16 @@ $hero-height: 300px;
}
&__hero-content {
padding: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 1.5);
height: 100%;
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
@media (min-width: 768px) {
padding: calc(globals.$spacing-unit * 2);
}
}
&__hero-buttons {
@@ -116,14 +120,22 @@ $hero-height: 300px;
}
&__game-logo {
width: 300px;
width: 200px;
align-self: flex-end;
@media (min-width: 768px) {
width: 250px;
}
@media (min-width: 1024px) {
width: 300px;
}
}
&__game-logo-text {
width: 300px;
width: 200px;
align-self: flex-end;
font-size: 2.5rem;
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
@@ -132,6 +144,16 @@ $hero-height: 300px;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
@media (min-width: 768px) {
width: 250px;
font-size: 2.2rem;
}
@media (min-width: 1024px) {
width: 300px;
font-size: 2.5rem;
}
}
&__hero-image-skeleton {
@@ -173,11 +195,19 @@ $hero-height: 300px;
user-select: text;
line-height: 22px;
font-size: globals.$body-font-size;
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
width: 100%;
margin-left: auto;
margin-right: auto;
@media (min-width: 768px) {
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
}
@media (min-width: 1024px) {
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
}
@media (min-width: 1280px) {
width: 60%;
}
@@ -206,11 +236,19 @@ $hero-height: 300px;
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 2) calc(globals.$spacing-unit * 1.5);
width: 100%;
margin-left: auto;
margin-right: auto;
@media (min-width: 768px) {
padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2);
}
@media (min-width: 1024px) {
padding: calc(globals.$spacing-unit * 3) calc(globals.$spacing-unit * 2);
}
@media (min-width: 1280px) {
width: 60%;
line-height: 22px;

View File

@@ -149,17 +149,17 @@ export function ChangeGamePlaytimeModal({
</div>
<div className="change-game-playtime-modal__actions">
<Button onClick={onClose} theme="outline">
{t("cancel")}
</Button>
<Button
onClick={handleChangePlaytime}
theme="outline"
theme="primary"
disabled={!isValid || isSubmitting}
>
{t("update_playtime")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
</Button>
</div>
</div>
</Modal>

View File

@@ -12,6 +12,28 @@
gap: 16px;
}
.edit-game-modal__asset-selector {
margin-bottom: 8px;
}
.edit-game-modal__asset-tabs {
display: flex;
gap: 8px;
margin-bottom: 4px;
button {
flex: 1;
min-width: 0;
}
}
.edit-game-modal__asset-label {
font-size: 14px;
font-weight: 500;
color: var(--color-text);
margin-bottom: 4px;
}
.edit-game-modal__image-section {
display: flex;
flex-direction: column;

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { ImageIcon, ReplyIcon } from "@primer/octicons-react";
import { ImageIcon, XIcon } from "@primer/octicons-react";
import { Modal, TextField, Button } from "@renderer/components";
import { useToast } from "@renderer/hooks";
@@ -16,6 +16,8 @@ export interface EditGameModalProps {
onGameUpdated: (updatedGame: LibraryGame | Game) => void;
}
type AssetType = "icon" | "logo" | "hero";
export function EditGameModal({
visible,
onClose,
@@ -31,37 +33,32 @@ export function EditGameModal({
const [logoPath, setLogoPath] = useState("");
const [heroPath, setHeroPath] = useState("");
const [isUpdating, setIsUpdating] = useState(false);
const [selectedAssetType, setSelectedAssetType] = useState<AssetType>("icon");
// 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 => {
return game.shop === "custom";
};
// Helper function to extract local path from URL
const extractLocalPath = (url: string | null | undefined): string => {
return url?.startsWith("local:") ? url.replace("local:", "") : "";
};
// Helper function to set asset paths for custom games
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
setIconPath(extractLocalPath(game.iconUrl));
setLogoPath(extractLocalPath(game.logoImageUrl));
setHeroPath(extractLocalPath(game.libraryHeroImageUrl));
}, []);
// Helper function to set asset paths for non-custom games
const setNonCustomGameAssets = useCallback(
(game: LibraryGame) => {
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
@@ -91,7 +88,47 @@ export function EditGameModal({
setGameName(event.target.value);
};
const handleSelectIcon = async () => {
const handleAssetTypeChange = (assetType: AssetType) => {
setSelectedAssetType(assetType);
};
const getAssetPath = (assetType: AssetType): string => {
switch (assetType) {
case "icon":
return iconPath;
case "logo":
return logoPath;
case "hero":
return heroPath;
}
};
const setAssetPath = (assetType: AssetType, path: string): void => {
switch (assetType) {
case "icon":
setIconPath(path);
break;
case "logo":
setLogoPath(path);
break;
case "hero":
setHeroPath(path);
break;
}
};
const getDefaultUrl = (assetType: AssetType): string | null => {
switch (assetType) {
case "icon":
return defaultIconUrl;
case "logo":
return defaultLogoUrl;
case "hero":
return defaultHeroUrl;
}
};
const handleSelectAsset = async (assetType: AssetType) => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"],
filters: [
@@ -104,91 +141,24 @@ export function EditGameModal({
if (filePaths && filePaths.length > 0) {
try {
// Copy the asset to the app's assets folder
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
filePaths[0],
"icon"
assetType
);
setIconPath(copiedAssetUrl.replace("local:", ""));
setAssetPath(assetType, copiedAssetUrl.replace("local:", ""));
} catch (error) {
console.error("Failed to copy icon asset:", error);
// Fallback to original behavior
setIconPath(filePaths[0]);
console.error(`Failed to copy ${assetType} asset:`, error);
setAssetPath(assetType, filePaths[0]);
}
}
};
const handleSelectLogo = async () => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"],
filters: [
{
name: t("edit_game_modal_image_filter"),
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
},
],
});
if (filePaths && filePaths.length > 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]);
}
}
const handleRestoreDefault = (assetType: AssetType) => {
setAssetPath(assetType, "");
};
const handleSelectHero = async () => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"],
filters: [
{
name: t("edit_game_modal_image_filter"),
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
},
],
});
if (filePaths && filePaths.length > 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]);
}
}
};
// Helper functions to restore default images for non-custom games
const handleRestoreDefaultIcon = () => {
setIconPath("");
};
const handleRestoreDefaultLogo = () => {
setLogoPath("");
};
const handleRestoreDefaultHero = () => {
setHeroPath("");
};
// Drag and drop state
const [dragOverTarget, setDragOverTarget] = useState<string | null>(null);
// Drag and drop handlers
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -203,7 +173,6 @@ export function EditGameModal({
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only clear drag state if we're leaving the drop zone entirely
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setDragOverTarget(null);
}
@@ -220,10 +189,7 @@ export function EditGameModal({
return validTypes.includes(file.type);
};
const processDroppedFile = async (
file: File,
assetType: "icon" | "logo" | "hero"
) => {
const processDroppedFile = async (file: File, assetType: AssetType) => {
setDragOverTarget(null);
if (!validateImageFile(file)) {
@@ -232,11 +198,8 @@ export function EditGameModal({
}
try {
// In Electron, we need to get the file path differently
let filePath: string;
// Try to get the path from the file object (Electron specific)
// In Electron, File objects have a path property
interface ElectronFile extends File {
path?: string;
}
@@ -244,11 +207,9 @@ export function EditGameModal({
if ("path" in file && typeof (file as ElectronFile).path === "string") {
filePath = (file as ElectronFile).path!;
} else {
// Fallback: create a temporary file from the file data
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Use a temporary file approach
const tempFileName = `temp_${Date.now()}_${file.name}`;
const tempPath = await window.electron.saveTempFile?.(
tempFileName,
@@ -264,31 +225,18 @@ export function EditGameModal({
filePath = tempPath;
}
// Copy the asset to the app's assets folder using the file path
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
filePath,
assetType
);
const assetPath = copiedAssetUrl.replace("local:", "");
switch (assetType) {
case "icon":
setIconPath(assetPath);
break;
case "logo":
setLogoPath(assetPath);
break;
case "hero":
setHeroPath(assetPath);
break;
}
setAssetPath(assetType, assetPath);
showSuccessToast(
`${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!`
);
// Clean up temporary file if we created one
if (!("path" in file) && filePath) {
try {
await window.electron.deleteTempFile?.(filePath);
@@ -304,7 +252,7 @@ export function EditGameModal({
}
};
const handleIconDrop = async (e: React.DragEvent) => {
const handleAssetDrop = async (e: React.DragEvent, assetType: AssetType) => {
e.preventDefault();
e.stopPropagation();
setDragOverTarget(null);
@@ -313,33 +261,7 @@ export function EditGameModal({
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
await processDroppedFile(files[0], "icon");
}
};
const handleLogoDrop = async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragOverTarget(null);
if (isUpdating) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
await processDroppedFile(files[0], "logo");
}
};
const handleHeroDrop = async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragOverTarget(null);
if (isUpdating) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
await processDroppedFile(files[0], "hero");
await processDroppedFile(files[0], assetType);
}
};
@@ -444,28 +366,111 @@ export function EditGameModal({
const isFormValid = gameName.trim();
const getIconPreviewUrl = (): string | undefined => {
const getPreviewUrl = (assetType: AssetType): string | undefined => {
const assetPath = getAssetPath(assetType);
const defaultUrl = getDefaultUrl(assetType);
if (game && !isCustomGame(game)) {
// For non-custom games, show custom image if set, otherwise show default
return iconPath ? `local:${iconPath}` : defaultIconUrl || undefined;
return assetPath ? `local:${assetPath}` : defaultUrl || undefined;
}
return iconPath ? `local:${iconPath}` : undefined;
return assetPath ? `local:${assetPath}` : undefined;
};
const getLogoPreviewUrl = (): string | undefined => {
if (game && !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 renderImageSection = (assetType: AssetType) => {
const assetPath = getAssetPath(assetType);
const defaultUrl = getDefaultUrl(assetType);
const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl);
const isDragOver = dragOverTarget === assetType;
const getHeroPreviewUrl = (): string | undefined => {
if (game && !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;
const getTranslationKey = (suffix: string) =>
`edit_game_modal_${assetType}${suffix}`;
const getResolutionKey = () => `edit_game_modal_${assetType}_resolution`;
return (
<div className="edit-game-modal__image-section">
<TextField
placeholder={t(`edit_game_modal_select_${assetType}`)}
value={assetPath}
readOnly
theme="dark"
rightContent={
<div style={{ display: "flex", gap: "8px" }}>
<Button
type="button"
theme="outline"
onClick={() => handleSelectAsset(assetType)}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && assetPath && (
<Button
type="button"
theme="outline"
onClick={() => handleRestoreDefault(assetType)}
disabled={isUpdating}
title={`Remove ${assetType}`}
>
<XIcon />
</Button>
)}
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t(getResolutionKey())}
</div>
{hasImage && (
<button
type="button"
aria-label={t(getTranslationKey("_drop_zone"))}
className={`edit-game-modal__image-preview ${
assetType === "icon" ? "edit-game-modal__icon-preview" : ""
} ${isDragOver ? "edit-game-modal__drop-zone--active" : ""}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, assetType)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleAssetDrop(e, assetType)}
onClick={() => handleSelectAsset(assetType)}
>
<img
src={getPreviewUrl(assetType)}
alt={t(getTranslationKey("_preview"))}
className="edit-game-modal__preview-image"
/>
{isDragOver && (
<div className="edit-game-modal__drop-overlay">
<span>Drop to replace {assetType}</span>
</div>
)}
</button>
)}
{!hasImage && (
<button
type="button"
aria-label={t(getTranslationKey("_drop_zone_empty"))}
className={`edit-game-modal__image-preview ${
assetType === "icon" ? "edit-game-modal__icon-preview" : ""
} edit-game-modal__drop-zone ${
isDragOver ? "edit-game-modal__drop-zone--active" : ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, assetType)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleAssetDrop(e, assetType)}
onClick={() => handleSelectAsset(assetType)}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop {assetType} image here</span>
</div>
</button>
)}
</div>
);
};
return (
@@ -486,266 +491,39 @@ export function EditGameModal({
disabled={isUpdating}
/>
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_game_modal_icon")}
placeholder={t("edit_game_modal_select_icon")}
value={iconPath}
readOnly
theme="dark"
rightContent={
<div style={{ display: "flex", gap: "8px" }}>
<Button
type="button"
theme="outline"
onClick={handleSelectIcon}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && iconPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultIcon}
disabled={isUpdating}
title="Restore default icon"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_icon_resolution")}
<div className="edit-game-modal__asset-selector">
<div className="edit-game-modal__asset-label">
{t("edit_game_modal_assets")}
</div>
{(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && (
<button
<div className="edit-game-modal__asset-tabs">
<Button
type="button"
aria-label={t("edit_game_modal_icon_drop_zone")}
className={`edit-game-modal__image-preview edit-game-modal__icon-preview ${
dragOverTarget === "icon"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "icon")}
onDragLeave={handleDragLeave}
onDrop={handleIconDrop}
onClick={handleSelectIcon}
theme={selectedAssetType === "icon" ? "primary" : "outline"}
onClick={() => handleAssetTypeChange("icon")}
disabled={isUpdating}
>
<img
src={getIconPreviewUrl()}
alt={t("edit_game_modal_icon_preview")}
className="edit-game-modal__preview-image"
/>
{dragOverTarget === "icon" && (
<div className="edit-game-modal__drop-overlay">
<span>Drop to replace icon</span>
</div>
)}
</button>
)}
{!iconPath && !(game && !isCustomGame(game) && defaultIconUrl) && (
<button
{t("edit_game_modal_icon")}
</Button>
<Button
type="button"
aria-label={t("edit_game_modal_icon_drop_zone_empty")}
className={`edit-game-modal__image-preview edit-game-modal__icon-preview edit-game-modal__drop-zone ${
dragOverTarget === "icon"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "icon")}
onDragLeave={handleDragLeave}
onDrop={handleIconDrop}
onClick={handleSelectIcon}
theme={selectedAssetType === "logo" ? "primary" : "outline"}
onClick={() => handleAssetTypeChange("logo")}
disabled={isUpdating}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop icon image here</span>
</div>
</button>
)}
{t("edit_game_modal_logo")}
</Button>
<Button
type="button"
theme={selectedAssetType === "hero" ? "primary" : "outline"}
onClick={() => handleAssetTypeChange("hero")}
disabled={isUpdating}
>
{t("edit_game_modal_hero")}
</Button>
</div>
</div>
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_game_modal_logo")}
placeholder={t("edit_game_modal_select_logo")}
value={logoPath}
readOnly
theme="dark"
rightContent={
<div style={{ display: "flex", gap: "8px" }}>
<Button
type="button"
theme="outline"
onClick={handleSelectLogo}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && logoPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultLogo}
disabled={isUpdating}
title="Restore default logo"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_logo_resolution")}
</div>
{(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && (
<button
type="button"
aria-label={t("edit_game_modal_logo_drop_zone")}
className={`edit-game-modal__image-preview ${
dragOverTarget === "logo"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "logo")}
onDragLeave={handleDragLeave}
onDrop={handleLogoDrop}
onClick={handleSelectLogo}
>
<img
src={getLogoPreviewUrl()}
alt={t("edit_game_modal_logo_preview")}
className="edit-game-modal__preview-image"
/>
{dragOverTarget === "logo" && (
<div className="edit-game-modal__drop-overlay">
<span>Drop to replace logo</span>
</div>
)}
</button>
)}
{!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl) && (
<button
type="button"
aria-label={t("edit_game_modal_logo_drop_zone_empty")}
className={`edit-game-modal__image-preview edit-game-modal__drop-zone ${
dragOverTarget === "logo"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "logo")}
onDragLeave={handleDragLeave}
onDrop={handleLogoDrop}
onClick={handleSelectLogo}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop logo image here</span>
</div>
</button>
)}
</div>
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_game_modal_hero")}
placeholder={t("edit_game_modal_select_hero")}
value={heroPath}
readOnly
theme="dark"
rightContent={
<div style={{ display: "flex", gap: "8px" }}>
<Button
type="button"
theme="outline"
onClick={handleSelectHero}
disabled={isUpdating}
>
<ImageIcon />
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && heroPath && (
<Button
type="button"
theme="outline"
onClick={handleRestoreDefaultHero}
disabled={isUpdating}
title="Restore default hero image"
>
<ReplyIcon />
</Button>
)}
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_hero_resolution")}
</div>
{(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && (
<button
type="button"
aria-label={t("edit_game_modal_hero_drop_zone")}
className={`edit-game-modal__image-preview ${
dragOverTarget === "hero"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "hero")}
onDragLeave={handleDragLeave}
onDrop={handleHeroDrop}
onClick={handleSelectHero}
>
<img
src={getHeroPreviewUrl()}
alt={t("edit_game_modal_hero_preview")}
className="edit-game-modal__preview-image"
/>
{dragOverTarget === "hero" && (
<div className="edit-game-modal__drop-overlay">
<span>Drop to replace hero image</span>
</div>
)}
</button>
)}
{!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl) && (
<button
type="button"
aria-label={t("edit_game_modal_hero_drop_zone_empty")}
className={`edit-game-modal__image-preview edit-game-modal__drop-zone ${
dragOverTarget === "hero"
? "edit-game-modal__drop-zone--active"
: ""
}`}
onDragOver={handleDragOver}
onDragEnter={(e) => handleDragEnter(e, "hero")}
onDragLeave={handleDragLeave}
onDrop={handleHeroDrop}
onClick={handleSelectHero}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop hero image here</span>
</div>
</button>
)}
</div>
{renderImageSection(selectedAssetType)}
</div>
<div className="edit-game-modal__actions">

View File

@@ -31,12 +31,12 @@ export function RemoveGameFromLibraryModal({
onClose={onClose}
>
<div className="remove-from-library-modal__actions">
<Button onClick={handleRemoveGame} theme="outline">
{t("remove")}
<Button onClick={onClose} theme="outline">
{t("cancel")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
<Button onClick={handleRemoveGame} theme="primary">
{t("remove")}
</Button>
</div>
</Modal>

View File

@@ -36,12 +36,12 @@ export function ResetAchievementsModal({
})}
>
<div className="reset-achievements-modal__actions">
<Button onClick={handleResetAchievements} theme="outline">
{t("reset_achievements")}
<Button onClick={onClose} theme="outline">
{t("cancel")}
</Button>
<Button onClick={onClose} theme="primary">
{t("cancel")}
<Button onClick={handleResetAchievements} theme="primary">
{t("reset_achievements")}
</Button>
</div>
</Modal>

View File

@@ -38,12 +38,12 @@ export const DeleteAllThemesModal = ({
onClose={onClose}
>
<div className="delete-all-themes-modal__container">
<Button theme="outline" onClick={handleDeleteAllThemes}>
{t("delete_all_themes")}
<Button theme="outline" onClick={onClose}>
{t("cancel")}
</Button>
<Button theme="primary" onClick={onClose}>
{t("cancel")}
<Button theme="primary" onClick={handleDeleteAllThemes}>
{t("delete_all_themes")}
</Button>
</div>
</Modal>

View File

@@ -41,12 +41,12 @@ export const DeleteThemeModal = ({
onClose={onClose}
>
<div className="delete-all-themes-modal__container">
<Button theme="outline" onClick={handleDeleteTheme}>
{t("delete_theme")}
<Button theme="outline" onClick={onClose}>
{t("cancel")}
</Button>
<Button theme="primary" onClick={onClose}>
{t("cancel")}
<Button theme="primary" onClick={handleDeleteTheme}>
{t("delete_theme")}
</Button>
</div>
</Modal>

View File

@@ -77,12 +77,12 @@ export const ImportThemeModal = ({
onClose={onClose}
>
<div className="delete-all-themes-modal__container">
<Button theme="outline" onClick={handleImportTheme}>
{t("import_theme")}
<Button theme="outline" onClick={onClose}>
{t("cancel")}
</Button>
<Button theme="primary" onClick={onClose}>
{t("cancel")}
<Button theme="primary" onClick={handleImportTheme}>
{t("import_theme")}
</Button>
</div>
</Modal>