Feat: Custom Games

This commit is contained in:
Moyasee
2025-09-19 16:22:12 +03:00
parent 7e59e02d03
commit 3409b53268
17 changed files with 158 additions and 111 deletions

View File

@@ -49,4 +49,4 @@
justify-content: flex-end;
gap: calc(globals.$spacing-unit * 2);
}
}
}

View File

@@ -5,7 +5,10 @@ import { FileDirectoryIcon } from "@primer/octicons-react";
import { Modal, TextField, Button } from "@renderer/components";
import { useLibrary, useToast } from "@renderer/hooks";
import { buildGameDetailsPath, generateRandomGradient } from "@renderer/helpers";
import {
buildGameDetailsPath,
generateRandomGradient,
} from "@renderer/helpers";
import "./sidebar-adding-custom-game-modal.scss";
@@ -41,7 +44,7 @@ export function SidebarAddingCustomGameModal({
if (filePaths && filePaths.length > 0) {
const selectedPath = filePaths[0];
setExecutablePath(selectedPath);
if (!gameName.trim()) {
const fileName = selectedPath.split(/[\\/]/).pop() || "";
const gameNameFromFile = fileName.replace(/\.[^/.]+$/, "");
@@ -54,8 +57,6 @@ export function SidebarAddingCustomGameModal({
setGameName(event.target.value);
};
const handleAddGame = async () => {
if (!gameName.trim() || !executablePath.trim()) {
showErrorToast(t("custom_game_modal_fill_required"));
@@ -70,7 +71,7 @@ export function SidebarAddingCustomGameModal({
const iconUrl = ""; // Don't use gradient for icon
const logoImageUrl = ""; // Don't use gradient for logo
const libraryHeroImageUrl = generateRandomGradient(); // Only use gradient for hero
const newGame = await window.electron.addCustomGameToLibrary(
gameNameForSeed,
executablePath,
@@ -81,24 +82,22 @@ export function SidebarAddingCustomGameModal({
showSuccessToast(t("custom_game_modal_success"));
updateLibrary();
const gameDetailsPath = buildGameDetailsPath({
shop: "custom",
objectId: newGame.objectId,
title: newGame.title
title: newGame.title,
});
navigate(gameDetailsPath);
setGameName("");
setExecutablePath("");
onClose();
} catch (error) {
console.error("Failed to add custom game:", error);
showErrorToast(
error instanceof Error
? error.message
: t("custom_game_modal_failed")
error instanceof Error ? error.message : t("custom_game_modal_failed")
);
} finally {
setIsAdding(false);
@@ -115,8 +114,6 @@ export function SidebarAddingCustomGameModal({
const isFormValid = gameName.trim() && executablePath.trim();
return (
<Modal
visible={visible}
@@ -153,31 +150,29 @@ export function SidebarAddingCustomGameModal({
theme="dark"
disabled={isAdding}
/>
</div>
<div className="sidebar-adding-custom-game-modal__actions">
<Button
type="button"
theme="outline"
<Button
type="button"
theme="outline"
onClick={handleClose}
disabled={isAdding}
>
{t("custom_game_modal_cancel")}
</Button>
<Button
type="button"
theme="primary"
<Button
type="button"
theme="primary"
onClick={handleAddGame}
disabled={!isFormValid || isAdding}
>
{isAdding ? t("custom_game_modal_adding") : t("custom_game_modal_add")}
{isAdding
? t("custom_game_modal_adding")
: t("custom_game_modal_add")}
</Button>
</div>
</div>
</Modal>
);
}
}

View File

@@ -22,7 +22,11 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import cn from "classnames";
import { CommentDiscussionIcon, PlayIcon, PlusIcon } from "@primer/octicons-react";
import {
CommentDiscussionIcon,
PlayIcon,
PlusIcon,
} from "@primer/octicons-react";
import { SidebarGameItem } from "./sidebar-game-item";
import { SidebarAddingCustomGameModal } from "./sidebar-adding-custom-game-modal";
import { setFriendRequestCount } from "@renderer/features/user-details-slice";
@@ -265,7 +269,9 @@ export function Sidebar() {
<small className="sidebar__section-title">
{t("my_library")}
</small>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<div
style={{ display: "flex", gap: "8px", alignItems: "center" }}
>
<button
type="button"
className="sidebar__add-button"

View File

@@ -87,9 +87,9 @@ export const removeCustomCss = (target: HTMLElement = document.head) => {
export const generateRandomGradient = (): string => {
// Use a single consistent gradient with softer colors for custom games as placeholder
const color1 = '#2c3e50'; // Dark blue-gray
const color2 = '#34495e'; // Darker slate
const color1 = "#2c3e50"; // Dark blue-gray
const color2 = "#34495e"; // Darker slate
// Create SVG data URL that works in img tags
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<defs>
@@ -100,7 +100,7 @@ export const generateRandomGradient = (): string => {
</defs>
<rect width="100%" height="100%" fill="url(#grad)" />
</svg>`;
// Return as data URL that works in img tags
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
};

View File

@@ -119,7 +119,7 @@ export function GameDetailsContent() {
? game?.libraryHeroImageUrl || game?.iconUrl || ""
: shopDetails?.assets?.libraryHeroImageUrl || "";
const logoImage = isCustomGame
? game?.logoImageUrl || "" // Don't use icon as fallback for custom games
? game?.logoImageUrl || "" // Don't use icon as fallback for custom games
: shopDetails?.assets?.logoImageUrl || "";
return (

View File

@@ -56,7 +56,7 @@ $hero-height: 300px;
display: flex;
gap: globals.$spacing-unit;
align-items: center;
&--right {
margin-left: auto;
}

View File

@@ -43,4 +43,4 @@
justify-content: flex-end;
margin-top: globals.$spacing-unit;
}
}
}

View File

@@ -33,17 +33,17 @@ export function EditCustomGameModal({
useEffect(() => {
if (game && visible) {
setGameName(game.title || "");
const currentIconPath = game.iconUrl?.startsWith("local:")
? game.iconUrl.replace("local:", "")
const currentIconPath = game.iconUrl?.startsWith("local:")
? game.iconUrl.replace("local:", "")
: "";
const currentLogoPath = game.logoImageUrl?.startsWith("local:")
? game.logoImageUrl.replace("local:", "")
const currentLogoPath = game.logoImageUrl?.startsWith("local:")
? game.logoImageUrl.replace("local:", "")
: "";
const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:")
? game.libraryHeroImageUrl.replace("local:", "")
const currentHeroPath = game.libraryHeroImageUrl?.startsWith("local:")
? game.libraryHeroImageUrl.replace("local:", "")
: "";
setIconPath(currentIconPath);
setLogoPath(currentLogoPath);
setHeroPath(currentHeroPath);
@@ -114,8 +114,10 @@ export function EditCustomGameModal({
// Preserve existing image URLs if not changed
const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl;
const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl;
const libraryHeroImageUrl = heroPath ? `local:${heroPath}` : game.libraryHeroImageUrl;
const libraryHeroImageUrl = heroPath
? `local:${heroPath}`
: game.libraryHeroImageUrl;
const updatedGame = await window.electron.updateCustomGame(
game.shop,
game.objectId,
@@ -131,8 +133,8 @@ export function EditCustomGameModal({
} catch (error) {
console.error("Failed to update custom game:", error);
showErrorToast(
error instanceof Error
? error.message
error instanceof Error
? error.message
: t("edit_custom_game_modal_failed")
);
} finally {
@@ -143,17 +145,17 @@ export function EditCustomGameModal({
const handleClose = () => {
if (!isUpdating) {
setGameName(game?.title || "");
const currentIconPath = game?.iconUrl?.startsWith("local:")
? game.iconUrl.replace("local:", "")
const currentIconPath = game?.iconUrl?.startsWith("local:")
? game.iconUrl.replace("local:", "")
: "";
const currentLogoPath = game?.logoImageUrl?.startsWith("local:")
? game.logoImageUrl.replace("local:", "")
const currentLogoPath = game?.logoImageUrl?.startsWith("local:")
? game.logoImageUrl.replace("local:", "")
: "";
const currentHeroPath = game?.libraryHeroImageUrl?.startsWith("local:")
? game.libraryHeroImageUrl.replace("local:", "")
const currentHeroPath = game?.libraryHeroImageUrl?.startsWith("local:")
? game.libraryHeroImageUrl.replace("local:", "")
: "";
setIconPath(currentIconPath);
setLogoPath(currentLogoPath);
setHeroPath(currentHeroPath);
@@ -212,7 +214,7 @@ export function EditCustomGameModal({
</Button>
}
/>
{iconPath && (
<div className="edit-custom-game-modal__image-preview">
<img
@@ -243,7 +245,7 @@ export function EditCustomGameModal({
</Button>
}
/>
{logoPath && (
<div className="edit-custom-game-modal__image-preview">
<img
@@ -274,7 +276,7 @@ export function EditCustomGameModal({
</Button>
}
/>
{heroPath && (
<div className="edit-custom-game-modal__image-preview">
<img
@@ -288,24 +290,26 @@ export function EditCustomGameModal({
</div>
<div className="edit-custom-game-modal__actions">
<Button
type="button"
theme="outline"
<Button
type="button"
theme="outline"
onClick={handleClose}
disabled={isUpdating}
>
{t("edit_custom_game_modal_cancel")}
</Button>
<Button
type="button"
theme="primary"
<Button
type="button"
theme="primary"
onClick={handleUpdateGame}
disabled={!isFormValid || isUpdating}
>
{isUpdating ? t("edit_custom_game_modal_updating") : t("edit_custom_game_modal_update")}
{isUpdating
? t("edit_custom_game_modal_updating")
: t("edit_custom_game_modal_update")}
</Button>
</div>
</div>
</Modal>
);
}
}