diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 86ef567b..96fc573b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -35,53 +35,42 @@ "custom_game_modal_description": "Add a custom game to your library by selecting an executable file", "custom_game_modal_executable_path": "Executable Path", "custom_game_modal_select_executable": "Select executable file", - "custom_game_modal_game_name": "Game Name", - "custom_game_modal_enter_name": "Enter game name", - "custom_game_modal_image": "Game Image", - "custom_game_modal_select_image": "Select game image", - "custom_game_modal_image_preview": "Game image preview", + "custom_game_modal_title": "Title", + "custom_game_modal_enter_title": "Enter title", "custom_game_modal_browse": "Browse", "custom_game_modal_cancel": "Cancel", "custom_game_modal_add": "Add Game", "custom_game_modal_adding": "Adding Game...", - "custom_game_modal_fill_required": "Please fill in all required fields", "custom_game_modal_success": "Custom game added successfully", "custom_game_modal_failed": "Failed to add custom game", "custom_game_modal_executable": "Executable", - "custom_game_modal_image_filter": "Image", - "custom_game_modal_icon": "Game Icon", - "custom_game_modal_select_icon": "Select game icon", - "custom_game_modal_icon_preview": "Game icon preview", - "custom_game_modal_logo": "Game Logo", - "custom_game_modal_select_logo": "Select game logo", - "custom_game_modal_logo_preview": "Game logo preview", - "custom_game_modal_hero": "Library Hero Image", - "custom_game_modal_select_hero": "Select library hero image", - "custom_game_modal_hero_preview": "Library hero image preview", - "edit_custom_game_modal": "Edit Custom Game", - "edit_custom_game_modal_description": "Edit your custom game details", - "edit_custom_game_modal_game_name": "Game Name", - "edit_custom_game_modal_enter_name": "Enter game name", - "edit_custom_game_modal_image": "Game Image", - "edit_custom_game_modal_select_image": "Select game image", - "edit_custom_game_modal_browse": "Browse", - "edit_custom_game_modal_image_preview": "Game image preview", - "edit_custom_game_modal_icon": "Game Icon", - "edit_custom_game_modal_select_icon": "Select game icon", - "edit_custom_game_modal_icon_preview": "Game icon preview", - "edit_custom_game_modal_logo": "Game Logo", - "edit_custom_game_modal_select_logo": "Select game logo", - "edit_custom_game_modal_logo_preview": "Game logo preview", - "edit_custom_game_modal_hero": "Library Hero Image", - "edit_custom_game_modal_select_hero": "Select library hero image", - "edit_custom_game_modal_hero_preview": "Library hero image preview", - "edit_custom_game_modal_cancel": "Cancel", - "edit_custom_game_modal_update": "Update Game", - "edit_custom_game_modal_updating": "Updating Game...", - "edit_custom_game_modal_fill_required": "Please fill in all required fields", - "edit_custom_game_modal_success": "Custom game updated successfully", - "edit_custom_game_modal_failed": "Failed to update custom game", - "edit_custom_game_modal_image_filter": "Image" + "edit_game_modal": "Customize Assets", + "edit_game_modal_description": "Customize game assets and details", + "edit_game_modal_title": "Title", + "edit_game_modal_enter_title": "Enter title", + "edit_game_modal_image": "Image", + "edit_game_modal_select_image": "Select image", + "edit_game_modal_browse": "Browse", + "edit_game_modal_image_preview": "Image preview", + "edit_game_modal_icon": "Icon", + "edit_game_modal_select_icon": "Select icon", + "edit_game_modal_icon_preview": "Icon preview", + "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_select_hero": "Select library hero image", + "edit_game_modal_hero_preview": "Library hero image preview", + "edit_game_modal_cancel": "Cancel", + "edit_game_modal_update": "Update", + "edit_game_modal_updating": "Updating...", + "edit_game_modal_fill_required": "Please fill in all required fields", + "edit_game_modal_success": "Assets updated successfully", + "edit_game_modal_failed": "Failed to update assets", + "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" }, "header": { "search": "Search games", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index ae8f2b9d..b256f90c 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -28,58 +28,47 @@ "friends": "Друзья", "need_help": "Нужна помощь?", "favorites": "Избранное", - "playable_button_title": "Показать только игры, в которые можно играть сейчас", + "playable_button_title": "Показать только установленные игры.", "custom_game_modal": "Добавить пользовательскую игру", "custom_game_modal_description": "Добавьте пользовательскую игру в библиотеку, выбрав исполняемый файл", "custom_game_modal_executable_path": "Путь к исполняемому файлу", "custom_game_modal_select_executable": "Выберите исполняемый файл", - "custom_game_modal_game_name": "Название игры", - "custom_game_modal_enter_name": "Введите название игры", - "custom_game_modal_image": "Изображение игры", - "custom_game_modal_select_image": "Выберите изображение игры", - "custom_game_modal_image_preview": "Предварительный просмотр изображения игры", + "custom_game_modal_title": "Название игры", + "custom_game_modal_enter_title": "Введите название игры", "custom_game_modal_browse": "Обзор", "custom_game_modal_cancel": "Отмена", "custom_game_modal_add": "Добавить игру", "custom_game_modal_adding": "Добавление игры...", - "custom_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля", "custom_game_modal_success": "Пользовательская игра успешно добавлена", "custom_game_modal_failed": "Не удалось добавить пользовательскую игру", "custom_game_modal_executable": "Исполняемый файл", - "custom_game_modal_image_filter": "Изображение", - "custom_game_modal_icon": "Иконка игры", - "custom_game_modal_select_icon": "Выберите иконку игры", - "custom_game_modal_icon_preview": "Предпросмотр иконки игры", - "custom_game_modal_logo": "Логотип игры", - "custom_game_modal_select_logo": "Выберите логотип игры", - "custom_game_modal_logo_preview": "Предпросмотр логотипа игры", - "custom_game_modal_hero": "Изображение героя библиотеки", - "custom_game_modal_select_hero": "Выберите изображение героя библиотеки", - "custom_game_modal_hero_preview": "Предпросмотр изображения героя библиотеки", - "edit_custom_game_modal": "Редактировать пользовательскую игру", - "edit_custom_game_modal_description": "Редактируйте детали вашей пользовательской игры", - "edit_custom_game_modal_game_name": "Название игры", - "edit_custom_game_modal_enter_name": "Введите название игры", - "edit_custom_game_modal_image": "Изображение игры", - "edit_custom_game_modal_select_image": "Выберите изображение игры", - "edit_custom_game_modal_browse": "Обзор", - "edit_custom_game_modal_image_preview": "Предпросмотр изображения игры", - "edit_custom_game_modal_icon": "Иконка игры", - "edit_custom_game_modal_select_icon": "Выберите иконку игры", - "edit_custom_game_modal_icon_preview": "Предпросмотр иконки игры", - "edit_custom_game_modal_logo": "Логотип игры", - "edit_custom_game_modal_select_logo": "Выберите логотип игры", - "edit_custom_game_modal_logo_preview": "Предпросмотр логотипа игры", - "edit_custom_game_modal_hero": "Изображение героя библиотеки", - "edit_custom_game_modal_select_hero": "Выберите изображение героя библиотеки", - "edit_custom_game_modal_hero_preview": "Предпросмотр изображения героя библиотеки", - "edit_custom_game_modal_cancel": "Отмена", - "edit_custom_game_modal_update": "Обновить игру", - "edit_custom_game_modal_updating": "Обновление игры...", - "edit_custom_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля", - "edit_custom_game_modal_success": "Пользовательская игра успешно обновлена", - "edit_custom_game_modal_failed": "Не удалось обновить пользовательскую игру", - "edit_custom_game_modal_image_filter": "Изображение" + "edit_game_modal": "Настроить ресурсы", + "edit_game_modal_description": "Настройте ресурсы и детали игры", + "edit_game_modal_title": "Название", + "edit_game_modal_enter_title": "Введите название", + "edit_game_modal_image": "Изображение", + "edit_game_modal_select_image": "Выберите изображение", + "edit_game_modal_browse": "Обзор", + "edit_game_modal_image_preview": "Предпросмотр изображения", + "edit_game_modal_icon": "Иконка", + "edit_game_modal_select_icon": "Выберите иконку", + "edit_game_modal_icon_preview": "Предпросмотр иконки", + "edit_game_modal_logo": "Логотип", + "edit_game_modal_select_logo": "Выберите логотип", + "edit_game_modal_logo_preview": "Предпросмотр логотипа", + "edit_game_modal_hero": "Изображение обложку игры", + "edit_game_modal_select_hero": "Выберите обложку игры", + "edit_game_modal_hero_preview": "Предпросмотр обложки игры", + "edit_game_modal_cancel": "Отмена", + "edit_game_modal_update": "Обновить", + "edit_game_modal_updating": "Обновление...", + "edit_game_modal_fill_required": "Пожалуйста, заполните все обязательные поля", + "edit_game_modal_success": "Ресурсы успешно обновлены", + "edit_game_modal_failed": "Не удалось обновить ресурсы", + "edit_game_modal_image_filter": "Изображение", + "edit_game_modal_icon_resolution": "Рекомендуемое разрешение: 256x256px", + "edit_game_modal_logo_resolution": "Рекомендуемое разрешение: 640x360px", + "edit_game_modal_hero_resolution": "Рекомендуемое разрешение: 1920x620px" }, "header": { "search": "Поиск", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index deab53a6..2bd402e7 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -50,6 +50,8 @@ import "./misc/show-item-in-folder"; import "./misc/get-badges"; import "./misc/install-common-redist"; import "./misc/can-install-common-redist"; +import "./misc/save-temp-file"; +import "./misc/delete-temp-file"; import "./torrenting/cancel-game-download"; import "./torrenting/pause-game-download"; import "./torrenting/resume-game-download"; diff --git a/src/main/events/misc/delete-temp-file.ts b/src/main/events/misc/delete-temp-file.ts new file mode 100644 index 00000000..b26dd975 --- /dev/null +++ b/src/main/events/misc/delete-temp-file.ts @@ -0,0 +1,18 @@ +import fs from "node:fs"; +import { registerEvent } from "../register-event"; + +const deleteTempFile = async ( + _event: Electron.IpcMainInvokeEvent, + filePath: string +): Promise => { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (error) { + // Silently fail - temp files will be cleaned up by OS eventually + console.warn(`Failed to delete temp file: ${error}`); + } +}; + +registerEvent("deleteTempFile", deleteTempFile); \ No newline at end of file diff --git a/src/main/events/misc/save-temp-file.ts b/src/main/events/misc/save-temp-file.ts new file mode 100644 index 00000000..c9776430 --- /dev/null +++ b/src/main/events/misc/save-temp-file.ts @@ -0,0 +1,24 @@ +import fs from "node:fs"; +import path from "node:path"; +import { app } from "electron"; +import { registerEvent } from "../register-event"; + +const saveTempFile = async ( + _event: Electron.IpcMainInvokeEvent, + fileName: string, + fileData: Uint8Array +): Promise => { + try { + const tempDir = app.getPath("temp"); + const tempFilePath = path.join(tempDir, `hydra-temp-${Date.now()}-${fileName}`); + + // Write the file data to temp directory + fs.writeFileSync(tempFilePath, fileData); + + return tempFilePath; + } catch (error) { + throw new Error(`Failed to save temp file: ${error}`); + } +}; + +registerEvent("saveTempFile", saveTempFile); \ No newline at end of file diff --git a/src/preload/index.ts b/src/preload/index.ts index 3bcd4524..f90e3f58 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -147,6 +147,10 @@ contextBridge.exposeInMainWorld("electron", { sourcePath: string, assetType: "icon" | "logo" | "hero" ) => ipcRenderer.invoke("copyCustomGameAsset", sourcePath, assetType), + saveTempFile: (fileName: string, fileData: Uint8Array) => + ipcRenderer.invoke("saveTempFile", fileName, fileData), + deleteTempFile: (filePath: string) => + ipcRenderer.invoke("deleteTempFile", filePath), cleanupUnusedAssets: () => ipcRenderer.invoke("cleanupUnusedAssets"), updateCustomGame: ( shop: GameShop, diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx index bb396297..f50bd814 100644 --- a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.tsx @@ -143,8 +143,8 @@ export function SidebarAddingCustomGameModal({ /> void ) => () => Electron.IpcRenderer; + saveTempFile: (fileName: string, fileData: Uint8Array) => Promise; + deleteTempFile: (filePath: string) => Promise; platform: NodeJS.Platform; /* Auto update */ diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 6f3c2ee2..f4619514 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -170,12 +170,28 @@ export function GameDetailsContent() { style={{ opacity: backdropOpacity }} >
- {logoImage && ( - {game?.title} + {isCustomGame ? ( + // For custom games, show logo image if available, otherwise show game title as text + logoImage ? ( + {game?.title} + ) : ( +
+ {game?.title} +
+ ) + ) : ( + // For non-custom games, show logo image if available + logoImage && ( + {game?.title} + ) )}
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index e84aeab9..46c6f9db 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -121,6 +121,20 @@ $hero-height: 300px; align-self: flex-end; } + &__game-logo-text { + width: 300px; + align-self: flex-end; + font-size: 2.5rem; + font-weight: bold; + color: #ffffff; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); + text-align: left; + line-height: 1.2; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + } + &__hero-image-skeleton { height: 300px; 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 e596865e..a4c04953 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 @@ -18,6 +18,13 @@ gap: 8px; } +.edit-game-modal__resolution-info { + font-size: 12px; + color: var(--color-text-secondary); + margin-top: -4px; + margin-bottom: 4px; +} + .edit-game-modal__image-preview { display: flex; justify-content: center; @@ -26,6 +33,86 @@ border: 1px solid var(--color-border); border-radius: 8px; background-color: var(--color-background-secondary); + background-image: + linear-gradient(45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.1) 75%); + background-size: 16px 16px; + background-position: 0 0, 0 8px, 8px -8px, -8px 0px; + transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease; + position: relative; + + &:hover { + border-color: var(--color-primary); + } +} + +.edit-game-modal__drop-zone { + min-height: 120px; + cursor: pointer; + border-style: dashed !important; + + &:hover { + border-color: var(--color-primary); + background-color: rgba(var(--color-primary-rgb), 0.05); + } + + &--active { + border-color: var(--color-primary) !important; + background-color: rgba(var(--color-primary-rgb), 0.1) !important; + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3); + } +} + +.edit-game-modal__drop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(var(--color-primary-rgb), 0.9); + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + color: white; + font-weight: 600; + font-size: 14px; + backdrop-filter: blur(2px); + animation: fadeIn 0.2s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.edit-game-modal__drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--color-text-secondary); + font-size: 14px; + + svg { + width: 24px; + height: 24px; + opacity: 0.6; + } +} + +.edit-game-modal__icon-preview { + max-width: 200px; + margin: 0 auto; } .edit-game-modal__preview-image { 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 ea34223c..b7d04ac5 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 @@ -93,7 +93,7 @@ export function EditGameModal({ properties: ["openFile"], filters: [ { - name: t("edit_custom_game_modal_image_filter"), + name: t("edit_game_modal_image_filter"), extensions: ["jpg", "jpeg", "png", "gif", "webp"], }, ], @@ -120,7 +120,7 @@ export function EditGameModal({ properties: ["openFile"], filters: [ { - name: t("edit_custom_game_modal_image_filter"), + name: t("edit_game_modal_image_filter"), extensions: ["jpg", "jpeg", "png", "gif", "webp"], }, ], @@ -147,7 +147,7 @@ export function EditGameModal({ properties: ["openFile"], filters: [ { - name: t("edit_custom_game_modal_image_filter"), + name: t("edit_game_modal_image_filter"), extensions: ["jpg", "jpeg", "png", "gif", "webp"], }, ], @@ -182,6 +182,141 @@ export function EditGameModal({ setHeroPath(""); }; + // Drag and drop state + const [dragOverTarget, setDragOverTarget] = useState(null); + + // Drag and drop handlers + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDragEnter = (e: React.DragEvent, target: string) => { + e.preventDefault(); + e.stopPropagation(); + setDragOverTarget(target); + }; + + 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); + } + }; + + const validateImageFile = (file: File): boolean => { + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + return validTypes.includes(file.type); + }; + + const processDroppedFile = async (file: File, assetType: 'icon' | 'logo' | 'hero') => { + setDragOverTarget(null); + + if (!validateImageFile(file)) { + showErrorToast('Invalid file type. Please select an image file.'); + return; + } + + 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) + if ('path' in file && typeof (file as any).path === 'string') { + filePath = (file as any).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, uint8Array); + + if (!tempPath) { + throw new Error('Unable to process file. Drag and drop may not be fully supported.'); + } + + 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; + } + + 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); + } catch (cleanupError) { + console.warn('Failed to clean up temporary file:', cleanupError); + } + } + } catch (error) { + console.error(`Failed to process dropped ${assetType}:`, error); + showErrorToast(`Failed to process dropped ${assetType}. Please try again.`); + } + }; + + const handleIconDrop = 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], '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'); + } + }; + // Helper function to prepare custom game assets const prepareCustomGameAssets = (game: LibraryGame | Game) => { const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl; @@ -234,18 +369,19 @@ export function EditGameModal({ const handleUpdateGame = async () => { if (!game || !gameName.trim()) { - showErrorToast(t("edit_custom_game_modal_fill_required")); + showErrorToast(t("edit_game_modal_fill_required")); return; } setIsUpdating(true); try { - const updatedGame = game && isCustomGame(game) - ? await updateCustomGame(game) - : await updateNonCustomGame(game as LibraryGame); + const updatedGame = + game && isCustomGame(game) + ? await updateCustomGame(game) + : await updateNonCustomGame(game as LibraryGame); - showSuccessToast(t("edit_custom_game_modal_success")); + showSuccessToast(t("edit_game_modal_success")); onGameUpdated(updatedGame); onClose(); } catch (error) { @@ -253,7 +389,7 @@ export function EditGameModal({ showErrorToast( error instanceof Error ? error.message - : t("edit_custom_game_modal_failed") + : t("edit_game_modal_failed") ); } finally { setIsUpdating(false); @@ -311,15 +447,15 @@ export function EditGameModal({ return (
- {t("edit_custom_game_modal_browse")} + {t("edit_game_modal_browse")} {game && !isCustomGame(game) && iconPath && ( {game && !isCustomGame(game) && logoPath && ( {game && !isCustomGame(game) && heroPath && (