feat: add a modal to select game installation folder (pt and en translation)

This commit is contained in:
Hydra 2024-04-20 15:54:08 -03:00 committed by José Luís
parent d9056ff0d6
commit e10b04b78b
13 changed files with 1232 additions and 24 deletions

View File

@ -20,7 +20,7 @@ const linuxPkgConfig = {
icon: "images/icon.png", icon: "images/icon.png",
genericName: "Games Launcher", genericName: "Games Launcher",
name: "hydra-launcher", name: "hydra-launcher",
productName: "Hydra" productName: "Hydra",
}; };
const config: ForgeConfig = { const config: ForgeConfig = {
@ -50,10 +50,10 @@ const config: ForgeConfig = {
}), }),
new MakerZIP({}, ["darwin", "linux"]), new MakerZIP({}, ["darwin", "linux"]),
new MakerRpm({ new MakerRpm({
options: linuxPkgConfig options: linuxPkgConfig,
}), }),
new MakerDeb({ new MakerDeb({
options: linuxPkgConfig options: linuxPkgConfig,
}), }),
], ],
publishers: [ publishers: [

View File

@ -77,7 +77,13 @@
"play": "Play", "play": "Play",
"deleting": "Deleting installer…", "deleting": "Deleting installer…",
"close": "Close", "close": "Close",
"playing_now": "Playing now" "playing_now": "Playing now",
"change": "Change",
"select_folder_description": "Select the folder where the game will be installed",
"downloads_path": "Downloads path",
"select_folder_hint": "To change the default folder, access the",
"hydra_settings": "Hydra settings",
"download_now": "Download now"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -73,7 +73,13 @@
"not_played_yet": "Você ainda não jogou {{title}}", "not_played_yet": "Você ainda não jogou {{title}}",
"close": "Fechar", "close": "Fechar",
"deleting": "Excluindo instalador…", "deleting": "Excluindo instalador…",
"playing_now": "Jogando agora" "playing_now": "Jogando agora",
"change": "Mudar",
"select_folder_description": "Selecione a pasta em que o jogo será baixado",
"downloads_path": "Diretório do download",
"select_folder_hint": "Para trocar a pasta padrão, acesse as ",
"hydra_settings": "Configurações do Hydra",
"download_now": "Baixe agora"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -10,7 +10,7 @@ const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
objectID: string, objectID: string,
title: string, title: string,
gameShop: GameShop, gameShop: GameShop
) => { ) => {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID)); const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));

View File

@ -5,7 +5,6 @@ import { GameStatus } from "@main/constants";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { getDownloadsPath } from "../helpers/get-downloads-path";
import { getImageBase64 } from "@main/helpers"; import { getImageBase64 } from "@main/helpers";
import { In } from "typeorm"; import { In } from "typeorm";
@ -14,7 +13,8 @@ const startGameDownload = async (
repackId: number, repackId: number,
objectID: string, objectID: string,
title: string, title: string,
gameShop: GameShop gameShop: GameShop,
downloadPath: string
) => { ) => {
const [game, repack] = await Promise.all([ const [game, repack] = await Promise.all([
gameRepository.findOne({ gameRepository.findOne({
@ -37,7 +37,7 @@ const startGameDownload = async (
writePipe.write({ action: "pause" }); writePipe.write({ action: "pause" });
const downloadsPath = game?.downloadPath ?? (await getDownloadsPath()); const downloadsPath = game?.downloadPath ?? downloadPath;
await gameRepository.update( await gameRepository.update(
{ {

View File

@ -15,8 +15,17 @@ contextBridge.exposeInMainWorld("electron", {
repackId: number, repackId: number,
objectID: string, objectID: string,
title: string, title: string,
shop: GameShop shop: GameShop,
) => ipcRenderer.invoke("startGameDownload", repackId, objectID, title, shop), downloadPath: string
) =>
ipcRenderer.invoke(
"startGameDownload",
repackId,
objectID,
title,
shop,
downloadPath
),
cancelGameDownload: (gameId: number) => cancelGameDownload: (gameId: number) =>
ipcRenderer.invoke("cancelGameDownload", gameId), ipcRenderer.invoke("cancelGameDownload", gameId),
pauseGameDownload: (gameId: number) => pauseGameDownload: (gameId: number) =>

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,8 @@ declare global {
repackId: number, repackId: number,
objectID: string, objectID: string,
title: string, title: string,
shop: GameShop shop: GameShop,
downloadPath: string
) => Promise<Game>; ) => Promise<Game>;
cancelGameDownload: (gameId: number) => Promise<void>; cancelGameDownload: (gameId: number) => Promise<void>;
pauseGameDownload: (gameId: number) => Promise<void>; pauseGameDownload: (gameId: number) => Promise<void>;

View File

@ -28,10 +28,11 @@ export function useDownload() {
repackId: number, repackId: number,
objectID: string, objectID: string,
title: string, title: string,
shop: GameShop shop: GameShop,
downloadPath: string
) => ) =>
window.electron window.electron
.startGameDownload(repackId, objectID, title, shop) .startGameDownload(repackId, objectID, title, shop, downloadPath)
.then((game) => { .then((game) => {
dispatch(clearDownload()); dispatch(clearDownload());
updateLibrary(); updateLibrary();

View File

@ -138,12 +138,16 @@ export function GameDetails() {
}; };
}, [game?.id, isGamePlaying, getGame]); }, [game?.id, isGamePlaying, getGame]);
const handleStartDownload = async (repackId: number) => { const handleStartDownload = async (
repackId: number,
downloadPath: string
) => {
return startDownload( return startDownload(
repackId, repackId,
gameDetails.objectID, gameDetails.objectID,
gameDetails.name, gameDetails.name,
shop as GameShop shop as GameShop,
downloadPath
).then(() => { ).then(() => {
getGame(); getGame();
setShowRepacksModal(false); setShowRepacksModal(false);

View File

@ -10,11 +10,12 @@ import type { DiskSpace } from "check-disk-space";
import { format } from "date-fns"; import { format } from "date-fns";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
import { formatBytes } from "@renderer/utils"; import { formatBytes } from "@renderer/utils";
import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps { export interface RepacksModalProps {
visible: boolean; visible: boolean;
gameDetails: ShopDetails; gameDetails: ShopDetails;
startDownload: (repackId: number) => Promise<void>; startDownload: (repackId: number, downloadPath: string) => Promise<void>;
onClose: () => void; onClose: () => void;
} }
@ -24,9 +25,10 @@ export function RepacksModal({
startDownload, startDownload,
onClose, onClose,
}: RepacksModalProps) { }: RepacksModalProps) {
const [downloadStarting, setDownloadStarting] = useState(false); const [openSelectFolderModal, setOpenSelectFolderModal] = useState(false);
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null); const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]); const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack>(null);
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
@ -45,10 +47,8 @@ export function RepacksModal({
}, [visible]); }, [visible]);
const handleRepackClick = (repack: GameRepack) => { const handleRepackClick = (repack: GameRepack) => {
setDownloadStarting(true); setRepack(repack);
startDownload(repack.id).finally(() => { setOpenSelectFolderModal(true);
setDownloadStarting(false);
});
}; };
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => { const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
@ -70,6 +70,13 @@ export function RepacksModal({
})} })}
onClose={onClose} onClose={onClose}
> >
<SelectFolderModal
visible={openSelectFolderModal}
onClose={() => setOpenSelectFolderModal(false)}
gameDetails={gameDetails}
startDownload={startDownload}
repack={repack}
/>
<div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}> <div style={{ marginBottom: `${SPACING_UNIT * 2}px` }}>
<TextField placeholder={t("filter")} onChange={handleFilter} /> <TextField placeholder={t("filter")} onChange={handleFilter} />
</div> </div>
@ -80,7 +87,6 @@ export function RepacksModal({
key={repack.id} key={repack.id}
theme="dark" theme="dark"
onClick={() => handleRepackClick(repack)} onClick={() => handleRepackClick(repack)}
disabled={downloadStarting}
className={styles.repackButton} className={styles.repackButton}
> >
<p style={{ color: "#DADBE1" }}>{repack.title}</p> <p style={{ color: "#DADBE1" }}>{repack.title}</p>

View File

@ -0,0 +1,19 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
export const container = style({
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
width: "100%",
});
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
});
export const hintText = style({
fontSize: 12,
color: vars.color.bodyText,
});

View File

@ -0,0 +1,103 @@
import { Button, Modal, TextField } from "@renderer/components";
import { GameRepack, ShopDetails } from "@types";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as styles from "./select-folder-modal.css";
import { Link } from "react-router-dom";
export interface SelectFolderModalProps {
visible: boolean;
gameDetails: ShopDetails;
onClose: () => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
repack: GameRepack;
}
export function SelectFolderModal({
visible,
gameDetails,
onClose,
startDownload,
repack,
}: SelectFolderModalProps) {
const { t } = useTranslation("game_details");
const [selectedPath, setSelectedPath] = useState({
downloadsPath: "",
});
const [downloadStarting, setDownloadStarting] = useState(false);
useEffect(() => {
Promise.all([
window.electron.getDefaultDownloadsPath(),
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setSelectedPath({
downloadsPath: userPreferences?.downloadsPath || path,
});
});
}, []);
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: selectedPath.downloadsPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
setSelectedPath({ downloadsPath: path });
}
};
const handleStartClick = () => {
setDownloadStarting(true);
startDownload(repack.id, selectedPath.downloadsPath).finally(() => {
setDownloadStarting(false);
});
};
return (
<Modal
visible={visible}
title={`${gameDetails.name} Installation folder`}
description={t("select_folder_description")}
onClose={onClose}
>
<div className={styles.container}>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={selectedPath.downloadsPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<p className={styles.hintText}>
{t("select_folder_hint")}{" "}
<Link
to="/settings"
style={{
textDecoration: "none",
color: "#C0C1C7",
}}
>
{t("hydra_settings")}
</Link>
</p>
<Button onClick={handleStartClick} disabled={downloadStarting}>
{t("download_now")}
</Button>
</div>
</Modal>
);
}