mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding aria2
This commit is contained in:
parent
a89e6760da
commit
4941709296
58 changed files with 895 additions and 1329 deletions
|
@ -19,7 +19,6 @@ import {
|
|||
setUserPreferences,
|
||||
toggleDraggingDisabled,
|
||||
} from "@renderer/features";
|
||||
import { GameStatusHelper } from "@shared";
|
||||
|
||||
document.body.classList.add(themeClass);
|
||||
|
||||
|
@ -54,7 +53,7 @@ export function App({ children }: AppProps) {
|
|||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onDownloadProgress(
|
||||
(downloadProgress) => {
|
||||
if (GameStatusHelper.isReady(downloadProgress.game.status)) {
|
||||
if (downloadProgress.game.progress === 1) {
|
||||
clearDownload();
|
||||
updateLibrary();
|
||||
return;
|
||||
|
|
|
@ -43,5 +43,11 @@ export const backdrop = recipe({
|
|||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
},
|
||||
windows: {
|
||||
true: {
|
||||
// SPACING_UNIT * 3 + title bar spacing
|
||||
paddingTop: `${SPACING_UNIT * 3 + 35}px`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,6 +7,13 @@ export interface BackdropProps {
|
|||
|
||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||
return (
|
||||
<div className={styles.backdrop({ closing: isClosing })}>{children}</div>
|
||||
<div
|
||||
className={styles.backdrop({
|
||||
closing: isClosing,
|
||||
windows: window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,17 +7,16 @@ import { vars } from "../../theme.css";
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { VERSION_CODENAME } from "@renderer/constants";
|
||||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
|
||||
export function BottomPanel() {
|
||||
const { t } = useTranslation("bottom_panel");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { game, progress, downloadSpeed, eta } = useDownload();
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game && GameStatusHelper.isDownloading(game.status ?? null);
|
||||
lastPacket?.game && lastPacket?.game.status === "active";
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
|
||||
|
@ -27,17 +26,8 @@ export function BottomPanel() {
|
|||
|
||||
const status = useMemo(() => {
|
||||
if (isGameDownloading) {
|
||||
if (game.status === GameStatus.DownloadingMetadata)
|
||||
return t("downloading_metadata", { title: game.title });
|
||||
|
||||
if (game.status === GameStatus.CheckingFiles)
|
||||
return t("checking_files", {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
|
||||
return t("downloading", {
|
||||
title: game?.title,
|
||||
title: lastPacket?.game.title,
|
||||
percentage: progress,
|
||||
eta,
|
||||
speed: downloadSpeed,
|
||||
|
@ -45,7 +35,7 @@ export function BottomPanel() {
|
|||
}
|
||||
|
||||
return t("no_downloads_in_progress");
|
||||
}, [t, isGameDownloading, game, progress, eta, downloadSpeed]);
|
||||
}, [t, isGameDownloading, lastPacket?.game, progress, eta, downloadSpeed]);
|
||||
|
||||
return (
|
||||
<footer
|
||||
|
|
|
@ -10,7 +10,6 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
|||
import { routes } from "./routes";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
|
@ -35,14 +34,14 @@ export function Sidebar() {
|
|||
|
||||
const location = useLocation();
|
||||
|
||||
const { game: gameDownloading, progress } = useDownload();
|
||||
const { lastPacket, progress } = useDownload();
|
||||
|
||||
useEffect(() => {
|
||||
updateLibrary();
|
||||
}, [gameDownloading?.id, updateLibrary]);
|
||||
}, [lastPacket?.game.id, updateLibrary]);
|
||||
|
||||
const isDownloading = library.some((game) =>
|
||||
GameStatusHelper.isDownloading(game.status)
|
||||
const isDownloading = library.some(
|
||||
(game) => game.status === "active" && game.progress !== 1
|
||||
);
|
||||
|
||||
const sidebarRef = useRef<HTMLElement>(null);
|
||||
|
@ -101,18 +100,9 @@ export function Sidebar() {
|
|||
}, [isResizing]);
|
||||
|
||||
const getGameTitle = (game: Game) => {
|
||||
if (game.status === GameStatus.Paused)
|
||||
return t("paused", { title: game.title });
|
||||
|
||||
if (gameDownloading?.id === game.id) {
|
||||
const isVerifying = GameStatusHelper.isVerifying(gameDownloading.status);
|
||||
|
||||
if (isVerifying)
|
||||
return t(gameDownloading.status!, {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
if (game.status === "paused") return t("paused", { title: game.title });
|
||||
|
||||
if (lastPacket?.game.id === game.id) {
|
||||
return t("downloading", {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
|
@ -183,7 +173,7 @@ export function Sidebar() {
|
|||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === GameStatus.Cancelled,
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -7,7 +7,7 @@ import type {
|
|||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
Steam250Game,
|
||||
TorrentProgress,
|
||||
DownloadProgress,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
@ -31,7 +31,7 @@ declare global {
|
|||
pauseGameDownload: (gameId: number) => Promise<void>;
|
||||
resumeGameDownload: (gameId: number) => Promise<void>;
|
||||
onDownloadProgress: (
|
||||
cb: (value: TorrentProgress) => void
|
||||
cb: (value: DownloadProgress) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Catalogue */
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { TorrentProgress } from "@types";
|
||||
import type { DownloadProgress } from "@types";
|
||||
|
||||
export interface DownloadState {
|
||||
lastPacket: TorrentProgress | null;
|
||||
lastPacket: DownloadProgress | null;
|
||||
gameId: number | null;
|
||||
gamesWithDeletionInProgress: number[];
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export const downloadSlice = createSlice({
|
|||
name: "download",
|
||||
initialState,
|
||||
reducers: {
|
||||
setLastPacket: (state, action: PayloadAction<TorrentProgress>) => {
|
||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
||||
state.lastPacket = action.payload;
|
||||
if (!state.gameId) state.gameId = action.payload.game.id;
|
||||
},
|
||||
|
|
|
@ -9,9 +9,9 @@ import {
|
|||
setGameDeleting,
|
||||
removeGameFromDeleting,
|
||||
} from "@renderer/features";
|
||||
import type { GameShop, TorrentProgress } from "@types";
|
||||
import type { DownloadProgress, GameShop } from "@types";
|
||||
import { useDate } from "./use-date";
|
||||
import { GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
||||
import { formatBytes } from "@shared";
|
||||
|
||||
export function useDownload() {
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
@ -38,16 +38,16 @@ export function useDownload() {
|
|||
return game;
|
||||
});
|
||||
|
||||
const pauseDownload = (gameId: number) =>
|
||||
window.electron.pauseGameDownload(gameId).then(() => {
|
||||
dispatch(clearDownload());
|
||||
updateLibrary();
|
||||
});
|
||||
const pauseDownload = async (gameId: number) => {
|
||||
await window.electron.pauseGameDownload(gameId);
|
||||
await updateLibrary();
|
||||
dispatch(clearDownload());
|
||||
};
|
||||
|
||||
const resumeDownload = (gameId: number) =>
|
||||
window.electron.resumeGameDownload(gameId).then(() => {
|
||||
updateLibrary();
|
||||
});
|
||||
const resumeDownload = async (gameId: number) => {
|
||||
await window.electron.resumeGameDownload(gameId);
|
||||
return updateLibrary();
|
||||
};
|
||||
|
||||
const cancelDownload = (gameId: number) =>
|
||||
window.electron.cancelGameDownload(gameId).then(() => {
|
||||
|
@ -61,14 +61,8 @@ export function useDownload() {
|
|||
updateLibrary();
|
||||
});
|
||||
|
||||
const isVerifying = GameStatusHelper.isVerifying(
|
||||
lastPacket?.game.status ?? null
|
||||
);
|
||||
|
||||
const getETA = () => {
|
||||
if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) {
|
||||
return "";
|
||||
}
|
||||
if (lastPacket && lastPacket.timeRemaining < 0) return "";
|
||||
|
||||
try {
|
||||
return formatDistance(
|
||||
|
@ -81,14 +75,6 @@ export function useDownload() {
|
|||
}
|
||||
};
|
||||
|
||||
const getProgress = () => {
|
||||
if (lastPacket?.game.status === GameStatus.CheckingFiles) {
|
||||
return formatDownloadProgress(lastPacket?.game.fileVerificationProgress);
|
||||
}
|
||||
|
||||
return formatDownloadProgress(lastPacket?.game.progress);
|
||||
};
|
||||
|
||||
const deleteGame = (gameId: number) =>
|
||||
window.electron
|
||||
.cancelGameDownload(gameId)
|
||||
|
@ -107,15 +93,9 @@ export function useDownload() {
|
|||
};
|
||||
|
||||
return {
|
||||
game: lastPacket?.game,
|
||||
bytesDownloaded: lastPacket?.game.bytesDownloaded,
|
||||
fileSize: lastPacket?.game.fileSize,
|
||||
isVerifying,
|
||||
gameId: lastPacket?.game.id,
|
||||
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
|
||||
progress: getProgress(),
|
||||
numPeers: lastPacket?.numPeers,
|
||||
numSeeds: lastPacket?.numSeeds,
|
||||
progress: formatDownloadProgress(lastPacket?.game.progress ?? 0),
|
||||
lastPacket,
|
||||
eta: getETA(),
|
||||
startDownload,
|
||||
pauseDownload,
|
||||
|
@ -125,6 +105,7 @@ export function useDownload() {
|
|||
deleteGame,
|
||||
isGameDeleting,
|
||||
clearDownload: () => dispatch(clearDownload()),
|
||||
setLastPacket: (packet: TorrentProgress) => dispatch(setLastPacket(packet)),
|
||||
setLastPacket: (packet: DownloadProgress) =>
|
||||
dispatch(setLastPacket(packet)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,10 +26,7 @@ export function Downloads() {
|
|||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
const {
|
||||
game: gameDownloading,
|
||||
progress,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
pauseDownload,
|
||||
resumeDownload,
|
||||
removeGameFromLibrary,
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { ShopDetails } from "@types";
|
||||
|
||||
import * as styles from "./game-details.css";
|
||||
import { useContext } from "react";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
|
||||
export interface DescriptionHeaderProps {
|
||||
gameDetails: ShopDetails;
|
||||
}
|
||||
export function DescriptionHeader() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
||||
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
if (!shopDetails) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.descriptionHeader}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<p>
|
||||
{t("release_date", {
|
||||
date: gameDetails?.release_date.date,
|
||||
date: shopDetails?.release_date.date,
|
||||
})}
|
||||
</p>
|
||||
<p>{t("publisher", { publisher: gameDetails.publishers[0] })}</p>
|
||||
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,37 +1,36 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||
|
||||
import type { ShopDetails } from "@types";
|
||||
|
||||
import * as styles from "./gallery-slider.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { gameDetailsContext } from "./game-details.context";
|
||||
|
||||
export interface GallerySliderProps {
|
||||
gameDetails: ShopDetails;
|
||||
}
|
||||
export function GallerySlider() {
|
||||
const { shopDetails } = useContext(gameDetailsContext);
|
||||
|
||||
export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const hasScreenshots = gameDetails && gameDetails.screenshots.length;
|
||||
const hasMovies = gameDetails && gameDetails.movies?.length;
|
||||
const hasScreenshots = shopDetails && shopDetails.screenshots.length;
|
||||
const hasMovies = shopDetails && shopDetails.movies?.length;
|
||||
|
||||
const [mediaCount] = useState<number>(() => {
|
||||
if (gameDetails.screenshots && gameDetails.movies) {
|
||||
return gameDetails.screenshots.length + gameDetails.movies.length;
|
||||
} else if (gameDetails.movies) {
|
||||
return gameDetails.movies.length;
|
||||
} else if (gameDetails.screenshots) {
|
||||
return gameDetails.screenshots.length;
|
||||
const mediaCount = useMemo(() => {
|
||||
if (!shopDetails) return 0;
|
||||
|
||||
if (shopDetails.screenshots && shopDetails.movies) {
|
||||
return shopDetails.screenshots.length + shopDetails.movies.length;
|
||||
} else if (shopDetails.movies) {
|
||||
return shopDetails.movies.length;
|
||||
} else if (shopDetails.screenshots) {
|
||||
return shopDetails.screenshots.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}, [shopDetails]);
|
||||
|
||||
const [mediaIndex, setMediaIndex] = useState<number>(0);
|
||||
const [mediaIndex, setMediaIndex] = useState(0);
|
||||
const [showArrows, setShowArrows] = useState(false);
|
||||
|
||||
const showNextImage = () => {
|
||||
|
@ -52,7 +51,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||
|
||||
useEffect(() => {
|
||||
setMediaIndex(0);
|
||||
}, [gameDetails]);
|
||||
}, [shopDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasMovies && mediaContainerRef.current) {
|
||||
|
@ -76,17 +75,17 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||
const scrollLeft = mediaIndex * itemWidth;
|
||||
container.scrollLeft = scrollLeft;
|
||||
}
|
||||
}, [gameDetails, mediaIndex, mediaCount]);
|
||||
}, [shopDetails, mediaIndex, mediaCount]);
|
||||
|
||||
const previews = useMemo(() => {
|
||||
const screenshotPreviews =
|
||||
gameDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
||||
shopDetails?.screenshots.map(({ id, path_thumbnail }) => ({
|
||||
id,
|
||||
thumbnail: path_thumbnail,
|
||||
})) ?? [];
|
||||
|
||||
if (gameDetails?.movies) {
|
||||
const moviePreviews = gameDetails.movies.map(({ id, thumbnail }) => ({
|
||||
if (shopDetails?.movies) {
|
||||
const moviePreviews = shopDetails.movies.map(({ id, thumbnail }) => ({
|
||||
id,
|
||||
thumbnail,
|
||||
}));
|
||||
|
@ -95,7 +94,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||
}
|
||||
|
||||
return screenshotPreviews;
|
||||
}, [gameDetails]);
|
||||
}, [shopDetails]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -107,8 +106,8 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||
className={styles.gallerySliderAnimationContainer}
|
||||
ref={mediaContainerRef}
|
||||
>
|
||||
{gameDetails.movies &&
|
||||
gameDetails.movies.map((video) => (
|
||||
{shopDetails.movies &&
|
||||
shopDetails.movies.map((video) => (
|
||||
<video
|
||||
key={video.id}
|
||||
controls
|
||||
|
@ -124,7 +123,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||
))}
|
||||
|
||||
{hasScreenshots &&
|
||||
gameDetails.screenshots.map((image, i) => (
|
||||
shopDetails.screenshots.map((image, i) => (
|
||||
<img
|
||||
key={image.id}
|
||||
className={styles.gallerySliderMedia}
|
||||
|
|
206
src/renderer/src/pages/game-details/game-details.context.tsx
Normal file
206
src/renderer/src/pages/game-details/game-details.context.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { getSteamLanguage } from "@renderer/helpers";
|
||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||
|
||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DODIInstallationGuide,
|
||||
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
|
||||
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
|
||||
OnlineFixInstallationGuide,
|
||||
RepacksModal,
|
||||
} from "./modals";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
gameTitle: string;
|
||||
isGameRunning: boolean;
|
||||
isLoading: boolean;
|
||||
objectID: string | undefined;
|
||||
gameColor: string;
|
||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||
openRepacksModal: () => void;
|
||||
updateGame: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||
game: null,
|
||||
shopDetails: null,
|
||||
repacks: [],
|
||||
gameTitle: "",
|
||||
isGameRunning: false,
|
||||
isLoading: false,
|
||||
objectID: undefined,
|
||||
gameColor: "",
|
||||
setGameColor: () => {},
|
||||
openRepacksModal: () => {},
|
||||
updateGame: async () => {},
|
||||
});
|
||||
|
||||
const { Provider } = gameDetailsContext;
|
||||
export const { Consumer: GameDetailsContextConsumer } = gameDetailsContext;
|
||||
|
||||
export interface GameDetailsContextProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function GameDetailsContextProvider({
|
||||
children,
|
||||
}: GameDetailsContextProps) {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [shopDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [gameColor, setGameColor] = useState("");
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
const [isGameRunning, setisGameRunning] = useState(false);
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const gameTitle = searchParams.get("title")!;
|
||||
|
||||
const { i18n } = useTranslation("game_details");
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { startDownload, lastPacket } = useDownload();
|
||||
|
||||
const updateGame = useCallback(async () => {
|
||||
return window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, objectID]);
|
||||
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
|
||||
useEffect(() => {
|
||||
updateGame();
|
||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
window.electron.getGameShopDetails(
|
||||
objectID!,
|
||||
shop as GameShop,
|
||||
getSteamLanguage(i18n.language)
|
||||
),
|
||||
window.electron.searchGameRepacks(gameTitle),
|
||||
])
|
||||
.then(([appDetails, repacks]) => {
|
||||
if (appDetails) setGameDetails(appDetails);
|
||||
setRepacks(repacks);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
updateGame();
|
||||
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
||||
|
||||
useEffect(() => {
|
||||
setGameDetails(null);
|
||||
setGame(null);
|
||||
setIsLoading(true);
|
||||
setisGameRunning(false);
|
||||
dispatch(setHeaderTitle(gameTitle));
|
||||
}, [objectID, gameTitle, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onGameClose(() => {
|
||||
if (isGameRunning) setisGameRunning(false);
|
||||
}),
|
||||
window.electron.onPlaytime((gameId) => {
|
||||
if (gameId === game?.id) {
|
||||
if (!isGameRunning) setisGameRunning(true);
|
||||
updateGame();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [game?.id, isGameRunning, updateGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloadPath: string
|
||||
) => {
|
||||
await startDownload(
|
||||
repack.id,
|
||||
objectID!,
|
||||
gameTitle,
|
||||
shop as GameShop,
|
||||
downloadPath
|
||||
);
|
||||
|
||||
await updateGame();
|
||||
setShowRepacksModal(false);
|
||||
|
||||
if (
|
||||
repack.repacker === "onlinefix" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("onlinefix");
|
||||
} else if (
|
||||
repack.repacker === "DODI" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("DODI");
|
||||
}
|
||||
};
|
||||
|
||||
const openRepacksModal = () => setShowRepacksModal(true);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
game,
|
||||
shopDetails,
|
||||
repacks,
|
||||
gameTitle,
|
||||
isGameRunning,
|
||||
isLoading,
|
||||
objectID,
|
||||
gameColor,
|
||||
setGameColor,
|
||||
openRepacksModal,
|
||||
updateGame,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
<DODIInstallationGuide
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</>
|
||||
</Provider>
|
||||
);
|
||||
}
|
|
@ -1,24 +1,11 @@
|
|||
import Color from "color";
|
||||
import { average } from "color.js";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { average } from "color.js";
|
||||
|
||||
import {
|
||||
Steam250Game,
|
||||
type Game,
|
||||
type GameRepack,
|
||||
type GameShop,
|
||||
type ShopDetails,
|
||||
} from "@types";
|
||||
import { Steam250Game } from "@types";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import {
|
||||
buildGameDetailsPath,
|
||||
getSteamLanguage,
|
||||
steamUrlBuilder,
|
||||
} from "@renderer/helpers";
|
||||
import { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||
|
||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||
|
||||
|
@ -29,153 +16,34 @@ import { DescriptionHeader } from "./description-header";
|
|||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||
import * as styles from "./game-details.css";
|
||||
import { HeroPanel } from "./hero";
|
||||
import { RepacksModal } from "./repacks-modal";
|
||||
|
||||
import { vars } from "../../theme.css";
|
||||
import {
|
||||
DODIInstallationGuide,
|
||||
DONT_SHOW_DODI_INSTRUCTIONS_KEY,
|
||||
DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY,
|
||||
OnlineFixInstallationGuide,
|
||||
} from "./installation-guides";
|
||||
|
||||
import { GallerySlider } from "./gallery-slider";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import {
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
} from "./game-details.context";
|
||||
|
||||
export function GameDetails() {
|
||||
const { objectID, shop } = useParams();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
const [color, setColor] = useState({ dark: "", light: "" });
|
||||
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
||||
const [showInstructionsModal, setShowInstructionsModal] = useState<
|
||||
null | "onlinefix" | "DODI"
|
||||
>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { objectID } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const fromRandomizer = searchParams.get("fromRandomizer");
|
||||
const title = searchParams.get("title")!;
|
||||
|
||||
const { t, i18n } = useTranslation("game_details");
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { game: gameDownloading, startDownload } = useDownload();
|
||||
|
||||
const heroImage = steamUrlBuilder.libraryHero(objectID!);
|
||||
|
||||
const handleHeroLoad = () => {
|
||||
average(heroImage, { amount: 1, format: "hex" })
|
||||
.then((color) => {
|
||||
const darkColor = new Color(color).darken(0.6).toString() as string;
|
||||
setColor({ light: color as string, dark: darkColor });
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const getGame = useCallback(() => {
|
||||
window.electron
|
||||
.getGameByObjectID(objectID!)
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, objectID]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
getGame();
|
||||
}, [getGame, gameDownloading?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setGameDetails(null);
|
||||
setGame(null);
|
||||
setIsLoading(true);
|
||||
setIsGamePlaying(false);
|
||||
dispatch(setHeaderTitle(title));
|
||||
|
||||
setRandomGame(null);
|
||||
window.electron.getRandomGame().then((randomGame) => {
|
||||
setRandomGame(randomGame);
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
window.electron.getGameShopDetails(
|
||||
objectID!,
|
||||
"steam",
|
||||
getSteamLanguage(i18n.language)
|
||||
),
|
||||
window.electron.searchGameRepacks(title),
|
||||
])
|
||||
.then(([appDetails, repacks]) => {
|
||||
if (appDetails) setGameDetails(appDetails);
|
||||
setRepacks(repacks);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
getGame();
|
||||
}, [getGame, dispatch, navigate, title, objectID, i18n.language]);
|
||||
|
||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (isGameDownloading)
|
||||
setGame((prev) => {
|
||||
if (prev === null || !gameDownloading?.status) return prev;
|
||||
return { ...prev, status: gameDownloading?.status };
|
||||
});
|
||||
}, [isGameDownloading, gameDownloading?.status]);
|
||||
|
||||
useEffect(() => {
|
||||
const listeners = [
|
||||
window.electron.onGameClose(() => {
|
||||
if (isGamePlaying) setIsGamePlaying(false);
|
||||
}),
|
||||
window.electron.onPlaytime((gameId) => {
|
||||
if (gameId === game?.id) {
|
||||
if (!isGamePlaying) setIsGamePlaying(true);
|
||||
getGame();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
listeners.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
}, [game?.id, isGamePlaying, getGame]);
|
||||
|
||||
const handleStartDownload = async (
|
||||
repack: GameRepack,
|
||||
downloadPath: string
|
||||
) => {
|
||||
return startDownload(
|
||||
repack.id,
|
||||
objectID!,
|
||||
title,
|
||||
shop as GameShop,
|
||||
downloadPath
|
||||
).then(() => {
|
||||
getGame();
|
||||
setShowRepacksModal(false);
|
||||
|
||||
if (
|
||||
repack.repacker === "onlinefix" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("onlinefix");
|
||||
} else if (
|
||||
repack.repacker === "DODI" &&
|
||||
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
|
||||
) {
|
||||
setShowInstructionsModal("DODI");
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [objectID]);
|
||||
|
||||
const handleRandomizerClick = () => {
|
||||
if (randomGame) {
|
||||
|
@ -189,97 +57,95 @@ export function GameDetails() {
|
|||
};
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<RepacksModal
|
||||
visible={showRepacksModal}
|
||||
repacks={repacks}
|
||||
startDownload={handleStartDownload}
|
||||
onClose={() => setShowRepacksModal(false)}
|
||||
/>
|
||||
<GameDetailsContextProvider>
|
||||
<GameDetailsContextConsumer>
|
||||
{({ game, shopDetails, isLoading, setGameColor }) => {
|
||||
const handleHeroLoad = async () => {
|
||||
const output = await average(
|
||||
steamUrlBuilder.libraryHero(objectID!),
|
||||
{
|
||||
amount: 1,
|
||||
format: "hex",
|
||||
}
|
||||
);
|
||||
|
||||
<OnlineFixInstallationGuide
|
||||
visible={showInstructionsModal === "onlinefix"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
setGameColor(output as string);
|
||||
};
|
||||
|
||||
<DODIInstallationGuide
|
||||
windowColor={color.light}
|
||||
visible={showInstructionsModal === "DODI"}
|
||||
onClose={() => setShowInstructionsModal(null)}
|
||||
/>
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectID!)}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
<div className={styles.heroBackdrop}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<GameDetailsSkeleton />
|
||||
) : (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<img
|
||||
src={heroImage}
|
||||
className={styles.heroImage}
|
||||
alt={game?.title}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
<div className={styles.heroBackdrop}>
|
||||
<div className={styles.heroContent}>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectID!)}
|
||||
style={{ width: 300, alignSelf: "flex-end" }}
|
||||
alt={game?.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HeroPanel />
|
||||
|
||||
<HeroPanel
|
||||
game={game}
|
||||
color={color.dark}
|
||||
objectID={objectID!}
|
||||
title={title}
|
||||
repacks={repacks}
|
||||
openRepacksModal={() => setShowRepacksModal(true)}
|
||||
getGame={getGame}
|
||||
isGamePlaying={isGamePlaying}
|
||||
/>
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
<DescriptionHeader />
|
||||
<GallerySlider />
|
||||
|
||||
<div className={styles.descriptionContainer}>
|
||||
<div className={styles.descriptionContent}>
|
||||
{gameDetails && <DescriptionHeader gameDetails={gameDetails} />}
|
||||
{gameDetails && <GallerySlider gameDetails={gameDetails} />}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
shopDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: gameDetails?.about_the_game ?? t("no_shop_details"),
|
||||
}}
|
||||
className={styles.description}
|
||||
/>
|
||||
</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Sidebar
|
||||
objectID={objectID!}
|
||||
title={title}
|
||||
gameDetails={gameDetails}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
{fromRandomizer && (
|
||||
<Button
|
||||
className={styles.randomizerButton}
|
||||
onClick={handleRandomizerClick}
|
||||
theme="outline"
|
||||
disabled={!randomGame}
|
||||
>
|
||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||
<Lottie
|
||||
animationData={starsAnimation}
|
||||
style={{
|
||||
width: 70,
|
||||
position: "absolute",
|
||||
top: -28,
|
||||
left: -27,
|
||||
}}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
{t("next_suggestion")}
|
||||
</Button>
|
||||
)}
|
||||
</SkeletonTheme>
|
||||
);
|
||||
}}
|
||||
</GameDetailsContextConsumer>
|
||||
</GameDetailsContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +1,14 @@
|
|||
import { GameStatus, GameStatusHelper } from "@shared";
|
||||
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||
|
||||
import { Button } from "@renderer/components";
|
||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
import type { Game, GameRepack } from "@types";
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./hero-panel-actions.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export interface HeroPanelActionsProps {
|
||||
game: Game | null;
|
||||
repacks: GameRepack[];
|
||||
isGamePlaying: boolean;
|
||||
isGameDownloading: boolean;
|
||||
objectID: string;
|
||||
title: string;
|
||||
openRepacksModal: () => void;
|
||||
openBinaryNotFoundModal: () => void;
|
||||
getGame: () => void;
|
||||
}
|
||||
|
||||
export function HeroPanelActions({
|
||||
game,
|
||||
isGamePlaying,
|
||||
isGameDownloading,
|
||||
repacks,
|
||||
objectID,
|
||||
title,
|
||||
openRepacksModal,
|
||||
openBinaryNotFoundModal,
|
||||
getGame,
|
||||
}: HeroPanelActionsProps) {
|
||||
export function HeroPanelActions() {
|
||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||
useState(false);
|
||||
|
||||
|
@ -43,6 +20,16 @@ export function HeroPanelActions({
|
|||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const {
|
||||
game,
|
||||
repacks,
|
||||
isGameRunning,
|
||||
objectID,
|
||||
gameTitle,
|
||||
openRepacksModal,
|
||||
updateGame,
|
||||
} = useContext(gameDetailsContext);
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
@ -86,15 +73,15 @@ export function HeroPanelActions({
|
|||
const gameExecutablePath = await selectGameExecutable();
|
||||
|
||||
await window.electron.addGameToLibrary(
|
||||
objectID,
|
||||
title,
|
||||
objectID!,
|
||||
gameTitle,
|
||||
"steam",
|
||||
gameExecutablePath
|
||||
);
|
||||
}
|
||||
|
||||
updateLibrary();
|
||||
getGame();
|
||||
updateGame();
|
||||
} finally {
|
||||
setToggleLibraryGameDisabled(false);
|
||||
}
|
||||
|
@ -145,59 +132,14 @@ export function HeroPanelActions({
|
|||
</Button>
|
||||
);
|
||||
|
||||
if (game && isGameDownloading) {
|
||||
if (game?.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => pauseDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(getGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
GameStatusHelper.isReady(game?.status ?? null) ||
|
||||
(game && !game.status)
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{GameStatusHelper.isReady(game?.status ?? null) ? (
|
||||
{game?.progress === 1 ? (
|
||||
<Button
|
||||
onClick={openGameInstaller}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("install")}
|
||||
|
@ -206,7 +148,7 @@ export function HeroPanelActions({
|
|||
toggleGameOnLibraryButton
|
||||
)}
|
||||
|
||||
{isGamePlaying ? (
|
||||
{isGameRunning ? (
|
||||
<Button
|
||||
onClick={closeGame}
|
||||
theme="outline"
|
||||
|
@ -219,7 +161,7 @@ export function HeroPanelActions({
|
|||
<Button
|
||||
onClick={openGame}
|
||||
theme="outline"
|
||||
disabled={deleting || isGamePlaying}
|
||||
disabled={deleting || isGameRunning}
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("play")}
|
||||
|
@ -229,7 +171,49 @@ export function HeroPanelActions({
|
|||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Cancelled) {
|
||||
if (game?.status === "active") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => pauseDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => cancelDownload(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
className={styles.heroPanelAction}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "removed") {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
@ -240,8 +224,9 @@ export function HeroPanelActions({
|
|||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => removeGameFromLibrary(game.id).then(getGame)}
|
||||
onClick={() => removeGameFromLibrary(game.id).then(updateGame)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
className={styles.heroPanelAction}
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { Game } from "@types";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export interface HeroPanelPlaytimeProps {
|
||||
game: Game;
|
||||
isGamePlaying: boolean;
|
||||
}
|
||||
|
||||
export function HeroPanelPlaytime({
|
||||
game,
|
||||
isGamePlaying,
|
||||
}: HeroPanelPlaytimeProps) {
|
||||
export function HeroPanelPlaytime() {
|
||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||
|
||||
const { game, isGameRunning } = useContext(gameDetailsContext);
|
||||
|
||||
const { i18n, t } = useTranslation("game_details");
|
||||
|
||||
const { formatDistance } = useDate();
|
||||
|
@ -52,8 +46,8 @@ export function HeroPanelPlaytime({
|
|||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
if (!game.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game.title })}</p>;
|
||||
if (!game?.lastTimePlayed) {
|
||||
return <p>{t("not_played_yet", { title: game?.title })}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -64,7 +58,7 @@ export function HeroPanelPlaytime({
|
|||
})}
|
||||
</p>
|
||||
|
||||
{isGamePlaying ? (
|
||||
{isGameRunning ? (
|
||||
<p>{t("playing_now")}</p>
|
||||
) : (
|
||||
<p>
|
||||
|
|
|
@ -1,72 +1,48 @@
|
|||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useContext, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Color from "color";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import type { Game, GameRepack } from "@types";
|
||||
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
import { HeroPanelActions } from "./hero-panel-actions";
|
||||
import { Downloader, GameStatus, GameStatusHelper, formatBytes } from "@shared";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import { BinaryNotFoundModal } from "../../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./hero-panel.css";
|
||||
import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export interface HeroPanelProps {
|
||||
game: Game | null;
|
||||
color: string;
|
||||
isGamePlaying: boolean;
|
||||
objectID: string;
|
||||
title: string;
|
||||
repacks: GameRepack[];
|
||||
openRepacksModal: () => void;
|
||||
getGame: () => void;
|
||||
}
|
||||
|
||||
export function HeroPanel({
|
||||
game,
|
||||
color,
|
||||
repacks,
|
||||
objectID,
|
||||
title,
|
||||
isGamePlaying,
|
||||
openRepacksModal,
|
||||
getGame,
|
||||
}: HeroPanelProps) {
|
||||
export function HeroPanel() {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const { game, repacks, gameColor } = useContext(gameDetailsContext);
|
||||
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
|
||||
const {
|
||||
game: gameDownloading,
|
||||
progress,
|
||||
eta,
|
||||
numPeers,
|
||||
numSeeds,
|
||||
isGameDeleting,
|
||||
} = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
gameDownloading?.id === game?.id &&
|
||||
GameStatusHelper.isDownloading(game?.status ?? null);
|
||||
const { progress, eta, lastPacket, isGameDeleting } = useDownload();
|
||||
|
||||
const finalDownloadSize = useMemo(() => {
|
||||
if (!game) return "N/A";
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
|
||||
if (gameDownloading?.fileSize && isGameDownloading)
|
||||
return formatBytes(gameDownloading.fileSize);
|
||||
if (lastPacket?.game.fileSize && game?.status === "active")
|
||||
return formatBytes(lastPacket?.game.fileSize);
|
||||
|
||||
return game.repack?.fileSize ?? "N/A";
|
||||
}, [game, isGameDownloading, gameDownloading]);
|
||||
}, [game, lastPacket?.game]);
|
||||
|
||||
const getInfo = () => {
|
||||
if (isGameDeleting(game?.id ?? -1)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
|
||||
|
||||
if (game?.progress === 1) return <HeroPanelPlaytime />;
|
||||
|
||||
if (game?.status === "active") {
|
||||
if (lastPacket?.downloadingMetadata) {
|
||||
return <p>{t("downloading_metadata")}</p>;
|
||||
}
|
||||
|
||||
if (isGameDownloading && gameDownloading?.status) {
|
||||
return (
|
||||
<>
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
|
@ -74,33 +50,25 @@ export function HeroPanel({
|
|||
{eta && <small>{t("eta", { eta })}</small>}
|
||||
</p>
|
||||
|
||||
{gameDownloading.status !== GameStatus.Downloading ? (
|
||||
<>
|
||||
<p>{t(gameDownloading.status)}</p>
|
||||
{eta && <small>{t("eta", { eta })}</small>}
|
||||
</>
|
||||
) : (
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
{formatBytes(gameDownloading.bytesDownloaded)} /{" "}
|
||||
{finalDownloadSize}
|
||||
<p className={styles.downloadDetailsRow}>
|
||||
{formatBytes(lastPacket?.game?.bytesDownloaded ?? 0)} /{" "}
|
||||
{finalDownloadSize}
|
||||
{game?.downloader === Downloader.Torrent && (
|
||||
<small>
|
||||
{game?.downloader === Downloader.Torrent &&
|
||||
`${numPeers} peers / ${numSeeds} seeds`}
|
||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||
</small>
|
||||
</p>
|
||||
)}
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === GameStatus.Paused) {
|
||||
if (game?.status === "paused") {
|
||||
const formattedProgress = formatDownloadProgress(game.progress);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{t("paused_progress", {
|
||||
progress: formatDownloadProgress(game.progress),
|
||||
})}
|
||||
</p>
|
||||
<p>{t("paused_progress", { progress: formattedProgress })}</p>
|
||||
<p>
|
||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||
</p>
|
||||
|
@ -108,10 +76,6 @@ export function HeroPanel({
|
|||
);
|
||||
}
|
||||
|
||||
if (game && GameStatusHelper.isReady(game?.status ?? GameStatus.Finished)) {
|
||||
return <HeroPanelPlaytime game={game} isGamePlaying={isGamePlaying} />;
|
||||
}
|
||||
|
||||
const [latestRepack] = repacks;
|
||||
|
||||
if (latestRepack) {
|
||||
|
@ -129,6 +93,10 @@ export function HeroPanel({
|
|||
return <p>{t("no_downloads")}</p>;
|
||||
};
|
||||
|
||||
const backgroundColor = gameColor
|
||||
? (new Color(gameColor).darken(0.6).toString() as string)
|
||||
: "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<BinaryNotFoundModal
|
||||
|
@ -136,19 +104,11 @@ export function HeroPanel({
|
|||
onClose={() => setShowBinaryNotFoundModal(false)}
|
||||
/>
|
||||
|
||||
<div style={{ backgroundColor: color }} className={styles.panel}>
|
||||
<div style={{ backgroundColor }} className={styles.panel}>
|
||||
<div className={styles.content}>{getInfo()}</div>
|
||||
<div className={styles.actions}>
|
||||
<HeroPanelActions
|
||||
game={game}
|
||||
repacks={repacks}
|
||||
objectID={objectID}
|
||||
title={title}
|
||||
getGame={getGame}
|
||||
openRepacksModal={openRepacksModal}
|
||||
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
||||
isGamePlaying={isGamePlaying}
|
||||
isGameDownloading={isGameDownloading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
3
src/renderer/src/pages/game-details/modals/index.ts
Normal file
3
src/renderer/src/pages/game-details/modals/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./installation-guides";
|
||||
export * from "./repacks-modal";
|
||||
export * from "./select-folder-modal";
|
|
@ -1,4 +1,4 @@
|
|||
import { vars } from "../../../theme.css";
|
||||
import { vars } from "../../../../theme.css";
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
export const slideIn = keyframes({
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, CheckboxField, Modal } from "@renderer/components";
|
||||
|
@ -7,18 +7,19 @@ import { SPACING_UNIT } from "@renderer/theme.css";
|
|||
import * as styles from "./dodi-installation-guide.css";
|
||||
import { ArrowUpIcon } from "@primer/octicons-react";
|
||||
import { DONT_SHOW_DODI_INSTRUCTIONS_KEY } from "./constants";
|
||||
import { gameDetailsContext } from "../../game-details.context";
|
||||
|
||||
export interface DODIInstallationGuideProps {
|
||||
windowColor: string;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DODIInstallationGuide({
|
||||
windowColor,
|
||||
visible,
|
||||
onClose,
|
||||
}: DODIInstallationGuideProps) {
|
||||
const { gameColor } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
@ -53,7 +54,7 @@ export function DODIInstallationGuide({
|
|||
|
||||
<div
|
||||
className={styles.windowContainer}
|
||||
style={{ backgroundColor: windowColor }}
|
||||
style={{ backgroundColor: gameColor }}
|
||||
>
|
||||
<div className={styles.windowContent}>
|
||||
<ArrowUpIcon size={24} />
|
|
@ -1,4 +1,4 @@
|
|||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
import { SPACING_UNIT } from "../../../../theme.css";
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const passwordField = style({
|
|
@ -1,5 +1,5 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const repacks = style({
|
||||
display: "flex",
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
|
@ -6,20 +6,19 @@ import type { GameRepack } from "@types";
|
|||
|
||||
import * as styles from "./repacks-modal.css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
import { SPACING_UNIT } from "../../../theme.css";
|
||||
import { format } from "date-fns";
|
||||
import { SelectFolderModal } from "./select-folder-modal";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export interface RepacksModalProps {
|
||||
visible: boolean;
|
||||
repacks: GameRepack[];
|
||||
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RepacksModal({
|
||||
visible,
|
||||
repacks,
|
||||
startDownload,
|
||||
onClose,
|
||||
}: RepacksModalProps) {
|
||||
|
@ -27,6 +26,8 @@ export function RepacksModal({
|
|||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||
|
||||
const { repacks } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
useEffect(() => {
|
|
@ -1,5 +1,5 @@
|
|||
import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const container = style({
|
||||
display: "flex",
|
|
@ -1,13 +1,14 @@
|
|||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import type { GameRepack } from "@types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { DiskSpace } from "check-disk-space";
|
||||
import * as styles from "./select-folder-modal.css";
|
||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import { DownloadIcon } from "@primer/octicons-react";
|
||||
import { formatBytes } from "@shared";
|
||||
|
||||
import type { GameRepack } from "@types";
|
||||
|
||||
export interface SelectFolderModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
|
@ -1,22 +1,13 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||
import type {
|
||||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
SteamAppDetails,
|
||||
} from "@types";
|
||||
import type { HowLongToBeatCategory, SteamAppDetails } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@renderer/components";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { gameDetailsContext } from "../game-details.context";
|
||||
|
||||
export interface SidebarProps {
|
||||
objectID: string;
|
||||
title: string;
|
||||
gameDetails: ShopDetails | null;
|
||||
}
|
||||
|
||||
export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
|
||||
export function Sidebar() {
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
|
@ -25,20 +16,24 @@ export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
|
|||
const [activeRequirement, setActiveRequirement] =
|
||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||
|
||||
const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
|
||||
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
useEffect(() => {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
if (objectID) {
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
|
||||
window.electron
|
||||
.getHowLongToBeat(objectID, "steam", title)
|
||||
.then((howLongToBeat) => {
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
})
|
||||
.catch(() => {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
});
|
||||
}, [objectID, title]);
|
||||
window.electron
|
||||
.getHowLongToBeat(objectID, "steam", gameTitle)
|
||||
.then((howLongToBeat) => {
|
||||
setHowLongToBeat({ isLoading: false, data: howLongToBeat });
|
||||
})
|
||||
.catch(() => {
|
||||
setHowLongToBeat({ isLoading: false, data: null });
|
||||
});
|
||||
}
|
||||
}, [objectID, gameTitle]);
|
||||
|
||||
return (
|
||||
<aside className={styles.contentSidebar}>
|
||||
|
@ -73,9 +68,9 @@ export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
|
|||
className={styles.requirementsDetails}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
gameDetails?.pc_requirements?.[activeRequirement] ??
|
||||
shopDetails?.pc_requirements?.[activeRequirement] ??
|
||||
t(`no_${activeRequirement}_requirements`, {
|
||||
title,
|
||||
gameTitle,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue