diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b1a9c61b..6401a311 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -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", diff --git a/src/renderer/src/pages/downloads/delete-game-modal.tsx b/src/renderer/src/pages/downloads/delete-game-modal.tsx index 7f4a530e..b8b4d8a6 100644 --- a/src/renderer/src/pages/downloads/delete-game-modal.tsx +++ b/src/renderer/src/pages/downloads/delete-game-modal.tsx @@ -28,12 +28,12 @@ export function DeleteGameModal({ onClose={onClose} >
- -
diff --git a/src/renderer/src/pages/game-details/description-header/description-header.scss b/src/renderer/src/pages/game-details/description-header/description-header.scss index a6faaee3..920e8068 100644 --- a/src/renderer/src/pages/game-details/description-header/description-header.scss +++ b/src/renderer/src/pages/game-details/description-header/description-header.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss index c7932a34..f66da32b 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index a5566a96..786a8d30 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx index 7355461a..6d5ef135 100644 --- a/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/change-game-playtime-modal.tsx @@ -149,17 +149,17 @@ export function ChangeGamePlaytimeModal({
+ + - -
diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss index 3a5d8943..5400df07 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.scss +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.scss @@ -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; diff --git a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx index 614d458b..04a27779 100644 --- a/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/edit-game-modal.tsx @@ -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("icon"); - // Store default image URLs for non-custom games const [defaultIconUrl, setDefaultIconUrl] = useState(null); const [defaultLogoUrl, setDefaultLogoUrl] = useState(null); const [defaultHeroUrl, setDefaultHeroUrl] = useState(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(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 ( +
+ + + {game && !isCustomGame(game) && assetPath && ( + + )} +
+ } + /> +
+ {t(getResolutionKey())} +
+ + {hasImage && ( + + )} + + {!hasImage && ( + + )} + + ); }; return ( @@ -486,266 +491,39 @@ export function EditGameModal({ disabled={isUpdating} /> -
- - - {game && !isCustomGame(game) && iconPath && ( - - )} -
- } - /> -
- {t("edit_game_modal_icon_resolution")} +
+
+ {t("edit_game_modal_assets")}
- - {(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && ( - - )} - - {!iconPath && !(game && !isCustomGame(game) && defaultIconUrl) && ( - - )} + {t("edit_game_modal_logo")} + + +
-
- - - {game && !isCustomGame(game) && logoPath && ( - - )} -
- } - /> -
- {t("edit_game_modal_logo_resolution")} -
- - {(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && ( - - )} - - {!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl) && ( - - )} - - -
- - - {game && !isCustomGame(game) && heroPath && ( - - )} -
- } - /> -
- {t("edit_game_modal_hero_resolution")} -
- - {(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && ( - - )} - - {!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl) && ( - - )} - + {renderImageSection(selectedAssetType)}
diff --git a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx index 85cea8cd..eb421ec7 100644 --- a/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/remove-from-library-modal.tsx @@ -31,12 +31,12 @@ export function RemoveGameFromLibraryModal({ onClose={onClose} >
- -
diff --git a/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx b/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx index fc71e2d0..b6eb38a2 100644 --- a/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/reset-achievements-modal.tsx @@ -36,12 +36,12 @@ export function ResetAchievementsModal({ })} >
- -
diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx index be46e56a..9439d273 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-all-themes-modal.tsx @@ -38,12 +38,12 @@ export const DeleteAllThemesModal = ({ onClose={onClose} >
- -
diff --git a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx index 5783ce01..c1a5a1e0 100644 --- a/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/delete-theme-modal.tsx @@ -41,12 +41,12 @@ export const DeleteThemeModal = ({ onClose={onClose} >
- -
diff --git a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx index db4abe8c..601e9568 100644 --- a/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx +++ b/src/renderer/src/pages/settings/aparence/modals/import-theme-modal.tsx @@ -77,12 +77,12 @@ export const ImportThemeModal = ({ onClose={onClose} >
- -