mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge remote-tracking branch 'origin/main' into feat/add-select-folder-modal-in-game-installation
This commit is contained in:
commit
af715aa110
30 changed files with 416 additions and 275 deletions
|
@ -203,7 +203,7 @@ export function Sidebar() {
|
|||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === null || game.status === "cancelled",
|
||||
muted: game.status === "cancelled",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
|
5
src/renderer/declaration.d.ts
vendored
5
src/renderer/declaration.d.ts
vendored
|
@ -55,12 +55,13 @@ declare global {
|
|||
addGameToLibrary: (
|
||||
objectID: string,
|
||||
title: string,
|
||||
shop: GameShop
|
||||
shop: GameShop,
|
||||
executablePath: string
|
||||
) => Promise<void>;
|
||||
getLibrary: () => Promise<Game[]>;
|
||||
getRepackersFriendlyNames: () => Promise<Record<string, string>>;
|
||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||
openGame: (gameId: number, path: string) => Promise<void>;
|
||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||
closeGame: (gameId: number) => Promise<boolean>;
|
||||
removeGame: (gameId: number) => Promise<void>;
|
||||
deleteGameFolder: (gameId: number) => Promise<unknown>;
|
||||
|
|
|
@ -32,7 +32,18 @@ import { store } from "./store";
|
|||
import * as resources from "@locales";
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
init({ dsn: process.env.SENTRY_DSN }, reactInit);
|
||||
init(
|
||||
{
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
beforeSend: async (event) => {
|
||||
const userPreferences = await window.electron.getUserPreferences();
|
||||
|
||||
if (userPreferences?.telemetryEnabled) return event;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
reactInit
|
||||
);
|
||||
}
|
||||
|
||||
const router = createHashRouter([
|
||||
|
|
|
@ -71,6 +71,7 @@ export function Catalogue() {
|
|||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottom: `1px solid ${vars.color.borderColor}`,
|
||||
}}
|
||||
>
|
||||
|
@ -103,7 +104,6 @@ export function Catalogue() {
|
|||
key={game.objectID}
|
||||
game={game}
|
||||
onClick={() => handleGameClick(game)}
|
||||
disabled={!game.repacks.length}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -83,7 +83,9 @@ export function GameDetails() {
|
|||
}, [getGame, gameDownloading?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setGame(null);
|
||||
setIsLoading(true);
|
||||
setIsGamePlaying(false);
|
||||
dispatch(setHeaderTitle(""));
|
||||
|
||||
getRandomGame();
|
||||
|
|
212
src/renderer/pages/game-details/hero-panel-actions.tsx
Normal file
212
src/renderer/pages/game-details/hero-panel-actions.tsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import type { Game, ShopDetails } from "@types";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface HeroPanelActionsProps {
|
||||
game: Game | null;
|
||||
gameDetails: ShopDetails | null;
|
||||
isGamePlaying: boolean;
|
||||
isGameDownloading: boolean;
|
||||
openRepacksModal: () => void;
|
||||
openBinaryNotFoundModal: () => void;
|
||||
getGame: () => void;
|
||||
}
|
||||
|
||||
export function HeroPanelActions({
|
||||
game,
|
||||
gameDetails,
|
||||
isGamePlaying,
|
||||
isGameDownloading,
|
||||
openRepacksModal,
|
||||
openBinaryNotFoundModal,
|
||||
getGame,
|
||||
}: HeroPanelActionsProps) {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
|
||||
const {
|
||||
resumeDownload,
|
||||
pauseDownload,
|
||||
cancelDownload,
|
||||
removeGame,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const selectGameExecutable = async () => {
|
||||
return window.electron
|
||||
.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{
|
||||
name: "Game executable",
|
||||
extensions: window.electron.platform === "win32" ? ["exe"] : [],
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(({ filePaths }) => {
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
return filePaths[0];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleGameOnLibrary = async () => {
|
||||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
if (game) {
|
||||
await removeGame(game.id);
|
||||
} else {
|
||||
const gameExecutablePath = await selectGameExecutable();
|
||||
|
||||
await window.electron.addGameToLibrary(
|
||||
gameDetails.objectID,
|
||||
gameDetails.name,
|
||||
"steam",
|
||||
gameExecutablePath
|
||||
);
|
||||
}
|
||||
|
||||
updateLibrary();
|
||||
getGame();
|
||||
} finally {
|
||||
setToggleLibraryGameDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openGameInstaller = () => {
|
||||
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) openBinaryNotFoundModal();
|
||||
updateLibrary();
|
||||
});
|
||||
};
|
||||
|
||||
const openGame = async () => {
|
||||
if (game.executablePath) {
|
||||
window.electron.openGame(game.id, game.executablePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game?.executablePath) {
|
||||
window.electron.openGame(game.id, game.executablePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const gameExecutablePath = await selectGameExecutable();
|
||||
window.electron.openGame(game.id, gameExecutablePath);
|
||||
};
|
||||
|
||||
const closeGame = () => window.electron.closeGame(game.id);
|
||||
|
||||
const deleting = isGameDeleting(game?.id);
|
||||
|
||||
const toggleGameOnLibraryButton = (
|
||||
<Button
|
||||
theme="outline"
|
||||
disabled={!gameDetails || toggleLibraryGameDisabled}
|
||||
onClick={toggleGameOnLibrary}
|
||||
>
|
||||
{game ? <NoEntryIcon /> : <PlusCircleIcon />}
|
||||
{game ? t("remove_from_library") : t("add_to_library")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "seeding" || (game && !game.status)) {
|
||||
return (
|
||||
<>
|
||||
{game?.status === "seeding" ? (
|
||||
<Button
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
) : (
|
||||
toggleGameOnLibraryButton
|
||||
)}
|
||||
|
||||
{isGamePlaying ? (
|
||||
<Button onClick={closeGame} theme="outline" disabled={deleting}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
>
|
||||
{t("play")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "cancelled") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={openRepacksModal} theme="outline" disabled={deleting}>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => removeGame(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
>
|
||||
{t("remove_from_list")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (gameDetails && gameDetails.repacks.length) {
|
||||
return (
|
||||
<>
|
||||
{toggleGameOnLibraryButton}
|
||||
<Button onClick={openRepacksModal} theme="outline">
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return toggleGameOnLibraryButton;
|
||||
}
|
|
@ -2,16 +2,15 @@ import { format } from "date-fns";
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import type { Game, ShopDetails } from "@types";
|
||||
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { useDate } from "@renderer/hooks/use-date";
|
||||
import { formatBytes } from "@renderer/utils";
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
game: Game | null;
|
||||
|
@ -44,21 +43,8 @@ export function HeroPanel({
|
|||
eta,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
resumeDownload,
|
||||
pauseDownload,
|
||||
cancelDownload,
|
||||
removeGame,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
const { updateLibrary, library } = useLibrary();
|
||||
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
|
||||
const gameOnLibrary = library.find(
|
||||
({ objectID }) => objectID === gameDetails?.objectID
|
||||
);
|
||||
|
||||
const isGameDownloading = isDownloading && gameDownloading?.id === game?.id;
|
||||
|
||||
const updateLastTimePlayed = useCallback(() => {
|
||||
|
@ -83,41 +69,6 @@ export function HeroPanel({
|
|||
}
|
||||
}, [game?.lastTimePlayed, updateLastTimePlayed]);
|
||||
|
||||
const openGameInstaller = () => {
|
||||
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||
updateLibrary();
|
||||
});
|
||||
};
|
||||
|
||||
const openGame = () => {
|
||||
if (game.executablePath) {
|
||||
window.electron.openGame(game.id, game.executablePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game?.executablePath) {
|
||||
window.electron.openGame(game.id, game.executablePath);
|
||||
return;
|
||||
}
|
||||
|
||||
window.electron
|
||||
.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Game executable (.exe)", extensions: ["exe"] }],
|
||||
})
|
||||
.then(({ filePaths }) => {
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
window.electron.openGame(game.id, path);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeGame = () => {
|
||||
window.electron.closeGame(game.id);
|
||||
};
|
||||
|
||||
const finalDownloadSize = useMemo(() => {
|
||||
if (!game) return "N/A";
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
|
@ -128,26 +79,6 @@ export function HeroPanel({
|
|||
return game.repack?.fileSize ?? "N/A";
|
||||
}, [game, isGameDownloading, gameDownloading]);
|
||||
|
||||
const toggleLibraryGame = async () => {
|
||||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
if (gameOnLibrary) {
|
||||
await window.electron.removeGame(gameOnLibrary.id);
|
||||
} else {
|
||||
await window.electron.addGameToLibrary(
|
||||
gameDetails.objectID,
|
||||
gameDetails.name,
|
||||
"steam"
|
||||
);
|
||||
}
|
||||
|
||||
await updateLibrary();
|
||||
} finally {
|
||||
setToggleLibraryGameDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getInfo = () => {
|
||||
if (!gameDetails) return null;
|
||||
|
||||
|
@ -196,7 +127,7 @@ export function HeroPanel({
|
|||
);
|
||||
}
|
||||
|
||||
if (game?.status === "seeding") {
|
||||
if (game?.status === "seeding" || (game && !game.status)) {
|
||||
if (!game.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game.title })}</p>;
|
||||
}
|
||||
|
@ -239,121 +170,26 @@ export function HeroPanel({
|
|||
return <p>{t("no_downloads")}</p>;
|
||||
};
|
||||
|
||||
const getActions = () => {
|
||||
const deleting = isGameDeleting(game?.id);
|
||||
|
||||
const toggleGameOnLibraryButton = (
|
||||
<Button
|
||||
theme="outline"
|
||||
disabled={!gameDetails || toggleLibraryGameDisabled}
|
||||
onClick={toggleLibraryGame}
|
||||
>
|
||||
{gameOnLibrary ? <NoEntryIcon /> : <PlusCircleIcon />}
|
||||
{gameOnLibrary ? t("remove_from_library") : t("add_to_library")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "seeding") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
|
||||
{isGamePlaying ? (
|
||||
<Button onClick={closeGame} theme="outline" disabled={deleting}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
>
|
||||
{t("play")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "cancelled") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={openRepacksModal}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => removeGame(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
>
|
||||
{t("remove_from_list")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (gameDetails && gameDetails.repacks.length) {
|
||||
return (
|
||||
<>
|
||||
{toggleGameOnLibraryButton}
|
||||
<Button onClick={openRepacksModal} theme="outline">
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return toggleGameOnLibraryButton;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BinaryNotFoundModal
|
||||
visible={showBinaryNotFoundModal}
|
||||
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||
/>
|
||||
|
||||
<div style={{ backgroundColor: color }} className={styles.panel}>
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>{getActions()}</div>
|
||||
<div className={styles.actions}>
|
||||
<HeroPanelActions
|
||||
game={game}
|
||||
gameDetails={gameDetails}
|
||||
getGame={getGame}
|
||||
openRepacksModal={openRepacksModal}
|
||||
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
||||
isGamePlaying={isGamePlaying}
|
||||
isGameDownloading={isGameDownloading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { DiskSpace } from "check-disk-space";
|
|||
import { format } from "date-fns";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { formatBytes } from "@renderer/utils";
|
||||
import { useAppSelector } from "@renderer/hooks";
|
||||
import { SelectFolderModal } from "./select-folder-modal";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
|
@ -30,6 +31,10 @@ export function RepacksModal({
|
|||
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
||||
const [repack, setRepack] = useState<GameRepack>(null);
|
||||
|
||||
const repackersFriendlyNames = useAppSelector(
|
||||
(state) => state.repackersFriendlyNames.value
|
||||
);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -91,7 +96,7 @@ export function RepacksModal({
|
|||
>
|
||||
<p style={{ color: "#DADBE1" }}>{repack.title}</p>
|
||||
<p style={{ fontSize: "12px" }}>
|
||||
{repack.fileSize} - {repack.repacker} -{" "}
|
||||
{repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
|
||||
{format(repack.uploadDate, "dd/MM/yyyy")}
|
||||
</p>
|
||||
</Button>
|
||||
|
|
|
@ -67,7 +67,6 @@ export function SearchResults() {
|
|||
key={game.objectID}
|
||||
game={game}
|
||||
onClick={() => handleGameClick(game)}
|
||||
disabled={!game.repacks.length}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -10,6 +10,7 @@ export function Settings() {
|
|||
downloadsPath: "",
|
||||
downloadNotificationsEnabled: false,
|
||||
repackUpdatesNotificationsEnabled: false,
|
||||
telemetryEnabled: false,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
@ -25,6 +26,7 @@ export function Settings() {
|
|||
userPreferences?.downloadNotificationsEnabled,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
userPreferences?.repackUpdatesNotificationsEnabled,
|
||||
telemetryEnabled: userPreferences?.telemetryEnabled,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
@ -95,6 +97,16 @@ export function Settings() {
|
|||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<h3>{t("telemetry")}</h3>
|
||||
|
||||
<CheckboxField
|
||||
label={t("telemetry_description")}
|
||||
checked={form.telemetryEnabled}
|
||||
onChange={() =>
|
||||
updateUserPreferences("telemetryEnabled", !form.telemetryEnabled)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue