mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
Merge pull request #1008 from hydralauncher/fix/migrate-repacks-from-sqlite-to-dexie
Fix/migrate repacks from sqlite to dexie
This commit is contained in:
commit
37111c11d8
@ -1,4 +1,4 @@
|
|||||||
appId: site.hydralauncher.hydra
|
appId: gg.hydralauncher.hydra
|
||||||
productName: Hydra
|
productName: Hydra
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
"create-desktop-shortcuts": "^1.11.0",
|
"create-desktop-shortcuts": "^1.11.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"dexie": "^4.0.8",
|
||||||
"electron-log": "^5.1.4",
|
"electron-log": "^5.1.4",
|
||||||
"electron-updater": "^6.1.8",
|
"electron-updater": "^6.1.8",
|
||||||
"fetch-cookie": "^3.0.1",
|
"fetch-cookie": "^3.0.1",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi, RepacksManager } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { CatalogueCategory, formatName, steamUrlBuilder } from "@shared";
|
import { CatalogueCategory, steamUrlBuilder } from "@shared";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
|
||||||
const getCatalogue = async (
|
const getCatalogue = async (
|
||||||
@ -26,14 +26,9 @@ const getCatalogue = async (
|
|||||||
name: "getById",
|
name: "getById",
|
||||||
});
|
});
|
||||||
|
|
||||||
const repacks = RepacksManager.search({
|
|
||||||
query: formatName(steamGame.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: steamGame.name,
|
title: steamGame.name,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
repacks,
|
|
||||||
cover: steamUrlBuilder.library(game.objectId),
|
cover: steamUrlBuilder.library(game.objectId),
|
||||||
objectID: game.objectId,
|
objectID: game.objectId,
|
||||||
};
|
};
|
||||||
|
@ -45,15 +45,17 @@ const getGameShopDetails = async (
|
|||||||
|
|
||||||
const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
|
const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
gameShopCacheRepository.upsert(
|
if (result) {
|
||||||
{
|
gameShopCacheRepository.upsert(
|
||||||
objectID,
|
{
|
||||||
shop: "steam",
|
objectID,
|
||||||
language,
|
shop: "steam",
|
||||||
serializedData: JSON.stringify(result),
|
language,
|
||||||
},
|
serializedData: JSON.stringify(result),
|
||||||
["objectID"]
|
},
|
||||||
);
|
["objectID"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import type { CatalogueEntry } from "@types";
|
|||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
|
|
||||||
const getGames = async (
|
const getGames = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -15,13 +14,14 @@ const getGames = async (
|
|||||||
{ name: "list" }
|
{ name: "list" }
|
||||||
);
|
);
|
||||||
|
|
||||||
const entries = RepacksManager.findRepacksForCatalogueEntries(
|
|
||||||
steamGames.map((game) => convertSteamGameToCatalogueEntry(game))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
results: entries,
|
results: steamGames.map((steamGame) => ({
|
||||||
cursor: cursor + entries.length,
|
title: steamGame.name,
|
||||||
|
shop: "steam",
|
||||||
|
cover: steamUrlBuilder.library(steamGame.id),
|
||||||
|
objectID: steamGame.id,
|
||||||
|
})),
|
||||||
|
cursor: cursor + steamGames.length,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,32 +3,15 @@ import { shuffle } from "lodash-es";
|
|||||||
import { getSteam250List } from "@main/services";
|
import { getSteam250List } from "@main/services";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { getSteamGameById } from "../helpers/search-games";
|
|
||||||
import type { Steam250Game } from "@types";
|
import type { Steam250Game } from "@types";
|
||||||
|
|
||||||
const state = { games: Array<Steam250Game>(), index: 0 };
|
const state = { games: Array<Steam250Game>(), index: 0 };
|
||||||
|
|
||||||
const filterGames = async (games: Steam250Game[]) => {
|
|
||||||
const results: Steam250Game[] = [];
|
|
||||||
|
|
||||||
for (const game of games) {
|
|
||||||
const steamGame = await getSteamGameById(game.objectID);
|
|
||||||
|
|
||||||
if (steamGame?.repacks.length) {
|
|
||||||
results.push(game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
if (state.games.length == 0) {
|
if (state.games.length == 0) {
|
||||||
const steam250List = await getSteam250List();
|
const steam250List = await getSteam250List();
|
||||||
|
|
||||||
const filteredSteam250List = await filterGames(steam250List);
|
state.games = shuffle(steam250List);
|
||||||
|
|
||||||
state.games = shuffle(filteredSteam250List);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.games.length == 0) {
|
if (state.games.length == 0) {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { RepacksManager } from "@main/services";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
|
|
||||||
const searchGameRepacks = (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
query: string
|
|
||||||
) => RepacksManager.search({ query });
|
|
||||||
|
|
||||||
registerEvent("searchGameRepacks", searchGameRepacks);
|
|
@ -1,7 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { CatalogueEntry } from "@types";
|
import { CatalogueEntry } from "@types";
|
||||||
import { HydraApi, RepacksManager } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
const searchGamesEvent = async (
|
const searchGamesEvent = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -11,15 +11,13 @@ const searchGamesEvent = async (
|
|||||||
{ objectId: string; title: string; shop: string }[]
|
{ objectId: string; title: string; shop: string }[]
|
||||||
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
|
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
|
||||||
|
|
||||||
const steamGames = games.map((game) => {
|
return games.map((game) => {
|
||||||
return convertSteamGameToCatalogueEntry({
|
return convertSteamGameToCatalogueEntry({
|
||||||
id: Number(game.objectId),
|
id: Number(game.objectId),
|
||||||
name: game.title,
|
name: game.title,
|
||||||
clientIcon: null,
|
clientIcon: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return RepacksManager.findRepacksForCatalogueEntries(steamGames);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("searchGames", searchGamesEvent);
|
registerEvent("searchGames", searchGamesEvent);
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { dataSource } from "@main/data-source";
|
|
||||||
import { DownloadSource } from "@main/entity";
|
|
||||||
import axios from "axios";
|
|
||||||
import { downloadSourceSchema } from "../helpers/validators";
|
|
||||||
import { insertDownloadsFromSource } from "@main/helpers";
|
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
|
|
||||||
const addDownloadSource = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
url: string
|
|
||||||
) => {
|
|
||||||
const response = await axios.get(url);
|
|
||||||
|
|
||||||
const source = downloadSourceSchema.parse(response.data);
|
|
||||||
|
|
||||||
const downloadSource = await dataSource.transaction(
|
|
||||||
async (transactionalEntityManager) => {
|
|
||||||
const downloadSource = await transactionalEntityManager
|
|
||||||
.getRepository(DownloadSource)
|
|
||||||
.save({
|
|
||||||
url,
|
|
||||||
name: source.name,
|
|
||||||
downloadCount: source.downloads.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
await insertDownloadsFromSource(
|
|
||||||
transactionalEntityManager,
|
|
||||||
downloadSource,
|
|
||||||
source.downloads
|
|
||||||
);
|
|
||||||
|
|
||||||
return downloadSource;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await RepacksManager.updateRepacks();
|
|
||||||
|
|
||||||
return downloadSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("addDownloadSource", addDownloadSource);
|
|
@ -0,0 +1,9 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { knexClient } from "@main/knex-client";
|
||||||
|
|
||||||
|
const deleteDownloadSource = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
id: number
|
||||||
|
) => knexClient("download_source").where({ id }).delete();
|
||||||
|
|
||||||
|
registerEvent("deleteDownloadSource", deleteDownloadSource);
|
@ -1,11 +1,7 @@
|
|||||||
import { downloadSourceRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { knexClient } from "@main/knex-client";
|
||||||
|
|
||||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||||
downloadSourceRepository.find({
|
knexClient.select("*").from("download_source");
|
||||||
order: {
|
|
||||||
createdAt: "DESC",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
registerEvent("getDownloadSources", getDownloadSources);
|
registerEvent("getDownloadSources", getDownloadSources);
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { downloadSourceRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
|
|
||||||
const removeDownloadSource = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
id: number
|
|
||||||
) => {
|
|
||||||
await downloadSourceRepository.delete(id);
|
|
||||||
await RepacksManager.updateRepacks();
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("removeDownloadSource", removeDownloadSource);
|
|
@ -1,7 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { fetchDownloadSourcesAndUpdate } from "@main/helpers";
|
|
||||||
|
|
||||||
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
|
||||||
fetchDownloadSourcesAndUpdate();
|
|
||||||
|
|
||||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
|
@ -1,27 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { downloadSourceRepository } from "@main/repository";
|
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
import { downloadSourceWorker } from "@main/workers";
|
|
||||||
|
|
||||||
const validateDownloadSource = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
url: string
|
|
||||||
) => {
|
|
||||||
const existingSource = await downloadSourceRepository.findOne({
|
|
||||||
where: { url },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingSource)
|
|
||||||
throw new Error("Source with the same url already exists");
|
|
||||||
|
|
||||||
const repacks = RepacksManager.repacks;
|
|
||||||
|
|
||||||
return downloadSourceWorker.run(
|
|
||||||
{ url, repacks },
|
|
||||||
{
|
|
||||||
name: "validateDownloadSource",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
|
@ -1,7 +1,6 @@
|
|||||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
|
||||||
export interface SearchGamesArgs {
|
export interface SearchGamesArgs {
|
||||||
@ -17,7 +16,6 @@ export const convertSteamGameToCatalogueEntry = (
|
|||||||
title: game.name,
|
title: game.name,
|
||||||
shop: "steam" as GameShop,
|
shop: "steam" as GameShop,
|
||||||
cover: steamUrlBuilder.library(String(game.id)),
|
cover: steamUrlBuilder.library(String(game.id)),
|
||||||
repacks: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getSteamGameById = async (
|
export const getSteamGameById = async (
|
||||||
@ -29,9 +27,5 @@ export const getSteamGameById = async (
|
|||||||
|
|
||||||
if (!steamGame) return null;
|
if (!steamGame) return null;
|
||||||
|
|
||||||
const catalogueEntry = convertSteamGameToCatalogueEntry(steamGame);
|
return convertSteamGameToCatalogueEntry(steamGame);
|
||||||
|
|
||||||
const result = RepacksManager.findRepacksForCatalogueEntry(catalogueEntry);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const downloadSourceSchema = z.object({
|
|
||||||
name: z.string().max(255),
|
|
||||||
downloads: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string().max(255),
|
|
||||||
uris: z.array(z.string()),
|
|
||||||
uploadDate: z.string().max(255),
|
|
||||||
fileSize: z.string().max(255),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
@ -7,7 +7,6 @@ import "./catalogue/get-games";
|
|||||||
import "./catalogue/get-how-long-to-beat";
|
import "./catalogue/get-how-long-to-beat";
|
||||||
import "./catalogue/get-random-game";
|
import "./catalogue/get-random-game";
|
||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
import "./catalogue/search-game-repacks";
|
|
||||||
import "./catalogue/get-game-stats";
|
import "./catalogue/get-game-stats";
|
||||||
import "./catalogue/get-trending-games";
|
import "./catalogue/get-trending-games";
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
@ -37,11 +36,8 @@ import "./user-preferences/auto-launch";
|
|||||||
import "./autoupdater/check-for-updates";
|
import "./autoupdater/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
import "./user-preferences/authenticate-real-debrid";
|
||||||
|
import "./download-sources/delete-download-source";
|
||||||
import "./download-sources/get-download-sources";
|
import "./download-sources/get-download-sources";
|
||||||
import "./download-sources/validate-download-source";
|
|
||||||
import "./download-sources/add-download-source";
|
|
||||||
import "./download-sources/remove-download-source";
|
|
||||||
import "./download-sources/sync-download-sources";
|
|
||||||
import "./auth/sign-out";
|
import "./auth/sign-out";
|
||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
@ -60,6 +56,7 @@ import "./profile/update-profile";
|
|||||||
import "./profile/process-profile-image";
|
import "./profile/process-profile-image";
|
||||||
import "./profile/send-friend-request";
|
import "./profile/send-friend-request";
|
||||||
import "./profile/sync-friend-requests";
|
import "./profile/sync-friend-requests";
|
||||||
|
import "./notifications/publish-new-repacks-notification";
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
@ -3,7 +3,6 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
@ -36,20 +35,12 @@ const addGameToLibrary = async (
|
|||||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository.insert({
|
||||||
.insert({
|
title,
|
||||||
title,
|
iconUrl,
|
||||||
iconUrl,
|
objectID,
|
||||||
objectID,
|
shop,
|
||||||
shop,
|
});
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (iconUrl) {
|
|
||||||
getFileBase64(iconUrl).then((base64) =>
|
|
||||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const game = await gameRepository.findOne({ where: { objectID } });
|
const game = await gameRepository.findOne({ where: { objectID } });
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Notification } from "electron";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
const publishNewRepacksNotification = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
newRepacksCount: number
|
||||||
|
) => {
|
||||||
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||||
|
new Notification({
|
||||||
|
title: t("repack_list_updated", {
|
||||||
|
ns: "notifications",
|
||||||
|
}),
|
||||||
|
body: t("repack_count", {
|
||||||
|
ns: "notifications",
|
||||||
|
count: newRepacksCount,
|
||||||
|
}),
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("publishNewRepacksNotification", publishNewRepacksNotification);
|
@ -1,7 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { getFileBase64 } from "@main/helpers";
|
|
||||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
@ -9,36 +8,25 @@ import { steamGamesWorker } from "@main/workers";
|
|||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game, Repack } from "@main/entity";
|
import { DownloadQueue, Game } from "@main/entity";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
payload: StartGameDownloadPayload
|
payload: StartGameDownloadPayload
|
||||||
) => {
|
) => {
|
||||||
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
const { objectID, title, shop, downloadPath, downloader, uri } = payload;
|
||||||
payload;
|
|
||||||
|
|
||||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||||
const repackRepository = transactionalEntityManager.getRepository(Repack);
|
|
||||||
const downloadQueueRepository =
|
const downloadQueueRepository =
|
||||||
transactionalEntityManager.getRepository(DownloadQueue);
|
transactionalEntityManager.getRepository(DownloadQueue);
|
||||||
|
|
||||||
const [game, repack] = await Promise.all([
|
const game = await gameRepository.findOne({
|
||||||
gameRepository.findOne({
|
where: {
|
||||||
where: {
|
objectID,
|
||||||
objectID,
|
shop,
|
||||||
shop,
|
},
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
repackRepository.findOne({
|
|
||||||
where: {
|
|
||||||
id: repackId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!repack) return;
|
|
||||||
|
|
||||||
await DownloadManager.pauseDownload();
|
await DownloadManager.pauseDownload();
|
||||||
|
|
||||||
@ -71,26 +59,16 @@ const startGameDownload = async (
|
|||||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await gameRepository
|
await gameRepository.insert({
|
||||||
.insert({
|
title,
|
||||||
title,
|
iconUrl,
|
||||||
iconUrl,
|
objectID,
|
||||||
objectID,
|
downloader,
|
||||||
downloader,
|
shop,
|
||||||
shop,
|
status: "active",
|
||||||
status: "active",
|
downloadPath,
|
||||||
downloadPath,
|
uri,
|
||||||
uri,
|
});
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
if (iconUrl) {
|
|
||||||
getFileBase64(iconUrl).then((base64) =>
|
|
||||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGame = await gameRepository.findOne({
|
const updatedGame = await gameRepository.findOne({
|
||||||
|
@ -73,7 +73,6 @@ const getUser = async (
|
|||||||
recentGames,
|
recentGames,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
import { dataSource } from "@main/data-source";
|
|
||||||
import { DownloadSource, Repack } from "@main/entity";
|
|
||||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
|
||||||
import { downloadSourceRepository } from "@main/repository";
|
|
||||||
import { RepacksManager } from "@main/services";
|
|
||||||
import { downloadSourceWorker } from "@main/workers";
|
|
||||||
import { chunk } from "lodash-es";
|
|
||||||
import type { EntityManager } from "typeorm";
|
|
||||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const insertDownloadsFromSource = async (
|
|
||||||
trx: EntityManager,
|
|
||||||
downloadSource: DownloadSource,
|
|
||||||
downloads: z.infer<typeof downloadSourceSchema>["downloads"]
|
|
||||||
) => {
|
|
||||||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
|
||||||
(download) => ({
|
|
||||||
title: download.title,
|
|
||||||
uris: JSON.stringify(download.uris),
|
|
||||||
magnet: download.uris[0]!,
|
|
||||||
fileSize: download.fileSize,
|
|
||||||
repacker: downloadSource.name,
|
|
||||||
uploadDate: download.uploadDate,
|
|
||||||
downloadSource: { id: downloadSource.id },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const downloadsChunks = chunk(repacks, 800);
|
|
||||||
|
|
||||||
for (const chunk of downloadsChunks) {
|
|
||||||
await trx
|
|
||||||
.getRepository(Repack)
|
|
||||||
.createQueryBuilder()
|
|
||||||
.insert()
|
|
||||||
.values(chunk)
|
|
||||||
.updateEntity(false)
|
|
||||||
.orIgnore()
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchDownloadSourcesAndUpdate = async () => {
|
|
||||||
const downloadSources = await downloadSourceRepository.find({
|
|
||||||
order: {
|
|
||||||
id: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await downloadSourceWorker.run(downloadSources, {
|
|
||||||
name: "getUpdatedRepacks",
|
|
||||||
});
|
|
||||||
|
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
|
||||||
for (const result of results) {
|
|
||||||
if (result.etag !== null) {
|
|
||||||
await transactionalEntityManager.getRepository(DownloadSource).update(
|
|
||||||
{ id: result.id },
|
|
||||||
{
|
|
||||||
etag: result.etag,
|
|
||||||
status: result.status,
|
|
||||||
downloadCount: result.downloads.length,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await insertDownloadsFromSource(
|
|
||||||
transactionalEntityManager,
|
|
||||||
result,
|
|
||||||
result.downloads
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await RepacksManager.updateRepacks();
|
|
||||||
});
|
|
||||||
};
|
|
@ -7,16 +7,6 @@ export const getFileBuffer = async (url: string) =>
|
|||||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFileBase64 = async (url: string) =>
|
|
||||||
fetch(url, { method: "GET" }).then((response) =>
|
|
||||||
response.arrayBuffer().then((buffer) => {
|
|
||||||
const base64 = Buffer.from(buffer).toString("base64");
|
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
|
|
||||||
return `data:${contentType};base64,${base64}`;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sleep = (ms: number) =>
|
export const sleep = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
@ -36,6 +26,4 @@ export const requestWebPage = async (url: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isPortableVersion = () =>
|
export const isPortableVersion = () =>
|
||||||
process.env.PORTABLE_EXECUTABLE_FILE != null;
|
process.env.PORTABLE_EXECUTABLE_FILE !== null;
|
||||||
|
|
||||||
export * from "./download-source";
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, net, protocol } from "electron";
|
import { app, BrowserWindow, net, protocol, session } from "electron";
|
||||||
import { init } from "@sentry/electron/main";
|
import { init } from "@sentry/electron/main";
|
||||||
import updater from "electron-updater";
|
import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
@ -68,14 +68,13 @@ const runMigrations = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await knexClient.migrate.latest(migrationConfig);
|
await knexClient.migrate.latest(migrationConfig);
|
||||||
await knexClient.destroy();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
electronApp.setAppUserModelId("gg.hydralauncher.hydra");
|
||||||
|
|
||||||
protocol.handle("local", (request) => {
|
protocol.handle("local", (request) => {
|
||||||
const filePath = request.url.slice("local:".length);
|
const filePath = request.url.slice("local:".length);
|
||||||
@ -104,6 +103,46 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||||
|
callback({
|
||||||
|
requestHeaders: {
|
||||||
|
...details.requestHeaders,
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
const headers = {
|
||||||
|
"access-control-allow-origin": ["*"],
|
||||||
|
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||||
|
"access-control-expose-headers": ["ETag"],
|
||||||
|
"access-control-allow-headers": [
|
||||||
|
"Content-Type, Authorization, X-Requested-With, If-None-Match",
|
||||||
|
],
|
||||||
|
"access-control-allow-credentials": ["true"],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.method === "OPTIONS") {
|
||||||
|
callback({
|
||||||
|
cancel: false,
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
statusLine: "HTTP/1.1 200 OK",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
@ -1,25 +1,14 @@
|
|||||||
import {
|
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
||||||
DownloadManager,
|
|
||||||
RepacksManager,
|
|
||||||
PythonInstance,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
repackRepository,
|
|
||||||
userPreferencesRepository,
|
userPreferencesRepository,
|
||||||
} from "./repository";
|
} from "./repository";
|
||||||
import { UserPreferences } from "./entity";
|
import { UserPreferences } from "./entity";
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
|
||||||
import { publishNewRepacksNotifications } from "./services/notifications";
|
|
||||||
import { MoreThan } from "typeorm";
|
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
RepacksManager.updateRepacks();
|
|
||||||
|
|
||||||
import("./events");
|
import("./events");
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
@ -46,18 +35,6 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
fetchDownloadSourcesAndUpdate().then(async () => {
|
|
||||||
const newRepacksCount = await repackRepository.count({
|
|
||||||
where: {
|
|
||||||
createdAt: MoreThan(now),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
userPreferencesRepository
|
||||||
|
@ -7,5 +7,4 @@ export * from "./download";
|
|||||||
export * from "./how-long-to-beat";
|
export * from "./how-long-to-beat";
|
||||||
export * from "./process-watcher";
|
export * from "./process-watcher";
|
||||||
export * from "./main-loop";
|
export * from "./main-loop";
|
||||||
export * from "./repacks-manager";
|
|
||||||
export * from "./hydra-api";
|
export * from "./hydra-api";
|
||||||
|
@ -49,24 +49,6 @@ export const publishDownloadCompleteNotification = async (game: Game) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const publishNewRepacksNotifications = async (count: number) => {
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
|
||||||
new Notification({
|
|
||||||
title: t("repack_list_updated", {
|
|
||||||
ns: "notifications",
|
|
||||||
}),
|
|
||||||
body: t("repack_count", {
|
|
||||||
ns: "notifications",
|
|
||||||
count: count,
|
|
||||||
}),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishNotificationUpdateReadyToInstall = async (
|
export const publishNotificationUpdateReadyToInstall = async (
|
||||||
version: string
|
version: string
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { repackRepository } from "@main/repository";
|
|
||||||
import { formatName } from "@shared";
|
|
||||||
import { CatalogueEntry, GameRepack } from "@types";
|
|
||||||
import flexSearch from "flexsearch";
|
|
||||||
|
|
||||||
export class RepacksManager {
|
|
||||||
public static repacks: GameRepack[] = [];
|
|
||||||
private static repacksIndex = new flexSearch.Index();
|
|
||||||
|
|
||||||
public static async updateRepacks() {
|
|
||||||
this.repacks = await repackRepository
|
|
||||||
.find({
|
|
||||||
order: {
|
|
||||||
createdAt: "DESC",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((repacks) =>
|
|
||||||
repacks.map((repack) => {
|
|
||||||
const uris: string[] = [];
|
|
||||||
const magnet = repack?.magnet;
|
|
||||||
|
|
||||||
if (magnet) uris.push(magnet);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...repack,
|
|
||||||
uris: [...uris, ...JSON.parse(repack.uris)],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < this.repacks.length; i++) {
|
|
||||||
this.repacksIndex.remove(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.repacksIndex = new flexSearch.Index();
|
|
||||||
|
|
||||||
for (let i = 0; i < this.repacks.length; i++) {
|
|
||||||
const repack = this.repacks[i];
|
|
||||||
|
|
||||||
const formattedTitle = formatName(repack.title);
|
|
||||||
|
|
||||||
this.repacksIndex.add(i, formattedTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static search(options: flexSearch.SearchOptions) {
|
|
||||||
return this.repacksIndex
|
|
||||||
.search({ ...options, query: formatName(options.query ?? "") })
|
|
||||||
.map((index) => this.repacks[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static findRepacksForCatalogueEntry(entry: CatalogueEntry) {
|
|
||||||
const repacks = this.search({ query: formatName(entry.title) });
|
|
||||||
return { ...entry, repacks };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static findRepacksForCatalogueEntries(entries: CatalogueEntry[]) {
|
|
||||||
return entries.map((entry) => {
|
|
||||||
const repacks = this.search({ query: formatName(entry.title) });
|
|
||||||
return { ...entry, repacks };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
|
||||||
import { DownloadSourceStatus } from "@shared";
|
|
||||||
import type { DownloadSource, GameRepack } from "@types";
|
|
||||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export type DownloadSourceResponse = z.infer<typeof downloadSourceSchema> & {
|
|
||||||
etag: string | null;
|
|
||||||
status: DownloadSourceStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => {
|
|
||||||
const results: DownloadSourceResponse[] = [];
|
|
||||||
|
|
||||||
for (const downloadSource of downloadSources) {
|
|
||||||
const headers = new AxiosHeaders();
|
|
||||||
|
|
||||||
if (downloadSource.etag) {
|
|
||||||
headers.set("If-None-Match", downloadSource.etag);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(downloadSource.url, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const source = downloadSourceSchema.parse(response.data);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
...downloadSource,
|
|
||||||
downloads: source.downloads,
|
|
||||||
etag: response.headers["etag"],
|
|
||||||
status: DownloadSourceStatus.UpToDate,
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const isNotModified = (err as AxiosError).response?.status === 304;
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
...downloadSource,
|
|
||||||
downloads: [],
|
|
||||||
etag: null,
|
|
||||||
status: isNotModified
|
|
||||||
? DownloadSourceStatus.UpToDate
|
|
||||||
: DownloadSourceStatus.Errored,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateDownloadSource = async ({
|
|
||||||
url,
|
|
||||||
repacks,
|
|
||||||
}: {
|
|
||||||
url: string;
|
|
||||||
repacks: GameRepack[];
|
|
||||||
}) => {
|
|
||||||
const response = await axios.get(url);
|
|
||||||
|
|
||||||
const source = downloadSourceSchema.parse(response.data);
|
|
||||||
|
|
||||||
const existingUris = source.downloads
|
|
||||||
.flatMap((download) => download.uris)
|
|
||||||
.filter((uri) => repacks.some((repack) => repack.magnet === uri));
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: source.name,
|
|
||||||
downloadCount: source.downloads.length - existingUris.length,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,6 +1,5 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
||||||
import downloadSourceWorkerPath from "./download-source.worker?modulePath";
|
|
||||||
|
|
||||||
import Piscina from "piscina";
|
import Piscina from "piscina";
|
||||||
|
|
||||||
@ -13,7 +12,3 @@ export const steamGamesWorker = new Piscina({
|
|||||||
},
|
},
|
||||||
maxThreads: 1,
|
maxThreads: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadSourceWorker = new Piscina({
|
|
||||||
filename: downloadSourceWorkerPath,
|
|
||||||
});
|
|
||||||
|
@ -60,13 +60,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||||
validateDownloadSource: (url: string) =>
|
deleteDownloadSource: (id: number) =>
|
||||||
ipcRenderer.invoke("validateDownloadSource", url),
|
ipcRenderer.invoke("deleteDownloadSource", id),
|
||||||
addDownloadSource: (url: string) =>
|
|
||||||
ipcRenderer.invoke("addDownloadSource", url),
|
|
||||||
removeDownloadSource: (id: number) =>
|
|
||||||
ipcRenderer.invoke("removeDownloadSource", id),
|
|
||||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||||
@ -182,4 +177,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.on("on-signout", listener);
|
ipcRenderer.on("on-signout", listener);
|
||||||
return () => ipcRenderer.removeListener("on-signout", listener);
|
return () => ipcRenderer.removeListener("on-signout", listener);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||||
|
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useContext, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
@ -26,6 +26,8 @@ import {
|
|||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||||
|
import { downloadSourcesWorker } from "./workers";
|
||||||
|
import { repacksContext } from "./context";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -37,8 +39,12 @@ export function App() {
|
|||||||
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
|
const downloadSourceMigrationLock = useRef(false);
|
||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
|
const { indexRepacks } = useContext(repacksContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFriendsModalVisible,
|
isFriendsModalVisible,
|
||||||
friendRequetsModalTab,
|
friendRequetsModalTab,
|
||||||
@ -197,7 +203,7 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new MutationObserver(() => {
|
new MutationObserver(() => {
|
||||||
const modal = document.body.querySelector("[role=modal]");
|
const modal = document.body.querySelector("[role=dialog]");
|
||||||
|
|
||||||
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
||||||
}).observe(document.body, {
|
}).observe(document.body, {
|
||||||
@ -206,6 +212,49 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}, [dispatch, draggingDisabled]);
|
}, [dispatch, draggingDisabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (downloadSourceMigrationLock.current) return;
|
||||||
|
|
||||||
|
downloadSourceMigrationLock.current = true;
|
||||||
|
|
||||||
|
window.electron.getDownloadSources().then(async (downloadSources) => {
|
||||||
|
if (!downloadSources.length) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
|
|
||||||
|
channel.onmessage = (event: MessageEvent<number>) => {
|
||||||
|
const newRepacksCount = event.data;
|
||||||
|
window.electron.publishNewRepacksNotification(newRepacksCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const downloadSource of downloadSources) {
|
||||||
|
const channel = new BroadcastChannel(
|
||||||
|
`download_sources:import:${downloadSource.url}`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
downloadSourcesWorker.postMessage([
|
||||||
|
"IMPORT_DOWNLOAD_SOURCE",
|
||||||
|
downloadSource.url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
channel.onmessage = () => {
|
||||||
|
window.electron.deleteDownloadSource(downloadSource.id).then(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
indexRepacks();
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
}).catch(() => channel.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSourceMigrationLock.current = false;
|
||||||
|
});
|
||||||
|
}, [indexRepacks]);
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
import type { CatalogueEntry, GameStats } from "@types";
|
import type { CatalogueEntry, GameRepack, GameStats } from "@types";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
|
||||||
import * as styles from "./game-card.css";
|
import * as styles from "./game-card.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { useFormat } from "@renderer/hooks";
|
import { useFormat } from "@renderer/hooks";
|
||||||
|
import { repacksContext } from "@renderer/context";
|
||||||
|
|
||||||
export interface GameCardProps
|
export interface GameCardProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
@ -25,9 +26,20 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
const { t } = useTranslation("game_card");
|
const { t } = useTranslation("game_card");
|
||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
|
|
||||||
|
const { searchRepacks, isIndexingRepacks } = useContext(repacksContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isIndexingRepacks) {
|
||||||
|
searchRepacks(game.title).then((repacks) => {
|
||||||
|
setRepacks(repacks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [game, isIndexingRepacks, searchRepacks]);
|
||||||
|
|
||||||
const uniqueRepackers = Array.from(
|
const uniqueRepackers = Array.from(
|
||||||
new Set(game.repacks.map(({ repacker }) => repacker))
|
new Set(repacks.map(({ repacker }) => repacker))
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHover = useCallback(() => {
|
const handleHover = useCallback(() => {
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { createContext, useCallback, useEffect, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useParams, useSearchParams } from "react-router-dom";
|
import { useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
@ -16,6 +22,7 @@ import type {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GameDetailsContext } from "./game-details.context.types";
|
import { GameDetailsContext } from "./game-details.context.types";
|
||||||
import { SteamContentDescriptor } from "@shared";
|
import { SteamContentDescriptor } from "@shared";
|
||||||
|
import { repacksContext } from "../repacks/repacks.context";
|
||||||
|
|
||||||
export const gameDetailsContext = createContext<GameDetailsContext>({
|
export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||||
game: null,
|
game: null,
|
||||||
@ -52,7 +59,6 @@ export function GameDetailsContextProvider({
|
|||||||
const { objectID, shop } = useParams();
|
const { objectID, shop } = useParams();
|
||||||
|
|
||||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
||||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
|
||||||
|
|
||||||
@ -64,10 +70,22 @@ export function GameDetailsContextProvider({
|
|||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
|
||||||
|
|
||||||
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const gameTitle = searchParams.get("title")!;
|
const gameTitle = searchParams.get("title")!;
|
||||||
|
|
||||||
|
const { searchRepacks, isIndexingRepacks } = useContext(repacksContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isIndexingRepacks) {
|
||||||
|
searchRepacks(gameTitle).then((repacks) => {
|
||||||
|
setRepacks(repacks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [game, gameTitle, isIndexingRepacks, searchRepacks]);
|
||||||
|
|
||||||
const { i18n } = useTranslation("game_details");
|
const { i18n } = useTranslation("game_details");
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -91,37 +109,31 @@ export function GameDetailsContextProvider({
|
|||||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.allSettled([
|
window.electron
|
||||||
window.electron.getGameShopDetails(
|
.getGameShopDetails(
|
||||||
objectID!,
|
objectID!,
|
||||||
shop as GameShop,
|
shop as GameShop,
|
||||||
getSteamLanguage(i18n.language)
|
getSteamLanguage(i18n.language)
|
||||||
),
|
)
|
||||||
window.electron.searchGameRepacks(gameTitle),
|
.then((result) => {
|
||||||
window.electron.getGameStats(objectID!, shop as GameShop),
|
setShopDetails(result);
|
||||||
])
|
|
||||||
.then(([appDetailsResult, repacksResult, statsResult]) => {
|
|
||||||
if (appDetailsResult.status === "fulfilled") {
|
|
||||||
setShopDetails(appDetailsResult.value);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
appDetailsResult.value?.content_descriptors.ids.includes(
|
result?.content_descriptors.ids.includes(
|
||||||
SteamContentDescriptor.AdultOnlySexualContent
|
SteamContentDescriptor.AdultOnlySexualContent
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setHasNSFWContentBlocked(true);
|
setHasNSFWContentBlocked(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repacksResult.status === "fulfilled")
|
|
||||||
setRepacks(repacksResult.value);
|
|
||||||
|
|
||||||
if (statsResult.status === "fulfilled") setStats(statsResult.value);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.electron.getGameStats(objectID!, shop as GameShop).then((result) => {
|
||||||
|
setStats(result);
|
||||||
|
});
|
||||||
|
|
||||||
updateGame();
|
updateGame();
|
||||||
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
}, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]);
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./game-details/game-details.context";
|
export * from "./game-details/game-details.context";
|
||||||
export * from "./settings/settings.context";
|
export * from "./settings/settings.context";
|
||||||
export * from "./user-profile/user-profile.context";
|
export * from "./user-profile/user-profile.context";
|
||||||
|
export * from "./repacks/repacks.context";
|
||||||
|
67
src/renderer/src/context/repacks/repacks.context.tsx
Normal file
67
src/renderer/src/context/repacks/repacks.context.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { GameRepack } from "@types";
|
||||||
|
import { createContext, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { repacksWorker } from "@renderer/workers";
|
||||||
|
|
||||||
|
export interface RepacksContext {
|
||||||
|
searchRepacks: (query: string) => Promise<GameRepack[]>;
|
||||||
|
indexRepacks: () => void;
|
||||||
|
isIndexingRepacks: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const repacksContext = createContext<RepacksContext>({
|
||||||
|
searchRepacks: async () => [] as GameRepack[],
|
||||||
|
indexRepacks: () => {},
|
||||||
|
isIndexingRepacks: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Provider } = repacksContext;
|
||||||
|
export const { Consumer: RepacksContextConsumer } = repacksContext;
|
||||||
|
|
||||||
|
export interface RepacksContextProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RepacksContextProvider({ children }: RepacksContextProps) {
|
||||||
|
const [isIndexingRepacks, setIsIndexingRepacks] = useState(true);
|
||||||
|
|
||||||
|
const searchRepacks = useCallback(async (query: string) => {
|
||||||
|
return new Promise<GameRepack[]>((resolve) => {
|
||||||
|
const channelId = crypto.randomUUID();
|
||||||
|
repacksWorker.postMessage([channelId, query]);
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`repacks:search:${channelId}`);
|
||||||
|
channel.onmessage = (event: MessageEvent<GameRepack[]>) => {
|
||||||
|
resolve(event.data);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const indexRepacks = useCallback(() => {
|
||||||
|
setIsIndexingRepacks(true);
|
||||||
|
repacksWorker.postMessage("INDEX_REPACKS");
|
||||||
|
|
||||||
|
repacksWorker.onmessage = () => {
|
||||||
|
setIsIndexingRepacks(false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
indexRepacks();
|
||||||
|
}, [indexRepacks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider
|
||||||
|
value={{
|
||||||
|
searchRepacks,
|
||||||
|
indexRepacks,
|
||||||
|
isIndexingRepacks,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
@ -102,12 +102,7 @@ declare global {
|
|||||||
|
|
||||||
/* Download sources */
|
/* Download sources */
|
||||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||||
validateDownloadSource: (
|
deleteDownloadSource: (id: number) => Promise<void>;
|
||||||
url: string
|
|
||||||
) => Promise<{ name: string; downloadCount: number }>;
|
|
||||||
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
|
||||||
removeDownloadSource: (id: number) => Promise<void>;
|
|
||||||
syncDownloadSources: () => Promise<void>;
|
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
@ -171,6 +166,9 @@ declare global {
|
|||||||
action: FriendRequestAction
|
action: FriendRequestAction
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
sendFriendRequest: (userId: string) => Promise<void>;
|
sendFriendRequest: (userId: string) => Promise<void>;
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
13
src/renderer/src/dexie.ts
Normal file
13
src/renderer/src/dexie.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Dexie } from "dexie";
|
||||||
|
|
||||||
|
export const db = new Dexie("Hydra");
|
||||||
|
|
||||||
|
db.version(1).stores({
|
||||||
|
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||||
|
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadSourcesTable = db.table("downloadSources");
|
||||||
|
export const repacksTable = db.table("repacks");
|
||||||
|
|
||||||
|
db.open();
|
@ -90,19 +90,9 @@ export function useUserDetails() {
|
|||||||
username: userDetails?.username || "",
|
username: userDetails?.username || "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updateUserDetails]
|
[updateUserDetails, userDetails?.username]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchFriendRequests = useCallback(async () => {
|
|
||||||
return window.electron
|
|
||||||
.getFriendRequests()
|
|
||||||
.then((friendRequests) => {
|
|
||||||
syncFriendRequests();
|
|
||||||
dispatch(setFriendRequests(friendRequests));
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const syncFriendRequests = useCallback(async () => {
|
const syncFriendRequests = useCallback(async () => {
|
||||||
return window.electron
|
return window.electron
|
||||||
.syncFriendRequests()
|
.syncFriendRequests()
|
||||||
@ -112,6 +102,16 @@ export function useUserDetails() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const fetchFriendRequests = useCallback(async () => {
|
||||||
|
return window.electron
|
||||||
|
.getFriendRequests()
|
||||||
|
.then((friendRequests) => {
|
||||||
|
syncFriendRequests();
|
||||||
|
dispatch(setFriendRequests(friendRequests));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [dispatch, syncFriendRequests]);
|
||||||
|
|
||||||
const showFriendsModal = useCallback(
|
const showFriendsModal = useCallback(
|
||||||
(initialTab: UserFriendModalTab, userId: string) => {
|
(initialTab: UserFriendModalTab, userId: string) => {
|
||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
||||||
|
@ -29,6 +29,9 @@ import { store } from "./store";
|
|||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
|
|
||||||
|
import "./workers";
|
||||||
|
import { RepacksContextProvider } from "./context";
|
||||||
|
|
||||||
Sentry.init({});
|
Sentry.init({});
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
@ -54,19 +57,21 @@ i18n
|
|||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<HashRouter>
|
<RepacksContextProvider>
|
||||||
<Routes>
|
<HashRouter>
|
||||||
<Route element={<App />}>
|
<Routes>
|
||||||
<Route path="/" Component={Home} />
|
<Route element={<App />}>
|
||||||
<Route path="/catalogue" Component={Catalogue} />
|
<Route path="/" Component={Home} />
|
||||||
<Route path="/downloads" Component={Downloads} />
|
<Route path="/catalogue" Component={Catalogue} />
|
||||||
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
<Route path="/downloads" Component={Downloads} />
|
||||||
<Route path="/search" Component={SearchResults} />
|
<Route path="/game/:shop/:objectID" Component={GameDetails} />
|
||||||
<Route path="/settings" Component={Settings} />
|
<Route path="/search" Component={SearchResults} />
|
||||||
<Route path="/profile/:userId" Component={Profile} />
|
<Route path="/settings" Component={Settings} />
|
||||||
</Route>
|
<Route path="/profile/:userId" Component={Profile} />
|
||||||
</Routes>
|
</Route>
|
||||||
</HashRouter>
|
</Routes>
|
||||||
|
</HashRouter>
|
||||||
|
</RepacksContextProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import parseTorrent from "parse-torrent";
|
|
||||||
|
|
||||||
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
import { Badge, Button, Modal, TextField } from "@renderer/components";
|
||||||
import type { GameRepack } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
@ -33,8 +32,6 @@ export function RepacksModal({
|
|||||||
const [repack, setRepack] = useState<GameRepack | null>(null);
|
const [repack, setRepack] = useState<GameRepack | null>(null);
|
||||||
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
|
||||||
|
|
||||||
const [infoHash, setInfoHash] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { repacks, game } = useContext(gameDetailsContext);
|
const { repacks, game } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
@ -43,18 +40,9 @@ export function RepacksModal({
|
|||||||
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
|
||||||
}, [repacks]);
|
}, [repacks]);
|
||||||
|
|
||||||
const getInfoHash = useCallback(async () => {
|
|
||||||
if (game?.uri?.startsWith("magnet:")) {
|
|
||||||
const torrent = await parseTorrent(game?.uri ?? "");
|
|
||||||
if (torrent.infoHash) setInfoHash(torrent.infoHash);
|
|
||||||
}
|
|
||||||
}, [game]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredRepacks(sortedRepacks);
|
setFilteredRepacks(sortedRepacks);
|
||||||
|
}, [sortedRepacks, visible, game]);
|
||||||
if (game?.uri) getInfoHash();
|
|
||||||
}, [sortedRepacks, visible, game, getInfoHash]);
|
|
||||||
|
|
||||||
const handleRepackClick = (repack: GameRepack) => {
|
const handleRepackClick = (repack: GameRepack) => {
|
||||||
setRepack(repack);
|
setRepack(repack);
|
||||||
@ -77,9 +65,6 @@ export function RepacksModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||||
if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
|
|
||||||
if (!game?.uri) return false;
|
|
||||||
|
|
||||||
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,6 +8,9 @@ import { useForm } from "react-hook-form";
|
|||||||
|
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { downloadSourcesTable } from "@renderer/dexie";
|
||||||
|
import { DownloadSourceValidationResult } from "@types";
|
||||||
|
import { downloadSourcesWorker } from "@renderer/workers";
|
||||||
|
|
||||||
interface AddDownloadSourceModalProps {
|
interface AddDownloadSourceModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -39,41 +42,48 @@ export function AddDownloadSourceModal({
|
|||||||
setValue,
|
setValue,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [validationResult, setValidationResult] = useState<{
|
const [validationResult, setValidationResult] =
|
||||||
name: string;
|
useState<DownloadSourceValidationResult | null>(null);
|
||||||
downloadCount: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const { sourceUrl } = useContext(settingsContext);
|
const { sourceUrl } = useContext(settingsContext);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (values: FormValues) => {
|
async (values: FormValues) => {
|
||||||
setIsLoading(true);
|
const existingDownloadSource = await downloadSourcesTable
|
||||||
|
.where({ url: values.url })
|
||||||
|
.first();
|
||||||
|
|
||||||
try {
|
if (existingDownloadSource) {
|
||||||
const result = await window.electron.validateDownloadSource(values.url);
|
setError("url", {
|
||||||
setValidationResult(result);
|
type: "server",
|
||||||
|
message: t("source_already_exists"),
|
||||||
|
});
|
||||||
|
|
||||||
setUrl(values.url);
|
return;
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (
|
|
||||||
error.message.endsWith("Source with the same url already exists")
|
|
||||||
) {
|
|
||||||
setError("url", {
|
|
||||||
type: "server",
|
|
||||||
message: t("source_already_exists"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadSourcesWorker.postMessage([
|
||||||
|
"VALIDATE_DOWNLOAD_SOURCE",
|
||||||
|
values.url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(
|
||||||
|
`download_sources:validate:${values.url}`
|
||||||
|
);
|
||||||
|
|
||||||
|
channel.onmessage = (
|
||||||
|
event: MessageEvent<DownloadSourceValidationResult>
|
||||||
|
) => {
|
||||||
|
setValidationResult(event.data);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
setUrl(values.url);
|
||||||
},
|
},
|
||||||
[setError, t]
|
[setError, t]
|
||||||
);
|
);
|
||||||
@ -91,9 +101,21 @@ export function AddDownloadSourceModal({
|
|||||||
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
|
}, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
|
||||||
|
|
||||||
const handleAddDownloadSource = async () => {
|
const handleAddDownloadSource = async () => {
|
||||||
await window.electron.addDownloadSource(url);
|
setIsLoading(true);
|
||||||
onClose();
|
|
||||||
onAddDownloadSource();
|
if (validationResult) {
|
||||||
|
const channel = new BroadcastChannel(`download_sources:import:${url}`);
|
||||||
|
|
||||||
|
downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
|
||||||
|
|
||||||
|
channel.onmessage = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
onAddDownloadSource();
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -122,7 +144,7 @@ export function AddDownloadSourceModal({
|
|||||||
theme="outline"
|
theme="outline"
|
||||||
style={{ alignSelf: "flex-end" }}
|
style={{ alignSelf: "flex-end" }}
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
disabled={isLoading}
|
disabled={isSubmitting || isLoading}
|
||||||
>
|
>
|
||||||
{t("validate_download_source")}
|
{t("validate_download_source")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -10,7 +10,9 @@ import { AddDownloadSourceModal } from "./add-download-source-modal";
|
|||||||
import { useToast } from "@renderer/hooks";
|
import { useToast } from "@renderer/hooks";
|
||||||
import { DownloadSourceStatus } from "@shared";
|
import { DownloadSourceStatus } from "@shared";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { settingsContext } from "@renderer/context";
|
import { repacksContext, settingsContext } from "@renderer/context";
|
||||||
|
import { downloadSourcesTable } from "@renderer/dexie";
|
||||||
|
import { downloadSourcesWorker } from "@renderer/workers";
|
||||||
|
|
||||||
export function SettingsDownloadSources() {
|
export function SettingsDownloadSources() {
|
||||||
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||||
@ -18,16 +20,23 @@ export function SettingsDownloadSources() {
|
|||||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
const [isSyncingDownloadSources, setIsSyncingDownloadSources] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
|
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
|
const { indexRepacks } = useContext(repacksContext);
|
||||||
|
|
||||||
const getDownloadSources = async () => {
|
const getDownloadSources = async () => {
|
||||||
return window.electron.getDownloadSources().then((sources) => {
|
await downloadSourcesTable
|
||||||
setDownloadSources(sources);
|
.toCollection()
|
||||||
});
|
.sortBy("createdAt")
|
||||||
|
.then((sources) => {
|
||||||
|
setDownloadSources(sources.reverse());
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,14 +47,24 @@ export function SettingsDownloadSources() {
|
|||||||
if (sourceUrl) setShowAddDownloadSourceModal(true);
|
if (sourceUrl) setShowAddDownloadSourceModal(true);
|
||||||
}, [sourceUrl]);
|
}, [sourceUrl]);
|
||||||
|
|
||||||
const handleRemoveSource = async (id: number) => {
|
const handleRemoveSource = (id: number) => {
|
||||||
await window.electron.removeDownloadSource(id);
|
setIsRemovingDownloadSource(true);
|
||||||
showSuccessToast(t("removed_download_source"));
|
const channel = new BroadcastChannel(`download_sources:delete:${id}`);
|
||||||
|
|
||||||
getDownloadSources();
|
downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]);
|
||||||
|
|
||||||
|
channel.onmessage = () => {
|
||||||
|
showSuccessToast(t("removed_download_source"));
|
||||||
|
|
||||||
|
getDownloadSources();
|
||||||
|
indexRepacks();
|
||||||
|
setIsRemovingDownloadSource(false);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddDownloadSource = async () => {
|
const handleAddDownloadSource = async () => {
|
||||||
|
indexRepacks();
|
||||||
await getDownloadSources();
|
await getDownloadSources();
|
||||||
showSuccessToast(t("added_download_source"));
|
showSuccessToast(t("added_download_source"));
|
||||||
};
|
};
|
||||||
@ -53,15 +72,17 @@ export function SettingsDownloadSources() {
|
|||||||
const syncDownloadSources = async () => {
|
const syncDownloadSources = async () => {
|
||||||
setIsSyncingDownloadSources(true);
|
setIsSyncingDownloadSources(true);
|
||||||
|
|
||||||
window.electron
|
const id = crypto.randomUUID();
|
||||||
.syncDownloadSources()
|
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||||
.then(() => {
|
|
||||||
showSuccessToast(t("download_sources_synced"));
|
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||||
getDownloadSources();
|
|
||||||
})
|
channel.onmessage = () => {
|
||||||
.finally(() => {
|
showSuccessToast(t("download_sources_synced"));
|
||||||
setIsSyncingDownloadSources(false);
|
getDownloadSources();
|
||||||
});
|
setIsSyncingDownloadSources(false);
|
||||||
|
channel.close();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusTitle = {
|
const statusTitle = {
|
||||||
@ -88,7 +109,11 @@ export function SettingsDownloadSources() {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={!downloadSources.length || isSyncingDownloadSources}
|
disabled={
|
||||||
|
!downloadSources.length ||
|
||||||
|
isSyncingDownloadSources ||
|
||||||
|
isRemovingDownloadSource
|
||||||
|
}
|
||||||
onClick={syncDownloadSources}
|
onClick={syncDownloadSources}
|
||||||
>
|
>
|
||||||
<SyncIcon />
|
<SyncIcon />
|
||||||
@ -99,6 +124,7 @@ export function SettingsDownloadSources() {
|
|||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => setShowAddDownloadSourceModal(true)}
|
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||||
|
disabled={isSyncingDownloadSources}
|
||||||
>
|
>
|
||||||
<PlusCircleIcon />
|
<PlusCircleIcon />
|
||||||
{t("add_download_source")}
|
{t("add_download_source")}
|
||||||
@ -148,6 +174,7 @@ export function SettingsDownloadSources() {
|
|||||||
type="button"
|
type="button"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||||
|
disabled={isRemovingDownloadSource}
|
||||||
>
|
>
|
||||||
<NoEntryIcon />
|
<NoEntryIcon />
|
||||||
{t("remove_download_source")}
|
{t("remove_download_source")}
|
||||||
|
165
src/renderer/src/workers/download-sources.worker.ts
Normal file
165
src/renderer/src/workers/download-sources.worker.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||||
|
import { DownloadSourceStatus } from "@shared";
|
||||||
|
|
||||||
|
export const downloadSourceSchema = z.object({
|
||||||
|
name: z.string().max(255),
|
||||||
|
downloads: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string().max(255),
|
||||||
|
uris: z.array(z.string()),
|
||||||
|
uploadDate: z.string().max(255),
|
||||||
|
fileSize: z.string().max(255),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload =
|
||||||
|
| ["IMPORT_DOWNLOAD_SOURCE", string]
|
||||||
|
| ["DELETE_DOWNLOAD_SOURCE", number]
|
||||||
|
| ["VALIDATE_DOWNLOAD_SOURCE", string]
|
||||||
|
| ["SYNC_DOWNLOAD_SOURCES", string];
|
||||||
|
|
||||||
|
self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||||
|
const [type, data] = event.data;
|
||||||
|
|
||||||
|
if (type === "VALIDATE_DOWNLOAD_SOURCE") {
|
||||||
|
const response =
|
||||||
|
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||||
|
|
||||||
|
const { name } = downloadSourceSchema.parse(response.data);
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`download_sources:validate:${data}`);
|
||||||
|
|
||||||
|
channel.postMessage({
|
||||||
|
name,
|
||||||
|
etag: response.headers["etag"],
|
||||||
|
downloadCount: response.data.downloads.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "DELETE_DOWNLOAD_SOURCE") {
|
||||||
|
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||||
|
await repacksTable.where({ downloadSourceId: data }).delete();
|
||||||
|
await downloadSourcesTable.where({ id: data }).delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`download_sources:delete:${data}`);
|
||||||
|
|
||||||
|
channel.postMessage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
||||||
|
const response =
|
||||||
|
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||||
|
|
||||||
|
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const id = await downloadSourcesTable.add({
|
||||||
|
url: data,
|
||||||
|
name: response.data.name,
|
||||||
|
etag: response.headers["etag"],
|
||||||
|
status: DownloadSourceStatus.UpToDate,
|
||||||
|
downloadCount: response.data.downloads.length,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadSource = await downloadSourcesTable.get(id);
|
||||||
|
|
||||||
|
const repacks = response.data.downloads.map((download) => ({
|
||||||
|
title: download.title,
|
||||||
|
uris: download.uris,
|
||||||
|
fileSize: download.fileSize,
|
||||||
|
repacker: response.data.name,
|
||||||
|
uploadDate: download.uploadDate,
|
||||||
|
downloadSourceId: downloadSource!.id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await repacksTable.bulkAdd(repacks);
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`download_sources:import:${data}`);
|
||||||
|
channel.postMessage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "SYNC_DOWNLOAD_SOURCES") {
|
||||||
|
const channel = new BroadcastChannel(`download_sources:sync:${data}`);
|
||||||
|
let newRepacksCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const downloadSources = await downloadSourcesTable.toArray();
|
||||||
|
const existingRepacks = await repacksTable.toArray();
|
||||||
|
|
||||||
|
for (const downloadSource of downloadSources) {
|
||||||
|
const headers = new AxiosHeaders();
|
||||||
|
|
||||||
|
if (downloadSource.etag) {
|
||||||
|
headers.set("If-None-Match", downloadSource.etag);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(downloadSource.url, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const source = downloadSourceSchema.parse(response.data);
|
||||||
|
|
||||||
|
await db.transaction(
|
||||||
|
"rw",
|
||||||
|
repacksTable,
|
||||||
|
downloadSourcesTable,
|
||||||
|
async () => {
|
||||||
|
await downloadSourcesTable.update(downloadSource.id, {
|
||||||
|
etag: response.headers["etag"],
|
||||||
|
downloadCount: source.downloads.length,
|
||||||
|
status: DownloadSourceStatus.UpToDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const repacks = source.downloads
|
||||||
|
.filter(
|
||||||
|
(download) =>
|
||||||
|
!existingRepacks.some(
|
||||||
|
(repack) => repack.title === download.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((download) => ({
|
||||||
|
title: download.title,
|
||||||
|
uris: download.uris,
|
||||||
|
fileSize: download.fileSize,
|
||||||
|
repacker: source.name,
|
||||||
|
uploadDate: download.uploadDate,
|
||||||
|
downloadSourceId: downloadSource.id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}));
|
||||||
|
|
||||||
|
newRepacksCount += repacks.length;
|
||||||
|
|
||||||
|
await repacksTable.bulkAdd(repacks);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const isNotModified = (err as AxiosError).response?.status === 304;
|
||||||
|
|
||||||
|
await downloadSourcesTable.update(downloadSource.id, {
|
||||||
|
status: isNotModified
|
||||||
|
? DownloadSourceStatus.UpToDate
|
||||||
|
: DownloadSourceStatus.Errored,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.postMessage(newRepacksCount);
|
||||||
|
} catch (err) {
|
||||||
|
channel.postMessage(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
5
src/renderer/src/workers/index.ts
Normal file
5
src/renderer/src/workers/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import RepacksWorker from "./repacks.worker?worker";
|
||||||
|
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
||||||
|
|
||||||
|
export const repacksWorker = new RepacksWorker();
|
||||||
|
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
50
src/renderer/src/workers/repacks.worker.ts
Normal file
50
src/renderer/src/workers/repacks.worker.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { repacksTable } from "@renderer/dexie";
|
||||||
|
import { formatName } from "@shared";
|
||||||
|
import { GameRepack } from "@types";
|
||||||
|
import flexSearch from "flexsearch";
|
||||||
|
|
||||||
|
const index = new flexSearch.Index();
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
repacks: [] as any[],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SerializedGameRepack extends Omit<GameRepack, "uris"> {
|
||||||
|
uris: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async (
|
||||||
|
event: MessageEvent<[string, string] | "INDEX_REPACKS">
|
||||||
|
) => {
|
||||||
|
if (event.data === "INDEX_REPACKS") {
|
||||||
|
repacksTable
|
||||||
|
.toCollection()
|
||||||
|
.sortBy("uploadDate")
|
||||||
|
.then((results) => {
|
||||||
|
state.repacks = results.reverse();
|
||||||
|
|
||||||
|
for (let i = 0; i < state.repacks.length; i++) {
|
||||||
|
const repack = state.repacks[i];
|
||||||
|
const formattedTitle = formatName(repack.title);
|
||||||
|
index.add(i, formattedTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage("INDEXING_COMPLETE");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const [requestId, query] = event.data;
|
||||||
|
|
||||||
|
const results = index.search(formatName(query)).map((index) => {
|
||||||
|
const repack = state.repacks.at(index as number) as SerializedGameRepack;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...repack,
|
||||||
|
uris: [...repack.uris, repack.magnet].filter(Boolean),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = new BroadcastChannel(`repacks:search:${requestId}`);
|
||||||
|
|
||||||
|
channel.postMessage(results);
|
||||||
|
}
|
||||||
|
};
|
@ -44,7 +44,6 @@ export interface CatalogueEntry {
|
|||||||
title: string;
|
title: string;
|
||||||
/* Epic Games covers cannot be guessed with objectID */
|
/* Epic Games covers cannot be guessed with objectID */
|
||||||
cover: string;
|
cover: string;
|
||||||
repacks: GameRepack[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserGame {
|
export interface UserGame {
|
||||||
@ -71,7 +70,6 @@ export interface Game {
|
|||||||
status: GameStatus | null;
|
status: GameStatus | null;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
downloadPath: string | null;
|
downloadPath: string | null;
|
||||||
repacks: GameRepack[];
|
|
||||||
progress: number;
|
progress: number;
|
||||||
bytesDownloaded: number;
|
bytesDownloaded: number;
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
@ -226,6 +224,19 @@ export interface UpdateProfileRequest {
|
|||||||
bio?: string;
|
bio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DownloadSourceDownload {
|
||||||
|
title: string;
|
||||||
|
uris: string[];
|
||||||
|
uploadDate: string;
|
||||||
|
fileSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadSourceValidationResult {
|
||||||
|
name: string;
|
||||||
|
etag: string;
|
||||||
|
downloadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -3638,6 +3638,11 @@ detect-node@^2.0.4:
|
|||||||
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
|
||||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||||
|
|
||||||
|
dexie@^4.0.8:
|
||||||
|
version "4.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.0.8.tgz#21fca70686bdaa1d86fad45b6b19316f6a084a1d"
|
||||||
|
integrity sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==
|
||||||
|
|
||||||
diff@^4.0.1:
|
diff@^4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||||
|
Loading…
Reference in New Issue
Block a user