mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: using retry system to connect to aria2
This commit is contained in:
parent
85516c1744
commit
ffb3d79954
34 changed files with 243 additions and 317 deletions
|
@ -156,10 +156,10 @@ export const newVersionButton = style({
|
|||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.bodyText,
|
||||
color: vars.color.body,
|
||||
borderBottom: "1px solid transparent",
|
||||
":hover": {
|
||||
borderBottom: `1px solid ${vars.color.bodyText}`,
|
||||
borderBottom: `1px solid ${vars.color.body}`,
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
import { Downloader } from "@shared";
|
||||
|
||||
export const VERSION_CODENAME = "Exodus";
|
||||
|
||||
export const DOWNLOADER_NAME = {
|
||||
[Downloader.RealDebrid]: "Real-Debrid",
|
||||
[Downloader.Torrent]: "Torrent",
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./use-download";
|
||||
export * from "./use-library";
|
||||
export * from "./use-date";
|
||||
export * from "./use-toast";
|
||||
export * from "./redux";
|
||||
|
|
33
src/renderer/src/hooks/use-toast.ts
Normal file
33
src/renderer/src/hooks/use-toast.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { useCallback } from "react";
|
||||
import { useAppDispatch } from "./redux";
|
||||
import { showToast } from "@renderer/features";
|
||||
|
||||
export function useToast() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const showSuccessToast = useCallback(
|
||||
(message: string) => {
|
||||
dispatch(
|
||||
showToast({
|
||||
message,
|
||||
type: "success",
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const showErrorToast = useCallback(
|
||||
(message: string) => {
|
||||
dispatch(
|
||||
showToast({
|
||||
message,
|
||||
type: "error",
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return { showSuccessToast, showErrorToast };
|
||||
}
|
|
@ -40,6 +40,7 @@ i18n
|
|||
})
|
||||
.then(() => {
|
||||
window.electron.updateUserPreferences({ language: i18n.language });
|
||||
i18n.changeLanguage("pt-BR");
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
|
|
|
@ -15,6 +15,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
|||
import * as styles from "./downloads.css";
|
||||
import { DeleteModal } from "./delete-modal";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
|
||||
export function Downloads() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
|
@ -55,7 +56,7 @@ export function Downloads() {
|
|||
});
|
||||
|
||||
const getFinalDownloadSize = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
if (!game) return "N/A";
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
|
@ -66,16 +67,11 @@ export function Downloads() {
|
|||
return game.repack?.fileSize ?? "N/A";
|
||||
};
|
||||
|
||||
const downloaderName = {
|
||||
[Downloader.RealDebrid]: t("real_debrid"),
|
||||
[Downloader.Torrent]: t("torrent"),
|
||||
};
|
||||
|
||||
const getGameInfo = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
|
||||
if (isGameDeleting(game?.id)) {
|
||||
if (isGameDeleting(game.id)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
}
|
||||
|
||||
|
@ -98,16 +94,16 @@ export function Downloads() {
|
|||
);
|
||||
}
|
||||
|
||||
if (game?.progress === 1) {
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<p>{game?.repack?.title}</p>
|
||||
<p>{game.repack?.title}</p>
|
||||
<p>{t("completed")}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "paused") {
|
||||
if (game.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
|
@ -116,7 +112,19 @@ export function Downloads() {
|
|||
);
|
||||
}
|
||||
|
||||
return <p>{t(game?.status)}</p>;
|
||||
if (game.status === "active") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
|
||||
<p>
|
||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t(game.status)}</p>;
|
||||
};
|
||||
|
||||
const openDeleteModal = (gameId: number) => {
|
||||
|
@ -125,37 +133,11 @@ export function Downloads() {
|
|||
};
|
||||
|
||||
const getGameActions = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
|
||||
if (isGameDownloading) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game?.progress === 1) {
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
@ -173,6 +155,32 @@ export function Downloads() {
|
|||
);
|
||||
}
|
||||
|
||||
if (isGameDownloading || game.status === "active") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => pauseDownload(game.id)} theme="outline">
|
||||
{t("pause")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => resumeDownload(game.id)} theme="outline">
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
@ -243,7 +251,7 @@ export function Downloads() {
|
|||
|
||||
<div className={styles.downloadCoverContent}>
|
||||
<small className={styles.downloaderName}>
|
||||
{downloaderName[game?.downloader]}
|
||||
{DOWNLOADER_NAME[game.downloader]}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,4 +25,10 @@ export const downloaders = style({
|
|||
|
||||
export const downloaderOption = style({
|
||||
flex: "1",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const downloaderIcon = style({
|
||||
position: "absolute",
|
||||
left: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
|
|
@ -7,8 +7,9 @@ import { Button, Link, Modal, TextField } from "@renderer/components";
|
|||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||
import { Downloader, formatBytes } from "@shared";
|
||||
|
||||
import type { GameRepack, UserPreferences } from "@types";
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
|
||||
export interface SelectFolderModalProps {
|
||||
visible: boolean;
|
||||
|
@ -21,6 +22,8 @@ export interface SelectFolderModalProps {
|
|||
repack: GameRepack | null;
|
||||
}
|
||||
|
||||
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
|
||||
|
||||
export function SelectFolderModal({
|
||||
visible,
|
||||
onClose,
|
||||
|
@ -32,14 +35,14 @@ 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);
|
||||
if (visible) {
|
||||
getDiskFreeSpace(selectedPath);
|
||||
}
|
||||
}, [visible, selectedPath]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -48,7 +51,6 @@ export function SelectFolderModal({
|
|||
window.electron.getUserPreferences(),
|
||||
]).then(([path, userPreferences]) => {
|
||||
setSelectedPath(userPreferences?.downloadsPath || path);
|
||||
setUserPreferences(userPreferences);
|
||||
|
||||
if (userPreferences?.realDebridApiToken) {
|
||||
setSelectedDownloader(Downloader.RealDebrid);
|
||||
|
@ -106,35 +108,21 @@ export function SelectFolderModal({
|
|||
</span>
|
||||
|
||||
<div className={styles.downloaders}>
|
||||
<Button
|
||||
className={styles.downloaderOption}
|
||||
theme={
|
||||
selectedDownloader === Downloader.Torrent
|
||||
? "primary"
|
||||
: "outline"
|
||||
}
|
||||
onClick={() => setSelectedDownloader(Downloader.Torrent)}
|
||||
>
|
||||
{selectedDownloader === Downloader.Torrent && (
|
||||
<CheckCircleFillIcon />
|
||||
)}
|
||||
Torrent
|
||||
</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>
|
||||
{downloaders.map((downloader) => (
|
||||
<Button
|
||||
key={downloader}
|
||||
className={styles.downloaderOption}
|
||||
theme={
|
||||
selectedDownloader === downloader ? "primary" : "outline"
|
||||
}
|
||||
onClick={() => setSelectedDownloader(downloader)}
|
||||
>
|
||||
{selectedDownloader === downloader && (
|
||||
<CheckCircleFillIcon className={styles.downloaderIcon} />
|
||||
)}
|
||||
{DOWNLOADER_NAME[downloader]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ import { Button, CheckboxField, Link, TextField } from "@renderer/components";
|
|||
import * as styles from "./settings-real-debrid.css";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { showToast } from "@renderer/features";
|
||||
import { useAppDispatch } from "@renderer/hooks";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
|
||||
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
|
||||
|
||||
|
@ -19,12 +18,13 @@ export function SettingsRealDebrid({
|
|||
userPreferences,
|
||||
updateUserPreferences,
|
||||
}: SettingsRealDebridProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
useRealDebrid: false,
|
||||
realDebridApiToken: null as string | null,
|
||||
});
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
|
@ -40,38 +40,40 @@ export function SettingsRealDebrid({
|
|||
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = async (
|
||||
event
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
event.preventDefault();
|
||||
|
||||
if (form.useRealDebrid) {
|
||||
const user = await window.electron.authenticateRealDebrid(
|
||||
form.realDebridApiToken!
|
||||
);
|
||||
|
||||
if (user.type === "premium") {
|
||||
dispatch(
|
||||
showToast({
|
||||
message: t("real_debrid_free_account", { username: user.username }),
|
||||
type: "error",
|
||||
})
|
||||
try {
|
||||
if (form.useRealDebrid) {
|
||||
const user = await window.electron.authenticateRealDebrid(
|
||||
form.realDebridApiToken!
|
||||
);
|
||||
|
||||
return;
|
||||
if (user.type === "free") {
|
||||
showErrorToast(
|
||||
t("real_debrid_free_account_error", { username: user.username })
|
||||
);
|
||||
|
||||
return;
|
||||
} else {
|
||||
showSuccessToast(
|
||||
t("real_debrid_linked_message", { username: user.username })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updateUserPreferences({
|
||||
realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
|
||||
});
|
||||
} catch (err) {
|
||||
showErrorToast(t("real_debrid_invalid_token"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
// dispatch(
|
||||
// showToast({
|
||||
// message: t("real_debrid_free_account", { username: "doctorp" }),
|
||||
// type: "error",
|
||||
// })
|
||||
// );
|
||||
|
||||
updateUserPreferences({
|
||||
realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null,
|
||||
});
|
||||
};
|
||||
|
||||
const isButtonDisabled = form.useRealDebrid && !form.realDebridApiToken;
|
||||
const isButtonDisabled =
|
||||
(form.useRealDebrid && !form.realDebridApiToken) || isLoading;
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={handleFormSubmit}>
|
||||
|
@ -90,7 +92,7 @@ export function SettingsRealDebrid({
|
|||
|
||||
{form.useRealDebrid && (
|
||||
<TextField
|
||||
label="API Private Token"
|
||||
label={t("real_debrid_api_token")}
|
||||
value={form.realDebridApiToken ?? ""}
|
||||
type="password"
|
||||
onChange={(event) =>
|
||||
|
|
|
@ -8,15 +8,16 @@ import { SettingsRealDebrid } from "./settings-real-debrid";
|
|||
import { SettingsGeneral } from "./settings-general";
|
||||
import { SettingsBehavior } from "./settings-behavior";
|
||||
|
||||
const categories = ["general", "behavior", "real_debrid"];
|
||||
|
||||
export function Settings() {
|
||||
const [currentCategory, setCurrentCategory] = useState(categories.at(0)!);
|
||||
const [userPreferences, setUserPreferences] =
|
||||
useState<UserPreferences | null>(null);
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const categories = [t("general"), t("behavior"), "Real-Debrid"];
|
||||
|
||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getUserPreferences().then((userPreferences) => {
|
||||
setUserPreferences(userPreferences);
|
||||
|
@ -33,7 +34,7 @@ export function Settings() {
|
|||
};
|
||||
|
||||
const renderCategory = () => {
|
||||
if (currentCategory === "general") {
|
||||
if (currentCategoryIndex === 0) {
|
||||
return (
|
||||
<SettingsGeneral
|
||||
userPreferences={userPreferences}
|
||||
|
@ -42,9 +43,9 @@ export function Settings() {
|
|||
);
|
||||
}
|
||||
|
||||
if (currentCategory === "real_debrid") {
|
||||
if (currentCategoryIndex === 1) {
|
||||
return (
|
||||
<SettingsRealDebrid
|
||||
<SettingsBehavior
|
||||
userPreferences={userPreferences}
|
||||
updateUserPreferences={handleUpdateUserPreferences}
|
||||
/>
|
||||
|
@ -52,7 +53,7 @@ export function Settings() {
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsBehavior
|
||||
<SettingsRealDebrid
|
||||
userPreferences={userPreferences}
|
||||
updateUserPreferences={handleUpdateUserPreferences}
|
||||
/>
|
||||
|
@ -63,18 +64,18 @@ export function Settings() {
|
|||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category) => (
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={currentCategory === category ? "primary" : "outline"}
|
||||
onClick={() => setCurrentCategory(category)}
|
||||
theme={currentCategoryIndex === index ? "primary" : "outline"}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{t(category)}
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<h2>{t(currentCategory)}</h2>
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
</section>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue