mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-24 03:11:03 +00:00
feat: migrating to electron-vite
This commit is contained in:
38
src/main/services/fifo.ts
Normal file
38
src/main/services/fifo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import path from "node:path";
|
||||
import net from "node:net";
|
||||
import crypto from "node:crypto";
|
||||
import os from "node:os";
|
||||
|
||||
export class FIFO {
|
||||
public socket: null | net.Socket = null;
|
||||
public socketPath = this.generateSocketFilename();
|
||||
|
||||
private generateSocketFilename() {
|
||||
const hash = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return "\\\\.\\pipe\\" + hash;
|
||||
}
|
||||
|
||||
return path.join(os.tmpdir(), hash);
|
||||
}
|
||||
|
||||
public write(data: any) {
|
||||
if (!this.socket) return;
|
||||
this.socket.write(Buffer.from(JSON.stringify(data)));
|
||||
}
|
||||
|
||||
public createPipe() {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer((socket) => {
|
||||
this.socket = socket;
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
server.listen(this.socketPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const writePipe = new FIFO();
|
||||
export const readPipe = new FIFO();
|
||||
60
src/main/services/how-long-to-beat.ts
Normal file
60
src/main/services/how-long-to-beat.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { formatName } from "@main/helpers";
|
||||
import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { requestWebPage } from "./repack-tracker/helpers";
|
||||
import { HowLongToBeatCategory } from "@types";
|
||||
|
||||
export interface HowLongToBeatResult {
|
||||
game_id: number;
|
||||
profile_steam: number;
|
||||
}
|
||||
|
||||
export interface HowLongToBeatSearchResponse {
|
||||
data: HowLongToBeatResult[];
|
||||
}
|
||||
|
||||
export const searchHowLongToBeat = async (gameName: string) => {
|
||||
const response = await axios.post(
|
||||
"https://howlongtobeat.com/api/search",
|
||||
{
|
||||
searchType: "games",
|
||||
searchTerms: formatName(gameName).split(" "),
|
||||
searchPage: 1,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"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",
|
||||
Referer: "https://howlongtobeat.com/",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data as HowLongToBeatSearchResponse;
|
||||
};
|
||||
|
||||
export const getHowLongToBeatGame = async (
|
||||
id: string
|
||||
): Promise<HowLongToBeatCategory[]> => {
|
||||
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
const { document } = window;
|
||||
|
||||
const $ul = document.querySelector(".shadow_shadow ul");
|
||||
const $lis = Array.from($ul.children);
|
||||
|
||||
return $lis.map(($li) => {
|
||||
const title = $li.querySelector("h4").textContent;
|
||||
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
|
||||
|
||||
const accuracy = accuracyClassName.split("time_").at(1);
|
||||
|
||||
return {
|
||||
title,
|
||||
duration: $li.querySelector("h5").textContent,
|
||||
accuracy,
|
||||
};
|
||||
});
|
||||
};
|
||||
11
src/main/services/index.ts
Normal file
11
src/main/services/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./logger";
|
||||
export * from "./repack-tracker";
|
||||
export * from "./steam";
|
||||
export * from "./steam-250";
|
||||
export * from "./steam-grid";
|
||||
export * from "./update-resolver";
|
||||
export * from "./window-manager";
|
||||
export * from "./fifo";
|
||||
export * from "./torrent-client";
|
||||
export * from "./how-long-to-beat";
|
||||
export * from "./process-watcher";
|
||||
11
src/main/services/logger.ts
Normal file
11
src/main/services/logger.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import winston from "winston";
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: "error.log", level: "error" }),
|
||||
new winston.transports.File({ filename: "info.log", level: "info" }),
|
||||
new winston.transports.File({ filename: "combined.log" }),
|
||||
],
|
||||
});
|
||||
80
src/main/services/process-watcher.ts
Normal file
80
src/main/services/process-watcher.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { IsNull, Not } from "typeorm";
|
||||
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { getProcesses } from "@main/helpers";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const startProcessWatcher = async () => {
|
||||
const sleepTime = 100;
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const processes = await getProcesses();
|
||||
|
||||
for (const game of games) {
|
||||
const gameProcess = processes.find((runningProcess) => {
|
||||
const basename = path.win32.basename(game.executablePath);
|
||||
const basenameWithoutExtension = path.win32.basename(
|
||||
game.executablePath,
|
||||
path.extname(game.executablePath)
|
||||
);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return runningProcess.name === basename;
|
||||
}
|
||||
|
||||
return [basename, basenameWithoutExtension].includes(
|
||||
runningProcess.name
|
||||
);
|
||||
});
|
||||
|
||||
if (gameProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
const zero = gamesPlaytime.get(game.id);
|
||||
const delta = performance.now() - zero;
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
|
||||
}
|
||||
|
||||
await gameRepository.update(game.id, {
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
});
|
||||
|
||||
gameRepository.update(game.id, {
|
||||
lastTimePlayed: new Date().toUTCString(),
|
||||
});
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
await sleep(sleepTime);
|
||||
continue;
|
||||
}
|
||||
|
||||
gamesPlaytime.set(game.id, performance.now());
|
||||
|
||||
await sleep(sleepTime);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
gamesPlaytime.delete(game.id);
|
||||
if (WindowManager.mainWindow) {
|
||||
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(sleepTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
135
src/main/services/repack-tracker/1337x.ts
Normal file
135
src/main/services/repack-tracker/1337x.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { formatUploadDate } from "@main/helpers";
|
||||
|
||||
import { Repack } from "@main/entity";
|
||||
import { requestWebPage, savePage } from "./helpers";
|
||||
import type { GameRepackInput } from "./helpers";
|
||||
|
||||
export const request1337x = async (path: string) =>
|
||||
requestWebPage(`https://1337xx.to${path}`);
|
||||
|
||||
/* TODO: $a will often be null */
|
||||
const getTorrentDetails = async (path: string) => {
|
||||
const response = await request1337x(path);
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
const { document } = window;
|
||||
|
||||
const $a = window.document.querySelector(
|
||||
".torrentdown1"
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
const $ul = Array.from(
|
||||
document.querySelectorAll(".torrent-detail-page .list")
|
||||
);
|
||||
const [$firstColumn, $secondColumn] = $ul;
|
||||
|
||||
if (!$firstColumn || !$secondColumn) {
|
||||
return { magnet: $a?.href };
|
||||
}
|
||||
|
||||
const [_$category, _$type, _$language, $totalSize] = $firstColumn.children;
|
||||
const [_$downloads, _$lastChecked, $dateUploaded] = $secondColumn.children;
|
||||
|
||||
return {
|
||||
magnet: $a?.href,
|
||||
fileSize: $totalSize.querySelector("span").textContent ?? undefined,
|
||||
uploadDate: formatUploadDate(
|
||||
$dateUploaded.querySelector("span").textContent!
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const getTorrentListLastPage = async (user: string) => {
|
||||
const response = await request1337x(`/user/${user}/1`);
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
|
||||
const $ul = window.document.querySelector(".pagination > ul");
|
||||
|
||||
if ($ul) {
|
||||
const $li = Array.from($ul.querySelectorAll("li")).at(-1);
|
||||
const text = $li?.textContent;
|
||||
|
||||
if (text === ">>") {
|
||||
const $previousLi = Array.from($ul.querySelectorAll("li")).at(-2);
|
||||
return Number($previousLi?.textContent);
|
||||
}
|
||||
|
||||
return Number(text);
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
export const extractTorrentsFromDocument = async (
|
||||
page: number,
|
||||
user: string,
|
||||
document: Document,
|
||||
existingRepacks: Repack[] = []
|
||||
): Promise<GameRepackInput[]> => {
|
||||
const $trs = Array.from(document.querySelectorAll("tbody tr"));
|
||||
|
||||
return Promise.all(
|
||||
$trs.map(async ($tr) => {
|
||||
const $td = $tr.querySelector("td");
|
||||
|
||||
const [, $name] = Array.from($td!.querySelectorAll("a"));
|
||||
const url = $name.href;
|
||||
const title = $name.textContent ?? "";
|
||||
|
||||
if (existingRepacks.some((repack) => repack.title === title)) {
|
||||
return {
|
||||
title,
|
||||
magnet: "",
|
||||
fileSize: null,
|
||||
uploadDate: null,
|
||||
repacker: user,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
const details = await getTorrentDetails(url);
|
||||
|
||||
return {
|
||||
title,
|
||||
magnet: details.magnet,
|
||||
fileSize: details.fileSize ?? null,
|
||||
uploadDate: details.uploadDate ?? null,
|
||||
repacker: user,
|
||||
page,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getNewRepacksFromUser = async (
|
||||
user: string,
|
||||
existingRepacks: Repack[],
|
||||
page = 1
|
||||
): Promise<Repack[]> => {
|
||||
const response = await request1337x(`/user/${user}/${page}`);
|
||||
const { window } = new JSDOM(response);
|
||||
|
||||
const repacks = await extractTorrentsFromDocument(
|
||||
page,
|
||||
user,
|
||||
window.document,
|
||||
existingRepacks
|
||||
);
|
||||
|
||||
const newRepacks = repacks.filter(
|
||||
(repack) =>
|
||||
repack.uploadDate &&
|
||||
!existingRepacks.some(
|
||||
(existingRepack) => existingRepack.title === repack.title
|
||||
)
|
||||
);
|
||||
|
||||
if (!newRepacks.length) return;
|
||||
|
||||
await savePage(newRepacks);
|
||||
|
||||
return getNewRepacksFromUser(user, existingRepacks, page + 1);
|
||||
};
|
||||
65
src/main/services/repack-tracker/cpg-repacks.ts
Normal file
65
src/main/services/repack-tracker/cpg-repacks.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { Repack } from "@main/entity";
|
||||
|
||||
import { requestWebPage, savePage } from "./helpers";
|
||||
import type { GameRepackInput } from "./helpers";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export const getNewRepacksFromCPG = async (
|
||||
existingRepacks: Repack[] = [],
|
||||
page = 1
|
||||
): Promise<void> => {
|
||||
const data = await requestWebPage(`https://cpgrepacks.site/page/${page}`);
|
||||
|
||||
const { window } = new JSDOM(data);
|
||||
|
||||
const repacks: GameRepackInput[] = [];
|
||||
|
||||
try {
|
||||
Array.from(window.document.querySelectorAll(".post")).forEach(($post) => {
|
||||
const $title = $post.querySelector(".entry-title");
|
||||
const uploadDate = $post.querySelector("time").getAttribute("datetime");
|
||||
|
||||
const $downloadInfo = Array.from(
|
||||
$post.querySelectorAll(".wp-block-heading")
|
||||
).find(($heading) => $heading.textContent.startsWith("Download"));
|
||||
|
||||
/* Side note: CPG often misspells "Magnet" as "Magent" */
|
||||
const $magnet = Array.from($post.querySelectorAll("a")).find(
|
||||
($a) =>
|
||||
$a.textContent.startsWith("Magnet") ||
|
||||
$a.textContent.startsWith("Magent")
|
||||
);
|
||||
|
||||
const fileSize = $downloadInfo.textContent
|
||||
.split("Download link => ")
|
||||
.at(1);
|
||||
|
||||
repacks.push({
|
||||
title: $title.textContent,
|
||||
fileSize: fileSize ?? "N/A",
|
||||
magnet: $magnet.href,
|
||||
repacker: "CPG",
|
||||
page,
|
||||
uploadDate: new Date(uploadDate),
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err.message, { method: "getNewRepacksFromCPG" });
|
||||
}
|
||||
|
||||
const newRepacks = repacks.filter(
|
||||
(repack) =>
|
||||
repack.uploadDate &&
|
||||
!existingRepacks.some(
|
||||
(existingRepack) => existingRepack.title === repack.title
|
||||
)
|
||||
);
|
||||
|
||||
if (!newRepacks.length) return;
|
||||
|
||||
await savePage(newRepacks);
|
||||
|
||||
return getNewRepacksFromCPG(existingRepacks, page + 1);
|
||||
};
|
||||
78
src/main/services/repack-tracker/gog.ts
Normal file
78
src/main/services/repack-tracker/gog.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { JSDOM, VirtualConsole } from "jsdom";
|
||||
import { GameRepackInput, requestWebPage, savePage } from "./helpers";
|
||||
import { Repack } from "@main/entity";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const virtualConsole = new VirtualConsole();
|
||||
|
||||
const getGOGGame = async (url: string) => {
|
||||
const data = await requestWebPage(url);
|
||||
const { window } = new JSDOM(data, { virtualConsole });
|
||||
|
||||
const $modifiedTime = window.document.querySelector(
|
||||
'[property="article:modified_time"]'
|
||||
) as HTMLMetaElement;
|
||||
|
||||
const $em = window.document.querySelector(
|
||||
"p:not(.lightweight-accordion *) em"
|
||||
);
|
||||
const fileSize = $em.textContent.split("Size: ").at(1);
|
||||
const $downloadButton = window.document.querySelector(
|
||||
".download-btn:not(.lightweight-accordion *)"
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
const { searchParams } = new URL($downloadButton.href);
|
||||
const magnet = Buffer.from(searchParams.get("url"), "base64").toString(
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
return {
|
||||
fileSize: fileSize ?? "N/A",
|
||||
uploadDate: new Date($modifiedTime.content),
|
||||
repacker: "GOG",
|
||||
magnet,
|
||||
page: 1,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNewGOGGames = async (existingRepacks: Repack[] = []) => {
|
||||
try {
|
||||
const data = await requestWebPage(
|
||||
"https://freegogpcgames.com/a-z-games-list/"
|
||||
);
|
||||
|
||||
const { window } = new JSDOM(data, { virtualConsole });
|
||||
|
||||
const $uls = Array.from(window.document.querySelectorAll(".az-columns"));
|
||||
|
||||
for (const $ul of $uls) {
|
||||
const repacks: GameRepackInput[] = [];
|
||||
const $lis = Array.from($ul.querySelectorAll("li"));
|
||||
|
||||
for (const $li of $lis) {
|
||||
const $a = $li.querySelector("a");
|
||||
const href = $a.href;
|
||||
|
||||
const title = $a.textContent.trim();
|
||||
|
||||
const gameExists = existingRepacks.some(
|
||||
(existingRepack) => existingRepack.title === title
|
||||
);
|
||||
|
||||
if (!gameExists) {
|
||||
try {
|
||||
const game = await getGOGGame(href);
|
||||
|
||||
repacks.push({ ...game, title });
|
||||
} catch (err) {
|
||||
logger.error(err.message, { method: "getGOGGame", url: href });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (repacks.length) await savePage(repacks);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err.message, { method: "getNewGOGGames" });
|
||||
}
|
||||
};
|
||||
18
src/main/services/repack-tracker/helpers.ts
Normal file
18
src/main/services/repack-tracker/helpers.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { repackRepository } from "@main/repository";
|
||||
|
||||
import type { GameRepack } from "@types";
|
||||
|
||||
export type GameRepackInput = Omit<
|
||||
GameRepack,
|
||||
"id" | "repackerFriendlyName" | "createdAt" | "updatedAt"
|
||||
>;
|
||||
|
||||
export const savePage = async (repacks: GameRepackInput[]) =>
|
||||
Promise.all(
|
||||
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
|
||||
);
|
||||
|
||||
export const requestWebPage = async (url: string) =>
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
}).then((response) => response.text());
|
||||
5
src/main/services/repack-tracker/index.ts
Normal file
5
src/main/services/repack-tracker/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./1337x";
|
||||
export * from "./xatab";
|
||||
export * from "./cpg-repacks";
|
||||
export * from "./gog";
|
||||
// export * from "./online-fix";
|
||||
207
src/main/services/repack-tracker/online-fix.ts
Normal file
207
src/main/services/repack-tracker/online-fix.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Repack } from "@main/entity";
|
||||
import { savePage } from "./helpers";
|
||||
import type { GameRepackInput } from "./helpers";
|
||||
import { logger } from "../logger";
|
||||
import parseTorrent, {
|
||||
toMagnetURI,
|
||||
Instance as TorrentInstance,
|
||||
} from "parse-torrent";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { gotScraping } from "got-scraping";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
import { format, parse, sub } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import { decode } from "windows-1251";
|
||||
import { onlinefixFormatter } from "@main/helpers";
|
||||
|
||||
export const getNewRepacksFromOnlineFix = async (
|
||||
existingRepacks: Repack[] = [],
|
||||
page = 1,
|
||||
cookieJar = new CookieJar()
|
||||
): Promise<void> => {
|
||||
const hasCredentials =
|
||||
import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME &&
|
||||
import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD;
|
||||
if (!hasCredentials) return;
|
||||
|
||||
const http = gotScraping.extend({
|
||||
headerGeneratorOptions: {
|
||||
browsers: [
|
||||
{
|
||||
name: "chrome",
|
||||
minVersion: 87,
|
||||
maxVersion: 89,
|
||||
},
|
||||
],
|
||||
devices: ["desktop"],
|
||||
locales: ["en-US"],
|
||||
operatingSystems: ["windows", "linux"],
|
||||
},
|
||||
cookieJar: cookieJar,
|
||||
});
|
||||
|
||||
if (page === 1) {
|
||||
await http.get("https://online-fix.me/");
|
||||
const preLogin =
|
||||
((await http
|
||||
.get("https://online-fix.me/engine/ajax/authtoken.php", {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: "https://online-fix.me/",
|
||||
},
|
||||
})
|
||||
.json()) as {
|
||||
field: string;
|
||||
value: string;
|
||||
}) || undefined;
|
||||
|
||||
if (!preLogin.field || !preLogin.value) return;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
login_name: import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME,
|
||||
login_password: import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD,
|
||||
login: "submit",
|
||||
[preLogin.field]: preLogin.value,
|
||||
});
|
||||
|
||||
await http
|
||||
.post("https://online-fix.me/", {
|
||||
encoding: "binary",
|
||||
headers: {
|
||||
Referer: "https://online-fix.me",
|
||||
Origin: "https://online-fix.me",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params.toString(),
|
||||
})
|
||||
.text();
|
||||
}
|
||||
|
||||
const pageParams = page > 1 ? `${`/page/${page}`}` : "";
|
||||
|
||||
const home = await http.get(`https://online-fix.me${pageParams}`, {
|
||||
encoding: "binary",
|
||||
});
|
||||
const document = new JSDOM(home.body).window.document;
|
||||
|
||||
const repacks: GameRepackInput[] = [];
|
||||
const articles = Array.from(document.querySelectorAll(".news"));
|
||||
const totalPages = Number(
|
||||
document.querySelector("nav > a:nth-child(13)").textContent
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
articles.map(async (article) => {
|
||||
const gameName = onlinefixFormatter(
|
||||
decode(article.querySelector("h2.title")?.textContent?.trim())
|
||||
);
|
||||
|
||||
const gameLink = article.querySelector("a")?.getAttribute("href");
|
||||
|
||||
if (!gameLink) return;
|
||||
|
||||
const gamePage = await http
|
||||
.get(gameLink, {
|
||||
encoding: "binary",
|
||||
})
|
||||
.text();
|
||||
|
||||
const gameDocument = new JSDOM(gamePage).window.document;
|
||||
|
||||
const uploadDateText = gameDocument.querySelector("time").textContent;
|
||||
|
||||
let decodedDateText = decode(uploadDateText);
|
||||
|
||||
// "Вчера" means yesterday.
|
||||
if (decodedDateText.includes("Вчера")) {
|
||||
const yesterday = sub(new Date(), { days: 1 });
|
||||
const formattedYesterday = format(yesterday, "d LLLL yyyy", {
|
||||
locale: ru,
|
||||
});
|
||||
decodedDateText = decodedDateText.replace(
|
||||
"Вчера", // "Change yesterday to the default expected date format"
|
||||
formattedYesterday
|
||||
);
|
||||
}
|
||||
|
||||
const uploadDate = parse(
|
||||
decodedDateText,
|
||||
"d LLLL yyyy, HH:mm",
|
||||
new Date(),
|
||||
{
|
||||
locale: ru,
|
||||
}
|
||||
);
|
||||
|
||||
const torrentButtons = Array.from(
|
||||
gameDocument.querySelectorAll("a")
|
||||
).filter((a) => a.textContent?.includes("Torrent"));
|
||||
|
||||
const torrentPrePage = torrentButtons[0]?.getAttribute("href");
|
||||
if (!torrentPrePage) return;
|
||||
|
||||
const torrentPage = await http
|
||||
.get(torrentPrePage, {
|
||||
encoding: "binary",
|
||||
headers: {
|
||||
Referer: gameLink,
|
||||
},
|
||||
})
|
||||
.text();
|
||||
|
||||
const torrentDocument = new JSDOM(torrentPage).window.document;
|
||||
|
||||
const torrentLink = torrentDocument
|
||||
.querySelector("a:nth-child(2)")
|
||||
?.getAttribute("href");
|
||||
|
||||
const torrentFile = Buffer.from(
|
||||
await http
|
||||
.get(`${torrentPrePage}/${torrentLink}`, {
|
||||
responseType: "buffer",
|
||||
})
|
||||
.buffer()
|
||||
);
|
||||
|
||||
const torrent = parseTorrent(torrentFile) as TorrentInstance;
|
||||
const magnetLink = toMagnetURI({
|
||||
infoHash: torrent.infoHash,
|
||||
});
|
||||
|
||||
const torrentSizeInBytes = torrent.length;
|
||||
const fileSizeFormatted =
|
||||
torrentSizeInBytes >= 1024 ** 3
|
||||
? `${(torrentSizeInBytes / 1024 ** 3).toFixed(1)}GBs`
|
||||
: `${(torrentSizeInBytes / 1024 ** 2).toFixed(1)}MBs`;
|
||||
|
||||
repacks.push({
|
||||
fileSize: fileSizeFormatted,
|
||||
magnet: magnetLink,
|
||||
page: 1,
|
||||
repacker: "onlinefix",
|
||||
title: gameName,
|
||||
uploadDate: uploadDate,
|
||||
});
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err.message, { method: "getNewRepacksFromOnlineFix" });
|
||||
}
|
||||
|
||||
const newRepacks = repacks.filter(
|
||||
(repack) =>
|
||||
repack.uploadDate &&
|
||||
!existingRepacks.some(
|
||||
(existingRepack) => existingRepack.title === repack.title
|
||||
)
|
||||
);
|
||||
|
||||
if (!newRepacks.length) return;
|
||||
if (page === totalPages) return;
|
||||
|
||||
await savePage(newRepacks);
|
||||
|
||||
return getNewRepacksFromOnlineFix(existingRepacks, page + 1, cookieJar);
|
||||
};
|
||||
95
src/main/services/repack-tracker/xatab.ts
Normal file
95
src/main/services/repack-tracker/xatab.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import parseTorrent, { toMagnetURI } from "parse-torrent";
|
||||
|
||||
import { Repack } from "@main/entity";
|
||||
import { logger } from "../logger";
|
||||
import { requestWebPage, savePage } from "./helpers";
|
||||
import type { GameRepackInput } from "./helpers";
|
||||
|
||||
const getTorrentBuffer = (url: string) =>
|
||||
fetch(url, { method: "GET" }).then((response) =>
|
||||
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
|
||||
);
|
||||
|
||||
const formatXatabDate = (str: string) => {
|
||||
const date = new Date();
|
||||
|
||||
const [day, month, year] = str.split(".");
|
||||
|
||||
date.setDate(Number(day));
|
||||
date.setMonth(Number(month) - 1);
|
||||
date.setFullYear(Number(year));
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
const formatXatabDownloadSize = (str: string) =>
|
||||
str.replace(",", ".").replace(/Гб/g, "GB").replace(/Мб/g, "MB");
|
||||
|
||||
const getXatabRepack = async (url: string) => {
|
||||
const data = await requestWebPage(url);
|
||||
const { window } = new JSDOM(data);
|
||||
|
||||
const $uploadDate = window.document.querySelector(".entry__date");
|
||||
const $size = window.document.querySelector(".entry__info-size");
|
||||
|
||||
const $downloadButton = window.document.querySelector(
|
||||
".download-torrent"
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
if (!$downloadButton) throw new Error("Download button not found");
|
||||
|
||||
const torrentBuffer = await getTorrentBuffer($downloadButton.href);
|
||||
|
||||
return {
|
||||
fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(),
|
||||
magnet: toMagnetURI({
|
||||
infoHash: parseTorrent(torrentBuffer).infoHash,
|
||||
}),
|
||||
uploadDate: formatXatabDate($uploadDate.textContent),
|
||||
};
|
||||
};
|
||||
|
||||
export const getNewRepacksFromXatab = async (
|
||||
existingRepacks: Repack[] = [],
|
||||
page = 1
|
||||
): Promise<void> => {
|
||||
const data = await requestWebPage(`https://byxatab.com/page/${page}`);
|
||||
|
||||
const { window } = new JSDOM(data);
|
||||
|
||||
const repacks: GameRepackInput[] = [];
|
||||
|
||||
for (const $a of Array.from(
|
||||
window.document.querySelectorAll(".entry__title a")
|
||||
)) {
|
||||
try {
|
||||
const repack = await getXatabRepack(($a as HTMLAnchorElement).href);
|
||||
|
||||
repacks.push({
|
||||
title: $a.textContent,
|
||||
repacker: "Xatab",
|
||||
...repack,
|
||||
page,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err.message, { method: "getNewRepacksFromXatab" });
|
||||
}
|
||||
}
|
||||
|
||||
const newRepacks = repacks.filter(
|
||||
(repack) =>
|
||||
repack.uploadDate &&
|
||||
!existingRepacks.some(
|
||||
(existingRepack) => existingRepack.title === repack.title
|
||||
)
|
||||
);
|
||||
|
||||
if (!newRepacks.length) return;
|
||||
|
||||
await savePage(newRepacks);
|
||||
|
||||
return getNewRepacksFromXatab(existingRepacks, page + 1);
|
||||
};
|
||||
34
src/main/services/steam-250.ts
Normal file
34
src/main/services/steam-250.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { shuffle } from "lodash-es";
|
||||
|
||||
export const requestSteam250 = async (path: string) => {
|
||||
return axios.get(`https://steam250.com${path}`).then((response) => {
|
||||
const { window } = new JSDOM(response.data);
|
||||
const { document } = window;
|
||||
|
||||
return Array.from(document.querySelectorAll(".appline .title a")).map(
|
||||
($title: HTMLAnchorElement) => {
|
||||
const steamGameUrl = $title.href;
|
||||
if (!steamGameUrl) return null;
|
||||
|
||||
return {
|
||||
title: $title.textContent,
|
||||
objectID: steamGameUrl.split("/").pop(),
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const steam250Paths = [
|
||||
"/hidden_gems",
|
||||
`/${new Date().getFullYear()}`,
|
||||
"/top250",
|
||||
"/most_played",
|
||||
];
|
||||
|
||||
export const getRandomSteam250List = async () => {
|
||||
const [path] = shuffle(steam250Paths);
|
||||
return requestSteam250(path);
|
||||
};
|
||||
71
src/main/services/steam-grid.ts
Normal file
71
src/main/services/steam-grid.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
|
||||
export interface SteamGridResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SteamGridGameResponse {
|
||||
data: {
|
||||
platforms: {
|
||||
steam: {
|
||||
metadata: {
|
||||
clienticon: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const getSteamGridData = async (
|
||||
objectID: string,
|
||||
path: string,
|
||||
shop: string,
|
||||
params: Record<string, string> = {}
|
||||
): Promise<SteamGridResponse> => {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
|
||||
const response = await fetch(
|
||||
`https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectID}?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getSteamGridGameById = async (
|
||||
id: number
|
||||
): Promise<SteamGridGameResponse> => {
|
||||
const response = await fetch(
|
||||
`https://www.steamgriddb.com/api/public/game/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Referer: "https://www.steamgriddb.com/",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getSteamGameIconUrl = async (objectID: string) => {
|
||||
const {
|
||||
data: { id: steamGridGameId },
|
||||
} = await getSteamGridData(objectID, "games", "steam");
|
||||
|
||||
const steamGridGame = await getSteamGridGameById(steamGridGameId);
|
||||
|
||||
return getSteamAppAsset(
|
||||
"icon",
|
||||
objectID,
|
||||
steamGridGame.data.platforms.steam.metadata.clienticon
|
||||
);
|
||||
};
|
||||
35
src/main/services/steam.ts
Normal file
35
src/main/services/steam.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import axios from "axios";
|
||||
|
||||
import type { SteamAppDetails } from "@types";
|
||||
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface SteamAppDetailsResponse {
|
||||
[key: string]: {
|
||||
success: boolean;
|
||||
data: SteamAppDetails;
|
||||
};
|
||||
}
|
||||
|
||||
export const getSteamAppDetails = async (
|
||||
objectID: string,
|
||||
language: string
|
||||
) => {
|
||||
const searchParams = new URLSearchParams({
|
||||
appids: objectID,
|
||||
l: language,
|
||||
});
|
||||
|
||||
return axios
|
||||
.get(
|
||||
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data[objectID].success) return response.data[objectID].data;
|
||||
return null;
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error(err, { method: "getSteamAppDetails" });
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
170
src/main/services/torrent-client.ts
Normal file
170
src/main/services/torrent-client.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import path from "node:path";
|
||||
import cp from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import { Notification, app, dialog } from "electron";
|
||||
import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
|
||||
import { Game } from "@main/entity";
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import { t } from "i18next";
|
||||
import { WindowManager } from "./window-manager";
|
||||
|
||||
const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: "hydra-download-manager",
|
||||
linux: "hydra-download-manager",
|
||||
win32: "hydra-download-manager.exe",
|
||||
};
|
||||
|
||||
enum TorrentState {
|
||||
CheckingFiles = 1,
|
||||
DownloadingMetadata = 2,
|
||||
Downloading = 3,
|
||||
Finished = 4,
|
||||
Seeding = 5,
|
||||
}
|
||||
|
||||
export interface TorrentUpdate {
|
||||
gameId: number;
|
||||
progress: number;
|
||||
downloadSpeed: number;
|
||||
timeRemaining: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
status: TorrentState;
|
||||
folderName: string;
|
||||
fileSize: number;
|
||||
bytesDownloaded: number;
|
||||
}
|
||||
|
||||
export const BITTORRENT_PORT = "5881";
|
||||
|
||||
export class TorrentClient {
|
||||
public static startTorrentClient(
|
||||
writePipePath: string,
|
||||
readPipePath: string
|
||||
) {
|
||||
const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath];
|
||||
|
||||
if (app.isPackaged) {
|
||||
const binaryName = binaryNameByPlatform[process.platform];
|
||||
const binaryPath = path.join(
|
||||
process.resourcesPath,
|
||||
"dist",
|
||||
"hydra-download-manager",
|
||||
binaryName
|
||||
);
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
dialog.showErrorBox(
|
||||
"Fatal",
|
||||
"Hydra download manager binary not found. Please check if it has been removed by Windows Defender."
|
||||
);
|
||||
|
||||
app.quit();
|
||||
}
|
||||
|
||||
cp.spawn(binaryPath, commonArgs, {
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"torrent-client",
|
||||
"main.py"
|
||||
);
|
||||
|
||||
cp.spawn("python3", [scriptPath, ...commonArgs], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
private static getTorrentStateName(state: TorrentState) {
|
||||
if (state === TorrentState.CheckingFiles) return "checking_files";
|
||||
if (state === TorrentState.Downloading) return "downloading";
|
||||
if (state === TorrentState.DownloadingMetadata)
|
||||
return "downloading_metadata";
|
||||
if (state === TorrentState.Finished) return "finished";
|
||||
if (state === TorrentState.Seeding) return "seeding";
|
||||
return "";
|
||||
}
|
||||
|
||||
private static getGameProgress(game: Game) {
|
||||
if (game.status === "checking_files") return game.fileVerificationProgress;
|
||||
return game.progress;
|
||||
}
|
||||
|
||||
public static async onSocketData(data: Buffer) {
|
||||
const message = Buffer.from(data).toString("utf-8");
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(message) as TorrentUpdate;
|
||||
|
||||
const updatePayload: QueryDeepPartialEntity<Game> = {
|
||||
bytesDownloaded: payload.bytesDownloaded,
|
||||
status: this.getTorrentStateName(payload.status),
|
||||
};
|
||||
|
||||
if (payload.status === TorrentState.CheckingFiles) {
|
||||
updatePayload.fileVerificationProgress = payload.progress;
|
||||
} else {
|
||||
if (payload.folderName) {
|
||||
updatePayload.folderName = payload.folderName;
|
||||
updatePayload.fileSize = payload.fileSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[TorrentState.Downloading, TorrentState.Seeding].includes(
|
||||
payload.status
|
||||
)
|
||||
) {
|
||||
updatePayload.progress = payload.progress;
|
||||
}
|
||||
|
||||
await gameRepository.update({ id: payload.gameId }, updatePayload);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: payload.gameId },
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
if (game.progress === 1) {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
if (userPreferences?.downloadNotificationsEnabled) {
|
||||
new Notification({
|
||||
title: t("download_complete", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
}),
|
||||
body: t("game_ready_to_install", {
|
||||
ns: "notifications",
|
||||
lng: userPreferences.language,
|
||||
title: game.title,
|
||||
}),
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow) {
|
||||
const progress = this.getGameProgress(game);
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
|
||||
WindowManager.mainWindow.webContents.send(
|
||||
"on-download-progress",
|
||||
JSON.parse(JSON.stringify({ ...payload, game }))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/main/services/update-resolver.ts
Normal file
159
src/main/services/update-resolver.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import path from "node:path";
|
||||
import { app } from "electron";
|
||||
|
||||
import { chunk } from "lodash-es";
|
||||
|
||||
import { createDataSource, dataSource } from "@main/data-source";
|
||||
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
|
||||
import {
|
||||
migrationScriptRepository,
|
||||
repackRepository,
|
||||
repackerFriendlyNameRepository,
|
||||
steamGameRepository,
|
||||
} from "@main/repository";
|
||||
import { MigrationScript } from "@main/entity/migration-script.entity";
|
||||
import { Like } from "typeorm";
|
||||
|
||||
const migrationScripts = {
|
||||
/*
|
||||
0.0.6 -> 0.0.7
|
||||
Xatab repacks were previously created with an incorrect upload date.
|
||||
This migration script will update the upload date of all Xatab repacks.
|
||||
*/
|
||||
"0.0.7": async (updateRepacks: Repack[]) => {
|
||||
const VERSION = "0.0.7";
|
||||
|
||||
const migrationScript = await migrationScriptRepository.findOne({
|
||||
where: {
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
if (!migrationScript) {
|
||||
const xatabRepacks = updateRepacks.filter(
|
||||
(repack) => repack.repacker === "Xatab"
|
||||
);
|
||||
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await Promise.all(
|
||||
xatabRepacks.map((repack) =>
|
||||
transactionalEntityManager.getRepository(Repack).update(
|
||||
{
|
||||
title: repack.title,
|
||||
repacker: repack.repacker,
|
||||
},
|
||||
{
|
||||
uploadDate: repack.uploadDate,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await transactionalEntityManager.getRepository(MigrationScript).insert({
|
||||
version: VERSION,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
/*
|
||||
1.0.1 -> 1.1.0
|
||||
A few torrents scraped from 1337x were previously created with an incorrect upload date.
|
||||
*/
|
||||
"1.1.0": async () => {
|
||||
const VERSION = "1.1.0";
|
||||
|
||||
const migrationScript = await migrationScriptRepository.findOne({
|
||||
where: {
|
||||
version: VERSION,
|
||||
},
|
||||
});
|
||||
|
||||
if (!migrationScript) {
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const repacks = await transactionalEntityManager
|
||||
.getRepository(Repack)
|
||||
.find({
|
||||
where: {
|
||||
uploadDate: Like("1%"),
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
repacks.map(async (repack) => {
|
||||
return transactionalEntityManager
|
||||
.getRepository(Repack)
|
||||
.update(
|
||||
{ id: repack.id },
|
||||
{ uploadDate: new Date(repack.uploadDate) }
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await transactionalEntityManager.getRepository(MigrationScript).insert({
|
||||
version: VERSION,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const runMigrationScripts = async (updateRepacks: Repack[]) => {
|
||||
return Promise.all(
|
||||
Object.values(migrationScripts).map((migrationScript) => {
|
||||
return migrationScript(updateRepacks);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const resolveDatabaseUpdates = async () => {
|
||||
const updateDataSource = createDataSource({
|
||||
database: app.isPackaged
|
||||
? path.join(process.resourcesPath, "hydra.db")
|
||||
: path.join(__dirname, "..", "..", "resources", "hydra.db"),
|
||||
});
|
||||
|
||||
return updateDataSource.initialize().then(async () => {
|
||||
const updateRepackRepository = updateDataSource.getRepository(Repack);
|
||||
const updateRepackerFriendlyNameRepository =
|
||||
updateDataSource.getRepository(RepackerFriendlyName);
|
||||
const updateSteamGameRepository = updateDataSource.getRepository(SteamGame);
|
||||
|
||||
const [updateRepacks, updateSteamGames, updateRepackerFriendlyNames] =
|
||||
await Promise.all([
|
||||
updateRepackRepository.find(),
|
||||
updateSteamGameRepository.find(),
|
||||
updateRepackerFriendlyNameRepository.find(),
|
||||
]);
|
||||
|
||||
await runMigrationScripts(updateRepacks);
|
||||
|
||||
await repackerFriendlyNameRepository
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(updateRepackerFriendlyNames)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
const updateRepacksChunks = chunk(updateRepacks, 800);
|
||||
|
||||
for (const chunk of updateRepacksChunks) {
|
||||
await repackRepository
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(chunk)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
const steamGamesChunks = chunk(updateSteamGames, 800);
|
||||
|
||||
for (const chunk of steamGamesChunks) {
|
||||
await steamGameRepository
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(chunk)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
};
|
||||
109
src/main/services/window-manager.ts
Normal file
109
src/main/services/window-manager.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { BrowserWindow, Menu, Tray, app } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { t } from "i18next";
|
||||
import path from "node:path";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
public static createMainWindow() {
|
||||
// Create the browser window.
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 720,
|
||||
minWidth: 1024,
|
||||
minHeight: 540,
|
||||
titleBarStyle: "hidden",
|
||||
// icon: path.join(__dirname, "..", "..", "images", "icon.png"),
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
color: "#151515",
|
||||
height: 34,
|
||||
},
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.mainWindow.removeMenu();
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||
} else {
|
||||
this.mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||
}
|
||||
|
||||
this.mainWindow.webContents.on("did-finish-load", () => {
|
||||
if (!app.isPackaged) {
|
||||
// Open the DevTools.
|
||||
this.mainWindow.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
this.mainWindow.on("close", () => {
|
||||
WindowManager.mainWindow.setProgressBar(-1);
|
||||
});
|
||||
}
|
||||
|
||||
public static redirect(path: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.mainWindow.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}#${path}`);
|
||||
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
|
||||
public static createSystemTray(language: string) {
|
||||
const tray = new Tray(
|
||||
app.isPackaged
|
||||
? path.join(process.resourcesPath, "icon_tray.png")
|
||||
: path.join(__dirname, "..", "..", "resources", "icon_tray.png")
|
||||
);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: t("open", {
|
||||
ns: "system_tray",
|
||||
lng: language,
|
||||
}),
|
||||
type: "normal",
|
||||
click: () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
} else {
|
||||
this.createMainWindow();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("quit", {
|
||||
ns: "system_tray",
|
||||
lng: language,
|
||||
}),
|
||||
type: "normal",
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setToolTip("Hydra");
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
tray.addListener("click", () => {
|
||||
if (this.mainWindow) {
|
||||
if (WindowManager.mainWindow.isMinimized())
|
||||
WindowManager.mainWindow.restore();
|
||||
|
||||
WindowManager.mainWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createMainWindow();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user