mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' into main
This commit is contained in:
commit
3fa18a4376
@ -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: [
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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 },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -66,7 +64,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;
|
||||||
@ -75,7 +73,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
@ -88,7 +86,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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,7 +94,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;
|
||||||
|
@ -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) =>
|
||||||
@ -90,7 +99,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
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { XIcon } from "@primer/octicons-react";
|
import { XIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
@ -23,8 +23,9 @@ export function Modal({
|
|||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const modalContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const handleCloseClick = () => {
|
const handleCloseClick = useCallback(() => {
|
||||||
setIsClosing(true);
|
setIsClosing(true);
|
||||||
const zero = performance.now();
|
const zero = performance.now();
|
||||||
|
|
||||||
@ -36,8 +37,44 @@ export function Modal({
|
|||||||
setIsClosing(false);
|
setIsClosing(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const isTopMostModal = () => {
|
||||||
|
const openModals = document.querySelectorAll("[role=modal]");
|
||||||
|
return (
|
||||||
|
openModals.length &&
|
||||||
|
openModals[openModals.length - 1] === modalContentRef.current
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && isTopMostModal()) {
|
||||||
|
handleCloseClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [handleCloseClick]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
if (!isTopMostModal()) return;
|
||||||
|
|
||||||
|
const clickedOutsideContent = !modalContentRef.current.contains(
|
||||||
|
e.target as Node
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clickedOutsideContent) {
|
||||||
|
handleCloseClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousedown", onMouseDown);
|
||||||
|
return () => window.removeEventListener("mousedown", onMouseDown);
|
||||||
|
}, [handleCloseClick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(toggleDragging(visible));
|
dispatch(toggleDragging(visible));
|
||||||
}, [dispatch, visible]);
|
}, [dispatch, visible]);
|
||||||
@ -46,7 +83,11 @@ export function Modal({
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={styles.backdrop({ closing: isClosing })}>
|
<div className={styles.backdrop({ closing: isClosing })}>
|
||||||
<div className={styles.modal({ closing: isClosing })}>
|
<div
|
||||||
|
className={styles.modal({ closing: isClosing })}
|
||||||
|
role="modal"
|
||||||
|
ref={modalContentRef}
|
||||||
|
>
|
||||||
<div className={styles.modalHeader}>
|
<div className={styles.modalHeader}>
|
||||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
|
5
src/renderer/declaration.d.ts
vendored
5
src/renderer/declaration.d.ts
vendored
@ -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>;
|
||||||
@ -75,7 +76,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>;
|
||||||
|
@ -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();
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
19
src/renderer/pages/game-details/select-folder-modal.css.tsx
Normal file
19
src/renderer/pages/game-details/select-folder-modal.css.tsx
Normal 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,
|
||||||
|
});
|
115
src/renderer/pages/game-details/select-folder-modal.tsx
Normal file
115
src/renderer/pages/game-details/select-folder-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user