mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 05:24:55 +03:00
feat: adding notification when all repacks are migrated
This commit is contained in:
parent
d88e06e289
commit
17febcd88a
@ -1,4 +1,4 @@
|
||||
appId: site.hydralauncher.hydra
|
||||
appId: gg.hydralauncher.hydra
|
||||
productName: Hydra
|
||||
directories:
|
||||
buildResources: build
|
||||
|
@ -3,32 +3,15 @@ import { shuffle } from "lodash-es";
|
||||
import { getSteam250List } from "@main/services";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
// import { getSteamGameById } from "../helpers/search-games";
|
||||
import type { Steam250Game } from "@types";
|
||||
|
||||
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) => {
|
||||
if (state.games.length == 0) {
|
||||
const steam250List = await getSteam250List();
|
||||
|
||||
const filteredSteam250List = await filterGames(steam250List);
|
||||
|
||||
state.games = shuffle(filteredSteam250List);
|
||||
state.games = shuffle(steam250List);
|
||||
}
|
||||
|
||||
if (state.games.length == 0) {
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { knexClient } from "@main/knex-client";
|
||||
|
||||
const getRepacks = (_event: Electron.IpcMainInvokeEvent) =>
|
||||
knexClient.select("*").from("repack");
|
||||
|
||||
registerEvent("getRepacks", getRepacks);
|
@ -1,7 +1,7 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||
import { CatalogueEntry } from "@types";
|
||||
import { HydraApi, RepacksManager } from "@main/services";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const searchGamesEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@ -11,15 +11,13 @@ const searchGamesEvent = async (
|
||||
{ objectId: string; title: string; shop: string }[]
|
||||
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
|
||||
|
||||
const steamGames = games.map((game) => {
|
||||
return games.map((game) => {
|
||||
return convertSteamGameToCatalogueEntry({
|
||||
id: Number(game.objectId),
|
||||
name: game.title,
|
||||
clientIcon: null,
|
||||
});
|
||||
});
|
||||
|
||||
return RepacksManager.findRepacksForCatalogueEntries(steamGames);
|
||||
};
|
||||
|
||||
registerEvent("searchGames", searchGamesEvent);
|
||||
|
@ -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,13 +0,0 @@
|
||||
import { downloadSourcesWorker } from "@main/workers";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { DownloadSource } from "@types";
|
||||
|
||||
const syncDownloadSources = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
downloadSources: DownloadSource[]
|
||||
) =>
|
||||
downloadSourcesWorker.run(downloadSources, {
|
||||
name: "getUpdatedRepacks",
|
||||
});
|
||||
|
||||
registerEvent("syncDownloadSources", syncDownloadSources);
|
@ -1,12 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadSourcesWorker } from "@main/workers";
|
||||
|
||||
const validateDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) =>
|
||||
downloadSourcesWorker.run(url, {
|
||||
name: "validateDownloadSource",
|
||||
});
|
||||
|
||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
@ -1,7 +1,6 @@
|
||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { RepacksManager } from "@main/services";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
export interface SearchGamesArgs {
|
||||
@ -28,9 +27,5 @@ export const getSteamGameById = async (
|
||||
|
||||
if (!steamGame) return null;
|
||||
|
||||
const catalogueEntry = convertSteamGameToCatalogueEntry(steamGame);
|
||||
|
||||
const result = RepacksManager.findRepacksForCatalogueEntry(catalogueEntry);
|
||||
|
||||
return result;
|
||||
return convertSteamGameToCatalogueEntry(steamGame);
|
||||
};
|
||||
|
@ -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),
|
||||
})
|
||||
),
|
||||
});
|
@ -9,7 +9,6 @@ import "./catalogue/get-random-game";
|
||||
import "./catalogue/search-games";
|
||||
import "./catalogue/get-game-stats";
|
||||
import "./catalogue/get-trending-games";
|
||||
import "./catalogue/get-repacks";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
@ -37,9 +36,8 @@ import "./user-preferences/auto-launch";
|
||||
import "./autoupdater/check-for-updates";
|
||||
import "./autoupdater/restart-and-install-update";
|
||||
import "./user-preferences/authenticate-real-debrid";
|
||||
import "./download-sources/delete-download-source";
|
||||
import "./download-sources/get-download-sources";
|
||||
import "./download-sources/validate-download-source";
|
||||
import "./download-sources/sync-download-sources";
|
||||
import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./auth/get-session-hash";
|
||||
@ -58,6 +56,7 @@ import "./profile/update-profile";
|
||||
import "./profile/process-profile-image";
|
||||
import "./profile/send-friend-request";
|
||||
import "./profile/sync-friend-requests";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
|
@ -3,7 +3,6 @@ import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import { getFileBase64 } from "@main/helpers";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { createGame } from "@main/services/library-sync";
|
||||
@ -36,20 +35,12 @@ const addGameToLibrary = async (
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository
|
||||
.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
shop,
|
||||
})
|
||||
.then(() => {
|
||||
if (iconUrl) {
|
||||
getFileBase64(iconUrl).then((base64) =>
|
||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
||||
);
|
||||
}
|
||||
});
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
shop,
|
||||
});
|
||||
}
|
||||
|
||||
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 type { StartGameDownloadPayload } from "@types";
|
||||
import { getFileBase64 } from "@main/helpers";
|
||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
@ -60,26 +59,16 @@ const startGameDownload = async (
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository
|
||||
.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
})
|
||||
.then((result) => {
|
||||
if (iconUrl) {
|
||||
getFileBase64(iconUrl).then((base64) =>
|
||||
gameRepository.update({ objectID }, { iconUrl: base64 })
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
|
@ -7,16 +7,6 @@ export const getFileBuffer = async (url: string) =>
|
||||
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) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
|
@ -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 updater from "electron-updater";
|
||||
import i18n from "i18next";
|
||||
@ -74,7 +74,7 @@ const runMigrations = async () => {
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
||||
electronApp.setAppUserModelId("gg.hydralauncher.hydra");
|
||||
|
||||
protocol.handle("local", (request) => {
|
||||
const filePath = request.url.slice("local:".length);
|
||||
@ -103,6 +103,46 @@ app.whenReady().then(async () => {
|
||||
|
||||
WindowManager.createMainWindow();
|
||||
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) => {
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { DownloadManager, PythonInstance, startMainLoop } from "./services";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
// repackRepository,
|
||||
userPreferencesRepository,
|
||||
} from "./repository";
|
||||
import { UserPreferences } from "./entity";
|
||||
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 { uploadGamesBatch } from "./services/library-sync";
|
||||
|
||||
@ -39,18 +35,6 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
}
|
||||
|
||||
startMainLoop();
|
||||
|
||||
// const now = new Date();
|
||||
|
||||
// fetchDownloadSourcesAndUpdate().then(async () => {
|
||||
// const newRepacksCount = await repackRepository.count({
|
||||
// where: {
|
||||
// createdAt: MoreThan(now),
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount);
|
||||
// });
|
||||
};
|
||||
|
||||
userPreferencesRepository
|
||||
|
@ -7,5 +7,4 @@ export * from "./download";
|
||||
export * from "./how-long-to-beat";
|
||||
export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./repacks-manager";
|
||||
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 (
|
||||
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,59 +0,0 @@
|
||||
import { downloadSourceSchema } from "@main/events/helpers/validators";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import type { DownloadSource } 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: string) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
return {
|
||||
...downloadSourceSchema.parse(response.data),
|
||||
etag: response.headers["etag"],
|
||||
};
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import path from "node:path";
|
||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
||||
import downloadSourcesWorkerPath from "./download-sources.worker?modulePath";
|
||||
|
||||
import Piscina from "piscina";
|
||||
|
||||
@ -13,7 +12,3 @@ export const steamGamesWorker = new Piscina({
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
export const downloadSourcesWorker = new Piscina({
|
||||
filename: downloadSourcesWorkerPath,
|
||||
});
|
||||
|
@ -11,7 +11,6 @@ import type {
|
||||
GameRunning,
|
||||
FriendRequestAction,
|
||||
UpdateProfileRequest,
|
||||
DownloadSource,
|
||||
} from "@types";
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
|
||||
@ -50,8 +49,6 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
getGameStats: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||
/* Meant for Dexie migration */
|
||||
getRepacks: () => ipcRenderer.invoke("getRepacks"),
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||
@ -63,10 +60,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
/* Download sources */
|
||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||
validateDownloadSource: (url: string) =>
|
||||
ipcRenderer.invoke("validateDownloadSource", url),
|
||||
syncDownloadSources: (downloadSources: DownloadSource[]) =>
|
||||
ipcRenderer.invoke("syncDownloadSources", downloadSources),
|
||||
deleteDownloadSource: (id: number) =>
|
||||
ipcRenderer.invoke("deleteDownloadSource", id),
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
|
||||
@ -182,4 +177,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.on("on-signout", listener);
|
||||
return () => ipcRenderer.removeListener("on-signout", listener);
|
||||
},
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
} from "@renderer/features";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||
// import { migrationWorker } from "./workers";
|
||||
import { downloadSourcesWorker } from "./workers";
|
||||
import { repacksContext } from "./context";
|
||||
|
||||
export interface AppProps {
|
||||
@ -39,6 +39,8 @@ export function App() {
|
||||
|
||||
const { t } = useTranslation("app");
|
||||
|
||||
const downloadSourceMigrationLock = useRef(false);
|
||||
|
||||
const { clearDownload, setLastPacket } = useDownload();
|
||||
|
||||
const { indexRepacks } = useContext(repacksContext);
|
||||
@ -211,22 +213,46 @@ export function App() {
|
||||
}, [dispatch, draggingDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
// window.electron.getRepacks().then((repacks) => {
|
||||
// migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]);
|
||||
// });
|
||||
// window.electron.getDownloadSources().then((downloadSources) => {
|
||||
// migrationWorker.postMessage([
|
||||
// "MIGRATE_DOWNLOAD_SOURCES",
|
||||
// downloadSources,
|
||||
// ]);
|
||||
// });
|
||||
// migrationWorker.onmessage = (
|
||||
// event: MessageEvent<"MIGRATE_REPACKS_COMPLETE">
|
||||
// ) => {
|
||||
// if (event.data === "MIGRATE_REPACKS_COMPLETE") {
|
||||
// indexRepacks();
|
||||
// }
|
||||
// };
|
||||
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(() => {
|
||||
|
@ -33,6 +33,7 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
|
||||
const channel = new BroadcastChannel(`repacks:search:${channelId}`);
|
||||
channel.onmessage = (event: MessageEvent<GameRepack[]>) => {
|
||||
resolve(event.data);
|
||||
channel.close();
|
||||
};
|
||||
|
||||
return [];
|
||||
|
11
src/renderer/src/declaration.d.ts
vendored
11
src/renderer/src/declaration.d.ts
vendored
@ -25,7 +25,6 @@ import type {
|
||||
UserStats,
|
||||
UserDetails,
|
||||
FriendRequestSync,
|
||||
DownloadSourceValidationResult,
|
||||
} from "@types";
|
||||
import type { DiskSpace } from "check-disk-space";
|
||||
|
||||
@ -66,8 +65,6 @@ declare global {
|
||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||
/* Meant for Dexie migration */
|
||||
getRepacks: () => Promise<GameRepack[]>;
|
||||
|
||||
/* Library */
|
||||
addGameToLibrary: (
|
||||
@ -105,10 +102,7 @@ declare global {
|
||||
|
||||
/* Download sources */
|
||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||
validateDownloadSource: (
|
||||
url: string
|
||||
) => Promise<DownloadSourceValidationResult>;
|
||||
syncDownloadSources: (downloadSources: DownloadSource[]) => Promise<void>;
|
||||
deleteDownloadSource: (id: number) => Promise<void>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||
@ -172,6 +166,9 @@ declare global {
|
||||
action: FriendRequestAction
|
||||
) => Promise<void>;
|
||||
sendFriendRequest: (userId: string) => Promise<void>;
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
@ -67,8 +67,21 @@ export function AddDownloadSourceModal({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await window.electron.validateDownloadSource(values.url);
|
||||
setValidationResult(result);
|
||||
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);
|
||||
},
|
||||
@ -93,16 +106,14 @@ export function AddDownloadSourceModal({
|
||||
if (validationResult) {
|
||||
const channel = new BroadcastChannel(`download_sources:import:${url}`);
|
||||
|
||||
downloadSourcesWorker.postMessage([
|
||||
"IMPORT_DOWNLOAD_SOURCE",
|
||||
{ ...validationResult, url },
|
||||
]);
|
||||
downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
|
||||
|
||||
channel.onmessage = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
onClose();
|
||||
onAddDownloadSource();
|
||||
channel.close();
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -159,9 +170,9 @@ export function AddDownloadSourceModal({
|
||||
<h4>{validationResult?.name}</h4>
|
||||
<small>
|
||||
{t("found_download_option", {
|
||||
count: validationResult?.downloads.length,
|
||||
count: validationResult?.downloadCount,
|
||||
countFormatted:
|
||||
validationResult?.downloads.length.toLocaleString(),
|
||||
validationResult?.downloadCount.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
|
@ -59,6 +59,7 @@ export function SettingsDownloadSources() {
|
||||
getDownloadSources();
|
||||
indexRepacks();
|
||||
setIsRemovingDownloadSource(false);
|
||||
channel.close();
|
||||
};
|
||||
};
|
||||
|
||||
@ -71,15 +72,17 @@ export function SettingsDownloadSources() {
|
||||
const syncDownloadSources = async () => {
|
||||
setIsSyncingDownloadSources(true);
|
||||
|
||||
window.electron
|
||||
.syncDownloadSources(downloadSources)
|
||||
.then(() => {
|
||||
showSuccessToast(t("download_sources_synced"));
|
||||
getDownloadSources();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSyncingDownloadSources(false);
|
||||
});
|
||||
const id = crypto.randomUUID();
|
||||
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
|
||||
|
||||
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
|
||||
|
||||
channel.onmessage = () => {
|
||||
showSuccessToast(t("download_sources_synced"));
|
||||
getDownloadSources();
|
||||
setIsSyncingDownloadSources(false);
|
||||
channel.close();
|
||||
};
|
||||
};
|
||||
|
||||
const statusTitle = {
|
||||
|
@ -1,16 +1,45 @@
|
||||
import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
|
||||
import { z } from "zod";
|
||||
import axios, { AxiosError, AxiosHeaders } from "axios";
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
import type { DownloadSourceValidationResult } from "@types";
|
||||
|
||||
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", DownloadSourceValidationResult & { url: string }]
|
||||
| ["DELETE_DOWNLOAD_SOURCE", number];
|
||||
|
||||
db.open();
|
||||
| ["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();
|
||||
@ -23,28 +52,29 @@ self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||
}
|
||||
|
||||
if (type === "IMPORT_DOWNLOAD_SOURCE") {
|
||||
const result = data;
|
||||
const response =
|
||||
await axios.get<z.infer<typeof downloadSourceSchema>>(data);
|
||||
|
||||
await db.transaction("rw", downloadSourcesTable, repacksTable, async () => {
|
||||
await db.transaction("rw", repacksTable, downloadSourcesTable, async () => {
|
||||
const now = new Date();
|
||||
|
||||
const id = await downloadSourcesTable.add({
|
||||
url: result.url,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
url: data,
|
||||
name: response.data.name,
|
||||
etag: response.headers["etag"],
|
||||
status: DownloadSourceStatus.UpToDate,
|
||||
downloadCount: result.downloads.length,
|
||||
downloadCount: response.data.downloads.length,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const downloadSource = await downloadSourcesTable.get(id);
|
||||
|
||||
const repacks = result.downloads.map((download) => ({
|
||||
const repacks = response.data.downloads.map((download) => ({
|
||||
title: download.title,
|
||||
uris: download.uris,
|
||||
fileSize: download.fileSize,
|
||||
repacker: result.name,
|
||||
repacker: response.data.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSourceId: downloadSource!.id,
|
||||
createdAt: now,
|
||||
@ -54,10 +84,82 @@ self.onmessage = async (event: MessageEvent<Payload>) => {
|
||||
await repacksTable.bulkAdd(repacks);
|
||||
});
|
||||
|
||||
const channel = new BroadcastChannel(
|
||||
`download_sources:import:${result.url}`
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,5 @@
|
||||
import MigrationWorker from "./migration.worker?worker";
|
||||
import RepacksWorker from "./repacks.worker?worker";
|
||||
import DownloadSourcesWorker from "./download-sources.worker?worker";
|
||||
|
||||
export const migrationWorker = new MigrationWorker();
|
||||
export const repacksWorker = new RepacksWorker();
|
||||
export const downloadSourcesWorker = new DownloadSourcesWorker();
|
||||
|
@ -1,35 +0,0 @@
|
||||
// import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie";
|
||||
import { DownloadSource, GameRepack } from "@types";
|
||||
|
||||
export type Payload = [DownloadSource[], GameRepack[]];
|
||||
|
||||
self.onmessage = async (_event: MessageEvent<Payload>) => {
|
||||
// const [downloadSources, gameRepacks] = event.data;
|
||||
// const downloadSourcesCount = await downloadSourcesTable.count();
|
||||
// if (downloadSources.length > downloadSourcesCount) {
|
||||
// await db.transaction(
|
||||
// "rw",
|
||||
// downloadSourcesTable,
|
||||
// repacksTable,
|
||||
// async () => {}
|
||||
// );
|
||||
// }
|
||||
// if (type === "MIGRATE_DOWNLOAD_SOURCES") {
|
||||
// const dexieDownloadSources = await downloadSourcesTable.count();
|
||||
// if (data.length > dexieDownloadSources) {
|
||||
// await downloadSourcesTable.clear();
|
||||
// await downloadSourcesTable.bulkAdd(data);
|
||||
// }
|
||||
// self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE");
|
||||
// }
|
||||
// if (type === "MIGRATE_REPACKS") {
|
||||
// const dexieRepacks = await repacksTable.count();
|
||||
// if (data.length > dexieRepacks) {
|
||||
// await repacksTable.clear();
|
||||
// await repacksTable.bulkAdd(
|
||||
// data.map((repack) => ({ ...repack, uris: JSON.stringify(repack.uris) }))
|
||||
// );
|
||||
// }
|
||||
// self.postMessage("MIGRATE_REPACKS_COMPLETE");
|
||||
// }
|
||||
};
|
@ -233,8 +233,8 @@ export interface DownloadSourceDownload {
|
||||
|
||||
export interface DownloadSourceValidationResult {
|
||||
name: string;
|
||||
downloads: DownloadSourceDownload[];
|
||||
etag: string;
|
||||
downloadCount: number;
|
||||
}
|
||||
|
||||
export interface DownloadSource {
|
||||
|
Loading…
Reference in New Issue
Block a user