mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
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:
parent
2c26fed478
commit
48b6d1c941
9 changed files with 283 additions and 1 deletions
|
@ -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";
|
||||||
|
|
22
src/main/events/library/open-game-executable-path.ts
Normal file
22
src/main/events/library/open-game-executable-path.ts
Normal 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);
|
27
src/main/events/library/open-game-installer-path.ts
Normal file
27
src/main/events/library/open-game-installer-path.ts
Normal 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);
|
20
src/main/events/library/update-executable-path.ts
Normal file
20
src/main/events/library/update-executable-path.ts
Normal 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);
|
|
@ -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),
|
||||||
|
|
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
|
@ -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>;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`,
|
||||||
|
});
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue