added drag and drop functionality and recommended resolution to each image

This commit is contained in:
Moyasee
2025-09-19 22:23:36 +03:00
parent 607bc6407c
commit e0496b6449
12 changed files with 500 additions and 120 deletions

View File

@@ -143,8 +143,8 @@ export function SidebarAddingCustomGameModal({
/>
<TextField
label={t("custom_game_modal_game_name")}
placeholder={t("custom_game_modal_enter_name")}
label={t("custom_game_modal_title")}
placeholder={t("custom_game_modal_enter_title")}
value={gameName}
onChange={handleGameNameChange}
theme="dark"

View File

@@ -298,6 +298,8 @@ declare global {
onCommonRedistProgress: (
cb: (value: { log: string; complete: boolean }) => void
) => () => Electron.IpcRenderer;
saveTempFile: (fileName: string, fileData: Uint8Array) => Promise<string>;
deleteTempFile: (filePath: string) => Promise<void>;
platform: NodeJS.Platform;
/* Auto update */

View File

@@ -170,12 +170,28 @@ export function GameDetailsContent() {
style={{ opacity: backdropOpacity }}
>
<div className="game-details__hero-content">
{logoImage && (
<img
src={logoImage}
className="game-details__game-logo"
alt={game?.title}
/>
{isCustomGame ? (
// For custom games, show logo image if available, otherwise show game title as text
logoImage ? (
<img
src={logoImage}
className="game-details__game-logo"
alt={game?.title}
/>
) : (
<div className="game-details__game-logo-text">
{game?.title}
</div>
)
) : (
// For non-custom games, show logo image if available
logoImage && (
<img
src={logoImage}
className="game-details__game-logo"
alt={game?.title}
/>
)
)}
<div className="game-details__hero-buttons game-details__hero-buttons--right">

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<string | null>(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 (
<Modal
visible={visible}
title={t("edit_custom_game_modal")}
description={t("edit_custom_game_modal_description")}
title={t("edit_game_modal")}
description={t("edit_game_modal_description")}
onClose={handleClose}
>
<div className="edit-game-modal__container">
<div className="edit-game-modal__form">
<TextField
label={t("edit_custom_game_modal_game_name")}
placeholder={t("edit_custom_game_modal_enter_name")}
label={t("edit_game_modal_title")}
placeholder={t("edit_game_modal_enter_title")}
value={gameName}
onChange={handleGameNameChange}
theme="dark"
@@ -328,8 +464,8 @@ export function EditGameModal({
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_custom_game_modal_icon")}
placeholder={t("edit_custom_game_modal_select_icon")}
label={t("edit_game_modal_icon")}
placeholder={t("edit_game_modal_select_icon")}
value={iconPath}
readOnly
theme="dark"
@@ -342,7 +478,7 @@ export function EditGameModal({
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && iconPath && (
<Button
@@ -358,22 +494,55 @@ export function EditGameModal({
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_icon_resolution")}
</div>
{(iconPath || (game && !isCustomGame(game) && defaultIconUrl)) && (
<div className="edit-game-modal__image-preview">
<div
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}
>
<img
src={getIconPreviewUrl()}
alt={t("edit_custom_game_modal_icon_preview")}
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>
)}
</div>
)}
{(!iconPath && !(game && !isCustomGame(game) && defaultIconUrl)) && (
<div
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}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop icon image here</span>
</div>
</div>
)}
</div>
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_custom_game_modal_logo")}
placeholder={t("edit_custom_game_modal_select_logo")}
label={t("edit_game_modal_logo")}
placeholder={t("edit_game_modal_select_logo")}
value={logoPath}
readOnly
theme="dark"
@@ -386,7 +555,7 @@ export function EditGameModal({
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && logoPath && (
<Button
@@ -402,22 +571,55 @@ export function EditGameModal({
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_logo_resolution")}
</div>
{(logoPath || (game && !isCustomGame(game) && defaultLogoUrl)) && (
<div className="edit-game-modal__image-preview">
<div
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}
>
<img
src={getLogoPreviewUrl()}
alt={t("edit_custom_game_modal_logo_preview")}
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>
)}
</div>
)}
{(!logoPath && !(game && !isCustomGame(game) && defaultLogoUrl)) && (
<div
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}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop logo image here</span>
</div>
</div>
)}
</div>
<div className="edit-game-modal__image-section">
<TextField
label={t("edit_custom_game_modal_hero")}
placeholder={t("edit_custom_game_modal_select_hero")}
label={t("edit_game_modal_hero")}
placeholder={t("edit_game_modal_select_hero")}
value={heroPath}
readOnly
theme="dark"
@@ -430,7 +632,7 @@ export function EditGameModal({
disabled={isUpdating}
>
<ImageIcon />
{t("edit_custom_game_modal_browse")}
{t("edit_game_modal_browse")}
</Button>
{game && !isCustomGame(game) && heroPath && (
<Button
@@ -446,14 +648,47 @@ export function EditGameModal({
</div>
}
/>
<div className="edit-game-modal__resolution-info">
{t("edit_game_modal_hero_resolution")}
</div>
{(heroPath || (game && !isCustomGame(game) && defaultHeroUrl)) && (
<div className="edit-game-modal__image-preview">
<div
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}
>
<img
src={getHeroPreviewUrl()}
alt={t("edit_custom_game_modal_hero_preview")}
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>
)}
</div>
)}
{(!heroPath && !(game && !isCustomGame(game) && defaultHeroUrl)) && (
<div
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}
>
<div className="edit-game-modal__drop-zone-content">
<ImageIcon />
<span>Drop hero image here</span>
</div>
</div>
)}
</div>
@@ -466,7 +701,7 @@ export function EditGameModal({
onClick={handleClose}
disabled={isUpdating}
>
{t("edit_custom_game_modal_cancel")}
{t("edit_game_modal_cancel")}
</Button>
<Button
type="button"
@@ -475,8 +710,8 @@ export function EditGameModal({
disabled={!isFormValid || isUpdating}
>
{isUpdating
? t("edit_custom_game_modal_updating")
: t("edit_custom_game_modal_update")}
? t("edit_game_modal_updating")
: t("edit_game_modal_update")}
</Button>
</div>
</div>