mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
Merge pull request #567 from hydralauncher/feature/download-queue
Feature/download queue
This commit is contained in:
commit
d6e57c20c7
File diff suppressed because one or more lines are too long
@ -14,12 +14,8 @@
|
|||||||
"paused": "{{title}} (Paused)",
|
"paused": "{{title}} (Paused)",
|
||||||
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
"downloading": "{{title}} ({{percentage}} - Downloading…)",
|
||||||
"filter": "Filter library",
|
"filter": "Filter library",
|
||||||
"follow_us": "Follow us",
|
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"discord": "Join our Discord",
|
"queued": "{{title}} (Queued)"
|
||||||
"telegram": "Join our Telegram",
|
|
||||||
"x": "Follow on X",
|
|
||||||
"github": "Contribute on GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@ -133,7 +129,11 @@
|
|||||||
"remove_from_list": "Remove",
|
"remove_from_list": "Remove",
|
||||||
"delete_modal_title": "Are you sure?",
|
"delete_modal_title": "Are you sure?",
|
||||||
"delete_modal_description": "This will remove all the installation files from your computer",
|
"delete_modal_description": "This will remove all the installation files from your computer",
|
||||||
"install": "Install"
|
"install": "Install",
|
||||||
|
"download_in_progress": "In progress",
|
||||||
|
"queued_downloads": "Queued downloads",
|
||||||
|
"downloads_completed": "Completed",
|
||||||
|
"queued": "Queued"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Destaque",
|
"featured": "Destaque",
|
||||||
"recently_added": "Recém adicionados",
|
|
||||||
"trending": "Populares",
|
"trending": "Populares",
|
||||||
"surprise_me": "Surpreenda-me",
|
"surprise_me": "Surpreenda-me",
|
||||||
"no_results": "Nenhum resultado encontrado"
|
"no_results": "Nenhum resultado encontrado"
|
||||||
@ -17,10 +16,7 @@
|
|||||||
"filter": "Filtrar biblioteca",
|
"filter": "Filtrar biblioteca",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"follow_us": "Acompanhe-nos",
|
"follow_us": "Acompanhe-nos",
|
||||||
"discord": "Entre no nosso Discord",
|
"queued": "{{title}} (Na fila)"
|
||||||
"telegram": "Entre no nosso Telegram",
|
|
||||||
"x": "Siga-nos no X",
|
|
||||||
"github": "Contribua no GitHub"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
@ -130,7 +126,11 @@
|
|||||||
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
||||||
"delete_modal_title": "Tem certeza?",
|
"delete_modal_title": "Tem certeza?",
|
||||||
"deleting": "Excluindo instalador…",
|
"deleting": "Excluindo instalador…",
|
||||||
"install": "Instalar"
|
"install": "Instalar",
|
||||||
|
"download_in_progress": "Baixando agora",
|
||||||
|
"queued_downloads": "Na fila",
|
||||||
|
"downloads_completed": "Completo",
|
||||||
|
"queued": "Na fila"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório dos downloads",
|
"downloads_path": "Diretório dos downloads",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import {
|
import {
|
||||||
|
DownloadQueue,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
Game,
|
Game,
|
||||||
GameShopCache,
|
GameShopCache,
|
||||||
@ -16,7 +17,14 @@ export const createDataSource = (
|
|||||||
) =>
|
) =>
|
||||||
new DataSource({
|
new DataSource({
|
||||||
type: "better-sqlite3",
|
type: "better-sqlite3",
|
||||||
entities: [Game, Repack, UserPreferences, GameShopCache, DownloadSource],
|
entities: [
|
||||||
|
Game,
|
||||||
|
Repack,
|
||||||
|
UserPreferences,
|
||||||
|
GameShopCache,
|
||||||
|
DownloadSource,
|
||||||
|
DownloadQueue,
|
||||||
|
],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
database: databasePath,
|
database: databasePath,
|
||||||
...options,
|
...options,
|
||||||
|
25
src/main/entity/download-queue.entity.ts
Normal file
25
src/main/entity/download-queue.entity.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
OneToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from "typeorm";
|
||||||
|
import type { Game } from "./game.entity";
|
||||||
|
|
||||||
|
@Entity("download_queue")
|
||||||
|
export class DownloadQueue {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@OneToOne("Game", "downloadQueue")
|
||||||
|
@JoinColumn()
|
||||||
|
game: Game;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
@ -6,7 +6,8 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { Repack } from "./repack.entity";
|
import type { Repack } from "./repack.entity";
|
||||||
|
|
||||||
import { DownloadSourceStatus } from "@shared";
|
import { DownloadSourceStatus } from "@shared";
|
||||||
|
|
||||||
@Entity("download_source")
|
@Entity("download_source")
|
||||||
@ -26,7 +27,7 @@ export class DownloadSource {
|
|||||||
@Column("text", { default: DownloadSourceStatus.UpToDate })
|
@Column("text", { default: DownloadSourceStatus.UpToDate })
|
||||||
status: DownloadSourceStatus;
|
status: DownloadSourceStatus;
|
||||||
|
|
||||||
@OneToMany(() => Repack, (repack) => repack.downloadSource, { cascade: true })
|
@OneToMany("Repack", "downloadSource", { cascade: true })
|
||||||
repacks: Repack[];
|
repacks: Repack[];
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
@ -12,6 +12,7 @@ import { Repack } from "./repack.entity";
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import type { Aria2Status } from "aria2";
|
import type { Aria2Status } from "aria2";
|
||||||
|
import type { DownloadQueue } from "./download-queue.entity";
|
||||||
|
|
||||||
@Entity("game")
|
@Entity("game")
|
||||||
export class Game {
|
export class Game {
|
||||||
@ -66,10 +67,16 @@ export class Game {
|
|||||||
@Column("text", { nullable: true })
|
@Column("text", { nullable: true })
|
||||||
uri: string | null;
|
uri: string | null;
|
||||||
|
|
||||||
@OneToOne(() => Repack, { nullable: true })
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
@OneToOne("Repack", "game", { nullable: true })
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
repack: Repack;
|
repack: Repack;
|
||||||
|
|
||||||
|
@OneToOne("DownloadQueue", "game")
|
||||||
|
downloadQueue: DownloadQueue;
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
|
||||||
|
@ -3,3 +3,4 @@ export * from "./repack.entity";
|
|||||||
export * from "./user-preferences.entity";
|
export * from "./user-preferences.entity";
|
||||||
export * from "./game-shop-cache.entity";
|
export * from "./game-shop-cache.entity";
|
||||||
export * from "./download-source.entity";
|
export * from "./download-source.entity";
|
||||||
|
export * from "./download-queue.entity";
|
||||||
|
@ -19,6 +19,9 @@ export class Repack {
|
|||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
@Column("int", { nullable: true })
|
@Column("int", { nullable: true })
|
||||||
page: number;
|
page: number;
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ import { registerEvent } from "../register-event";
|
|||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||||
import { stateManager } from "@main/state-manager";
|
|
||||||
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -27,9 +28,9 @@ const addGameToLibrary = async (
|
|||||||
)
|
)
|
||||||
.then(async ({ affected }) => {
|
.then(async ({ affected }) => {
|
||||||
if (!affected) {
|
if (!affected) {
|
||||||
const steamGame = stateManager
|
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||||
.getValue("steamGames")
|
name: "getById",
|
||||||
.find((game) => game.id === Number(objectID));
|
});
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||||
|
@ -6,8 +6,11 @@ const getLibrary = async () =>
|
|||||||
where: {
|
where: {
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
|
relations: {
|
||||||
|
downloadQueue: true,
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
updatedAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
|
import { dataSource } from "@main/data-source";
|
||||||
|
import { DownloadQueue, Game } from "@main/entity";
|
||||||
|
|
||||||
const cancelGameDownload = async (
|
const cancelGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
await DownloadManager.cancelDownload(gameId);
|
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
|
await DownloadManager.cancelDownload(gameId);
|
||||||
|
|
||||||
await gameRepository.update(
|
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||||
{
|
game: { id: gameId },
|
||||||
id: gameId,
|
});
|
||||||
},
|
|
||||||
{
|
await transactionalEntityManager.getRepository(Game).update(
|
||||||
status: "removed",
|
{
|
||||||
bytesDownloaded: 0,
|
id: gameId,
|
||||||
progress: 0,
|
},
|
||||||
}
|
{
|
||||||
);
|
status: "removed",
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
progress: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
|
import { dataSource } from "@main/data-source";
|
||||||
|
import { DownloadQueue, Game } from "@main/entity";
|
||||||
|
|
||||||
const pauseGameDownload = async (
|
const pauseGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
await DownloadManager.pauseDownload();
|
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
await gameRepository.update({ id: gameId }, { status: "paused" });
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
|
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||||
|
game: { id: gameId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(Game)
|
||||||
|
.update({ id: gameId }, { status: "paused" });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||||
|
@ -5,7 +5,7 @@ import { gameRepository } from "../../repository";
|
|||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { Game } from "@main/entity";
|
import { DownloadQueue, Game } from "@main/entity";
|
||||||
|
|
||||||
const resumeGameDownload = async (
|
const resumeGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -30,6 +30,14 @@ const resumeGameDownload = async (
|
|||||||
|
|
||||||
await DownloadManager.resumeDownload(game);
|
await DownloadManager.resumeDownload(game);
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(DownloadQueue)
|
||||||
|
.delete({ game: { id: gameId } });
|
||||||
|
|
||||||
|
await transactionalEntityManager
|
||||||
|
.getRepository(DownloadQueue)
|
||||||
|
.insert({ game: { id: gameId } });
|
||||||
|
|
||||||
await transactionalEntityManager
|
await transactionalEntityManager
|
||||||
.getRepository(Game)
|
.getRepository(Game)
|
||||||
.update({ id: gameId }, { status: "active" });
|
.update({ id: gameId }, { status: "active" });
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { gameRepository, repackRepository } from "@main/repository";
|
import {
|
||||||
|
downloadQueueRepository,
|
||||||
|
gameRepository,
|
||||||
|
repackRepository,
|
||||||
|
} from "@main/repository";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { stateManager } from "@main/state-manager";
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -53,9 +58,9 @@ const startGameDownload = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const steamGame = stateManager
|
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||||
.getValue("steamGames")
|
name: "getById",
|
||||||
.find((game) => game.id === Number(objectID));
|
});
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
const iconUrl = steamGame?.clientIcon
|
||||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||||
@ -89,6 +94,9 @@ const startGameDownload = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||||
|
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||||
|
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { DownloadManager, RepacksManager, startMainLoop } from "./services";
|
import { DownloadManager, RepacksManager, startMainLoop } from "./services";
|
||||||
import { gameRepository, userPreferencesRepository } from "./repository";
|
import {
|
||||||
|
downloadQueueRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "./repository";
|
||||||
import { UserPreferences } from "./entity";
|
import { UserPreferences } from "./entity";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { Not } from "typeorm";
|
|
||||||
import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
@ -15,15 +17,17 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
if (userPreferences?.realDebridApiToken)
|
if (userPreferences?.realDebridApiToken)
|
||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||||
where: {
|
order: {
|
||||||
status: "active",
|
id: "DESC",
|
||||||
progress: Not(1),
|
},
|
||||||
isDeleted: false,
|
relations: {
|
||||||
|
game: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game) DownloadManager.startDownload(game);
|
if (nextQueueItem?.game.status === "active")
|
||||||
|
DownloadManager.startDownload(nextQueueItem.game);
|
||||||
|
|
||||||
fetchDownloadSourcesAndUpdate();
|
fetchDownloadSourcesAndUpdate();
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
import {
|
import {
|
||||||
|
DownloadQueue,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
Game,
|
Game,
|
||||||
GameShopCache,
|
GameShopCache,
|
||||||
@ -18,3 +19,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
|||||||
|
|
||||||
export const downloadSourceRepository =
|
export const downloadSourceRepository =
|
||||||
dataSource.getRepository(DownloadSource);
|
dataSource.getRepository(DownloadSource);
|
||||||
|
|
||||||
|
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import Aria2, { StatusResponse } from "aria2";
|
import Aria2, { StatusResponse } from "aria2";
|
||||||
|
|
||||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
import {
|
||||||
|
downloadQueueRepository,
|
||||||
|
gameRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
|
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { RealDebridClient } from "./real-debrid";
|
import { RealDebridClient } from "./real-debrid";
|
||||||
@ -194,19 +198,6 @@ export class DownloadManager {
|
|||||||
where: { id: this.game.id, isDeleted: false },
|
where: { id: this.game.id, isDeleted: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
|
||||||
await this.publishNotification();
|
|
||||||
|
|
||||||
/*
|
|
||||||
Only cancel bittorrent downloads to stop seeding
|
|
||||||
*/
|
|
||||||
if (status.bittorrent) {
|
|
||||||
await this.cancelDownload(this.game.id);
|
|
||||||
} else {
|
|
||||||
this.clearCurrentDownload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WindowManager.mainWindow && game) {
|
if (WindowManager.mainWindow && game) {
|
||||||
if (!isNaN(progress))
|
if (!isNaN(progress))
|
||||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||||
@ -229,6 +220,32 @@ export class DownloadManager {
|
|||||||
JSON.parse(JSON.stringify(payload))
|
JSON.parse(JSON.stringify(payload))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||||
|
await this.publishNotification();
|
||||||
|
|
||||||
|
await downloadQueueRepository.delete({ game: this.game });
|
||||||
|
|
||||||
|
/*
|
||||||
|
Only cancel bittorrent downloads to stop seeding
|
||||||
|
*/
|
||||||
|
if (status.bittorrent) {
|
||||||
|
await this.cancelDownload(this.game.id);
|
||||||
|
} else {
|
||||||
|
this.clearCurrentDownload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||||
|
order: {
|
||||||
|
id: "DESC",
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
game: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resumeDownload(nextQueueItem!.game);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static clearCurrentDownload() {
|
private static clearCurrentDownload() {
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
import type { Repack } from "@main/entity";
|
|
||||||
import type { SteamGame } from "@types";
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
repacks: Repack[];
|
|
||||||
steamGames: SteamGame[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: State = {
|
|
||||||
repacks: [],
|
|
||||||
steamGames: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export class StateManager {
|
|
||||||
private state = initialState;
|
|
||||||
|
|
||||||
public setValue<T extends keyof State>(key: T, value: State[T]) {
|
|
||||||
this.state = { ...this.state, [key]: value };
|
|
||||||
}
|
|
||||||
|
|
||||||
public getValue<T extends keyof State>(key: T) {
|
|
||||||
return this.state[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearValue<T extends keyof State>(key: T) {
|
|
||||||
this.state = { ...this.state, [key]: initialState[key] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const stateManager = new StateManager();
|
|
@ -7,7 +7,7 @@ import { formatName } from "@shared";
|
|||||||
import { workerData } from "node:worker_threads";
|
import { workerData } from "node:worker_threads";
|
||||||
|
|
||||||
const steamGamesIndex = new flexSearch.Index({
|
const steamGamesIndex = new flexSearch.Index({
|
||||||
tokenize: "forward",
|
tokenize: "reverse",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { steamGamesPath } = workerData;
|
const { steamGamesPath } = workerData;
|
||||||
|
@ -40,6 +40,8 @@ export function Sidebar() {
|
|||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.game.id, updateLibrary]);
|
}, [lastPacket?.game.id, updateLibrary]);
|
||||||
|
|
||||||
|
console.log(library);
|
||||||
|
|
||||||
const isDownloading = library.some(
|
const isDownloading = library.some(
|
||||||
(game) => game.status === "active" && game.progress !== 1
|
(game) => game.status === "active" && game.progress !== 1
|
||||||
);
|
);
|
||||||
@ -100,8 +102,6 @@ export function Sidebar() {
|
|||||||
}, [isResizing]);
|
}, [isResizing]);
|
||||||
|
|
||||||
const getGameTitle = (game: LibraryGame) => {
|
const getGameTitle = (game: LibraryGame) => {
|
||||||
if (game.status === "paused") return t("paused", { title: game.title });
|
|
||||||
|
|
||||||
if (lastPacket?.game.id === game.id) {
|
if (lastPacket?.game.id === game.id) {
|
||||||
return t("downloading", {
|
return t("downloading", {
|
||||||
title: game.title,
|
title: game.title,
|
||||||
@ -109,6 +109,12 @@ export function Sidebar() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (game.downloadQueue !== null) {
|
||||||
|
return t("queued", { title: game.title });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.status === "paused") return t("paused", { title: game.title });
|
||||||
|
|
||||||
return game.title;
|
return game.title;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
119
src/renderer/src/pages/downloads/download-group.css.ts
Normal file
119
src/renderer/src/pages/downloads/download-group.css.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
|
export const downloadTitleWrapper = style({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: `${SPACING_UNIT}px`,
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadTitle = style({
|
||||||
|
fontWeight: "bold",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: vars.color.body,
|
||||||
|
textAlign: "left",
|
||||||
|
fontSize: "16px",
|
||||||
|
display: "block",
|
||||||
|
":hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloads = style({
|
||||||
|
width: "100%",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
margin: "0",
|
||||||
|
padding: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCover = style({
|
||||||
|
width: "280px",
|
||||||
|
minWidth: "280px",
|
||||||
|
height: "auto",
|
||||||
|
borderRight: `solid 1px ${vars.color.border}`,
|
||||||
|
position: "relative",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverContent = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
padding: `${SPACING_UNIT}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverBackdrop = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
||||||
|
display: "flex",
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadCoverImage = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: "-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const download = recipe({
|
||||||
|
base: {
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow: "0px 0px 15px 0px #000000",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
height: "140px",
|
||||||
|
minHeight: "140px",
|
||||||
|
maxHeight: "140px",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
cancelled: {
|
||||||
|
true: {
|
||||||
|
opacity: vars.opacity.disabled,
|
||||||
|
":hover": {
|
||||||
|
opacity: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadDetails = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: "1",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
fontSize: "14px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadRightContent = style({
|
||||||
|
display: "flex",
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
flex: "1",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadActions = style({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadGroup = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
246
src/renderer/src/pages/downloads/download-group.tsx
Normal file
246
src/renderer/src/pages/downloads/download-group.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
|
import { Badge, Button } from "@renderer/components";
|
||||||
|
import {
|
||||||
|
buildGameDetailsPath,
|
||||||
|
formatDownloadProgress,
|
||||||
|
steamUrlBuilder,
|
||||||
|
} from "@renderer/helpers";
|
||||||
|
|
||||||
|
import { Downloader, formatBytes } from "@shared";
|
||||||
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
|
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
|
import * as styles from "./download-group.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface DownloadGroupProps {
|
||||||
|
library: LibraryGame[];
|
||||||
|
title: string;
|
||||||
|
openDeleteGameModal: (gameId: number) => void;
|
||||||
|
openGameInstaller: (gameId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadGroup({
|
||||||
|
library,
|
||||||
|
title,
|
||||||
|
openDeleteGameModal,
|
||||||
|
openGameInstaller,
|
||||||
|
}: DownloadGroupProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
|
const userPreferences = useAppSelector(
|
||||||
|
(state) => state.userPreferences.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
lastPacket,
|
||||||
|
progress,
|
||||||
|
pauseDownload,
|
||||||
|
resumeDownload,
|
||||||
|
removeGameFromLibrary,
|
||||||
|
cancelDownload,
|
||||||
|
isGameDeleting,
|
||||||
|
} = useDownload();
|
||||||
|
|
||||||
|
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||||
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
|
if (game.fileSize) return formatBytes(game.fileSize);
|
||||||
|
|
||||||
|
if (lastPacket?.game.fileSize && isGameDownloading)
|
||||||
|
return formatBytes(lastPacket?.game.fileSize);
|
||||||
|
|
||||||
|
return "N/A";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGameInfo = (game: LibraryGame) => {
|
||||||
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
const finalDownloadSize = getFinalDownloadSize(game);
|
||||||
|
|
||||||
|
if (isGameDeleting(game.id)) {
|
||||||
|
return <p>{t("deleting")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGameDownloading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{progress}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
|
||||||
|
{finalDownloadSize}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{game.downloader === Downloader.Torrent && (
|
||||||
|
<small>
|
||||||
|
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.progress === 1) {
|
||||||
|
return <p>{t("completed")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.status === "paused") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{formatDownloadProgress(game.progress)}</p>
|
||||||
|
<p>{t(game.downloadQueue && lastPacket ? "queued" : "paused")}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.status === "active") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{formatDownloadProgress(game.progress)}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <p>{t(game.status)}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGameActions = (game: LibraryGame) => {
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
|
||||||
|
{t("delete")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
disabled={
|
||||||
|
game.downloader === Downloader.RealDebrid &&
|
||||||
|
!userPreferences?.realDebridApiToken
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("resume")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("download_again")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => removeGameFromLibrary(game.id)}
|
||||||
|
theme="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{t("remove_from_list")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!library.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.downloadGroup}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
|
||||||
|
<ul className={styles.downloads}>
|
||||||
|
{library.map((game) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={game.id}
|
||||||
|
className={styles.download({
|
||||||
|
cancelled: game.status === "removed",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.downloadCover}>
|
||||||
|
<div className={styles.downloadCoverBackdrop}>
|
||||||
|
<img
|
||||||
|
src={steamUrlBuilder.library(game.objectID)}
|
||||||
|
className={styles.downloadCoverImage}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.downloadCoverContent}>
|
||||||
|
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.downloadRightContent}>
|
||||||
|
<div className={styles.downloadDetails}>
|
||||||
|
<div className={styles.downloadTitleWrapper}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.downloadTitle}
|
||||||
|
onClick={() => navigate(buildGameDetailsPath(game))}
|
||||||
|
>
|
||||||
|
{game.title}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{getGameInfo(game)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.downloadActions}>
|
||||||
|
{getGameActions(game)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,117 +1,5 @@
|
|||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
|
|
||||||
export const downloadTitleWrapper = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: `${SPACING_UNIT}px`,
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadTitle = style({
|
|
||||||
fontWeight: "bold",
|
|
||||||
cursor: "pointer",
|
|
||||||
color: vars.color.body,
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: "16px",
|
|
||||||
display: "block",
|
|
||||||
":hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloads = style({
|
|
||||||
width: "100%",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
margin: "0",
|
|
||||||
padding: "0",
|
|
||||||
marginTop: `${SPACING_UNIT * 3}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCover = style({
|
|
||||||
width: "280px",
|
|
||||||
minWidth: "280px",
|
|
||||||
height: "auto",
|
|
||||||
borderRight: `solid 1px ${vars.color.border}`,
|
|
||||||
position: "relative",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverContent = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
padding: `${SPACING_UNIT}px`,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverBackdrop = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
|
||||||
display: "flex",
|
|
||||||
overflow: "hidden",
|
|
||||||
zIndex: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadCoverImage = style({
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: "-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const download = recipe({
|
|
||||||
base: {
|
|
||||||
width: "100%",
|
|
||||||
backgroundColor: vars.color.background,
|
|
||||||
display: "flex",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: `solid 1px ${vars.color.border}`,
|
|
||||||
overflow: "hidden",
|
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
|
||||||
transition: "all ease 0.2s",
|
|
||||||
height: "140px",
|
|
||||||
minHeight: "140px",
|
|
||||||
maxHeight: "140px",
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
cancelled: {
|
|
||||||
true: {
|
|
||||||
opacity: vars.opacity.disabled,
|
|
||||||
":hover": {
|
|
||||||
opacity: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadDetails = style({
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
flex: "1",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: `${SPACING_UNIT / 2}px`,
|
|
||||||
fontSize: "14px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadRightContent = style({
|
|
||||||
display: "flex",
|
|
||||||
padding: `${SPACING_UNIT * 2}px`,
|
|
||||||
flex: "1",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadActions = style({
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const downloadsContainer = style({
|
export const downloadsContainer = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -119,3 +7,9 @@ export const downloadsContainer = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const downloadGroups = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
flexDirection: "column",
|
||||||
|
});
|
||||||
|
@ -1,227 +1,96 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Badge, Button, TextField } from "@renderer/components";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import {
|
|
||||||
buildGameDetailsPath,
|
|
||||||
formatDownloadProgress,
|
|
||||||
steamUrlBuilder,
|
|
||||||
} from "@renderer/helpers";
|
|
||||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
|
||||||
import type { LibraryGame } from "@types";
|
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||||
import * as styles from "./downloads.css";
|
import * as styles from "./downloads.css";
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { Downloader, formatBytes } from "@shared";
|
import { DownloadGroup } from "./download-group";
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { LibraryGame } from "@types";
|
||||||
|
import { orderBy } from "lodash-es";
|
||||||
|
|
||||||
export function Downloads() {
|
export function Downloads() {
|
||||||
const { library, updateLibrary } = useLibrary();
|
const { library, updateLibrary } = useLibrary();
|
||||||
|
|
||||||
const { t } = useTranslation("downloads");
|
const { t } = useTranslation("downloads");
|
||||||
|
|
||||||
const userPreferences = useAppSelector(
|
|
||||||
(state) => state.userPreferences.value
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const gameToBeDeleted = useRef<number | null>(null);
|
const gameToBeDeleted = useRef<number | null>(null);
|
||||||
|
|
||||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
|
||||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
const {
|
const { removeGameInstaller } = useDownload();
|
||||||
lastPacket,
|
|
||||||
progress,
|
|
||||||
pauseDownload,
|
|
||||||
resumeDownload,
|
|
||||||
removeGameFromLibrary,
|
|
||||||
cancelDownload,
|
|
||||||
removeGameInstaller,
|
|
||||||
isGameDeleting,
|
|
||||||
} = useDownload();
|
|
||||||
|
|
||||||
const libraryWithDownloadedGamesOnly = useMemo(() => {
|
|
||||||
return library.filter((game) => game.status);
|
|
||||||
}, [library]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFilteredLibrary(libraryWithDownloadedGamesOnly);
|
|
||||||
}, [libraryWithDownloadedGamesOnly]);
|
|
||||||
|
|
||||||
const openGameInstaller = (gameId: number) =>
|
|
||||||
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
|
||||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
|
||||||
updateLibrary();
|
|
||||||
});
|
|
||||||
|
|
||||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
|
||||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
|
||||||
|
|
||||||
if (game.fileSize) return formatBytes(game.fileSize);
|
|
||||||
|
|
||||||
if (lastPacket?.game.fileSize && isGameDownloading)
|
|
||||||
return formatBytes(lastPacket?.game.fileSize);
|
|
||||||
|
|
||||||
return "N/A";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGameInfo = (game: LibraryGame) => {
|
|
||||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
|
||||||
const finalDownloadSize = getFinalDownloadSize(game);
|
|
||||||
|
|
||||||
if (isGameDeleting(game.id)) {
|
|
||||||
return <p>{t("deleting")}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGameDownloading) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{progress}</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
|
|
||||||
{finalDownloadSize}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{game.downloader === Downloader.Torrent && (
|
|
||||||
<small>
|
|
||||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.progress === 1) {
|
|
||||||
return <p>{t("completed")}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.status === "paused") {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{formatDownloadProgress(game.progress)}</p>
|
|
||||||
<p>{t("paused")}</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) => {
|
|
||||||
gameToBeDeleted.current = gameId;
|
|
||||||
setShowDeleteModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGameActions = (game: LibraryGame) => {
|
|
||||||
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>
|
|
||||||
|
|
||||||
<Button onClick={() => openDeleteModal(game.id)} theme="outline">
|
|
||||||
{t("delete")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
disabled={
|
|
||||||
game.downloader === Downloader.RealDebrid &&
|
|
||||||
!userPreferences?.realDebridApiToken
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("resume")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => cancelDownload(game.id)} theme="outline">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
|
||||||
theme="outline"
|
|
||||||
disabled={deleting}
|
|
||||||
>
|
|
||||||
{t("download_again")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => removeGameFromLibrary(game.id)}
|
|
||||||
theme="outline"
|
|
||||||
disabled={deleting}
|
|
||||||
>
|
|
||||||
{t("remove_from_list")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
|
||||||
setFilteredLibrary(
|
|
||||||
libraryWithDownloadedGamesOnly.filter((game) =>
|
|
||||||
game.title
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(event.target.value.toLocaleLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteGame = async () => {
|
const handleDeleteGame = async () => {
|
||||||
if (gameToBeDeleted.current)
|
if (gameToBeDeleted.current)
|
||||||
await removeGameInstaller(gameToBeDeleted.current);
|
await removeGameInstaller(gameToBeDeleted.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { lastPacket } = useDownload();
|
||||||
|
|
||||||
|
const handleOpenGameInstaller = (gameId: number) =>
|
||||||
|
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
||||||
|
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||||
|
updateLibrary();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenDeleteGameModal = (gameId: number) => {
|
||||||
|
gameToBeDeleted.current = gameId;
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const libraryGroup: Record<string, LibraryGame[]> = useMemo(() => {
|
||||||
|
const initialValue: Record<string, LibraryGame[]> = {
|
||||||
|
downloading: [],
|
||||||
|
queued: [],
|
||||||
|
complete: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = library.reduce((prev, next) => {
|
||||||
|
if (lastPacket?.game.id === next.id) {
|
||||||
|
return { ...prev, downloading: [...prev.downloading, next] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next.downloadQueue || next.status === "paused") {
|
||||||
|
return { ...prev, queued: [...prev.queued, next] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, complete: [...prev.complete, next] };
|
||||||
|
}, initialValue);
|
||||||
|
|
||||||
|
const queued = orderBy(
|
||||||
|
result.queued,
|
||||||
|
(game) => game.downloadQueue?.id ?? -1,
|
||||||
|
["desc"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const complete = orderBy(result.complete, (game) =>
|
||||||
|
game.status === "complete" ? 0 : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
queued,
|
||||||
|
complete,
|
||||||
|
};
|
||||||
|
}, [library, lastPacket?.game.id]);
|
||||||
|
|
||||||
|
const downloadGroups = [
|
||||||
|
{
|
||||||
|
title: t("download_in_progress"),
|
||||||
|
library: libraryGroup.downloading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("queued_downloads"),
|
||||||
|
library: libraryGroup.queued,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("downloads_completed"),
|
||||||
|
library: libraryGroup.complete,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.downloadsContainer}>
|
<section className={styles.downloadsContainer}>
|
||||||
<BinaryNotFoundModal
|
<BinaryNotFoundModal
|
||||||
@ -235,53 +104,17 @@ export function Downloads() {
|
|||||||
deleteGame={handleDeleteGame}
|
deleteGame={handleDeleteGame}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField placeholder={t("filter")} onChange={handleFilter} />
|
<div className={styles.downloadGroups}>
|
||||||
|
{downloadGroups.map((group) => (
|
||||||
<ul className={styles.downloads}>
|
<DownloadGroup
|
||||||
{filteredLibrary.map((game) => {
|
key={group.title}
|
||||||
return (
|
title={group.title}
|
||||||
<li
|
library={group.library}
|
||||||
key={game.id}
|
openDeleteGameModal={handleOpenDeleteGameModal}
|
||||||
className={styles.download({
|
openGameInstaller={handleOpenGameInstaller}
|
||||||
cancelled: game.status === "removed",
|
/>
|
||||||
})}
|
))}
|
||||||
>
|
</div>
|
||||||
<div className={styles.downloadCover}>
|
|
||||||
<div className={styles.downloadCoverBackdrop}>
|
|
||||||
<img
|
|
||||||
src={steamUrlBuilder.library(game.objectID)}
|
|
||||||
className={styles.downloadCoverImage}
|
|
||||||
alt={game.title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.downloadCoverContent}>
|
|
||||||
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.downloadRightContent}>
|
|
||||||
<div className={styles.downloadDetails}>
|
|
||||||
<div className={styles.downloadTitleWrapper}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.downloadTitle}
|
|
||||||
onClick={() => navigate(buildGameDetailsPath(game))}
|
|
||||||
>
|
|
||||||
{game.title}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{getGameInfo(game)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.downloadActions}>
|
|
||||||
{getGameActions(game)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ export function AddDownloadSourceModal({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.validateDownloadSource(value);
|
const result = await window.electron.validateDownloadSource(value);
|
||||||
console.log(result);
|
|
||||||
setValidationResult(result);
|
setValidationResult(result);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -86,6 +86,12 @@ export interface CatalogueEntry {
|
|||||||
repacks: GameRepack[];
|
repacks: GameRepack[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadQueue {
|
||||||
|
id: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
/* Used by the library */
|
/* Used by the library */
|
||||||
export interface Game {
|
export interface Game {
|
||||||
id: number;
|
id: number;
|
||||||
@ -104,6 +110,7 @@ export interface Game {
|
|||||||
fileSize: number;
|
fileSize: number;
|
||||||
objectID: string;
|
objectID: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
|
downloadQueue: DownloadQueue | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user