feat: using retry system to connect to aria2

This commit is contained in:
Chubby Granny Chaser 2024-05-29 21:50:35 +01:00
parent 85516c1744
commit ffb3d79954
No known key found for this signature in database
34 changed files with 243 additions and 317 deletions

View file

@ -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",
},
});

View file

@ -1 +1,8 @@
import { Downloader } from "@shared";
export const VERSION_CODENAME = "Exodus";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent",
};

View file

@ -1,4 +1,5 @@
export * from "./use-download";
export * from "./use-library";
export * from "./use-date";
export * from "./use-toast";
export * from "./redux";

View 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 };
}

View file

@ -40,6 +40,7 @@ i18n
})
.then(() => {
window.electron.updateUserPreferences({ language: i18n.language });
i18n.changeLanguage("pt-BR");
});
ReactDOM.createRoot(document.getElementById("root")!).render(

View file

@ -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>

View file

@ -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`,
});

View file

@ -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>

View file

@ -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) =>

View file

@ -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>