feat: adding new messages to hero panel

This commit is contained in:
Chubby Granny Chaser 2024-05-24 00:26:21 +01:00
parent a240c3ae24
commit d431c01d1b
No known key found for this signature in database
28 changed files with 398 additions and 358 deletions

View file

@ -15,8 +15,7 @@ export function BottomPanel() {
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const isGameDownloading =
lastPacket?.game && lastPacket?.game.status === "active";
const isGameDownloading = !!lastPacket?.game;
const [version, setVersion] = useState("");

View file

@ -18,7 +18,6 @@ const base = style({
},
":disabled": {
opacity: vars.opacity.disabled,
pointerEvents: "none",
cursor: "not-allowed",
},
});
@ -30,6 +29,9 @@ export const button = styleVariants({
":hover": {
backgroundColor: "#DADBE1",
},
":disabled": {
backgroundColor: vars.color.muted,
},
},
],
outline: [
@ -41,6 +43,9 @@ export const button = styleVariants({
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
":disabled": {
backgroundColor: "transparent",
},
},
],
dark: [

View file

@ -28,7 +28,6 @@ export const content = recipe({
flexDirection: "column",
padding: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT * 2}px`,
paddingBottom: "0",
width: "100%",
overflow: "auto",
},

View file

@ -48,10 +48,8 @@ export const toast = recipe({
export const toastContent = style({
display: "flex",
position: "relative",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 5}px`,
paddingLeft: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
justifyContent: "center",
alignItems: "center",
});
@ -63,13 +61,11 @@ export const progress = style({
backgroundColor: vars.color.darkBackground,
},
"::-webkit-progress-value": {
backgroundColor: "#1c9749",
backgroundColor: vars.color.muted,
},
});
export const closeButton = style({
position: "absolute",
right: `${SPACING_UNIT}px`,
color: vars.color.bodyText,
cursor: "pointer",
padding: "0",
@ -77,5 +73,9 @@ export const closeButton = style({
});
export const successIcon = style({
color: "#1c9749",
color: vars.color.success,
});
export const errorIcon = style({
color: vars.color.danger,
});

View file

@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
CheckCircleFillIcon,
CheckCircleIcon,
XCircleIcon,
XCircleFillIcon,
XIcon,
} from "@primer/octicons-react";
import * as styles from "./toast.css";
import { SPACING_UNIT } from "@renderer/theme.css";
export interface ToastProps {
visible: boolean;
@ -78,8 +78,13 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
return (
<div className={styles.toast({ closing: isClosing })}>
<div className={styles.toastContent}>
<CheckCircleFillIcon className={styles.successIcon} />
<span style={{ fontWeight: "bold" }}>{message}</span>
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
{type === "success" && (
<CheckCircleFillIcon className={styles.successIcon} />
)}
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
<span style={{ fontWeight: "bold" }}>{message}</span>
</div>
<button
type="button"
@ -87,7 +92,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
onClick={startAnimateClosing}
aria-label="Close toast"
>
<XCircleIcon />
<XIcon />
</button>
</div>

View file

@ -10,6 +10,7 @@ import type {
Steam250Game,
DownloadProgress,
UserPreferences,
StartGameDownloadPayload,
} from "@types";
import type { DiskSpace } from "check-disk-space";
@ -21,13 +22,7 @@ declare global {
interface Electron {
/* Torrenting */
startGameDownload: (
repackId: number,
objectID: string,
title: string,
shop: GameShop,
downloadPath: string
) => Promise<Game>;
startGameDownload: (payload: StartGameDownloadPayload) => Promise<Game>;
cancelGameDownload: (gameId: number) => Promise<void>;
pauseGameDownload: (gameId: number) => Promise<void>;
resumeGameDownload: (gameId: number) => Promise<void>;

View file

@ -9,7 +9,7 @@ import {
setGameDeleting,
removeGameFromDeleting,
} from "@renderer/features";
import type { DownloadProgress, GameShop } from "@types";
import type { DownloadProgress, StartGameDownloadPayload } from "@types";
import { useDate } from "./use-date";
import { formatBytes } from "@shared";
@ -22,21 +22,13 @@ export function useDownload() {
);
const dispatch = useAppDispatch();
const startDownload = (
repackId: number,
objectID: string,
title: string,
shop: GameShop,
downloadPath: string
) =>
window.electron
.startGameDownload(repackId, objectID, title, shop, downloadPath)
.then((game) => {
dispatch(clearDownload());
updateLibrary();
const startDownload = (payload: StartGameDownloadPayload) =>
window.electron.startGameDownload(payload).then((game) => {
dispatch(clearDownload());
updateLibrary();
return game;
});
return game;
});
const pauseDownload = async (gameId: number) => {
await window.electron.pauseGameDownload(gameId);
@ -62,11 +54,11 @@ export function useDownload() {
});
const getETA = () => {
if (lastPacket && lastPacket.timeRemaining < 0) return "";
if (!lastPacket || lastPacket.timeRemaining < 0) return "";
try {
return formatDistance(
addMilliseconds(new Date(), lastPacket?.timeRemaining ?? 1),
addMilliseconds(new Date(), lastPacket.timeRemaining),
new Date(),
{ addSuffix: true }
);
@ -94,7 +86,7 @@ export function useDownload() {
return {
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
progress: formatDownloadProgress(lastPacket?.game.progress ?? 0),
progress: formatDownloadProgress(lastPacket?.game.progress),
lastPacket,
eta: getETA(),
startDownload,

View file

@ -254,9 +254,7 @@ export function Downloads() {
<button
type="button"
className={styles.downloadTitle}
onClick={() =>
navigate(`/game/${game.shop}/${game.objectID}`)
}
onClick={() => navigate(buildGameDetailsPath(game))}
>
{game.title}
</button>

View file

@ -67,6 +67,7 @@ export const mediaPreviewButton = recipe({
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
":hover": {
opacity: "0.8",
},
@ -84,7 +85,6 @@ export const mediaPreview = style({
width: "100%",
height: "100%",
display: "flex",
flex: "1",
});
export const gallerySliderButton = recipe({

View file

@ -15,6 +15,7 @@ import {
OnlineFixInstallationGuide,
RepacksModal,
} from "./modals";
import { Downloader } from "@shared";
export interface GameDetailsContext {
game: Game | null;
@ -138,15 +139,17 @@ export function GameDetailsContextProvider({
const handleStartDownload = async (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
) => {
await startDownload(
repack.id,
objectID!,
gameTitle,
shop as GameShop,
downloadPath
);
await startDownload({
repackId: repack.id,
objectID: objectID!,
title: gameTitle,
downloader,
shop: shop as GameShop,
downloadPath,
});
await updateGame();
setShowRepacksModal(false);

View file

@ -1,5 +1,6 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
export const panel = style({
width: "100%",
@ -11,6 +12,8 @@ export const panel = style({
justifyContent: "space-between",
transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
});
export const content = style({
@ -29,3 +32,27 @@ export const downloadDetailsRow = style({
display: "flex",
alignItems: "flex-end",
});
export const progressBar = recipe({
base: {
position: "absolute",
bottom: "0",
left: "0",
width: "100%",
height: "3px",
transition: "all ease 0.2s",
"::-webkit-progress-bar": {
backgroundColor: "transparent",
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
},
variants: {
disabled: {
true: {
opacity: vars.opacity.disabled,
},
},
},
});

View file

@ -33,29 +33,46 @@ export function HeroPanel() {
return game.repack?.fileSize ?? "N/A";
}, [game, lastPacket?.game]);
const isGameDownloading =
game?.status === "active" && lastPacket?.game.id === game?.id;
const getInfo = () => {
if (isGameDeleting(game?.id ?? -1)) return <p>{t("deleting")}</p>;
if (game?.progress === 1) return <HeroPanelPlaytime />;
console.log(lastPacket?.game.id, game?.id);
if (game?.status === "active" && lastPacket?.game.id === game?.id) {
if (lastPacket?.downloadingMetadata) {
return <p>{t("downloading_metadata")}</p>;
if (game?.status === "active") {
if (lastPacket?.downloadingMetadata && isGameDownloading) {
return (
<>
<p>{progress}</p>
<p>{t("downloading_metadata")}</p>
</>
);
}
const sizeDownloaded = formatBytes(
lastPacket?.game?.bytesDownloaded ?? game?.bytesDownloaded
);
const showPeers =
game?.downloader === Downloader.Torrent &&
lastPacket?.numPeers !== undefined;
return (
<>
<p className={styles.downloadDetailsRow}>
{progress}
{isGameDownloading
? progress
: formatDownloadProgress(game?.progress)}
{eta && <small>{t("eta", { eta })}</small>}
</p>
<p className={styles.downloadDetailsRow}>
{formatBytes(lastPacket?.game?.bytesDownloaded ?? 0)} /{" "}
{finalDownloadSize}
{game?.downloader === Downloader.Torrent && (
<span>
{sizeDownloaded} / {finalDownloadSize}
</span>
{showPeers && (
<small>
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
</small>
@ -99,6 +116,10 @@ export function HeroPanel() {
? (new Color(gameColor).darken(0.6).toString() as string)
: "";
const showProgressBar =
(game?.status === "active" && game?.progress < 1) ||
game?.status === "paused";
return (
<>
<BinaryNotFoundModal
@ -113,6 +134,18 @@ export function HeroPanel() {
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
/>
</div>
{showProgressBar && (
<progress
max={1}
value={
isGameDownloading ? lastPacket?.game.progress : game?.progress
}
className={styles.progressBar({
disabled: game?.status === "paused",
})}
/>
)}
</div>
</>
);

View file

@ -10,10 +10,15 @@ import { SPACING_UNIT } from "../../../theme.css";
import { format } from "date-fns";
import { SelectFolderModal } from "./select-folder-modal";
import { gameDetailsContext } from "../game-details.context";
import { Downloader } from "@shared";
export interface RepacksModalProps {
visible: boolean;
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
) => Promise<void>;
onClose: () => void;
}

View file

@ -4,15 +4,19 @@ 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 { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes } from "@shared";
import type { GameRepack } from "@types";
import type { GameRepack, UserPreferences } from "@types";
export interface SelectFolderModalProps {
visible: boolean;
onClose: () => void;
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
) => Promise<void>;
repack: GameRepack | null;
}
@ -27,6 +31,11 @@ export function SelectFolderModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
const [userPreferences, setUserPreferences] =
useState<UserPreferences | null>(null);
const [selectedDownloader, setSelectedDownloader] = useState(
Downloader.Torrent
);
useEffect(() => {
visible && getDiskFreeSpace(selectedPath);
@ -38,6 +47,11 @@ export function SelectFolderModal({
window.electron.getUserPreferences(),
]).then(([path, userPreferences]) => {
setSelectedPath(userPreferences?.downloadsPath || path);
setUserPreferences(userPreferences);
if (userPreferences?.realDebridApiToken) {
setSelectedDownloader(Downloader.RealDebrid);
}
});
}, []);
@ -63,7 +77,7 @@ export function SelectFolderModal({
if (repack) {
setDownloadStarting(true);
startDownload(repack, selectedPath).finally(() => {
startDownload(repack, selectedDownloader, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
@ -73,7 +87,7 @@ export function SelectFolderModal({
return (
<Modal
visible={visible}
title={t("download_path")}
title="Download options"
description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0),
})}
@ -81,23 +95,43 @@ export function SelectFolderModal({
>
<div className={styles.container}>
<div>
<label style={{ marginBottom: 0, padding: 0 }}>Download method</label>
<label style={{ marginBottom: 0, padding: 0 }}>Method</label>
<div className={styles.downloaders}>
<Button className={styles.downloaderOption} theme="outline">
<Button
className={styles.downloaderOption}
theme={
selectedDownloader === Downloader.Torrent
? "primary"
: "outline"
}
onClick={() => setSelectedDownloader(Downloader.Torrent)}
>
{selectedDownloader === Downloader.Torrent && (
<CheckCircleFillIcon />
)}
Torrent
</Button>
<Button className={styles.downloaderOption}>Real Debrid</Button>
<Button
className={styles.downloaderOption}
theme={
selectedDownloader === Downloader.RealDebrid
? "primary"
: "outline"
}
onClick={() => setSelectedDownloader(Downloader.RealDebrid)}
disabled={!userPreferences?.realDebridApiToken}
>
{selectedDownloader === Downloader.RealDebrid && (
<CheckCircleFillIcon />
)}
Real Debrid
</Button>
</div>
</div>
<div className={styles.downloadsPathField}>
<TextField
value={selectedPath}
readOnly
disabled
label="Download path"
/>
<TextField value={selectedPath} readOnly disabled label="Path" />
<Button
style={{ alignSelf: "flex-end" }}

View file

@ -9,6 +9,8 @@ export const [themeClass, vars] = createTheme({
muted: "#c0c1c7",
bodyText: "#8e919b",
border: "#424244",
success: "#1c9749",
danger: "#e11d48",
},
opacity: {
disabled: "0.5",