Merge branch 'main' of https://github.com/hydralauncher/hydra into refactor/change-game-delete-to-soft-delete

This commit is contained in:
JackEnx 2024-04-23 13:31:35 -03:00
commit 66a8349263
13 changed files with 1265 additions and 53 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",
"repacks_modal_description": "Choose the repack you want to download",
"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",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
"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

@ -1,10 +1,11 @@
import checkDiskSpace from "check-disk-space"; import checkDiskSpace from "check-disk-space";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { getDownloadsPath } from "../helpers/get-downloads-path";
const getDiskFreeSpace = async (_event: Electron.IpcMainInvokeEvent) => const getDiskFreeSpace = async (
checkDiskSpace(await getDownloadsPath()); _event: Electron.IpcMainInvokeEvent,
path: string
) => checkDiskSpace(path);
registerEvent(getDiskFreeSpace, { registerEvent(getDiskFreeSpace, {
name: "getDiskFreeSpace", name: "getDiskFreeSpace",

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,8 +37,6 @@ const startGameDownload = async (
writePipe.write({ action: "pause" }); writePipe.write({ action: "pause" });
const downloadsPath = game?.downloadPath ?? (await getDownloadsPath());
await gameRepository.update( await gameRepository.update(
{ {
status: In([ status: In([
@ -57,7 +55,7 @@ const startGameDownload = async (
}, },
{ {
status: GameStatus.DownloadingMetadata, status: GameStatus.DownloadingMetadata,
downloadPath: downloadsPath, downloadPath: downloadPath,
repack: { id: repackId }, repack: { id: repackId },
isDeleted: false, isDeleted: false,
} }
@ -67,7 +65,7 @@ const startGameDownload = async (
action: "start", action: "start",
game_id: game.id, game_id: game.id,
magnet: repack.magnet, magnet: repack.magnet,
save_path: downloadsPath, save_path: downloadPath,
}); });
game.status = GameStatus.DownloadingMetadata; game.status = GameStatus.DownloadingMetadata;
@ -82,7 +80,7 @@ const startGameDownload = async (
objectID, objectID,
shop: gameShop, shop: gameShop,
status: GameStatus.DownloadingMetadata, status: GameStatus.DownloadingMetadata,
downloadPath: downloadsPath, downloadPath: downloadPath,
repack: { id: repackId }, repack: { id: repackId },
}); });
@ -90,7 +88,7 @@ const startGameDownload = async (
action: "start", action: "start",
game_id: createdGame.id, game_id: createdGame.id,
magnet: repack.magnet, magnet: repack.magnet,
save_path: downloadsPath, save_path: downloadPath,
}); });
const { repack: _, ...rest } = createdGame; const { repack: _, ...rest } = createdGame;

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) =>
@ -93,7 +102,8 @@ contextBridge.exposeInMainWorld("electron", {
}, },
/* Hardware */ /* Hardware */
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"), getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path),
/* Misc */ /* Misc */
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url), getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),

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>;
@ -76,7 +77,7 @@ declare global {
) => Promise<void>; ) => Promise<void>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: () => Promise<DiskSpace>; getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
/* Misc */ /* Misc */
getOrCacheImage: (url: string) => Promise<string>; getOrCacheImage: (url: string) => Promise<string>;

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

@ -19,15 +19,15 @@ import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json"; import starsAnimation from "@renderer/assets/lottie/stars.json";
import { vars } from "@renderer/theme.css"; import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SkeletonTheme } from "react-loading-skeleton"; import { SkeletonTheme } from "react-loading-skeleton";
import { DescriptionHeader } from "./description-header";
import { GameDetailsSkeleton } from "./game-details-skeleton"; import { GameDetailsSkeleton } from "./game-details-skeleton";
import * as styles from "./game-details.css"; import * as styles from "./game-details.css";
import { HeroPanel } from "./hero-panel"; import { HeroPanel } from "./hero-panel";
import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { RepacksModal } from "./repacks-modal"; import { RepacksModal } from "./repacks-modal";
import Lottie from "lottie-react";
import { DescriptionHeader } from "./description-header";
export function GameDetails() { export function GameDetails() {
const { objectID, shop } = useParams(); const { objectID, shop } = useParams();
@ -51,6 +51,7 @@ export function GameDetails() {
const { t, i18n } = useTranslation("game_details"); const { t, i18n } = useTranslation("game_details");
const [showRepacksModal, setShowRepacksModal] = useState(false); const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const randomGameObjectID = useRef<string | null>(null); const randomGameObjectID = useRef<string | null>(null);
@ -140,15 +141,20 @@ 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);
setShowSelectFolderModal(false);
}); });
}; };
@ -173,6 +179,8 @@ export function GameDetails() {
visible={showRepacksModal} visible={showRepacksModal}
gameDetails={gameDetails} gameDetails={gameDetails}
startDownload={handleStartDownload} startDownload={handleStartDownload}
showSelectFolderModal={showSelectFolderModal}
setShowSelectFolderModal={setShowSelectFolderModal}
onClose={() => setShowRepacksModal(false)} onClose={() => setShowRepacksModal(false)}
/> />
)} )}

View File

@ -6,28 +6,30 @@ import type { GameRepack, ShopDetails } from "@types";
import * as styles from "./repacks-modal.css"; import * as styles from "./repacks-modal.css";
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 { useAppSelector } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { format } from "date-fns";
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>; showSelectFolderModal: boolean;
setShowSelectFolderModal: (value: boolean) => void;
startDownload: (repackId: number, downloadPath: string) => Promise<void>;
onClose: () => void; onClose: () => void;
} }
export function RepacksModal({ export function RepacksModal({
visible, visible,
gameDetails, gameDetails,
showSelectFolderModal,
setShowSelectFolderModal,
startDownload, startDownload,
onClose, onClose,
}: RepacksModalProps) { }: RepacksModalProps) {
const [downloadStarting, setDownloadStarting] = useState(false);
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]); const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack>(null);
const repackersFriendlyNames = useAppSelector( const repackersFriendlyNames = useAppSelector(
(state) => state.repackersFriendlyNames.value (state) => state.repackersFriendlyNames.value
@ -39,21 +41,9 @@ export function RepacksModal({
setFilteredRepacks(gameDetails.repacks); setFilteredRepacks(gameDetails.repacks);
}, [gameDetails.repacks]); }, [gameDetails.repacks]);
const getDiskFreeSpace = () => {
window.electron.getDiskFreeSpace().then((result) => {
setDiskFreeSpace(result);
});
};
useEffect(() => {
getDiskFreeSpace();
}, [visible]);
const handleRepackClick = (repack: GameRepack) => { const handleRepackClick = (repack: GameRepack) => {
setDownloadStarting(true); setRepack(repack);
startDownload(repack.id).finally(() => { setShowSelectFolderModal(true);
setDownloadStarting(false);
});
}; };
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => { const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
@ -70,11 +60,16 @@ export function RepacksModal({
<Modal <Modal
visible={visible} visible={visible}
title={`${gameDetails.name} Repacks`} title={`${gameDetails.name} Repacks`}
description={t("space_left_on_disk", { description={t("repacks_modal_description")}
space: formatBytes(diskFreeSpace?.free ?? 0),
})}
onClose={onClose} onClose={onClose}
> >
<SelectFolderModal
visible={showSelectFolderModal}
onClose={() => setShowSelectFolderModal(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>
@ -85,7 +80,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,115 @@
import { Button, Modal, TextField } from "@renderer/components";
import { GameRepack, ShopDetails } from "@types";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@renderer/utils";
import { DiskSpace } from "check-disk-space";
import { Link } from "react-router-dom";
import * as styles from "./select-folder-modal.css";
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 [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
useEffect(() => {
visible && getDiskFreeSpace(selectedPath);
}, [visible, selectedPath]);
useEffect(() => {
Promise.all([
window.electron.getDefaultDownloadsPath(),
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setSelectedPath(userPreferences?.downloadsPath || path);
});
}, []);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
setDiskFreeSpace(result);
});
};
const handleChooseDownloadsPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
defaultPath: selectedPath,
properties: ["openDirectory"],
});
if (filePaths && filePaths.length > 0) {
const path = filePaths[0];
setSelectedPath(path);
}
};
const handleStartClick = () => {
setDownloadStarting(true);
startDownload(repack.id, selectedPath).finally(() => {
setDownloadStarting(false);
});
};
return (
<Modal
visible={visible}
title={`${gameDetails.name} Installation folder`}
description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0),
})}
onClose={onClose}
>
<div className={styles.container}>
<div className={styles.downloadsPathField}>
<TextField
label={t("downloads_path")}
value={selectedPath}
readOnly
disabled
/>
<Button
style={{ alignSelf: "flex-end" }}
theme="outline"
onClick={handleChooseDownloadsPath}
disabled={downloadStarting}
>
{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>
);
}