mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
chore: adding download sorting
This commit is contained in:
parent
7fed104afd
commit
7e54a6d0a9
@ -184,7 +184,11 @@
|
|||||||
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
"reset_achievements_description": "This will reset all achievements for {{game}}",
|
||||||
"reset_achievements_title": "Are you sure?",
|
"reset_achievements_title": "Are you sure?",
|
||||||
"reset_achievements_success": "Achievements successfully reset",
|
"reset_achievements_success": "Achievements successfully reset",
|
||||||
"reset_achievements_error": "Failed to reset achievements"
|
"reset_achievements_error": "Failed to reset achievements",
|
||||||
|
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
|
||||||
|
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -173,7 +173,11 @@
|
|||||||
"reset_achievements_title": "Tem certeza?",
|
"reset_achievements_title": "Tem certeza?",
|
||||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||||
"reset_achievements_error": "Falha ao resetar conquistas",
|
"reset_achievements_error": "Falha ao resetar conquistas",
|
||||||
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais."
|
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
|
||||||
|
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
|
||||||
|
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
|
||||||
|
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
|
||||||
|
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { Download, StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -77,21 +78,49 @@ const startGameDownload = async (
|
|||||||
|
|
||||||
await downloadsSublevel.put(gameKey, download);
|
await downloadsSublevel.put(gameKey, download);
|
||||||
|
|
||||||
await DownloadManager.startDownload(download);
|
try {
|
||||||
|
await DownloadManager.startDownload(download);
|
||||||
|
|
||||||
const updatedGame = await gamesSublevel.get(gameKey);
|
const updatedGame = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createGame(updatedGame!).catch(() => {}),
|
createGame(updatedGame!).catch(() => {}),
|
||||||
HydraApi.post(
|
HydraApi.post(
|
||||||
"/games/download",
|
"/games/download",
|
||||||
{
|
{
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
},
|
},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
).catch(() => {}),
|
).catch(() => {}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
logger.error("Failed to start download", err);
|
||||||
|
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
|
||||||
|
return { ok: false, error: DownloadError.GofileQuotaExceeded };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
err.response?.status === 403 &&
|
||||||
|
downloader === Downloader.RealDebrid
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: DownloadError.RealDebridAccountNotAuthorized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return { ok: false, error: err.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("startGameDownload", startGameDownload);
|
registerEvent("startGameDownload", startGameDownload);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Downloader } from "@shared";
|
import { Downloader, DownloadError } from "@shared";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||||
@ -263,6 +263,8 @@ export class DownloadManager {
|
|||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
|
|
||||||
|
await GofileApi.checkDownloadUrl(downloadLink);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: downloadId,
|
game_id: downloadId,
|
||||||
@ -318,10 +320,7 @@ export class DownloadManager {
|
|||||||
case Downloader.RealDebrid: {
|
case Downloader.RealDebrid: {
|
||||||
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
if (!downloadUrl)
|
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
|
||||||
throw new Error(
|
|
||||||
"This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available."
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
|
@ -60,4 +60,12 @@ export class GofileApi {
|
|||||||
|
|
||||||
throw new Error("Failed to get download link");
|
throw new Error("Failed to get download link");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async checkDownloadUrl(url: string) {
|
||||||
|
return axios.head(url, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `accountToken=${this.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,6 +282,7 @@ export function App() {
|
|||||||
message={toast.message}
|
message={toast.message}
|
||||||
type={toast.type}
|
type={toast.type}
|
||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
|
duration={toast.duration}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@ -40,7 +40,9 @@ declare global {
|
|||||||
|
|
||||||
interface Electron {
|
interface Electron {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>;
|
startGameDownload: (
|
||||||
|
payload: StartGameDownloadPayload
|
||||||
|
) => Promise<{ ok: boolean; error?: string }>;
|
||||||
cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
|
||||||
|
@ -6,6 +6,7 @@ export interface ToastState {
|
|||||||
title: string;
|
title: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
type: ToastProps["type"];
|
type: ToastProps["type"];
|
||||||
|
duration?: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ const initialState: ToastState = {
|
|||||||
title: "",
|
title: "",
|
||||||
message: "",
|
message: "",
|
||||||
type: "success",
|
type: "success",
|
||||||
|
duration: 5000,
|
||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,6 +26,7 @@ export const toastSlice = createSlice({
|
|||||||
state.title = action.payload.title;
|
state.title = action.payload.title;
|
||||||
state.message = action.payload.message;
|
state.message = action.payload.message;
|
||||||
state.type = action.payload.type;
|
state.type = action.payload.type;
|
||||||
|
state.duration = action.payload.duration ?? 5000;
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
},
|
},
|
||||||
closeToast: (state) => {
|
closeToast: (state) => {
|
||||||
|
@ -29,10 +29,11 @@ export function useDownload() {
|
|||||||
const startDownload = async (payload: StartGameDownloadPayload) => {
|
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||||
dispatch(clearDownload());
|
dispatch(clearDownload());
|
||||||
|
|
||||||
const game = await window.electron.startGameDownload(payload);
|
const response = await window.electron.startGameDownload(payload);
|
||||||
|
|
||||||
await updateLibrary();
|
if (response.ok) updateLibrary();
|
||||||
return game;
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
const pauseDownload = async (shop: GameShop, objectId: string) => {
|
||||||
|
@ -6,12 +6,13 @@ export function useToast() {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const showSuccessToast = useCallback(
|
const showSuccessToast = useCallback(
|
||||||
(title: string, message?: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type: "success",
|
type: "success",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -19,12 +20,13 @@ export function useToast() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showErrorToast = useCallback(
|
const showErrorToast = useCallback(
|
||||||
(title: string, message?: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type: "error",
|
type: "error",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -32,12 +34,13 @@ export function useToast() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showWarningToast = useCallback(
|
const showWarningToast = useCallback(
|
||||||
(title: string, message?: string) => {
|
(title: string, message?: string, duration?: number) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showToast({
|
showToast({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
type: "warning",
|
type: "warning",
|
||||||
|
duration,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import * as styles from "./downloads.css";
|
|||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy, sortBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export default function Downloads() {
|
export default function Downloads() {
|
||||||
@ -58,21 +58,24 @@ export default function Downloads() {
|
|||||||
complete: [],
|
complete: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = library.reduce((prev, next) => {
|
const result = sortBy(library, (game) => game.download?.timestamp).reduce(
|
||||||
/* Game has been manually added to the library or has been canceled */
|
(prev, next) => {
|
||||||
if (!next.download?.status || next.download?.status === "removed")
|
/* Game has been manually added to the library or has been canceled */
|
||||||
return prev;
|
if (!next.download?.status || next.download?.status === "removed")
|
||||||
|
return prev;
|
||||||
|
|
||||||
/* Is downloading */
|
/* Is downloading */
|
||||||
if (lastPacket?.gameId === next.id)
|
if (lastPacket?.gameId === next.id)
|
||||||
return { ...prev, downloading: [...prev.downloading, next] };
|
return { ...prev, downloading: [...prev.downloading, next] };
|
||||||
|
|
||||||
/* Is either queued or paused */
|
/* Is either queued or paused */
|
||||||
if (next.download.queued || next.download?.status === "paused")
|
if (next.download.queued || next.download?.status === "paused")
|
||||||
return { ...prev, queued: [...prev.queued, next] };
|
return { ...prev, queued: [...prev.queued, next] };
|
||||||
|
|
||||||
return { ...prev, complete: [...prev.complete, next] };
|
return { ...prev, complete: [...prev.complete, next] };
|
||||||
}, initialValue);
|
},
|
||||||
|
initialValue
|
||||||
|
);
|
||||||
|
|
||||||
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
||||||
"desc",
|
"desc",
|
||||||
|
@ -102,19 +102,23 @@ export default function GameDetails() {
|
|||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
downloadPath: string
|
downloadPath: string
|
||||||
) => {
|
) => {
|
||||||
await startDownload({
|
const response = await startDownload({
|
||||||
repackId: repack.id,
|
repackId: repack.id,
|
||||||
objectId: objectId!,
|
objectId: objectId!,
|
||||||
title: gameTitle,
|
title: gameTitle,
|
||||||
downloader,
|
downloader,
|
||||||
shop: shop as GameShop,
|
shop,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
uri: selectRepackUri(repack, downloader),
|
uri: selectRepackUri(repack, downloader),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateGame();
|
if (response.ok) {
|
||||||
setShowRepacksModal(false);
|
await updateGame();
|
||||||
setShowGameOptionsModal(false);
|
setShowRepacksModal(false);
|
||||||
|
setShowGameOptionsModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNSFWContentRefuse = () => {
|
const handleNSFWContentRefuse = () => {
|
||||||
@ -123,10 +127,7 @@ export default function GameDetails() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CloudSyncContextProvider
|
<CloudSyncContextProvider objectId={objectId!} shop={shop}>
|
||||||
objectId={objectId!}
|
|
||||||
shop={shop! as GameShop}
|
|
||||||
>
|
|
||||||
<CloudSyncContextConsumer>
|
<CloudSyncContextConsumer>
|
||||||
{({
|
{({
|
||||||
showCloudSyncModal,
|
showCloudSyncModal,
|
||||||
|
@ -18,7 +18,7 @@ export interface DownloadSettingsModalProps {
|
|||||||
repack: GameRepack,
|
repack: GameRepack,
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
downloadPath: string
|
downloadPath: string
|
||||||
) => Promise<void>;
|
) => Promise<{ ok: boolean; error?: string }>;
|
||||||
repack: GameRepack | null;
|
repack: GameRepack | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ export function DownloadSettingsModal({
|
|||||||
onClose,
|
onClose,
|
||||||
startDownload,
|
startDownload,
|
||||||
repack,
|
repack,
|
||||||
}: DownloadSettingsModalProps) {
|
}: Readonly<DownloadSettingsModalProps>) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
const { showErrorToast } = useToast();
|
const { showErrorToast } = useToast();
|
||||||
@ -117,20 +117,30 @@ export function DownloadSettingsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartClick = () => {
|
const handleStartClick = async () => {
|
||||||
if (repack) {
|
if (repack) {
|
||||||
setDownloadStarting(true);
|
setDownloadStarting(true);
|
||||||
|
|
||||||
startDownload(repack, selectedDownloader!, selectedPath)
|
try {
|
||||||
.then(() => {
|
const response = await startDownload(
|
||||||
|
repack,
|
||||||
|
selectedDownloader!,
|
||||||
|
selectedPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
onClose();
|
onClose();
|
||||||
})
|
return;
|
||||||
.catch((error) => {
|
} else if (response.error) {
|
||||||
showErrorToast(t("download_error"), error.message);
|
showErrorToast(t("download_error"), t(response.error), 4_000);
|
||||||
})
|
}
|
||||||
.finally(() => {
|
} catch (error) {
|
||||||
setDownloadStarting(false);
|
if (error instanceof Error) {
|
||||||
});
|
showErrorToast(t("download_error"), error.message, 4_000);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setDownloadStarting(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export interface RepacksModalProps {
|
|||||||
repack: GameRepack,
|
repack: GameRepack,
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
downloadPath: string
|
downloadPath: string
|
||||||
) => Promise<void>;
|
) => Promise<{ ok: boolean; error?: string }>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ export function RepacksModal({
|
|||||||
visible,
|
visible,
|
||||||
startDownload,
|
startDownload,
|
||||||
onClose,
|
onClose,
|
||||||
}: RepacksModalProps) {
|
}: Readonly<RepacksModalProps>) {
|
||||||
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
|
||||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||||
@ -111,7 +111,7 @@ export function RepacksModal({
|
|||||||
|
|
||||||
<p style={{ fontSize: "12px" }}>
|
<p style={{ fontSize: "12px" }}>
|
||||||
{repack.fileSize} - {repack.repacker} -{" "}
|
{repack.fileSize} - {repack.repacker} -{" "}
|
||||||
{repack.uploadDate ? formatDate(repack.uploadDate!) : ""}
|
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
|
||||||
</p>
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -49,3 +49,10 @@ export enum AuthPage {
|
|||||||
UpdateEmail = "/update-email",
|
UpdateEmail = "/update-email",
|
||||||
UpdatePassword = "/update-password",
|
UpdatePassword = "/update-password",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DownloadError {
|
||||||
|
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
|
||||||
|
NotCachedInTorbox = "download_error_not_cached_in_torbox",
|
||||||
|
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
|
||||||
|
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user