mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
feat: migrating user preferences
This commit is contained in:
parent
d760d0139d
commit
f1e0ba4dd6
@ -75,7 +75,6 @@
|
||||
"sound-play": "^1.1.0",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"tar": "^7.4.3",
|
||||
"typeorm": "^0.3.20",
|
||||
"user-agents": "^1.1.387",
|
||||
"yaml": "^2.6.1",
|
||||
"yup": "^1.5.0",
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import { UserPreferences } from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
|
||||
export const dataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
entities: [UserPreferences],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
});
|
@ -1 +0,0 @@
|
||||
export * from "./user-preferences.entity";
|
@ -1,55 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity("user_preferences")
|
||||
export class UserPreferences {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
downloadsPath: string | null;
|
||||
|
||||
@Column("text", { default: "en" })
|
||||
language: string;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
realDebridApiToken: string | null;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
downloadNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
repackUpdatesNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
achievementNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
runAtStartup: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
startMinimized: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
disableNsfwAlert: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
seedAfterDownloadComplete: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
showHiddenAchievementsDescription: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
import { levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import type { TrendingGame } from "@types";
|
||||
import { db } from "@main/level";
|
||||
|
||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const language = userPreferences?.language || "en";
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||
"/games/trending",
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { defaultDownloadsPath } from "@main/constants";
|
||||
import { levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
import { db } from "@main/level";
|
||||
|
||||
export const getDownloadsPath = async () => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences && userPreferences.downloadsPath)
|
||||
return userPreferences.downloadsPath;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { levelKeys } from "@main/level";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
@ -9,9 +9,14 @@ const getGameByObjectId = async (
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
const [game, download] = await Promise.all([
|
||||
gamesSublevel.get(gameKey),
|
||||
downloadsSublevel.get(gameKey),
|
||||
]);
|
||||
|
||||
return game;
|
||||
if (!game) return null;
|
||||
|
||||
return { id: gameKey, ...game, download };
|
||||
};
|
||||
|
||||
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import type { LibraryGame } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
||||
|
||||
const getLibrary = async () => {
|
||||
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||
return gamesSublevel
|
||||
.iterator()
|
||||
.all()
|
||||
@ -13,8 +14,9 @@ const getLibrary = async () => {
|
||||
const download = await downloadsSublevel.get(key);
|
||||
|
||||
return {
|
||||
id: key,
|
||||
...game,
|
||||
download,
|
||||
download: download ?? null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -3,20 +3,20 @@ import path from "node:path";
|
||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const openGameInstallerPath = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!game || !game.folderName || !game.downloadPath) return true;
|
||||
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||
|
||||
const gamePath = path.join(
|
||||
game.downloadPath ?? (await getDownloadsPath()),
|
||||
game.folderName!
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName!
|
||||
);
|
||||
|
||||
shell.showItemInFolder(gamePath);
|
||||
|
@ -10,7 +10,7 @@ const openGame = async (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
executablePath: string,
|
||||
launchOptions: string | null
|
||||
launchOptions?: string | null
|
||||
) => {
|
||||
// TODO: revisit this for launchOptions
|
||||
const parsedPath = parseExecutablePath(executablePath);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Notification } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { t } from "i18next";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
const publishNewRepacksNotification = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -9,9 +11,12 @@ const publishNewRepacksNotification = async (
|
||||
) => {
|
||||
if (newRepacksCount < 1) return;
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||
new Notification({
|
||||
|
@ -9,9 +9,11 @@ const cancelGameDownload = async (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await DownloadManager.cancelDownload(shop, objectId);
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
|
||||
await downloadsSublevel.del(levelKeys.game(shop, objectId));
|
||||
await DownloadManager.cancelDownload(downloadKey);
|
||||
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||
|
@ -1,24 +1,26 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const pauseGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (download) {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||
game: { id: gameId },
|
||||
});
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "paused" });
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
status: "paused",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||
|
@ -19,7 +19,7 @@ const pauseGameSeed = async (
|
||||
shouldSeed: false,
|
||||
});
|
||||
|
||||
await DownloadManager.pauseSeeding(download);
|
||||
await DownloadManager.pauseSeeding(downloadKey);
|
||||
};
|
||||
|
||||
registerEvent("pauseGameSeed", pauseGameSeed);
|
||||
|
@ -1,44 +1,36 @@
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { GameShop } from "@types";
|
||||
|
||||
const resumeGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
if (!game) return;
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (game.status === "paused") {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
if (download?.status === "paused") {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||
if (value.status === "active" && value.progress !== 1) {
|
||||
await downloadsSublevel.put(key, {
|
||||
...value,
|
||||
status: "paused",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await DownloadManager.resumeDownload(game);
|
||||
await DownloadManager.resumeDownload(download);
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.delete({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.insert({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "active" });
|
||||
await downloadsSublevel.put(gameKey, {
|
||||
...download,
|
||||
status: "active",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import type { Download, StartGameDownloadPayload } from "@types";
|
||||
import { DownloadManager, HydraApi } from "@main/services";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -15,40 +13,29 @@ const startGameDownload = async (
|
||||
) => {
|
||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||
|
||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||
const downloadQueueRepository =
|
||||
transactionalEntityManager.getRepository(DownloadQueue);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await gameRepository.update(
|
||||
{ status: "active", progress: Not(1) },
|
||||
{ status: "paused" }
|
||||
);
|
||||
|
||||
if (game) {
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: game.id,
|
||||
},
|
||||
{
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
isDeleted: false,
|
||||
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||
if (value.status === "active" && value.progress !== 1) {
|
||||
await downloadsSublevel.put(key, {
|
||||
...value,
|
||||
status: "paused",
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
/* Delete any previous download */
|
||||
await downloadsSublevel.del(gameKey);
|
||||
|
||||
if (game?.isDeleted) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: false,
|
||||
});
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
@ -58,42 +45,52 @@ const startGameDownload = async (
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository.insert({
|
||||
await gamesSublevel.put(gameKey, {
|
||||
title,
|
||||
iconUrl,
|
||||
objectID: objectId,
|
||||
downloader,
|
||||
objectId,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
remoteId: null,
|
||||
playTimeInMilliseconds: 0,
|
||||
lastTimePlayed: null,
|
||||
isDeleted: false,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID: objectId,
|
||||
},
|
||||
});
|
||||
await DownloadManager.cancelDownload(gameKey);
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
const download: Download = {
|
||||
shop,
|
||||
objectId,
|
||||
status: "active",
|
||||
progress: 0,
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri,
|
||||
folderName: null,
|
||||
fileSize: null,
|
||||
shouldSeed: false,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
await downloadsSublevel.put(gameKey, download);
|
||||
|
||||
await DownloadManager.startDownload(download);
|
||||
|
||||
const updatedGame = await gamesSublevel.get(gameKey);
|
||||
|
||||
await Promise.all([
|
||||
createGame(updatedGame!).catch(() => {}),
|
||||
HydraApi.post(
|
||||
"/games/download",
|
||||
{
|
||||
objectId: updatedGame!.objectID,
|
||||
shop: updatedGame!.shop,
|
||||
objectId,
|
||||
shop,
|
||||
},
|
||||
{ needsAuth: false }
|
||||
).catch(() => {}),
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("startGameDownload", startGameDownload);
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
const getUserPreferences = async () =>
|
||||
userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
registerEvent("getUserPreferences", getUserPreferences);
|
||||
|
@ -1,23 +1,35 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { UserPreferences } from "@types";
|
||||
import i18next from "i18next";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
|
||||
const updateUserPreferences = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
preferences: Partial<UserPreferences>
|
||||
) => {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
if (preferences.language) {
|
||||
await db.put<string, string>(levelKeys.language, preferences.language, {
|
||||
valueEncoding: "utf-8",
|
||||
});
|
||||
|
||||
i18next.changeLanguage(preferences.language);
|
||||
}
|
||||
|
||||
return userPreferencesRepository.upsert(
|
||||
await db.put<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
id: 1,
|
||||
...userPreferences,
|
||||
...preferences,
|
||||
},
|
||||
["id"]
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type { ComparedAchievements, GameShop } from "@types";
|
||||
import type { ComparedAchievements, GameShop, UserPreferences } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
|
||||
import { HydraApi } from "@main/services";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level";
|
||||
|
||||
const getComparedUnlockedAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -9,9 +11,12 @@ const getComparedUnlockedAchievements = async (
|
||||
shop: GameShop,
|
||||
userId: string
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const showHiddenAchievementsDescription =
|
||||
userPreferences?.showHiddenAchievementsDescription || false;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import type { GameShop, UserAchievement } from "@types";
|
||||
import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getUnlockedAchievements = async (
|
||||
objectId: string,
|
||||
@ -13,9 +12,12 @@ export const getUnlockedAchievements = async (
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const showHiddenAchievementsDescription =
|
||||
userPreferences?.showHiddenAchievementsDescription || false;
|
||||
|
@ -5,11 +5,10 @@ import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import resources from "@locales";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { Aria2 } from "./services/aria2";
|
||||
import { db, levelKeys } from "./level";
|
||||
|
||||
const { autoUpdater } = updater;
|
||||
|
||||
@ -58,23 +57,19 @@ app.whenReady().then(async () => {
|
||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
|
||||
await import("./main");
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
const language = await db.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
});
|
||||
|
||||
if (userPreferences?.language) {
|
||||
i18n.changeLanguage(userPreferences.language);
|
||||
}
|
||||
if (language) i18n.changeLanguage(language);
|
||||
|
||||
if (!process.argv.includes("--hidden")) {
|
||||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||
WindowManager.createSystemTray(language || "en");
|
||||
});
|
||||
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
|
@ -10,4 +10,7 @@ export const levelKeys = {
|
||||
`${shop}:${objectId}:${language}`,
|
||||
gameAchievements: "gameAchievements",
|
||||
downloads: "downloads",
|
||||
userPreferences: "userPreferences",
|
||||
language: "language",
|
||||
sqliteMigrationDone: "sqliteMigrationDone",
|
||||
};
|
||||
|
127
src/main/main.ts
127
src/main/main.ts
@ -1,6 +1,4 @@
|
||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
||||
import { userPreferencesRepository } from "./repository";
|
||||
import { UserPreferences } from "./entity";
|
||||
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
|
||||
import { RealDebridClient } from "./services/download/real-debrid";
|
||||
import { HydraApi } from "./services/hydra-api";
|
||||
import { uploadGamesBatch } from "./services/library-sync";
|
||||
@ -8,6 +6,10 @@ import { Aria2 } from "./services/aria2";
|
||||
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||
import { sortBy } from "lodash-es";
|
||||
import { Downloader } from "@shared";
|
||||
import { gameAchievementsSublevel, gamesSublevel, levelKeys } from "./level";
|
||||
import { Auth, User, type UserPreferences } from "@types";
|
||||
import { db } from "./level";
|
||||
import { knexClient } from "./knex-client";
|
||||
|
||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
import("./events");
|
||||
@ -46,10 +48,121 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
startMainLoop();
|
||||
};
|
||||
|
||||
userPreferencesRepository
|
||||
.findOne({
|
||||
where: { id: 1 },
|
||||
const migrateFromSqlite = async () => {
|
||||
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||
|
||||
if (sqliteMigrationDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateGames = knexClient("game")
|
||||
.select("*")
|
||||
.then((games) => {
|
||||
return gamesSublevel.batch(
|
||||
games.map((game) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(game.shop, game.objectID),
|
||||
value: {
|
||||
objectId: game.objectID,
|
||||
shop: game.shop,
|
||||
title: game.title,
|
||||
iconUrl: game.iconUrl,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
remoteId: game.remoteId,
|
||||
isDeleted: game.isDeleted,
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then((userPreferences) => {
|
||||
.then(() => {
|
||||
logger.info("Games migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUserPreferences = knexClient("user_preferences")
|
||||
.select("*")
|
||||
.then(async (userPreferences) => {
|
||||
if (userPreferences.length > 0) {
|
||||
await db.put(levelKeys.userPreferences, userPreferences[0]);
|
||||
|
||||
if (userPreferences[0].language) {
|
||||
await db.put(levelKeys.language, userPreferences[0].language);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User preferences migrated successfully");
|
||||
});
|
||||
|
||||
const migrateAchievements = knexClient("game_achievement")
|
||||
.select("*")
|
||||
.then((achievements) => {
|
||||
return gameAchievementsSublevel.batch(
|
||||
achievements.map((achievement) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(achievement.shop, achievement.objectId),
|
||||
value: {
|
||||
achievements: JSON.parse(achievement.achievements),
|
||||
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
|
||||
},
|
||||
}))
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("Achievements migrated successfully");
|
||||
});
|
||||
|
||||
const migrateUser = knexClient("user_auth")
|
||||
.select("*")
|
||||
.then(async (users) => {
|
||||
if (users.length > 0) {
|
||||
await db.put<string, User>(
|
||||
levelKeys.user,
|
||||
{
|
||||
id: users[0].userId,
|
||||
displayName: users[0].displayName,
|
||||
profileImageUrl: users[0].profileImageUrl,
|
||||
backgroundImageUrl: users[0].backgroundImageUrl,
|
||||
subscription: users[0].subscription,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
await db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: users[0].accessToken,
|
||||
refreshToken: users[0].refreshToken,
|
||||
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||
},
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info("User data migrated successfully");
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
migrateGames,
|
||||
migrateUserPreferences,
|
||||
migrateAchievements,
|
||||
migrateUser,
|
||||
]);
|
||||
};
|
||||
|
||||
migrateFromSqlite().then(async () => {
|
||||
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||
valueEncoding: "json",
|
||||
});
|
||||
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
}).then((userPreferences) => {
|
||||
loadState(userPreferences);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { dataSource } from "./data-source";
|
||||
import { UserPreferences } from "@main/entity";
|
||||
|
||||
export const userPreferencesRepository =
|
||||
dataSource.getRepository(UserPreferences);
|
@ -1,9 +1,8 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import type { GameShop, SteamAchievement } from "@types";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
@ -17,14 +16,16 @@ export const getGameAchievementData = async (
|
||||
if (cachedAchievements && useCachedData)
|
||||
return cachedAchievements.achievements;
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const language = await db
|
||||
.get<string, string>(levelKeys.language, {
|
||||
valueEncoding: "utf-8",
|
||||
})
|
||||
.then((language) => language || "en");
|
||||
|
||||
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
language,
|
||||
})
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import type { GameShop, UnlockedAchievement } from "@types";
|
||||
import type {
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||
import { Game } from "@main/entity";
|
||||
import { publishNewAchievementNotification } from "../notifications";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
@ -46,8 +49,10 @@ export const mergeAchievements = async (
|
||||
publishNotification: boolean
|
||||
) => {
|
||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
}),
|
||||
]);
|
||||
|
||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||
@ -131,13 +136,13 @@ export const mergeAchievements = async (
|
||||
if (err! instanceof SubscriptionRequiredError) {
|
||||
achievementsLogger.log(
|
||||
"Achievements not synchronized on API due to lack of subscription",
|
||||
game.objectID,
|
||||
game.objectId,
|
||||
game.title
|
||||
);
|
||||
}
|
||||
|
||||
return saveAchievementsOnLocal(
|
||||
game.objectID,
|
||||
game.objectId,
|
||||
game.shop,
|
||||
mergedLocalAchievements,
|
||||
publishNotification
|
||||
@ -145,7 +150,7 @@ export const mergeAchievements = async (
|
||||
});
|
||||
} else {
|
||||
await saveAchievementsOnLocal(
|
||||
game.objectID,
|
||||
game.objectId,
|
||||
game.shop,
|
||||
mergedLocalAchievements,
|
||||
publishNotification
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Downloader } from "@shared";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { Download, DownloadProgress } from "@types";
|
||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
@ -11,11 +10,11 @@ import {
|
||||
PauseDownloadPayload,
|
||||
} from "./types";
|
||||
import { calculateETA, getDirSize } from "./helpers";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import path from "path";
|
||||
import { logger } from "../logger";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { sortBy } from "lodash-es";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: string | null = null;
|
||||
@ -44,10 +43,8 @@ export class DownloadManager {
|
||||
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
||||
"/status"
|
||||
);
|
||||
|
||||
if (response.data === null || !this.downloadingGameId) return null;
|
||||
|
||||
const gameId = this.downloadingGameId;
|
||||
const downloadId = this.downloadingGameId;
|
||||
|
||||
try {
|
||||
const {
|
||||
@ -63,24 +60,21 @@ export class DownloadManager {
|
||||
|
||||
const isDownloadingMetadata =
|
||||
status === LibtorrentStatus.DownloadingMetadata;
|
||||
|
||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||
|
||||
const download = await downloadsSublevel.get(downloadId);
|
||||
|
||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||
const update: QueryDeepPartialEntity<Game> = {
|
||||
if (!download) return null;
|
||||
|
||||
await downloadsSublevel.put(downloadId, {
|
||||
...download,
|
||||
bytesDownloaded,
|
||||
fileSize,
|
||||
progress,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{
|
||||
...update,
|
||||
folderName,
|
||||
}
|
||||
);
|
||||
status: "active",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@ -91,7 +85,8 @@ export class DownloadManager {
|
||||
isDownloadingMetadata,
|
||||
isCheckingFiles,
|
||||
progress,
|
||||
gameId,
|
||||
gameId: downloadId,
|
||||
download,
|
||||
} as DownloadProgress;
|
||||
} catch (err) {
|
||||
return null;
|
||||
@ -103,15 +98,22 @@ export class DownloadManager {
|
||||
|
||||
if (status) {
|
||||
const { gameId, progress } = status;
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOneBy({
|
||||
id: 1,
|
||||
});
|
||||
const [download, game] = await Promise.all([
|
||||
downloadsSublevel.get(gameId),
|
||||
gamesSublevel.get(gameId),
|
||||
]);
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
if (!download || !game) return;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (WindowManager.mainWindow && download) {
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
@ -123,40 +125,42 @@ export class DownloadManager {
|
||||
)
|
||||
);
|
||||
}
|
||||
if (progress === 1 && game) {
|
||||
|
||||
if (progress === 1 && download) {
|
||||
publishDownloadCompleteNotification(game);
|
||||
|
||||
if (
|
||||
userPreferences?.seedAfterDownloadComplete &&
|
||||
game.downloader === Downloader.Torrent
|
||||
download.downloader === Downloader.Torrent
|
||||
) {
|
||||
gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ status: "seeding", shouldSeed: true }
|
||||
);
|
||||
downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "seeding",
|
||||
shouldSeed: true,
|
||||
});
|
||||
} else {
|
||||
gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ status: "complete", shouldSeed: false }
|
||||
);
|
||||
downloadsSublevel.put(gameId, {
|
||||
...download,
|
||||
status: "complete",
|
||||
shouldSeed: false,
|
||||
});
|
||||
|
||||
this.cancelDownload(gameId);
|
||||
}
|
||||
|
||||
await downloadsSublevel.del(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
relations: {
|
||||
game: true,
|
||||
},
|
||||
const downloads = await downloadsSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => {
|
||||
return sortBy(games, "timestamp", "DESC");
|
||||
});
|
||||
if (nextQueueItem) {
|
||||
this.resumeDownload(nextQueueItem.game);
|
||||
|
||||
const [nextItemOnQueue] = downloads;
|
||||
|
||||
if (nextItemOnQueue) {
|
||||
this.resumeDownload(nextItemOnQueue);
|
||||
} else {
|
||||
this.downloadingGameId = -1;
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,20 +176,19 @@ export class DownloadManager {
|
||||
logger.log(seedStatus);
|
||||
|
||||
seedStatus.forEach(async (status) => {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: status.gameId },
|
||||
});
|
||||
const download = await downloadsSublevel.get(status.gameId);
|
||||
|
||||
if (!game) return;
|
||||
if (!download) return;
|
||||
|
||||
const totalSize = await getDirSize(
|
||||
path.join(game.downloadPath!, status.folderName)
|
||||
path.join(download.downloadPath!, status.folderName)
|
||||
);
|
||||
|
||||
if (totalSize < status.fileSize) {
|
||||
await this.cancelDownload(game.id);
|
||||
await this.cancelDownload(status.gameId);
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
await downloadsSublevel.put(status.gameId, {
|
||||
...download,
|
||||
status: "paused",
|
||||
shouldSeed: false,
|
||||
progress: totalSize / status.fileSize,
|
||||
@ -207,114 +210,109 @@ export class DownloadManager {
|
||||
.catch(() => {});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
|
||||
static async resumeDownload(game: Game) {
|
||||
return this.startDownload(game);
|
||||
static async resumeDownload(download: Download) {
|
||||
return this.startDownload(download);
|
||||
}
|
||||
|
||||
static async cancelDownload(gameId = this.downloadingGameId!) {
|
||||
static async cancelDownload(downloadKey = this.downloadingGameId!) {
|
||||
await PythonRPC.rpc.post("/action", {
|
||||
action: "cancel",
|
||||
game_id: gameId,
|
||||
game_id: downloadKey,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
|
||||
if (gameId === this.downloadingGameId) {
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeSeeding(game: Game) {
|
||||
static async resumeSeeding(download: Download) {
|
||||
await PythonRPC.rpc.post("/action", {
|
||||
action: "resume_seeding",
|
||||
game_id: game.id,
|
||||
url: game.uri,
|
||||
save_path: game.downloadPath,
|
||||
game_id: levelKeys.game(download.shop, download.objectId),
|
||||
url: download.uri,
|
||||
save_path: download.downloadPath,
|
||||
});
|
||||
}
|
||||
|
||||
static async pauseSeeding(gameId: number) {
|
||||
static async pauseSeeding(downloadKey: string) {
|
||||
await PythonRPC.rpc.post("/action", {
|
||||
action: "pause_seeding",
|
||||
game_id: gameId,
|
||||
game_id: downloadKey,
|
||||
});
|
||||
}
|
||||
|
||||
private static async getDownloadPayload(game: Game) {
|
||||
switch (game.downloader) {
|
||||
case Downloader.Gofile: {
|
||||
const id = game.uri!.split("/").pop();
|
||||
private static async getDownloadPayload(download: Download) {
|
||||
const downloadId = levelKeys.game(download.shop, download.objectId);
|
||||
|
||||
switch (download.downloader) {
|
||||
case Downloader.Gofile: {
|
||||
const id = download.uri!.split("/").pop();
|
||||
const token = await GofileApi.authorize();
|
||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
game_id: downloadId,
|
||||
url: downloadLink,
|
||||
save_path: game.downloadPath!,
|
||||
save_path: download.downloadPath!,
|
||||
header: `Cookie: accountToken=${token}`,
|
||||
};
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = game.uri!.split("/").pop();
|
||||
|
||||
const id = download.uri!.split("/").pop();
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
game_id: downloadId,
|
||||
url: `https://pixeldrain.com/api/file/${id}?download`,
|
||||
save_path: game.downloadPath!,
|
||||
save_path: download.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Qiwi: {
|
||||
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
||||
|
||||
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri!);
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
game_id: downloadId,
|
||||
url: downloadUrl,
|
||||
save_path: game.downloadPath!,
|
||||
save_path: download.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Datanodes: {
|
||||
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
|
||||
|
||||
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri!);
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
game_id: downloadId,
|
||||
url: downloadUrl,
|
||||
save_path: game.downloadPath!,
|
||||
save_path: download.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Torrent:
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
url: game.uri!,
|
||||
save_path: game.downloadPath!,
|
||||
game_id: downloadId,
|
||||
url: download.uri!,
|
||||
save_path: download.downloadPath!,
|
||||
};
|
||||
case Downloader.RealDebrid: {
|
||||
const downloadUrl = await RealDebridClient.getDownloadUrl(game.uri!);
|
||||
|
||||
const downloadUrl = await RealDebridClient.getDownloadUrl(
|
||||
download.uri!
|
||||
);
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
game_id: downloadId,
|
||||
url: downloadUrl!,
|
||||
save_path: game.downloadPath!,
|
||||
save_path: download.downloadPath!,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async startDownload(game: Game) {
|
||||
const payload = await this.getDownloadPayload(game);
|
||||
|
||||
static async startDownload(download: Download) {
|
||||
const payload = await this.getDownloadPayload(download);
|
||||
await PythonRPC.rpc.post("/action", payload);
|
||||
|
||||
this.downloadingGameId = game.id;
|
||||
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export interface LibtorrentPayload {
|
||||
fileSize: number;
|
||||
folderName: string;
|
||||
status: LibtorrentStatus;
|
||||
gameId: number;
|
||||
gameId: string;
|
||||
}
|
||||
|
||||
export interface ProcessPayload {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Notification, app } from "electron";
|
||||
import { t } from "i18next";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import fs from "node:fs";
|
||||
import axios from "axios";
|
||||
import path from "node:path";
|
||||
@ -10,7 +9,9 @@ import { achievementSoundPath } from "@main/constants";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import { NotificationOptions, toXmlString } from "./xml";
|
||||
import { logger } from "../logger";
|
||||
import type { Game } from "@types";
|
||||
import type { Game, UserPreferences } from "@types";
|
||||
import { levelKeys } from "@main/level";
|
||||
import { db } from "@main/level";
|
||||
|
||||
async function downloadImage(url: string | null) {
|
||||
if (!url) return undefined;
|
||||
@ -38,9 +39,12 @@ async function downloadImage(url: string | null) {
|
||||
}
|
||||
|
||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
|
@ -13,11 +13,11 @@ import i18next, { t } from "i18next";
|
||||
import path from "node:path";
|
||||
import icon from "@resources/icon.png?asset";
|
||||
import trayIcon from "@resources/tray-icon.png?asset";
|
||||
import { userPreferencesRepository } from "@main/repository";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { slice, sortBy } from "lodash-es";
|
||||
import type { UserPreferences } from "@types";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
@ -131,9 +131,12 @@ export class WindowManager {
|
||||
});
|
||||
|
||||
this.mainWindow.on("close", async () => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||
app.quit();
|
||||
@ -211,20 +214,17 @@ export class WindowManager {
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) =>
|
||||
slice(
|
||||
sortBy(
|
||||
games.filter(
|
||||
.then((games) => {
|
||||
const filteredGames = games.filter(
|
||||
(game) =>
|
||||
!game.isDeleted && game.executablePath && game.lastTimePlayed
|
||||
),
|
||||
"lastTimePlayed",
|
||||
"DESC"
|
||||
),
|
||||
5
|
||||
)
|
||||
);
|
||||
|
||||
const sortedGames = sortBy(filteredGames, "lastTimePlayed", "DESC");
|
||||
|
||||
return slice(sortedGames, 5);
|
||||
});
|
||||
|
||||
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
|
||||
games.map(({ title, executablePath }) => ({
|
||||
label: title.length > 15 ? `${title.slice(0, 15)}…` : title,
|
||||
|
@ -132,7 +132,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
executablePath: string,
|
||||
launchOptions: string | null
|
||||
launchOptions?: string | null
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"openGame",
|
||||
@ -149,8 +149,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("removeGame", shop, objectId),
|
||||
deleteGameFolder: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
||||
getGameByObjectId: (objectId: string) =>
|
||||
ipcRenderer.invoke("getGameByObjectId", objectId),
|
||||
getGameByObjectId: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
onGamesRunning: (
|
||||
|
@ -84,7 +84,7 @@ export function App() {
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onDownloadProgress(
|
||||
(downloadProgress) => {
|
||||
if (downloadProgress.game.progress === 1) {
|
||||
if (downloadProgress.progress === 1) {
|
||||
clearDownload();
|
||||
updateLibrary();
|
||||
return;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload, useUserDetails } from "@renderer/hooks";
|
||||
import { useDownload, useLibrary, useUserDetails } from "@renderer/hooks";
|
||||
|
||||
import "./bottom-panel.scss";
|
||||
|
||||
@ -15,9 +15,11 @@ export function BottomPanel() {
|
||||
|
||||
const { userDetails } = useUserDetails();
|
||||
|
||||
const { library } = useLibrary();
|
||||
|
||||
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
|
||||
|
||||
const isGameDownloading = !!lastPacket?.game;
|
||||
const isGameDownloading = !!lastPacket;
|
||||
|
||||
const [version, setVersion] = useState("");
|
||||
const [sessionHash, setSessionHash] = useState<null | string>("");
|
||||
@ -32,27 +34,29 @@ export function BottomPanel() {
|
||||
|
||||
const status = useMemo(() => {
|
||||
if (isGameDownloading) {
|
||||
const game = library.find((game) => game.id === lastPacket?.gameId)!;
|
||||
|
||||
if (lastPacket?.isCheckingFiles)
|
||||
return t("checking_files", {
|
||||
title: lastPacket?.game.title,
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
|
||||
if (lastPacket?.isDownloadingMetadata)
|
||||
return t("downloading_metadata", {
|
||||
title: lastPacket?.game.title,
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
|
||||
if (!eta) {
|
||||
return t("calculating_eta", {
|
||||
title: lastPacket?.game.title,
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
}
|
||||
|
||||
return t("downloading", {
|
||||
title: lastPacket?.game.title,
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
eta,
|
||||
speed: downloadSpeed,
|
||||
@ -60,16 +64,7 @@ export function BottomPanel() {
|
||||
}
|
||||
|
||||
return t("no_downloads_in_progress");
|
||||
}, [
|
||||
t,
|
||||
isGameDownloading,
|
||||
lastPacket?.game,
|
||||
lastPacket?.isDownloadingMetadata,
|
||||
lastPacket?.isCheckingFiles,
|
||||
progress,
|
||||
eta,
|
||||
downloadSpeed,
|
||||
]);
|
||||
}, [t, isGameDownloading, library, lastPacket, progress, eta, downloadSpeed]);
|
||||
|
||||
return (
|
||||
<footer className="bottom-panel">
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import type { Game } from "@types";
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
import { TextField } from "@renderer/components";
|
||||
import {
|
||||
@ -35,7 +35,7 @@ export function Sidebar() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [filteredLibrary, setFilteredLibrary] = useState<Game[]>([]);
|
||||
const [filteredLibrary, setFilteredLibrary] = useState<LibraryGame[]>([]);
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(
|
||||
@ -56,7 +56,7 @@ export function Sidebar() {
|
||||
|
||||
useEffect(() => {
|
||||
updateLibrary();
|
||||
}, [lastPacket?.game.id, updateLibrary]);
|
||||
}, [lastPacket?.gameId, updateLibrary]);
|
||||
|
||||
const sidebarRef = useRef<HTMLElement>(null);
|
||||
|
||||
@ -117,20 +117,21 @@ export function Sidebar() {
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
const getGameTitle = (game: Game) => {
|
||||
if (lastPacket?.game.id === game.id) {
|
||||
const getGameTitle = (game: LibraryGame) => {
|
||||
if (lastPacket?.gameId === game.id) {
|
||||
return t("downloading", {
|
||||
title: game.title,
|
||||
percentage: progress,
|
||||
});
|
||||
}
|
||||
|
||||
if (game.downloadQueue !== null) {
|
||||
if (game.download?.status === "paused")
|
||||
return t("paused", { title: game.title });
|
||||
|
||||
if (game.download) {
|
||||
return t("queued", { title: game.title });
|
||||
}
|
||||
|
||||
if (game.status === "paused") return t("paused", { title: game.title });
|
||||
|
||||
return game.title;
|
||||
};
|
||||
|
||||
@ -140,10 +141,13 @@ export function Sidebar() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSidebarGameClick = (event: React.MouseEvent, game: Game) => {
|
||||
const handleSidebarGameClick = (
|
||||
event: React.MouseEvent,
|
||||
game: LibraryGame
|
||||
) => {
|
||||
const path = buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectID,
|
||||
objectId: game.objectId,
|
||||
});
|
||||
if (path !== location.pathname) {
|
||||
navigate(path);
|
||||
@ -152,7 +156,8 @@ export function Sidebar() {
|
||||
if (event.detail === 2) {
|
||||
if (game.executablePath) {
|
||||
window.electron.openGame(
|
||||
game.id,
|
||||
game.shop,
|
||||
game.objectId,
|
||||
game.executablePath,
|
||||
game.launchOptions
|
||||
);
|
||||
@ -216,12 +221,12 @@ export function Sidebar() {
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
key={`${game.shop}-${game.objectId}`}
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname ===
|
||||
`/game/${game.shop}/${game.objectID}`,
|
||||
muted: game.status === "removed",
|
||||
`/game/${game.shop}/${game.objectId}`,
|
||||
muted: game.download?.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
|
@ -18,9 +18,9 @@ import {
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import type {
|
||||
Game,
|
||||
GameShop,
|
||||
GameStats,
|
||||
LibraryGame,
|
||||
ShopDetails,
|
||||
UserAchievement,
|
||||
} from "@types";
|
||||
@ -73,7 +73,7 @@ export function GameDetailsContextProvider({
|
||||
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
||||
null
|
||||
);
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [game, setGame] = useState<LibraryGame | null>(null);
|
||||
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
@ -105,11 +105,12 @@ export function GameDetailsContextProvider({
|
||||
.then((result) => setGame(result));
|
||||
}, [setGame, shop, objectId]);
|
||||
|
||||
const isGameDownloading = lastPacket?.game.id === game?.id;
|
||||
const isGameDownloading =
|
||||
lastPacket?.gameId === game?.id && game?.download?.status === "active";
|
||||
|
||||
useEffect(() => {
|
||||
updateGame();
|
||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||
}, [updateGame, isGameDownloading, lastPacket?.gameId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (abortControllerRef.current) abortControllerRef.current.abort();
|
||||
@ -190,9 +191,9 @@ export function GameDetailsContextProvider({
|
||||
}, [game?.id, isGameRunning, updateGame]);
|
||||
|
||||
const lastDownloadedOption = useMemo(() => {
|
||||
if (game?.uri) {
|
||||
if (game?.download) {
|
||||
const repack = repacks.find((repack) =>
|
||||
repack.uris.some((uri) => uri.includes(game.uri!))
|
||||
repack.uris.some((uri) => uri.includes(game.download!.uri!))
|
||||
);
|
||||
|
||||
if (!repack) return null;
|
||||
@ -200,7 +201,7 @@ export function GameDetailsContextProvider({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [game?.uri, repacks]);
|
||||
}, [game?.download, repacks]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electron.onUpdateAchievements(
|
||||
|
@ -1,14 +1,14 @@
|
||||
import type {
|
||||
Game,
|
||||
GameRepack,
|
||||
GameShop,
|
||||
GameStats,
|
||||
LibraryGame,
|
||||
ShopDetails,
|
||||
UserAchievement,
|
||||
} from "@types";
|
||||
|
||||
export interface GameDetailsContext {
|
||||
game: Game | null;
|
||||
game: LibraryGame | null;
|
||||
shopDetails: ShopDetails | null;
|
||||
repacks: GameRepack[];
|
||||
shop: GameShop;
|
||||
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@ -1,7 +1,6 @@
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type {
|
||||
AppUpdaterEvent,
|
||||
Game,
|
||||
GameShop,
|
||||
HowLongToBeatCategory,
|
||||
ShopDetails,
|
||||
@ -27,6 +26,7 @@ import type {
|
||||
UserAchievement,
|
||||
ComparedAchievements,
|
||||
CatalogueSearchPayload,
|
||||
LibraryGame,
|
||||
} from "@types";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type disk from "diskusage";
|
||||
@ -103,7 +103,7 @@ declare global {
|
||||
winePrefixPath: string | null
|
||||
) => Promise<void>;
|
||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||
getLibrary: () => Promise<Game[]>;
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
openGameInstallerPath: (
|
||||
shop: GameShop,
|
||||
@ -114,7 +114,7 @@ declare global {
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
executablePath: string,
|
||||
launchOptions: string | null
|
||||
launchOptions?: string | null
|
||||
) => Promise<void>;
|
||||
closeGame: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
@ -123,7 +123,7 @@ declare global {
|
||||
getGameByObjectId: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<Game | null>;
|
||||
) => Promise<LibraryGame | null>;
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
|
@ -4,8 +4,8 @@ import type { DownloadProgress } from "@types";
|
||||
|
||||
export interface DownloadState {
|
||||
lastPacket: DownloadProgress | null;
|
||||
gameId: number | null;
|
||||
gamesWithDeletionInProgress: number[];
|
||||
gameId: string | null;
|
||||
gamesWithDeletionInProgress: string[];
|
||||
}
|
||||
|
||||
const initialState: DownloadState = {
|
||||
@ -20,13 +20,13 @@ export const downloadSlice = createSlice({
|
||||
reducers: {
|
||||
setLastPacket: (state, action: PayloadAction<DownloadProgress>) => {
|
||||
state.lastPacket = action.payload;
|
||||
if (!state.gameId) state.gameId = action.payload.game.id;
|
||||
if (!state.gameId) state.gameId = action.payload.gameId;
|
||||
},
|
||||
clearDownload: (state) => {
|
||||
state.lastPacket = null;
|
||||
state.gameId = null;
|
||||
},
|
||||
setGameDeleting: (state, action: PayloadAction<number>) => {
|
||||
setGameDeleting: (state, action: PayloadAction<string>) => {
|
||||
if (
|
||||
!state.gamesWithDeletionInProgress.includes(action.payload) &&
|
||||
action.payload
|
||||
@ -34,7 +34,7 @@ export const downloadSlice = createSlice({
|
||||
state.gamesWithDeletionInProgress.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeGameFromDeleting: (state, action: PayloadAction<number>) => {
|
||||
removeGameFromDeleting: (state, action: PayloadAction<string>) => {
|
||||
const index = state.gamesWithDeletionInProgress.indexOf(action.payload);
|
||||
if (index >= 0) state.gamesWithDeletionInProgress.splice(index, 1);
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
import type { Game } from "@types";
|
||||
import type { LibraryGame } from "@types";
|
||||
|
||||
export interface LibraryState {
|
||||
value: Game[];
|
||||
value: LibraryGame[];
|
||||
}
|
||||
|
||||
const initialState: LibraryState = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { Game, SeedingStatus } from "@types";
|
||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||
|
||||
import { Badge, Button } from "@renderer/components";
|
||||
import {
|
||||
@ -32,10 +32,10 @@ import {
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
export interface DownloadGroupProps {
|
||||
library: Game[];
|
||||
library: LibraryGame[];
|
||||
title: string;
|
||||
openDeleteGameModal: (gameId: number) => void;
|
||||
openGameInstaller: (gameId: number) => void;
|
||||
openDeleteGameModal: (shop: GameShop, objectId: string) => void;
|
||||
openGameInstaller: (shop: GameShop, objectId: string) => void;
|
||||
seedingStatus: SeedingStatus[];
|
||||
}
|
||||
|
||||
@ -65,19 +65,20 @@ export function DownloadGroup({
|
||||
resumeSeeding,
|
||||
} = useDownload();
|
||||
|
||||
const getFinalDownloadSize = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||
const download = game.download!;
|
||||
const isGameDownloading = lastPacket?.gameId === game.id;
|
||||
|
||||
if (game.fileSize) return formatBytes(game.fileSize);
|
||||
if (download.fileSize) return formatBytes(download.fileSize);
|
||||
|
||||
if (lastPacket?.game.fileSize && isGameDownloading)
|
||||
return formatBytes(lastPacket?.game.fileSize);
|
||||
if (download.fileSize && isGameDownloading)
|
||||
return formatBytes(download?.fileSize);
|
||||
|
||||
return "N/A";
|
||||
};
|
||||
|
||||
const seedingMap = useMemo(() => {
|
||||
const map = new Map<number, SeedingStatus>();
|
||||
const map = new Map<string, SeedingStatus>();
|
||||
|
||||
seedingStatus.forEach((seed) => {
|
||||
map.set(seed.gameId, seed);
|
||||
@ -86,8 +87,12 @@ export function DownloadGroup({
|
||||
return map;
|
||||
}, [seedingStatus]);
|
||||
|
||||
const getGameInfo = (game: Game) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const download = game.download!;
|
||||
|
||||
console.log(game);
|
||||
|
||||
const isGameDownloading = lastPacket?.gameId === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
const seedingStatus = seedingMap.get(game.id);
|
||||
|
||||
@ -114,11 +119,11 @@ export function DownloadGroup({
|
||||
<p>{progress}</p>
|
||||
|
||||
<p>
|
||||
{formatBytes(lastPacket?.game.bytesDownloaded)} /{" "}
|
||||
{formatBytes(lastPacket.download.bytesDownloaded)} /{" "}
|
||||
{finalDownloadSize}
|
||||
</p>
|
||||
|
||||
{game.downloader === Downloader.Torrent && (
|
||||
{download.downloader === Downloader.Torrent && (
|
||||
<small>
|
||||
{lastPacket?.numPeers} peers / {lastPacket?.numSeeds} seeds
|
||||
</small>
|
||||
@ -127,11 +132,11 @@ export function DownloadGroup({
|
||||
);
|
||||
}
|
||||
|
||||
if (game.progress === 1) {
|
||||
if (download.progress === 1) {
|
||||
const uploadSpeed = formatBytes(seedingStatus?.uploadSpeed ?? 0);
|
||||
|
||||
return game.status === "seeding" &&
|
||||
game.downloader === Downloader.Torrent ? (
|
||||
return download.status === "seeding" &&
|
||||
download.downloader === Downloader.Torrent ? (
|
||||
<>
|
||||
<p>{t("seeding")}</p>
|
||||
{uploadSpeed && <p>{uploadSpeed}/s</p>}
|
||||
@ -141,41 +146,42 @@ export function DownloadGroup({
|
||||
);
|
||||
}
|
||||
|
||||
if (game.status === "paused") {
|
||||
if (download.status === "paused") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
<p>{t(game.downloadQueue && lastPacket ? "queued" : "paused")}</p>
|
||||
<p>{formatDownloadProgress(download.progress)}</p>
|
||||
{/* <p>{t(game.downloadQueue && lastPacket ? "queued" : "paused")}</p> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (game.status === "active") {
|
||||
if (download.status === "active") {
|
||||
return (
|
||||
<>
|
||||
<p>{formatDownloadProgress(game.progress)}</p>
|
||||
<p>{formatDownloadProgress(download.progress)}</p>
|
||||
|
||||
<p>
|
||||
{formatBytes(game.bytesDownloaded)} / {finalDownloadSize}
|
||||
{formatBytes(download.bytesDownloaded)} / {finalDownloadSize}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t(game.status as string)}</p>;
|
||||
return <p>{t(download.status as string)}</p>;
|
||||
};
|
||||
|
||||
const getGameActions = (game: Game): DropdownMenuItem[] => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const getGameActions = (game: LibraryGame): DropdownMenuItem[] => {
|
||||
const download = lastPacket?.download;
|
||||
const isGameDownloading = lastPacket?.gameId === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
|
||||
if (game.progress === 1) {
|
||||
if (download?.progress === 1) {
|
||||
return [
|
||||
{
|
||||
label: t("install"),
|
||||
disabled: deleting,
|
||||
onClick: () => openGameInstaller(game.id),
|
||||
onClick: () => openGameInstaller(game.shop, game.objectId),
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
@ -183,36 +189,38 @@ export function DownloadGroup({
|
||||
disabled: deleting,
|
||||
icon: <UnlinkIcon />,
|
||||
show:
|
||||
game.status === "seeding" && game.downloader === Downloader.Torrent,
|
||||
onClick: () => pauseSeeding(game.id),
|
||||
download.status === "seeding" &&
|
||||
download.downloader === Downloader.Torrent,
|
||||
onClick: () => pauseSeeding(game.shop, game.objectId),
|
||||
},
|
||||
{
|
||||
label: t("resume_seeding"),
|
||||
disabled: deleting,
|
||||
icon: <LinkIcon />,
|
||||
show:
|
||||
game.status !== "seeding" && game.downloader === Downloader.Torrent,
|
||||
onClick: () => resumeSeeding(game.id),
|
||||
download.status !== "seeding" &&
|
||||
download.downloader === Downloader.Torrent,
|
||||
onClick: () => resumeSeeding(game.shop, game.objectId),
|
||||
},
|
||||
{
|
||||
label: t("delete"),
|
||||
disabled: deleting,
|
||||
icon: <TrashIcon />,
|
||||
onClick: () => openDeleteGameModal(game.id),
|
||||
onClick: () => openDeleteGameModal(game.shop, game.objectId),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (isGameDownloading || game.status === "active") {
|
||||
if (isGameDownloading || download?.status === "active") {
|
||||
return [
|
||||
{
|
||||
label: t("pause"),
|
||||
onClick: () => pauseDownload(game.id),
|
||||
onClick: () => pauseDownload(game.shop, game.objectId),
|
||||
icon: <ColumnsIcon />,
|
||||
},
|
||||
{
|
||||
label: t("cancel"),
|
||||
onClick: () => cancelDownload(game.id),
|
||||
onClick: () => cancelDownload(game.shop, game.objectId),
|
||||
icon: <XCircleIcon />,
|
||||
},
|
||||
];
|
||||
@ -222,14 +230,14 @@ export function DownloadGroup({
|
||||
{
|
||||
label: t("resume"),
|
||||
disabled:
|
||||
game.downloader === Downloader.RealDebrid &&
|
||||
download?.downloader === Downloader.RealDebrid &&
|
||||
!userPreferences?.realDebridApiToken,
|
||||
onClick: () => resumeDownload(game.id),
|
||||
onClick: () => resumeDownload(game.shop, game.objectId),
|
||||
icon: <PlayIcon />,
|
||||
},
|
||||
{
|
||||
label: t("cancel"),
|
||||
onClick: () => cancelDownload(game.id),
|
||||
onClick: () => cancelDownload(game.shop, game.objectId),
|
||||
icon: <XCircleIcon />,
|
||||
},
|
||||
];
|
||||
@ -270,13 +278,19 @@ export function DownloadGroup({
|
||||
<div className={styles.downloadCover}>
|
||||
<div className={styles.downloadCoverBackdrop}>
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectID)}
|
||||
src={steamUrlBuilder.library(game.objectId)}
|
||||
className={styles.downloadCoverImage}
|
||||
alt={game.title}
|
||||
/>
|
||||
|
||||
<div className={styles.downloadCoverContent}>
|
||||
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
||||
<Badge>
|
||||
{
|
||||
DOWNLOADER_NAME[
|
||||
game?.download?.downloader as Downloader
|
||||
]
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -290,7 +304,7 @@ export function DownloadGroup({
|
||||
navigate(
|
||||
buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectID,
|
||||
objectId: game.objectId,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { Game, SeedingStatus } from "@types";
|
||||
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
@ -16,7 +16,7 @@ export default function Downloads() {
|
||||
|
||||
const { t } = useTranslation("downloads");
|
||||
|
||||
const gameToBeDeleted = useRef<number | null>(null);
|
||||
const gameToBeDeleted = useRef<[GameShop, string] | null>(null);
|
||||
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
@ -25,8 +25,10 @@ export default function Downloads() {
|
||||
|
||||
const handleDeleteGame = async () => {
|
||||
if (gameToBeDeleted.current) {
|
||||
await pauseSeeding(gameToBeDeleted.current);
|
||||
await removeGameInstaller(gameToBeDeleted.current);
|
||||
const [shop, objectId] = gameToBeDeleted.current;
|
||||
|
||||
await pauseSeeding(shop, objectId);
|
||||
await removeGameInstaller(shop, objectId);
|
||||
}
|
||||
};
|
||||
|
||||
@ -38,19 +40,19 @@ export default function Downloads() {
|
||||
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
|
||||
}, []);
|
||||
|
||||
const handleOpenGameInstaller = (gameId: number) =>
|
||||
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
||||
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
|
||||
window.electron.openGameInstaller(shop, objectId).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||
updateLibrary();
|
||||
});
|
||||
|
||||
const handleOpenDeleteGameModal = (gameId: number) => {
|
||||
gameToBeDeleted.current = gameId;
|
||||
const handleOpenDeleteGameModal = (shop: GameShop, objectId: string) => {
|
||||
gameToBeDeleted.current = [shop, objectId];
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const libraryGroup: Record<string, Game[]> = useMemo(() => {
|
||||
const initialValue: Record<string, Game[]> = {
|
||||
const libraryGroup: Record<string, LibraryGame[]> = useMemo(() => {
|
||||
const initialValue: Record<string, LibraryGame[]> = {
|
||||
downloading: [],
|
||||
queued: [],
|
||||
complete: [],
|
||||
@ -58,27 +60,26 @@ export default function Downloads() {
|
||||
|
||||
const result = library.reduce((prev, next) => {
|
||||
/* Game has been manually added to the library or has been canceled */
|
||||
if (!next.status || next.status === "removed") return prev;
|
||||
if (!next.download?.status || next.download?.status === "removed")
|
||||
return prev;
|
||||
|
||||
/* Is downloading */
|
||||
if (lastPacket?.game.id === next.id)
|
||||
if (lastPacket?.gameId === next.id)
|
||||
return { ...prev, downloading: [...prev.downloading, next] };
|
||||
|
||||
/* Is either queued or paused */
|
||||
if (next.downloadQueue || next.status === "paused")
|
||||
if (next.download?.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 queued = orderBy(result.queued, (game) => game.download?.timestamp, [
|
||||
"desc",
|
||||
]);
|
||||
|
||||
const complete = orderBy(result.complete, (game) =>
|
||||
game.progress === 1 ? 0 : 1
|
||||
game.download?.progress === 1 ? 0 : 1
|
||||
);
|
||||
|
||||
return {
|
||||
@ -86,7 +87,7 @@ export default function Downloads() {
|
||||
queued,
|
||||
complete,
|
||||
};
|
||||
}, [library, lastPacket?.game.id]);
|
||||
}, [library, lastPacket?.gameId]);
|
||||
|
||||
const downloadGroups = [
|
||||
{
|
||||
|
@ -22,6 +22,7 @@ export function HeroPanelActions() {
|
||||
game,
|
||||
repacks,
|
||||
isGameRunning,
|
||||
shop,
|
||||
objectId,
|
||||
gameTitle,
|
||||
setShowGameOptionsModal,
|
||||
@ -33,7 +34,7 @@ export function HeroPanelActions() {
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||
game?.download?.status === "active" && lastPacket?.gameId === game?.id;
|
||||
|
||||
const { updateLibrary } = useLibrary();
|
||||
|
||||
@ -43,7 +44,7 @@ export function HeroPanelActions() {
|
||||
setToggleLibraryGameDisabled(true);
|
||||
|
||||
try {
|
||||
await window.electron.addGameToLibrary(objectId!, gameTitle, "steam");
|
||||
await window.electron.addGameToLibrary(shop, objectId!, gameTitle);
|
||||
|
||||
updateLibrary();
|
||||
updateGame();
|
||||
@ -56,7 +57,8 @@ export function HeroPanelActions() {
|
||||
if (game) {
|
||||
if (game.executablePath) {
|
||||
window.electron.openGame(
|
||||
game.id,
|
||||
game.shop,
|
||||
game.objectId,
|
||||
game.executablePath,
|
||||
game.launchOptions
|
||||
);
|
||||
@ -66,7 +68,8 @@ export function HeroPanelActions() {
|
||||
const gameExecutablePath = await selectGameExecutable();
|
||||
if (gameExecutablePath)
|
||||
window.electron.openGame(
|
||||
game.id,
|
||||
game.shop,
|
||||
game.objectId,
|
||||
gameExecutablePath,
|
||||
game.launchOptions
|
||||
);
|
||||
@ -74,7 +77,7 @@ export function HeroPanelActions() {
|
||||
};
|
||||
|
||||
const closeGame = () => {
|
||||
if (game) window.electron.closeGame(game.id);
|
||||
if (game) window.electron.closeGame(game.shop, game.objectId);
|
||||
};
|
||||
|
||||
const deleting = game ? isGameDeleting(game?.id) : false;
|
||||
|
@ -49,21 +49,24 @@ export function HeroPanelPlaytime() {
|
||||
if (!game) return null;
|
||||
|
||||
const hasDownload =
|
||||
["active", "paused"].includes(game.status as string) && game.progress !== 1;
|
||||
["active", "paused"].includes(game.download?.status as string) &&
|
||||
game.download?.progress !== 1;
|
||||
|
||||
const isGameDownloading =
|
||||
game.status === "active" && lastPacket?.game.id === game.id;
|
||||
game.download?.status === "active" && lastPacket?.gameId === game.id;
|
||||
|
||||
const downloadInProgressInfo = (
|
||||
<div className={styles.downloadDetailsRow}>
|
||||
<Link to="/downloads" className={styles.downloadsLink}>
|
||||
{game.status === "active"
|
||||
{game.download?.status === "active"
|
||||
? t("download_in_progress")
|
||||
: t("download_paused")}
|
||||
</Link>
|
||||
|
||||
<small>
|
||||
{isGameDownloading ? progress : formatDownloadProgress(game.progress)}
|
||||
{isGameDownloading
|
||||
? progress
|
||||
: formatDownloadProgress(game.download?.progress)}
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game?.status === "active" && lastPacket?.game.id === game?.id;
|
||||
game?.download?.status === "active" && lastPacket?.gameId === game?.id;
|
||||
|
||||
const getInfo = () => {
|
||||
if (!game) {
|
||||
@ -50,8 +50,8 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||
};
|
||||
|
||||
const showProgressBar =
|
||||
(game?.status === "active" && game?.progress < 1) ||
|
||||
game?.status === "paused";
|
||||
(game?.download?.status === "active" && game?.download?.progress < 1) ||
|
||||
game?.download?.status === "paused";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -68,10 +68,12 @@ export function HeroPanel({ isHeaderStuck }: HeroPanelProps) {
|
||||
<progress
|
||||
max={1}
|
||||
value={
|
||||
isGameDownloading ? lastPacket?.game.progress : game?.progress
|
||||
isGameDownloading
|
||||
? lastPacket?.progress
|
||||
: game?.download?.progress
|
||||
}
|
||||
className={styles.progressBar({
|
||||
disabled: game?.status === "paused",
|
||||
disabled: game?.download?.status === "paused",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, TextField } from "@renderer/components";
|
||||
import type { Game } from "@types";
|
||||
import type { LibraryGame } from "@types";
|
||||
import * as styles from "./game-options-modal.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||
@ -13,7 +13,7 @@ import { debounce } from "lodash-es";
|
||||
|
||||
export interface GameOptionsModalProps {
|
||||
visible: boolean;
|
||||
game: Game;
|
||||
game: LibraryGame;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@ -59,21 +59,25 @@ export function GameOptionsModal({
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const isGameDownloading =
|
||||
game.status === "active" && lastPacket?.game.id === game.id;
|
||||
game.download?.status === "active" && lastPacket?.gameId === game.id;
|
||||
|
||||
const debounceUpdateLaunchOptions = useRef(
|
||||
debounce(async (value: string) => {
|
||||
await window.electron.updateLaunchOptions(game.id, value);
|
||||
await window.electron.updateLaunchOptions(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
value
|
||||
);
|
||||
updateGame();
|
||||
}, 1000)
|
||||
).current;
|
||||
|
||||
const handleRemoveGameFromLibrary = async () => {
|
||||
if (isGameDownloading) {
|
||||
await cancelDownload(game.id);
|
||||
await cancelDownload(game.shop, game.objectId);
|
||||
}
|
||||
|
||||
await removeGameFromLibrary(game.id);
|
||||
await removeGameFromLibrary(game.shop, game.objectId);
|
||||
updateGame();
|
||||
onClose();
|
||||
};
|
||||
@ -92,12 +96,16 @@ export function GameOptionsModal({
|
||||
return;
|
||||
}
|
||||
|
||||
window.electron.updateExecutablePath(game.id, path).then(updateGame);
|
||||
window.electron
|
||||
.updateExecutablePath(game.shop, game.objectId, path)
|
||||
.then(updateGame);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateShortcut = async () => {
|
||||
window.electron.createGameShortcut(game.id).then((success) => {
|
||||
window.electron
|
||||
.createGameShortcut(game.shop, game.objectId)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
showSuccessToast(t("create_shortcut_success"));
|
||||
} else {
|
||||
@ -107,20 +115,20 @@ export function GameOptionsModal({
|
||||
};
|
||||
|
||||
const handleOpenDownloadFolder = async () => {
|
||||
await window.electron.openGameInstallerPath(game.id);
|
||||
await window.electron.openGameInstallerPath(game.shop, game.objectId);
|
||||
};
|
||||
|
||||
const handleDeleteGame = async () => {
|
||||
await removeGameInstaller(game.id);
|
||||
await removeGameInstaller(game.shop, game.objectId);
|
||||
updateGame();
|
||||
};
|
||||
|
||||
const handleOpenGameExecutablePath = async () => {
|
||||
await window.electron.openGameExecutablePath(game.id);
|
||||
await window.electron.openGameExecutablePath(game.shop, game.objectId);
|
||||
};
|
||||
|
||||
const handleClearExecutablePath = async () => {
|
||||
await window.electron.updateExecutablePath(game.id, null);
|
||||
await window.electron.updateExecutablePath(game.shop, game.objectId, null);
|
||||
updateGame();
|
||||
};
|
||||
|
||||
@ -130,13 +138,17 @@ export function GameOptionsModal({
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
await window.electron.selectGameWinePrefix(game.id, filePaths[0]);
|
||||
await window.electron.selectGameWinePrefix(
|
||||
game.shop,
|
||||
game.objectId,
|
||||
filePaths[0]
|
||||
);
|
||||
await updateGame();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearWinePrefixPath = async () => {
|
||||
await window.electron.selectGameWinePrefix(game.id, null);
|
||||
await window.electron.selectGameWinePrefix(game.shop, game.objectId, null);
|
||||
updateGame();
|
||||
};
|
||||
|
||||
@ -150,7 +162,9 @@ export function GameOptionsModal({
|
||||
const handleClearLaunchOptions = async () => {
|
||||
setLaunchOptions("");
|
||||
|
||||
window.electron.updateLaunchOptions(game.id, null).then(updateGame);
|
||||
window.electron
|
||||
.updateLaunchOptions(game.shop, game.objectId, null)
|
||||
.then(updateGame);
|
||||
};
|
||||
|
||||
const shouldShowWinePrefixConfiguration =
|
||||
@ -159,7 +173,7 @@ export function GameOptionsModal({
|
||||
const handleResetAchievements = async () => {
|
||||
setIsDeletingAchievements(true);
|
||||
try {
|
||||
await window.electron.resetGameAchievements(game.id);
|
||||
await window.electron.resetGameAchievements(game.shop, game.objectId);
|
||||
await updateGame();
|
||||
showSuccessToast(t("reset_achievements_success"));
|
||||
} catch (error) {
|
||||
@ -322,7 +336,7 @@ export function GameOptionsModal({
|
||||
>
|
||||
{t("open_download_options")}
|
||||
</Button>
|
||||
{game.downloadPath && (
|
||||
{game.download?.downloadPath && (
|
||||
<Button
|
||||
onClick={handleOpenDownloadFolder}
|
||||
theme="outline"
|
||||
@ -367,7 +381,9 @@ export function GameOptionsModal({
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
theme="danger"
|
||||
disabled={isGameDownloading || deleting || !game.downloadPath}
|
||||
disabled={
|
||||
isGameDownloading || deleting || !game.download?.downloadPath
|
||||
}
|
||||
>
|
||||
{t("remove_files")}
|
||||
</Button>
|
||||
|
@ -67,8 +67,8 @@ export function RepacksModal({
|
||||
};
|
||||
|
||||
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||
if (!game) return false;
|
||||
return repack.uris.some((uri) => uri.includes(game.uri!));
|
||||
if (!game || !game.download) return false;
|
||||
return repack.uris.some((uri) => uri.includes(game.download!.uri));
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -254,7 +254,7 @@ export function ProfileHero() {
|
||||
if (gameRunning)
|
||||
return {
|
||||
...gameRunning,
|
||||
objectId: gameRunning.objectID,
|
||||
objectId: gameRunning.objectId,
|
||||
sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
|
||||
};
|
||||
|
||||
|
@ -57,10 +57,30 @@ export function SettingsGeneral() {
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(updateFormWithUserPreferences, [
|
||||
userPreferences,
|
||||
defaultDownloadsPath,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (userPreferences) {
|
||||
const languageKeys = Object.keys(languageResources);
|
||||
const language =
|
||||
languageKeys.find(
|
||||
(language) => language === userPreferences.language
|
||||
) ??
|
||||
languageKeys.find((language) => {
|
||||
return language.startsWith(userPreferences.language.split("-")[0]);
|
||||
});
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
|
||||
downloadNotificationsEnabled:
|
||||
userPreferences.downloadNotificationsEnabled,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
userPreferences.repackUpdatesNotificationsEnabled,
|
||||
achievementNotificationsEnabled:
|
||||
userPreferences.achievementNotificationsEnabled,
|
||||
language: language ?? "en",
|
||||
}));
|
||||
}
|
||||
}, [userPreferences, defaultDownloadsPath]);
|
||||
|
||||
const handleLanguageChange = (event) => {
|
||||
const value = event.target.value;
|
||||
@ -86,31 +106,6 @@ export function SettingsGeneral() {
|
||||
}
|
||||
};
|
||||
|
||||
function updateFormWithUserPreferences() {
|
||||
if (userPreferences) {
|
||||
const languageKeys = Object.keys(languageResources);
|
||||
const language =
|
||||
languageKeys.find((language) => {
|
||||
return language === userPreferences.language;
|
||||
}) ??
|
||||
languageKeys.find((language) => {
|
||||
return language.startsWith(userPreferences.language.split("-")[0]);
|
||||
});
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
|
||||
downloadNotificationsEnabled:
|
||||
userPreferences.downloadNotificationsEnabled,
|
||||
repackUpdatesNotificationsEnabled:
|
||||
userPreferences.repackUpdatesNotificationsEnabled,
|
||||
achievementNotificationsEnabled:
|
||||
userPreferences.achievementNotificationsEnabled,
|
||||
language: language ?? "en",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
|
@ -1,5 +1,13 @@
|
||||
import type { GameStatus } from "./game.types";
|
||||
import { Game } from "./level.types";
|
||||
import type { Download } from "./level.types";
|
||||
|
||||
export type DownloadStatus =
|
||||
| "active"
|
||||
| "waiting"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "seeding"
|
||||
| "removed";
|
||||
|
||||
export interface DownloadProgress {
|
||||
downloadSpeed: number;
|
||||
@ -9,8 +17,8 @@ export interface DownloadProgress {
|
||||
isDownloadingMetadata: boolean;
|
||||
isCheckingFiles: boolean;
|
||||
progress: number;
|
||||
gameId: number;
|
||||
game: Game;
|
||||
gameId: string;
|
||||
download: Download;
|
||||
}
|
||||
|
||||
/* Torbox */
|
||||
@ -162,7 +170,7 @@ export interface RealDebridUser {
|
||||
|
||||
/* Torrent */
|
||||
export interface SeedingStatus {
|
||||
gameId: number;
|
||||
status: GameStatus;
|
||||
gameId: string;
|
||||
status: DownloadStatus;
|
||||
uploadSpeed: number;
|
||||
}
|
||||
|
@ -1,12 +1,3 @@
|
||||
export type GameStatus =
|
||||
| "active"
|
||||
| "waiting"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "seeding"
|
||||
| "removed";
|
||||
|
||||
export type GameShop = "steam" | "epic";
|
||||
|
||||
export interface UnlockedAchievement {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Cracker, DownloadSourceStatus, Downloader } from "@shared";
|
||||
import type { SteamAppDetails } from "./steam.types";
|
||||
import type { Subscription } from "./level.types";
|
||||
import type { Download, Game, Subscription } from "./level.types";
|
||||
import type { GameShop } from "./game.types";
|
||||
|
||||
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
|
||||
@ -45,29 +45,13 @@ export interface UserGame {
|
||||
}
|
||||
|
||||
export interface GameRunning {
|
||||
id?: number;
|
||||
title: string;
|
||||
iconUrl: string | null;
|
||||
objectID: string;
|
||||
objectId: string;
|
||||
shop: GameShop;
|
||||
sessionDurationInMillis: number;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
downloadsPath: string | null;
|
||||
language: string;
|
||||
downloadNotificationsEnabled: boolean;
|
||||
repackUpdatesNotificationsEnabled: boolean;
|
||||
achievementNotificationsEnabled: boolean;
|
||||
realDebridApiToken: string | null;
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
runAtStartup: boolean;
|
||||
startMinimized: boolean;
|
||||
disableNsfwAlert: boolean;
|
||||
seedAfterDownloadComplete: boolean;
|
||||
showHiddenAchievementsDescription: boolean;
|
||||
}
|
||||
|
||||
export interface Steam250Game {
|
||||
title: string;
|
||||
objectId: string;
|
||||
@ -298,6 +282,11 @@ export interface CatalogueSearchPayload {
|
||||
developers: string[];
|
||||
}
|
||||
|
||||
export interface LibraryGame extends Game {
|
||||
id: string;
|
||||
download: Download | null;
|
||||
}
|
||||
|
||||
export * from "./game.types";
|
||||
export * from "./steam.types";
|
||||
export * from "./download.types";
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { Downloader } from "@shared";
|
||||
import type {
|
||||
GameShop,
|
||||
GameStatus,
|
||||
SteamAchievement,
|
||||
UnlockedAchievement,
|
||||
} from "./game.types";
|
||||
import type { DownloadStatus } from "./download.types";
|
||||
|
||||
export type SubscriptionStatus = "active" | "pending" | "cancelled";
|
||||
|
||||
@ -48,17 +48,14 @@ export interface Download {
|
||||
shop: GameShop;
|
||||
objectId: string;
|
||||
uri: string;
|
||||
folderName: string;
|
||||
folderName: string | null;
|
||||
downloadPath: string;
|
||||
progress: number;
|
||||
downloader: Downloader;
|
||||
bytesDownloaded: number;
|
||||
playTimeInMilliseconds: number;
|
||||
lastTimePlayed: Date | null;
|
||||
fileSize: number;
|
||||
fileSize: number | null;
|
||||
shouldSeed: boolean;
|
||||
// TODO: Rename to DownloadStatus
|
||||
status: GameStatus | null;
|
||||
status: DownloadStatus | null;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@ -66,3 +63,18 @@ export interface GameAchievement {
|
||||
achievements: SteamAchievement[];
|
||||
unlockedAchievements: UnlockedAchievement[];
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
downloadsPath: string | null;
|
||||
language: string;
|
||||
downloadNotificationsEnabled: boolean;
|
||||
repackUpdatesNotificationsEnabled: boolean;
|
||||
achievementNotificationsEnabled: boolean;
|
||||
realDebridApiToken: string | null;
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
runAtStartup: boolean;
|
||||
startMinimized: boolean;
|
||||
disableNsfwAlert: boolean;
|
||||
seedAfterDownloadComplete: boolean;
|
||||
showHiddenAchievementsDescription: boolean;
|
||||
}
|
||||
|
185
yarn.lock
185
yarn.lock
@ -3026,11 +3026,6 @@
|
||||
"@smithy/types" "^3.7.2"
|
||||
tslib "^2.6.2"
|
||||
|
||||
"@sqltools/formatter@^1.2.5":
|
||||
version "1.2.5"
|
||||
resolved "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz"
|
||||
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz"
|
||||
@ -3821,11 +3816,6 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
any-promise@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
|
||||
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
@ -3877,11 +3867,6 @@ app-builder-lib@25.1.8:
|
||||
tar "^6.1.12"
|
||||
temp-file "^3.4.0"
|
||||
|
||||
app-root-path@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz"
|
||||
integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
|
||||
|
||||
applescript@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz"
|
||||
@ -4467,18 +4452,6 @@ cli-cursor@^3.1.0:
|
||||
dependencies:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-highlight@^2.1.11:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.11.tgz#49736fa452f0aaf4fae580e30acb26828d2dc1bf"
|
||||
integrity sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
highlight.js "^10.7.1"
|
||||
mz "^2.4.0"
|
||||
parse5 "^5.1.1"
|
||||
parse5-htmlparser2-tree-adapter "^6.0.0"
|
||||
yargs "^16.0.0"
|
||||
|
||||
cli-spinners@^2.5.0:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
|
||||
@ -4492,15 +4465,6 @@ cli-truncate@^2.1.0:
|
||||
slice-ansi "^3.0.0"
|
||||
string-width "^4.2.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
@ -4809,11 +4773,6 @@ date-fns@^3.6.0:
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
|
||||
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||
|
||||
dayjs@^1.11.9:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||
@ -5017,16 +4976,16 @@ dotenv-expand@^11.0.6:
|
||||
dependencies:
|
||||
dotenv "^16.4.4"
|
||||
|
||||
dotenv@^16.0.3, dotenv@^16.4.4, dotenv@^16.4.5:
|
||||
version "16.4.5"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||
|
||||
dotenv@^16.3.1:
|
||||
version "16.4.7"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
|
||||
integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
|
||||
|
||||
dotenv@^16.4.4, dotenv@^16.4.5:
|
||||
version "16.4.5"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||
|
||||
dunder-proto@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80"
|
||||
@ -6004,17 +5963,6 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^10.3.10:
|
||||
version "10.3.15"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz"
|
||||
integrity sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^2.3.6"
|
||||
minimatch "^9.0.1"
|
||||
minipass "^7.0.4"
|
||||
path-scurry "^1.11.0"
|
||||
|
||||
glob@^10.3.12, glob@^10.3.7:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||
@ -6206,11 +6154,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
highlight.js@^10.7.1:
|
||||
version "10.7.3"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
|
||||
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
|
||||
|
||||
hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
@ -6382,7 +6325,7 @@ inflight@^1.0.4:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4:
|
||||
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@ -6757,15 +6700,6 @@ iterator.prototype@^1.1.3:
|
||||
reflect.getprototypeof "^1.0.8"
|
||||
set-function-name "^2.0.2"
|
||||
|
||||
jackspeak@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
|
||||
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^8.0.2"
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
jackspeak@^3.1.2:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
||||
@ -7344,7 +7278,7 @@ minimatch@^8.0.2:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.1, minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
@ -7450,11 +7384,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mkdirp@^2.1.3:
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
|
||||
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
||||
|
||||
mkdirp@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
|
||||
@ -7490,15 +7419,6 @@ ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
mz@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
|
||||
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@^2.18.0:
|
||||
version "2.22.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3"
|
||||
@ -7641,7 +7561,7 @@ nwsapi@^2.2.12:
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655"
|
||||
integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==
|
||||
|
||||
object-assign@^4.0.1, object-assign@^4.1.1:
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
@ -7814,23 +7734,6 @@ parse-torrent@^11.0.17:
|
||||
queue-microtask "^1.2.3"
|
||||
uint8-util "^2.2.5"
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
|
||||
dependencies:
|
||||
parse5 "^6.0.1"
|
||||
|
||||
parse5@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||
|
||||
parse5@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
parse5@^7.0.0, parse5@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
|
||||
@ -7863,7 +7766,7 @@ path-parse@^1.0.7:
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-scurry@^1.11.0, path-scurry@^1.11.1, path-scurry@^1.6.1:
|
||||
path-scurry@^1.11.1, path-scurry@^1.6.1:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
|
||||
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
|
||||
@ -8232,11 +8135,6 @@ redux@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
|
||||
integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
|
||||
|
||||
reflect-metadata@^0.2.1:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b"
|
||||
integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==
|
||||
|
||||
reflect.getprototypeof@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz#c58afb17a4007b4d1118c07b92c23fca422c5d82"
|
||||
@ -8688,14 +8586,6 @@ set-function-name@^2.0.1, set-function-name@^2.0.2:
|
||||
functions-have-names "^1.2.3"
|
||||
has-property-descriptors "^1.0.2"
|
||||
|
||||
sha.js@^2.4.11:
|
||||
version "2.4.11"
|
||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
||||
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
@ -9110,20 +9000,6 @@ text-table@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
|
||||
thenify-all@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
|
||||
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
|
||||
dependencies:
|
||||
thenify ">= 3.1.0 < 4"
|
||||
|
||||
"thenify@>= 3.1.0 < 4":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
|
||||
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
"through@>=2.2.7 <3":
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
@ -9244,7 +9120,7 @@ tslib@^2.0.0, tslib@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.5.0, tslib@^2.6.2:
|
||||
tslib@^2.0.3, tslib@^2.6.2:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
@ -9322,27 +9198,6 @@ typed-array-length@^1.0.6:
|
||||
is-typed-array "^1.1.13"
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
typeorm@^0.3.20:
|
||||
version "0.3.20"
|
||||
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab"
|
||||
integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==
|
||||
dependencies:
|
||||
"@sqltools/formatter" "^1.2.5"
|
||||
app-root-path "^3.1.0"
|
||||
buffer "^6.0.3"
|
||||
chalk "^4.1.2"
|
||||
cli-highlight "^2.1.11"
|
||||
dayjs "^1.11.9"
|
||||
debug "^4.3.4"
|
||||
dotenv "^16.0.3"
|
||||
glob "^10.3.10"
|
||||
mkdirp "^2.1.3"
|
||||
reflect-metadata "^0.2.1"
|
||||
sha.js "^2.4.11"
|
||||
tslib "^2.5.0"
|
||||
uuid "^9.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
typescript@^5.3.3, typescript@^5.4.3:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
|
||||
@ -9489,7 +9344,7 @@ util-deprecate@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
uuid@^9.0.0, uuid@^9.0.1:
|
||||
uuid@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
@ -9790,29 +9645,11 @@ yaml@^2.6.1:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773"
|
||||
integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==
|
||||
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.9"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^16.0.0:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yargs@^17.0.0, yargs@^17.0.1, yargs@^17.6.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
|
Loading…
Reference in New Issue
Block a user