mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'feature/seed-completed-downloads' of github.com:hydralauncher/hydra into feature/seed-completed-downloads
This commit is contained in:
commit
2d8b63c803
12 changed files with 585 additions and 79 deletions
|
@ -202,7 +202,8 @@
|
|||
"checking_files": "Checking files…",
|
||||
"seeding": "Seeding",
|
||||
"stop_seeding": "Stop seeding",
|
||||
"resume_seeding": "Resume seeding"
|
||||
"resume_seeding": "Resume seeding",
|
||||
"options": "Manage"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Downloads path",
|
||||
|
|
|
@ -198,7 +198,8 @@
|
|||
"checking_files": "Verificando arquivos…",
|
||||
"seeding": "Semeando",
|
||||
"stop_seeding": "Parar semeio",
|
||||
"resume_seeding": "Retomar semeio"
|
||||
"resume_seeding": "Retomar semeio",
|
||||
"options": "Gerenciar"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
|
|
|
@ -5,7 +5,7 @@ export const AddShouldSeedColumn: HydraMigration = {
|
|||
name: "AddShouldSeedColumn",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.boolean("shouldSeed").notNullable().defaultTo(false);
|
||||
return table.boolean("shouldSeed").notNullable().defaultTo(true);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import type { DownloadProgress } from "@types";
|
|||
import { GofileApi, QiwiApi } from "../hosters";
|
||||
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||
import { In, Not } from "typeorm";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
export class DownloadManager {
|
||||
private static currentDownloader: Downloader | null = null;
|
||||
|
@ -85,6 +87,30 @@ export class DownloadManager {
|
|||
}
|
||||
|
||||
const gameIds = seedStatus.map((status) => status.gameId);
|
||||
|
||||
for (const gameId of gameIds) {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId },
|
||||
});
|
||||
|
||||
if (game) {
|
||||
const isNotDeleted = fs.existsSync(
|
||||
path.join(game.downloadPath!, game.folderName!)
|
||||
);
|
||||
|
||||
if (!isNotDeleted) {
|
||||
await this.pauseSeeding(game.id);
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
status: "complete",
|
||||
shouldSeed: false,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send("on-hard-delete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateList = await gameRepository.find({
|
||||
where: {
|
||||
id: In(gameIds),
|
||||
|
|
|
@ -39,6 +39,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
ipcRenderer.on("on-download-progress", listener);
|
||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||
},
|
||||
onHardDelete: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-hard-delete", listener);
|
||||
return () => ipcRenderer.removeListener("on-hard-delete", listener);
|
||||
},
|
||||
onSeedingStatus: (cb: (value: SeedingStatus[]) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
|
|
|
@ -109,6 +109,14 @@ export function App() {
|
|||
};
|
||||
}, [clearDownload, setLastPacket, updateLibrary]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onHardDelete(() => {
|
||||
updateLibrary();
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [updateLibrary]);
|
||||
|
||||
useEffect(() => {
|
||||
const cachedUserDetails = window.localStorage.getItem("userDetails");
|
||||
|
||||
|
|
68
src/renderer/src/components/dropdown-menu/dropdown-menu.scss
Normal file
68
src/renderer/src/components/dropdown-menu/dropdown-menu.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
@use "../../scss/globals.scss";
|
||||
|
||||
.dropdown-menu {
|
||||
&__content {
|
||||
background-color: globals.$dark-background-color;
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&__title-bar {
|
||||
width: 100%;
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: globals.$border-color;
|
||||
}
|
||||
|
||||
&__item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__item--disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:not(&__item--disabled) &__item:hover {
|
||||
background-color: globals.$background-color;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__item:focus {
|
||||
background-color: globals.$background-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
81
src/renderer/src/components/dropdown-menu/dropdown-menu.tsx
Normal file
81
src/renderer/src/components/dropdown-menu/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import "./dropdown-menu.scss";
|
||||
|
||||
export interface DropdownMenuItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
show?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface DropdownMenuProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
loop?: boolean;
|
||||
items: DropdownMenuItem[];
|
||||
sideOffset?: number;
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
align?: "start" | "center" | "end";
|
||||
alignOffset?: number;
|
||||
}
|
||||
|
||||
export function DropdownMenu({
|
||||
children,
|
||||
title,
|
||||
items,
|
||||
sideOffset = 5,
|
||||
side = "bottom",
|
||||
loop = true,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
}: DropdownMenuProps) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root>
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<button aria-label={title}>{children}</button>
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
sideOffset={sideOffset}
|
||||
side={side}
|
||||
loop={loop}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
className="dropdown-menu__content"
|
||||
>
|
||||
{title && (
|
||||
<DropdownMenuPrimitive.Group className="dropdown-menu__group">
|
||||
<div className="dropdown-menu__title-bar">{title}</div>
|
||||
</DropdownMenuPrimitive.Group>
|
||||
)}
|
||||
|
||||
<DropdownMenuPrimitive.Separator className="dropdown-menu__separator" />
|
||||
|
||||
<DropdownMenuPrimitive.Group className="dropdown-menu__group">
|
||||
{items.map(
|
||||
(item) =>
|
||||
item.show !== false && (
|
||||
<DropdownMenuPrimitive.Item
|
||||
key={item.label}
|
||||
aria-label={item.label}
|
||||
onSelect={item.onClick}
|
||||
className={`dropdown-menu__item ${item.disabled ? "dropdown-menu__item--disabled" : ""}`}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<div className="dropdown-menu__item-icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
)}
|
||||
{item.label}
|
||||
</DropdownMenuPrimitive.Item>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuPrimitive.Group>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
);
|
||||
}
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
|
@ -55,6 +55,7 @@ declare global {
|
|||
onSeedingStatus: (
|
||||
cb: (value: SeedingStatus[]) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onHardDelete: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
|
|
|
@ -15,8 +15,12 @@ import { useAppSelector, useDownload } from "@renderer/hooks";
|
|||
import * as styles from "./download-group.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { XCircleIcon } from "@primer/octicons-react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
} from "@renderer/components/dropdown-menu/dropdown-menu";
|
||||
import { ThreeBarsIcon } from "@primer/octicons-react";
|
||||
|
||||
export interface DownloadGroupProps {
|
||||
library: LibraryGame[];
|
||||
|
@ -152,65 +156,64 @@ export function DownloadGroup({
|
|||
return <p>{t(game.status as string)}</p>;
|
||||
};
|
||||
|
||||
const getGameActions = (game: LibraryGame) => {
|
||||
const getGameActions = (game: LibraryGame): DropdownMenuItem[] => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => openGameInstaller(game.id)}
|
||||
theme="outline"
|
||||
disabled={deleting}
|
||||
>
|
||||
{t("install")}
|
||||
</Button>
|
||||
|
||||
{game.status === "seeding" ? (
|
||||
<Button onClick={() => pauseSeeding(game.id)} theme="outline">
|
||||
{t("stop_seeding")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => resumeSeeding(game.id)} theme="outline">
|
||||
{t("resume_seeding")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: t("install"),
|
||||
disabled: deleting,
|
||||
onClick: () => openGameInstaller(game.id),
|
||||
},
|
||||
{
|
||||
label: t("stop_seeding"),
|
||||
disabled: deleting,
|
||||
show: game.status === "seeding",
|
||||
onClick: () => pauseSeeding(game.id),
|
||||
},
|
||||
{
|
||||
label: t("resume_seeding"),
|
||||
disabled: deleting,
|
||||
show: game.status !== "seeding",
|
||||
onClick: () => resumeSeeding(game.id),
|
||||
},
|
||||
{
|
||||
label: t("delete"),
|
||||
disabled: deleting,
|
||||
onClick: () => openDeleteGameModal(game.id),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: t("pause"),
|
||||
onClick: () => pauseDownload(game.id),
|
||||
},
|
||||
{
|
||||
label: t("cancel"),
|
||||
onClick: () => cancelDownload(game.id),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => resumeDownload(game.id)}
|
||||
theme="outline"
|
||||
disabled={
|
||||
game.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken
|
||||
}
|
||||
>
|
||||
{t("resume")}
|
||||
</Button>
|
||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
return [
|
||||
{
|
||||
label: t("resume"),
|
||||
disabled:
|
||||
game.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken,
|
||||
onClick: () => resumeDownload(game.id),
|
||||
},
|
||||
{
|
||||
label: t("cancel"),
|
||||
onClick: () => cancelDownload(game.id),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
if (!library.length) return null;
|
||||
|
@ -280,26 +283,28 @@ export function DownloadGroup({
|
|||
{getGameInfo(game)}
|
||||
</div>
|
||||
|
||||
<div className={styles.downloadActions}>
|
||||
{getGameActions(game)}
|
||||
</div>
|
||||
<Button
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
padding: 8,
|
||||
margin: 6,
|
||||
border: "none",
|
||||
minHeight: "unset",
|
||||
borderRadius: "50%",
|
||||
color: vars.color.danger,
|
||||
}}
|
||||
onClick={() => openDeleteGameModal(game.id)}
|
||||
theme="outline"
|
||||
>
|
||||
<XCircleIcon />
|
||||
</Button>
|
||||
{getGameActions(game) !== null && (
|
||||
<DropdownMenu
|
||||
align="end"
|
||||
items={getGameActions(game)}
|
||||
sideOffset={-70}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "12px",
|
||||
borderRadius: "50%",
|
||||
border: "none",
|
||||
padding: "8px",
|
||||
minHeight: "unset",
|
||||
}}
|
||||
theme="outline"
|
||||
>
|
||||
<ThreeBarsIcon />
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue