Merge branch 'feature/seed-completed-downloads' of github.com:hydralauncher/hydra into feature/seed-completed-downloads

This commit is contained in:
Chubby Granny Chaser 2024-11-28 10:34:15 +00:00
commit 2d8b63c803
No known key found for this signature in database
12 changed files with 585 additions and 79 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -55,6 +55,7 @@ declare global {
onSeedingStatus: (
cb: (value: SeedingStatus[]) => void
) => () => Electron.IpcRenderer;
onHardDelete: (cb: () => void) => () => Electron.IpcRenderer;
/* Catalogue */
searchGames: (query: string) => Promise<CatalogueEntry[]>;

View file

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