mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-26 04:11:02 +00:00
resolved conflicts
This commit is contained in:
@@ -23,23 +23,21 @@ const saveAchievementsOnLocal = async (
|
||||
return gameAchievementsSublevel
|
||||
.get(levelKey)
|
||||
.then(async (gameAchievement) => {
|
||||
if (gameAchievement) {
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
...gameAchievement,
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
});
|
||||
await gameAchievementsSublevel.put(levelKey, {
|
||||
achievements: gameAchievement?.achievements ?? [],
|
||||
unlockedAchievements: unlockedAchievements,
|
||||
});
|
||||
|
||||
if (!sendUpdateEvent) return;
|
||||
if (!sendUpdateEvent) return;
|
||||
|
||||
return getUnlockedAchievements(objectId, shop, true)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
return getUnlockedAchievements(objectId, shop, true)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,7 +131,7 @@ export const mergeAchievements = async (
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err! instanceof SubscriptionRequiredError) {
|
||||
if (err instanceof SubscriptionRequiredError) {
|
||||
achievementsLogger.log(
|
||||
"Achievements not synchronized on API due to lack of subscription",
|
||||
game.objectId,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseAchievementFile } from "./parse-achievement-file";
|
||||
import { mergeAchievements } from "./merge-achievements";
|
||||
import type { Game, UnlockedAchievement } from "@types";
|
||||
|
||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
|
||||
const achievementFileInsideDirectory =
|
||||
|
||||
112
src/main/services/cloud-sync.ts
Normal file
112
src/main/services/cloud-sync.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { levelKeys, gamesSublevel, db } from "@main/level";
|
||||
import { app } from "electron";
|
||||
import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
import { Ludusavi } from "./ludusavi";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
|
||||
export class CloudSync {
|
||||
private static async bundleBackup(
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix: string | null
|
||||
) {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
// Remove existing backup
|
||||
if (fs.existsSync(backupPath)) {
|
||||
fs.rmSync(backupPath, { recursive: true });
|
||||
}
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
|
||||
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
|
||||
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
return tarLocation;
|
||||
}
|
||||
|
||||
public static async uploadSaveGame(
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
downloadOptionTitle: string | null,
|
||||
label?: string
|
||||
) {
|
||||
const hasActiveSubscription = await db
|
||||
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
|
||||
.then((user) => {
|
||||
const expiresAt = user?.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
});
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
throw new SubscriptionRequiredError();
|
||||
}
|
||||
|
||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
const bundleLocation = await this.bundleBackup(
|
||||
shop,
|
||||
objectId,
|
||||
game?.winePrefixPath ?? null
|
||||
);
|
||||
|
||||
const stat = await fs.promises.stat(bundleLocation);
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(app.getPath("home")),
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
label,
|
||||
});
|
||||
|
||||
const fileBuffer = await fs.promises.readFile(bundleLocation);
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
logger.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { safeStorage } from "electron";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class Crypto {
|
||||
public static encrypt(str: string) {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
return safeStorage.encryptString(str).toString("base64");
|
||||
} else {
|
||||
logger.warn(
|
||||
"Encrypt method returned raw string because encryption is not available"
|
||||
);
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public static decrypt(b64: string) {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
return safeStorage.decryptString(Buffer.from(b64, "base64"));
|
||||
} else {
|
||||
logger.warn(
|
||||
"Decrypt method returned raw string because encryption is not available"
|
||||
);
|
||||
|
||||
return b64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,13 @@ import { Downloader, DownloadError } from "@shared";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
|
||||
import {
|
||||
GofileApi,
|
||||
QiwiApi,
|
||||
DatanodesApi,
|
||||
MediafireApi,
|
||||
PixelDrainApi,
|
||||
} from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
LibtorrentPayload,
|
||||
@@ -219,8 +225,10 @@ export class DownloadManager {
|
||||
} as PauseDownloadPayload)
|
||||
.catch(() => {});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
this.downloadingGameId = null;
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeDownload(download: Download) {
|
||||
@@ -228,14 +236,17 @@ export class DownloadManager {
|
||||
}
|
||||
|
||||
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||
await PythonRPC.rpc.post("/action", {
|
||||
action: "cancel",
|
||||
game_id: downloadKey,
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
await PythonRPC.rpc
|
||||
.post("/action", {
|
||||
action: "cancel",
|
||||
game_id: downloadKey,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to cancel game download", err);
|
||||
});
|
||||
|
||||
if (downloadKey === this.downloadingGameId) {
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
|
||||
this.downloadingGameId = null;
|
||||
}
|
||||
@@ -278,11 +289,12 @@ export class DownloadManager {
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = download.uri.split("/").pop();
|
||||
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: downloadId,
|
||||
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
|
||||
url: downloadUrl,
|
||||
save_path: download.downloadPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TorBoxAddTorrentRequest,
|
||||
TorBoxRequestLinkRequest,
|
||||
} from "@types";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class TorBoxClient {
|
||||
private static instance: AxiosInstance;
|
||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
||||
baseURL: this.baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
"User-Agent": `Hydra/${appVersion}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,47 +1,71 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly session = axios.create({});
|
||||
private static readonly jar = new CookieJar();
|
||||
|
||||
private static readonly session = wrapper(
|
||||
axios.create({
|
||||
jar: DatanodesApi.jar,
|
||||
withCredentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
try {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
|
||||
const fileCode = pathSegments[0];
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
method_free: "Free Download >>",
|
||||
dl: "1",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
Referer: "https://datanodes.to/download",
|
||||
Origin: "https://datanodes.to",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
if (typeof response.data === "object" && response.data.url) {
|
||||
return decodeURIComponent(response.data.url);
|
||||
}
|
||||
|
||||
const htmlContent = String(response.data);
|
||||
if (!htmlContent) {
|
||||
throw new Error("Empty response received");
|
||||
}
|
||||
|
||||
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
|
||||
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
|
||||
if (downloadLinkMatch) {
|
||||
return downloadLinkMatch[1];
|
||||
}
|
||||
|
||||
throw new Error("Failed to get the download link");
|
||||
} catch (error) {
|
||||
console.error("Error fetching download URL:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
export * from "./datanodes";
|
||||
export * from "./mediafire";
|
||||
export * from "./pixeldrain";
|
||||
|
||||
42
src/main/services/hosters/pixeldrain.ts
Normal file
42
src/main/services/hosters/pixeldrain.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class PixelDrainApi {
|
||||
private static readonly browserHeaders = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
DNT: "1",
|
||||
Connection: "keep-alive",
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
};
|
||||
|
||||
public static async getDownloadUrl(fileId: string): Promise<string> {
|
||||
try {
|
||||
const response = await axios.get(`https://pd.cybar.xyz/${fileId}`, {
|
||||
headers: this.browserHeaders,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) =>
|
||||
status === 301 || status === 302 || status === 200,
|
||||
});
|
||||
|
||||
if (
|
||||
response.headers.location ||
|
||||
response.status === 301 ||
|
||||
response.status === 302
|
||||
) {
|
||||
return response.headers.location;
|
||||
}
|
||||
|
||||
throw new Error(`No redirect URL found (status: ${response.status})`);
|
||||
} catch (error) {
|
||||
console.error("Error fetching PixelDrain URL:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { isFuture, isToday } from "date-fns";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
import { Crypto } from "./crypto";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth?: boolean;
|
||||
@@ -32,8 +31,9 @@ export class HydraApi {
|
||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||
|
||||
private static readonly secondsToMilliseconds = (seconds: number) =>
|
||||
seconds * 1000;
|
||||
private static secondsToMilliseconds(seconds: number) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
private static userAuth: HydraApiUserAuth = {
|
||||
authToken: "",
|
||||
@@ -81,8 +81,8 @@ export class HydraApi {
|
||||
db.put<string, Auth>(
|
||||
levelKeys.auth,
|
||||
{
|
||||
accessToken: Crypto.encrypt(accessToken),
|
||||
refreshToken: Crypto.encrypt(refreshToken),
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
@@ -204,12 +204,8 @@ export class HydraApi {
|
||||
const user = result.at(1) as User | undefined;
|
||||
|
||||
this.userAuth = {
|
||||
authToken: userAuth?.accessToken
|
||||
? Crypto.decrypt(userAuth.accessToken)
|
||||
: "",
|
||||
refreshToken: userAuth?.refreshToken
|
||||
? Crypto.decrypt(userAuth.refreshToken)
|
||||
: "",
|
||||
authToken: userAuth?.accessToken ?? "",
|
||||
refreshToken: userAuth?.refreshToken ?? "",
|
||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||
subscription: user?.subscription
|
||||
? { expiresAt: user.subscription?.expiresAt }
|
||||
@@ -258,7 +254,7 @@ export class HydraApi {
|
||||
levelKeys.auth,
|
||||
{
|
||||
...auth,
|
||||
accessToken: Crypto.encrypt(accessToken),
|
||||
accessToken,
|
||||
tokenExpirationTimestamp,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./crypto";
|
||||
export * from "./logger";
|
||||
export * from "./steam";
|
||||
export * from "./steam-250";
|
||||
@@ -8,3 +7,4 @@ export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./hydra-api";
|
||||
export * from "./ludusavi";
|
||||
export * from "./cloud-sync";
|
||||
|
||||
@@ -24,7 +24,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
? game.playTimeInMilliseconds
|
||||
: localGame.playTimeInMilliseconds;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...localGame,
|
||||
remoteId: game.id,
|
||||
lastTimePlayed: updatedLastTimePlayed,
|
||||
@@ -39,7 +39,7 @@ export const mergeWithRemoteGames = async () => {
|
||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
objectId: game.objectId,
|
||||
title: steamGame?.name,
|
||||
remoteId: game.id,
|
||||
|
||||
@@ -6,6 +6,9 @@ import axios from "axios";
|
||||
import { exec } from "child_process";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { t } from "i18next";
|
||||
import { CloudSync } from "./cloud-sync";
|
||||
import { format } from "date-fns";
|
||||
|
||||
const commands = {
|
||||
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||
@@ -225,6 +228,18 @@ function onOpenGame(game: Game) {
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
null,
|
||||
t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date: format(new Date(), "dd/MM/yyyy"),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||
}
|
||||
@@ -287,6 +302,18 @@ const onCloseGame = (game: Game) => {
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
game.shop,
|
||||
null,
|
||||
t("automatic_backup_from", {
|
||||
ns: "game_details",
|
||||
date: format(new Date(), "dd/MM/yyyy"),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
createGame(game).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import { isStaging } from "@main/constants";
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
|
||||
{
|
||||
width: 1200,
|
||||
@@ -36,7 +38,7 @@ export class WindowManager {
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
color: "#151515",
|
||||
color: "#00000000",
|
||||
height: 34,
|
||||
},
|
||||
webPreferences: {
|
||||
@@ -46,6 +48,23 @@ export class WindowManager {
|
||||
show: false,
|
||||
};
|
||||
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// 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"]}#/${hash}`
|
||||
);
|
||||
} else {
|
||||
this.mainWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static async saveScreenConfig({
|
||||
...configScreenWhenClosed
|
||||
}: {
|
||||
@@ -66,6 +85,7 @@ export class WindowManager {
|
||||
});
|
||||
return data ?? {};
|
||||
}
|
||||
|
||||
private static updateInitialConfig(
|
||||
newConfig: Partial<Electron.BrowserWindowConstructorOptions>
|
||||
) {
|
||||
@@ -75,23 +95,6 @@ export class WindowManager {
|
||||
};
|
||||
}
|
||||
|
||||
private static loadMainWindowURL(hash = "") {
|
||||
// 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"]}#/${hash}`
|
||||
);
|
||||
} else {
|
||||
this.mainWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async createMainWindow() {
|
||||
if (this.mainWindow) return;
|
||||
|
||||
@@ -261,6 +264,87 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static openEditorWindow(themeId: string) {
|
||||
if (this.mainWindow) {
|
||||
const existingWindow = this.editorWindows.get(themeId);
|
||||
if (existingWindow) {
|
||||
if (existingWindow.isMinimized()) {
|
||||
existingWindow.restore();
|
||||
}
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const editorWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 720,
|
||||
minWidth: 600,
|
||||
minHeight: 540,
|
||||
backgroundColor: "#1c1c1c",
|
||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
color: "#151515",
|
||||
height: 34,
|
||||
},
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
this.editorWindows.set(themeId, editorWindow);
|
||||
|
||||
editorWindow.removeMenu();
|
||||
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
editorWindow.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/theme-editor?themeId=${themeId}`
|
||||
);
|
||||
} else {
|
||||
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||
hash: `theme-editor?themeId=${themeId}`,
|
||||
});
|
||||
}
|
||||
|
||||
editorWindow.once("ready-to-show", () => {
|
||||
editorWindow.show();
|
||||
this.mainWindow?.webContents.openDevTools();
|
||||
if (isStaging) {
|
||||
editorWindow.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
editorWindow.webContents.on("before-input-event", (event, input) => {
|
||||
if (input.key === "F12") {
|
||||
event.preventDefault();
|
||||
this.mainWindow?.webContents.toggleDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
editorWindow.on("close", () => {
|
||||
this.mainWindow?.webContents.closeDevTools();
|
||||
this.editorWindows.delete(themeId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static closeEditorWindow(themeId?: string) {
|
||||
if (themeId) {
|
||||
const editorWindow = this.editorWindows.get(themeId);
|
||||
if (editorWindow) {
|
||||
editorWindow.close();
|
||||
}
|
||||
} else {
|
||||
this.editorWindows.forEach((editorWindow) => {
|
||||
editorWindow.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static redirect(hash: string) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadMainWindowURL(hash);
|
||||
|
||||
Reference in New Issue
Block a user