mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 08:43:57 +00:00
feat: improving visuals on assets
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user