mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding toast component
This commit is contained in:
parent
da607fe741
commit
0162ebd133
13 changed files with 136 additions and 82 deletions
|
@ -120,7 +120,7 @@
|
||||||
"verifying": "Verifying…",
|
"verifying": "Verifying…",
|
||||||
"completed_at": "Completed in {{date}}",
|
"completed_at": "Completed in {{date}}",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"cancelled": "Cancelled",
|
"removed": "Removed",
|
||||||
"download_again": "Download again",
|
"download_again": "Download again",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"filter": "Filter downloaded games",
|
"filter": "Filter downloaded games",
|
||||||
|
|
|
@ -116,7 +116,7 @@
|
||||||
"verifying": "Verificando…",
|
"verifying": "Verificando…",
|
||||||
"completed_at": "Concluído em {{date}}",
|
"completed_at": "Concluído em {{date}}",
|
||||||
"completed": "Concluído",
|
"completed": "Concluído",
|
||||||
"cancelled": "Cancelado",
|
"removed": "Removido",
|
||||||
"download_again": "Baixar novamente",
|
"download_again": "Baixar novamente",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"filter": "Filtrar jogos baixados",
|
"filter": "Filtrar jogos baixados",
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { Not } from "typeorm";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { gameRepository } from "../../repository";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
|
import { dataSource } from "@main/data-source";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
|
||||||
const resumeGameDownload = async (
|
const resumeGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -18,11 +22,19 @@ const resumeGameDownload = async (
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
if (game.status === "paused") {
|
if (game.status === "paused") {
|
||||||
await DownloadManager.pauseDownload();
|
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
await gameRepository.update({ id: gameId }, { status: "active" });
|
await transactionalEntityManager
|
||||||
|
.getRepository(Game)
|
||||||
|
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
||||||
|
|
||||||
await DownloadManager.resumeDownload(gameId);
|
await DownloadManager.resumeDownload(gameId);
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(Game)
|
||||||
|
.update({ id: gameId }, { status: "active" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,8 @@ const startGameDownload = async (
|
||||||
|
|
||||||
if (!repack || game?.status === "active") return;
|
if (!repack || game?.status === "active") return;
|
||||||
|
|
||||||
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
await gameRepository.update(
|
await gameRepository.update(
|
||||||
{ status: "active", progress: Not(1) },
|
{ status: "active", progress: Not(1) },
|
||||||
{ status: "paused" }
|
{ status: "paused" }
|
||||||
|
@ -65,9 +67,7 @@ const startGameDownload = async (
|
||||||
|
|
||||||
await DownloadManager.startDownload(game.id);
|
await DownloadManager.startDownload(game.id);
|
||||||
|
|
||||||
game.status = "active";
|
return { ...game, stauts: "active" };
|
||||||
|
|
||||||
return game;
|
|
||||||
} else {
|
} else {
|
||||||
const steamGame = stateManager
|
const steamGame = stateManager
|
||||||
.getValue("steamGames")
|
.getValue("steamGames")
|
||||||
|
@ -98,11 +98,9 @@ const startGameDownload = async (
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
DownloadManager.startDownload(createdGame.id);
|
await DownloadManager.startDownload(createdGame.id);
|
||||||
|
|
||||||
const { repack: _, ...rest } = createdGame;
|
return createdGame;
|
||||||
|
|
||||||
return rest;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,16 @@ export class DownloadManager {
|
||||||
"aria2c"
|
"aria2c"
|
||||||
);
|
);
|
||||||
|
|
||||||
spawn(binary, ["--enable-rpc", "--rpc-listen-all"], { stdio: "inherit" });
|
spawn(
|
||||||
|
binary,
|
||||||
|
[
|
||||||
|
"--enable-rpc",
|
||||||
|
"--rpc-listen-all",
|
||||||
|
"--file-allocation=none",
|
||||||
|
"--allow-overwrite=true",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" }
|
||||||
|
);
|
||||||
|
|
||||||
await this.aria2.open();
|
await this.aria2.open();
|
||||||
this.attachListener();
|
this.attachListener();
|
||||||
|
@ -76,6 +85,7 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async attachListener() {
|
private static async attachListener() {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
if (!this.gid || !this.gameId) {
|
if (!this.gid || !this.gameId) {
|
||||||
|
@ -205,8 +215,6 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(gameId: number) {
|
static async resumeDownload(gameId: number) {
|
||||||
await this.aria2.call("forcePauseAll");
|
|
||||||
|
|
||||||
if (this.downloads.has(gameId)) {
|
if (this.downloads.has(gameId)) {
|
||||||
const gid = this.downloads.get(gameId)!;
|
const gid = this.downloads.get(gameId)!;
|
||||||
await this.aria2.call("unpause", gid);
|
await this.aria2.call("unpause", gid);
|
||||||
|
@ -219,8 +227,6 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(gameId: number) {
|
static async startDownload(gameId: number) {
|
||||||
await this.aria2.call("forcePauseAll");
|
|
||||||
|
|
||||||
const game = await this.getGame(gameId)!;
|
const game = await this.getGame(gameId)!;
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
|
|
|
@ -79,6 +79,10 @@ globalStyle("img", {
|
||||||
WebkitUserDrag: "none",
|
WebkitUserDrag: "none",
|
||||||
} as Record<string, string>);
|
} as Record<string, string>);
|
||||||
|
|
||||||
|
globalStyle("progress[value]", {
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
});
|
||||||
|
|
||||||
export const container = style({
|
export const container = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { keyframes, style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const modalSlideIn = keyframes({
|
export const fadeIn = keyframes({
|
||||||
"0%": { opacity: 0 },
|
"0%": { opacity: 0 },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalSlideOut = keyframes({
|
export const fadeOut = keyframes({
|
||||||
"0%": { opacity: 1 },
|
"0%": { opacity: 1 },
|
||||||
"100%": {
|
"100%": {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
@ -18,7 +18,7 @@ export const modalSlideOut = keyframes({
|
||||||
|
|
||||||
export const modal = recipe({
|
export const modal = recipe({
|
||||||
base: {
|
base: {
|
||||||
animationName: modalSlideIn,
|
animationName: fadeIn,
|
||||||
animationDuration: "0.3s",
|
animationDuration: "0.3s",
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
|
@ -33,7 +33,7 @@ export const modal = recipe({
|
||||||
variants: {
|
variants: {
|
||||||
closing: {
|
closing: {
|
||||||
true: {
|
true: {
|
||||||
animationName: modalSlideOut,
|
animationName: fadeOut,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const content = recipe({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
paddingBottom: "0",
|
paddingBottom: "0",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
|
@ -118,7 +119,6 @@ export const sectionTitle = style({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const section = style({
|
export const section = style({
|
||||||
padding: `${SPACING_UNIT * 2}px 0`,
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
|
@ -29,7 +29,6 @@ export const downloaderName = style({
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
alignSelf: "flex-start",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloads = style({
|
export const downloads = style({
|
||||||
|
@ -46,9 +45,34 @@ export const downloadCover = style({
|
||||||
width: "280px",
|
width: "280px",
|
||||||
minWidth: "280px",
|
minWidth: "280px",
|
||||||
height: "auto",
|
height: "auto",
|
||||||
objectFit: "cover",
|
|
||||||
objectPosition: "center",
|
|
||||||
borderRight: `solid 1px ${vars.color.border}`,
|
borderRight: `solid 1px ${vars.color.border}`,
|
||||||
|
position: "relative",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverContent = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
padding: `${SPACING_UNIT}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverBackdrop = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverImage = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: "-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const download = recipe({
|
export const download = recipe({
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Button, TextField } from "@renderer/components";
|
import { Button, TextField } from "@renderer/components";
|
||||||
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
|
import {
|
||||||
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
steamUrlBuilder,
|
||||||
|
} from "@renderer/helpers";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import type { Game } from "@types";
|
import type { Game } from "@types";
|
||||||
|
|
||||||
|
@ -86,9 +90,9 @@ export function Downloads() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{game.downloader === Downloader.Torrent && (
|
{game.downloader === Downloader.Torrent && (
|
||||||
<p>
|
<small>
|
||||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||||
</p>
|
</small>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -102,7 +106,6 @@ export function Downloads() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (game?.status === "removed") return <p>{t("cancelled")}</p>;
|
|
||||||
|
|
||||||
if (game?.status === "paused") {
|
if (game?.status === "paused") {
|
||||||
return (
|
return (
|
||||||
|
@ -113,7 +116,7 @@ export function Downloads() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return <p>{t(game?.status)}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeleteModal = (gameId: number) => {
|
const openDeleteModal = (gameId: number) => {
|
||||||
|
@ -173,7 +176,7 @@ export function Downloads() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate(`/game/${game.shop}/${game.objectID}`)}
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
>
|
>
|
||||||
|
@ -230,11 +233,21 @@ export function Downloads() {
|
||||||
cancelled: game.status === "removed",
|
cancelled: game.status === "removed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<img
|
<div className={styles.downloadCover}>
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
<div className={styles.downloadCoverBackdrop}>
|
||||||
className={styles.downloadCover}
|
<img
|
||||||
alt={game.title}
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
/>
|
className={styles.downloadCoverImage}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.downloadCoverContent}>
|
||||||
|
<small className={styles.downloaderName}>
|
||||||
|
{downloaderName[game?.downloader]}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className={styles.downloadRightContent}>
|
<div className={styles.downloadRightContent}>
|
||||||
<div className={styles.downloadDetails}>
|
<div className={styles.downloadDetails}>
|
||||||
<div className={styles.downloadTitleWrapper}>
|
<div className={styles.downloadTitleWrapper}>
|
||||||
|
@ -249,9 +262,6 @@ export function Downloads() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small className={styles.downloaderName}>
|
|
||||||
{downloaderName[game?.downloader]}
|
|
||||||
</small>
|
|
||||||
{getGameInfo(game)}
|
{getGameInfo(game)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { globalStyle, keyframes, style } from "@vanilla-extract/css";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const slideIn = keyframes({
|
export const slideIn = keyframes({
|
||||||
"0%": { transform: `translateY(${40 + 16}px)` },
|
"0%": { transform: `translateY(${40 + SPACING_UNIT * 2}px)` },
|
||||||
"100%": { transform: "translateY(0)" },
|
"100%": { transform: "translateY(0)" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./settings.css";
|
import * as styles from "./settings.css";
|
||||||
|
@ -7,6 +7,7 @@ import { UserPreferences } from "@types";
|
||||||
import { SettingsRealDebrid } from "./settings-real-debrid";
|
import { SettingsRealDebrid } from "./settings-real-debrid";
|
||||||
import { SettingsGeneral } from "./settings-general";
|
import { SettingsGeneral } from "./settings-general";
|
||||||
import { SettingsBehavior } from "./settings-behavior";
|
import { SettingsBehavior } from "./settings-behavior";
|
||||||
|
import { Toast } from "@renderer/components/toast/toast";
|
||||||
|
|
||||||
const categories = ["general", "behavior", "real_debrid"];
|
const categories = ["general", "behavior", "real_debrid"];
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ export function Settings() {
|
||||||
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
|
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
|
||||||
const [userPreferences, setUserPreferences] =
|
const [userPreferences, setUserPreferences] =
|
||||||
useState<UserPreferences | null>(null);
|
useState<UserPreferences | null>(null);
|
||||||
|
const [isToastVisible, setIsToastVisible] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
@ -23,8 +25,14 @@ export function Settings() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUpdateUserPreferences = (values: Partial<UserPreferences>) => {
|
const handleUpdateUserPreferences = async (
|
||||||
window.electron.updateUserPreferences(values);
|
values: Partial<UserPreferences>
|
||||||
|
) => {
|
||||||
|
await window.electron.updateUserPreferences(values);
|
||||||
|
window.electron.getUserPreferences().then((userPreferences) => {
|
||||||
|
setUserPreferences(userPreferences);
|
||||||
|
setIsToastVisible(true);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCategory = () => {
|
const renderCategory = () => {
|
||||||
|
@ -54,24 +62,37 @@ export function Settings() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const handleToastClose = useCallback(() => {
|
||||||
<section className={styles.container}>
|
setIsToastVisible(false);
|
||||||
<div className={styles.content}>
|
}, []);
|
||||||
<section className={styles.settingsCategories}>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<Button
|
|
||||||
key={category}
|
|
||||||
theme={currentCategory === category ? "primary" : "outline"}
|
|
||||||
onClick={() => setCurrentCategory(category)}
|
|
||||||
>
|
|
||||||
{t(category)}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<h2>{t(currentCategory)}</h2>
|
return (
|
||||||
{renderCategory()}
|
<>
|
||||||
</div>
|
<section className={styles.container}>
|
||||||
</section>
|
<div className={styles.content}>
|
||||||
|
<section className={styles.settingsCategories}>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<Button
|
||||||
|
key={category}
|
||||||
|
theme={currentCategory === category ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentCategory(category)}
|
||||||
|
>
|
||||||
|
{t(category)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2>{t(currentCategory)}</h2>
|
||||||
|
{renderCategory()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Toast
|
||||||
|
message="Settings have been saved"
|
||||||
|
visible={isToastVisible}
|
||||||
|
onClose={handleToastClose}
|
||||||
|
type="success"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
export enum GameStatus {
|
|
||||||
Seeding = "seeding",
|
|
||||||
Downloading = "downloading",
|
|
||||||
Paused = "paused",
|
|
||||||
CheckingFiles = "checking_files",
|
|
||||||
DownloadingMetadata = "downloading_metadata",
|
|
||||||
Cancelled = "cancelled",
|
|
||||||
Decompressing = "decompressing",
|
|
||||||
Finished = "finished",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Downloader {
|
export enum Downloader {
|
||||||
RealDebrid,
|
RealDebrid,
|
||||||
Torrent,
|
Torrent,
|
||||||
|
@ -29,13 +18,3 @@ export const formatBytes = (bytes: number): string => {
|
||||||
|
|
||||||
return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`;
|
return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class GameStatusHelper {
|
|
||||||
public static isDownloading(status: string | null) {
|
|
||||||
return status === "active";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isReady(status: string | null) {
|
|
||||||
return status === "complete";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue