feat: create game options modal

fix error when getSteamAppDetails fails

dont set game as removed when deleting instalation folder

fix game not deleting installation folder

organize code

feat: add open game executable path and installer path
This commit is contained in:
Zamitto 2024-06-04 17:33:21 -03:00
parent 2c26fed478
commit 48b6d1c941
9 changed files with 283 additions and 1 deletions

View file

@ -15,7 +15,10 @@ import "./library/delete-game-folder";
import "./library/get-game-by-object-id"; import "./library/get-game-by-object-id";
import "./library/get-library"; import "./library/get-library";
import "./library/open-game"; import "./library/open-game";
import "./library/open-game-executable-path";
import "./library/open-game-installer"; import "./library/open-game-installer";
import "./library/open-game-installer-path";
import "./library/update-executable-path";
import "./library/remove-game"; import "./library/remove-game";
import "./library/remove-game-from-library"; import "./library/remove-game-from-library";
import "./misc/open-external"; import "./misc/open-external";

View file

@ -0,0 +1,22 @@
import { shell } from "electron";
import path from "node:path";
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
const openGameExecutablePath = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
if (!game || !game.executablePath) return true;
const gamePath = path.join(game.executablePath, "../");
shell.openPath(gamePath);
return true;
};
registerEvent("openGameExecutablePath", openGameExecutablePath);

View file

@ -0,0 +1,27 @@
import { shell } from "electron";
import path from "node:path";
import { gameRepository } from "@main/repository";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { registerEvent } from "../register-event";
const openGameInstallerPath = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false },
});
if (!game || !game.folderName) return true;
const gamePath = path.join(
game.downloadPath ?? (await getDownloadsPath()),
game.folderName!
);
shell.openPath(gamePath);
return true;
};
registerEvent("openGameInstallerPath", openGameInstallerPath);

View file

@ -0,0 +1,20 @@
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
const updateExecutablePath = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
executablePath: string
) => {
return gameRepository.update(
{
id,
},
{
executablePath,
}
);
};
registerEvent("updateExecutablePath", updateExecutablePath);

View file

@ -73,9 +73,15 @@ contextBridge.exposeInMainWorld("electron", {
shop, shop,
executablePath executablePath
), ),
updateExecutablePath: (id: number, executablePath: string) =>
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
getLibrary: () => ipcRenderer.invoke("getLibrary"), getLibrary: () => ipcRenderer.invoke("getLibrary"),
openGameInstaller: (gameId: number) => openGameInstaller: (gameId: number) =>
ipcRenderer.invoke("openGameInstaller", gameId), ipcRenderer.invoke("openGameInstaller", gameId),
openGameInstallerPath: (gameId: number) =>
ipcRenderer.invoke("openGameInstallerPath", gameId),
openGameExecutablePath: (gameId: number) =>
ipcRenderer.invoke("openGameExecutablePath", gameId),
openGame: (gameId: number, executablePath: string) => openGame: (gameId: number, executablePath: string) =>
ipcRenderer.invoke("openGame", gameId, executablePath), ipcRenderer.invoke("openGame", gameId, executablePath),
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),

View file

@ -59,8 +59,11 @@ declare global {
shop: GameShop, shop: GameShop,
executablePath: string | null executablePath: string | null
) => Promise<void>; ) => Promise<void>;
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
getLibrary: () => Promise<LibraryGame[]>; getLibrary: () => Promise<LibraryGame[]>;
openGameInstaller: (gameId: number) => Promise<boolean>; openGameInstaller: (gameId: number) => Promise<boolean>;
openGameInstallerPath: (gameId: number) => Promise<boolean>;
openGameExecutablePath: (gameId: number) => Promise<boolean>;
openGame: (gameId: number, executablePath: string) => Promise<void>; openGame: (gameId: number, executablePath: string) => Promise<void>;
closeGame: (gameId: number) => Promise<boolean>; closeGame: (gameId: number) => Promise<boolean>;
removeGameFromLibrary: (gameId: number) => Promise<void>; removeGameFromLibrary: (gameId: number) => Promise<void>;

View file

@ -1,4 +1,4 @@
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react"; import { GearIcon, NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal"; import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
@ -10,11 +10,13 @@ import { useTranslation } from "react-i18next";
import * as styles from "./hero-panel-actions.css"; import * as styles from "./hero-panel-actions.css";
import { gameDetailsContext } from "../game-details.context"; import { gameDetailsContext } from "../game-details.context";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { GameOptionsModal } from "../modals/game-options-modal";
export function HeroPanelActions() { export function HeroPanelActions() {
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] = const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
useState(false); useState(false);
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false); const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
const { const {
resumeDownload, resumeDownload,
@ -224,6 +226,25 @@ export function HeroPanelActions() {
if (game) { if (game) {
return ( return (
<> <>
<GameOptionsModal
visible={showGameOptionsModal}
game={game}
onClose={() => {
setShowGameOptionsModal(false);
}}
selectGameExecutable={selectGameExecutable}
/>
<Button
onClick={() => {
setShowGameOptionsModal(true);
}}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
<GearIcon />
</Button>
{game?.progress === 1 && game?.folderName && ( {game?.progress === 1 && game?.folderName && (
<> <>
<BinaryNotFoundModal <BinaryNotFoundModal

View file

@ -0,0 +1,13 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../../theme.css";
export const optionsContainer = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
flexDirection: "column",
});
export const downloadSourceField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View file

@ -0,0 +1,167 @@
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import type { Game } from "@types";
import * as styles from "./game-options-modal.css";
import { SPACING_UNIT } from "../../../theme.css";
import { gameDetailsContext } from "../game-details.context";
import {
FileDirectoryOpenFillIcon,
FileSymlinkFileIcon,
TrashIcon,
} from "@primer/octicons-react";
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
import { useDownload } from "@renderer/hooks";
export interface GameOptionsModalProps {
visible: boolean;
game: Game;
onClose: () => void;
selectGameExecutable: () => Promise<string | null>;
}
export function GameOptionsModal({
visible,
game,
onClose,
selectGameExecutable,
}: GameOptionsModalProps) {
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
const { updateGame, openRepacksModal } = useContext(gameDetailsContext);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const { removeGameInstaller, isGameDeleting } = useDownload();
const deleting = game ? isGameDeleting(game?.id) : false;
const { t } = useTranslation("game_details");
const handleChangeExecutableLocation = async () => {
const location = await selectGameExecutable();
if (location) {
await window.electron.updateExecutablePath(game.id, location);
updateGame();
}
};
const handleDeleteGame = async () => {
await removeGameInstaller(game.id);
};
const handleOpenGameInstallerPath = async () => {
await window.electron.openGameInstallerPath(game.id);
};
const handleOpenGameExecutablePath = async () => {
await window.electron.openGameExecutablePath(game.id);
};
return (
<>
<Modal visible={visible} title={game.title} onClose={onClose}>
<DeleteGameModal
visible={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
deleteGame={handleDeleteGame}
/>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
minWidth: "500px",
}}
>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<Button
key={"general"}
theme={currentCategoryIndex === 0 ? "primary" : "outline"}
onClick={() => setCurrentCategoryIndex(0)}
>
General
</Button>
</div>
<div className={styles.downloadSourceField}>
<TextField
label="Caminho do executável"
value={game.executablePath || ""}
readOnly
theme="dark"
disabled
placeholder="Selecione um executável"
/>
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-end" }}
onClick={handleOpenGameExecutablePath}
>
<FileDirectoryOpenFillIcon />
{"Abrir local do executável"}
</Button>
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-end" }}
onClick={handleChangeExecutableLocation}
>
<FileSymlinkFileIcon />
{"Alterar"}
</Button>
</div>
<div className={styles.downloadSourceField}>
<TextField
label="Caminho do instalador"
value={game.downloadPath + game.folderName}
readOnly
theme="dark"
disabled
placeholder=""
/>
</div>
<div className={styles.downloadSourceField}>
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-end" }}
onClick={handleOpenGameInstallerPath}
>
<FileDirectoryOpenFillIcon />
{"Abrir pasta do instalador"}
</Button>
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-end" }}
onClick={() => {
setShowDeleteModal(true);
}}
>
<TrashIcon />
{"Remover instalador"}
</Button>
<Button
onClick={openRepacksModal}
theme="outline"
disabled={deleting}
>
{t("open_download_options")}
</Button>
</div>
</div>
</Modal>
</>
);
}