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-library";
|
||||
import "./library/open-game";
|
||||
import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
import "./library/open-game-installer-path";
|
||||
import "./library/update-executable-path";
|
||||
import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
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,
|
||||
executablePath
|
||||
),
|
||||
updateExecutablePath: (id: number, executablePath: string) =>
|
||||
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
|
||||
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||
openGameInstaller: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameInstaller", gameId),
|
||||
openGameInstallerPath: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameInstallerPath", gameId),
|
||||
openGameExecutablePath: (gameId: number) =>
|
||||
ipcRenderer.invoke("openGameExecutablePath", gameId),
|
||||
openGame: (gameId: number, executablePath: string) =>
|
||||
ipcRenderer.invoke("openGame", gameId, executablePath),
|
||||
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,
|
||||
executablePath: string | null
|
||||
) => Promise<void>;
|
||||
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||
openGameInstallerPath: (gameId: number) => Promise<boolean>;
|
||||
openGameExecutablePath: (gameId: number) => Promise<boolean>;
|
||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||
closeGame: (gameId: number) => Promise<boolean>;
|
||||
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";
|
||||
|
||||
|
@ -10,11 +10,13 @@ import { useTranslation } from "react-i18next";
|
|||
import * as styles from "./hero-panel-actions.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
import { Downloader } from "@shared";
|
||||
import { GameOptionsModal } from "../modals/game-options-modal";
|
||||
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||
|
||||
const {
|
||||
resumeDownload,
|
||||
|
@ -224,6 +226,25 @@ export function HeroPanelActions() {
|
|||
if (game) {
|
||||
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 && (
|
||||
<>
|
||||
<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