mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 01:03:57 +00:00
Merge branch 'main' into feat/souvenirs-for-achievements
This commit is contained in:
@@ -167,6 +167,8 @@ export class AchievementWatcherManager {
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) {
|
||||
if (shop === "custom") return;
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
if (this.alreadySyncedGames.get(gameKey)) return;
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ export const getGameAchievementData = async (
|
||||
shop: GameShop,
|
||||
useCachedData: boolean
|
||||
) => {
|
||||
if (shop === "custom") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly jar = new CookieJar();
|
||||
@@ -20,51 +21,42 @@ export class DatanodesApi {
|
||||
|
||||
await this.jar.setCookie("lang=english;", "https://datanodes.to");
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
method_free: "Free Download >>",
|
||||
dl: "1",
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("op", "download2");
|
||||
formData.append("id", fileCode);
|
||||
formData.append("rand", "");
|
||||
formData.append("referer", "https://datanodes.to/download");
|
||||
formData.append("method_free", "Free Download >>");
|
||||
formData.append("method_premium", "");
|
||||
formData.append("__dl", "1");
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
accept: "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua":
|
||||
'"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
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 (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);
|
||||
logger.error("Error fetching download URL:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getUserData } from "./user/get-user-data";
|
||||
import { db } from "@main/level";
|
||||
import { levelKeys } from "@main/level/sublevels";
|
||||
import type { Auth, User } from "@types";
|
||||
import { WSClient } from "./ws";
|
||||
|
||||
export interface HydraApiOptions {
|
||||
needsAuth?: boolean;
|
||||
@@ -29,7 +30,7 @@ export class HydraApi {
|
||||
private static instance: AxiosInstance;
|
||||
|
||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||
private static readonly ADD_LOG_INTERCEPTOR = false;
|
||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||
|
||||
private static secondsToMilliseconds(seconds: number) {
|
||||
return seconds * 1000;
|
||||
@@ -46,7 +47,7 @@ export class HydraApi {
|
||||
return this.userAuth.authToken !== "";
|
||||
}
|
||||
|
||||
private static hasActiveSubscription() {
|
||||
public static hasActiveSubscription() {
|
||||
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
|
||||
return expiresAt > new Date();
|
||||
}
|
||||
@@ -103,12 +104,10 @@ export class HydraApi {
|
||||
await clearGamesRemoteIds();
|
||||
uploadGamesBatch();
|
||||
|
||||
// WSClient.close();
|
||||
// WSClient.connect();
|
||||
WSClient.close();
|
||||
WSClient.connect();
|
||||
|
||||
const { syncDownloadSourcesFromApi } = await import(
|
||||
"../events/download-sources/sync-download-sources-from-api"
|
||||
);
|
||||
const { syncDownloadSourcesFromApi } = await import("./user");
|
||||
syncDownloadSourcesFromApi();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ export * from "./library-sync";
|
||||
export * from "./wine";
|
||||
export * from "./lock";
|
||||
export * from "./decky-plugin";
|
||||
export * from "./resource-cache";
|
||||
export * from "./user";
|
||||
|
||||
@@ -3,6 +3,10 @@ import { HydraApi } from "../hydra-api";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const createGame = async (game: Game) => {
|
||||
if (game.shop === "custom") {
|
||||
return;
|
||||
}
|
||||
|
||||
return HydraApi.post(`/profile/games`, {
|
||||
objectId: game.objectId,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds ?? 0),
|
||||
|
||||
@@ -60,18 +60,26 @@ export const mergeWithRemoteGames = async () => {
|
||||
|
||||
const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey);
|
||||
|
||||
// Construct coverImageUrl if not provided by backend (Steam games use predictable pattern)
|
||||
const coverImageUrl =
|
||||
game.coverImageUrl ||
|
||||
(game.shop === "steam"
|
||||
? `https://shared.steamstatic.com/store_item_assets/steam/apps/${game.objectId}/library_600x900_2x.jpg`
|
||||
: null);
|
||||
|
||||
await gamesShopAssetsSublevel.put(gameKey, {
|
||||
updatedAt: Date.now(),
|
||||
...localGameShopAsset,
|
||||
shop: game.shop,
|
||||
objectId: game.objectId,
|
||||
title: localGame?.title || game.title, // Preserve local title if it exists
|
||||
coverImageUrl: game.coverImageUrl,
|
||||
coverImageUrl,
|
||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||
libraryImageUrl: game.libraryImageUrl,
|
||||
logoImageUrl: game.logoImageUrl,
|
||||
iconUrl: game.iconUrl,
|
||||
logoPosition: game.logoPosition,
|
||||
downloadSources: game.downloadSources,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { Game } from "@types";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const updateGamePlaytime = async (
|
||||
export const trackGamePlaytime = async (
|
||||
game: Game,
|
||||
deltaInMillis: number,
|
||||
lastTimePlayed: Date
|
||||
) => {
|
||||
return HydraApi.put(`/profile/games/${game.remoteId}`, {
|
||||
if (game.shop === "custom") {
|
||||
return;
|
||||
}
|
||||
|
||||
return HydraApi.put(`/profile/games/${game.shop}/${game.objectId}`, {
|
||||
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
||||
lastTimePlayed,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||
import type { Game, GameRunning } from "@types";
|
||||
import { createGame, trackGamePlaytime } from "./library-sync";
|
||||
import type { Game, GameRunning, UserPreferences } from "@types";
|
||||
import { PythonRPC } from "./python-rpc";
|
||||
import axios from "axios";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { CloudSync } from "./cloud-sync";
|
||||
import { logger } from "./logger";
|
||||
import path from "path";
|
||||
@@ -198,19 +198,32 @@ export const watchProcesses = async () => {
|
||||
function onOpenGame(game: Game) {
|
||||
const now = performance.now();
|
||||
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
lastTick: now,
|
||||
firstTick: now,
|
||||
lastSyncTick: now,
|
||||
});
|
||||
|
||||
// Hide Hydra to tray on game startup if enabled
|
||||
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
|
||||
valueEncoding: "json",
|
||||
})
|
||||
.then((userPreferences) => {
|
||||
if (userPreferences?.hideToTrayOnGameStart) {
|
||||
WindowManager.mainWindow?.hide();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (game.shop === "custom") return;
|
||||
|
||||
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
trackGamePlaytime(
|
||||
game,
|
||||
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
|
||||
new Date()
|
||||
@@ -244,43 +257,46 @@ function onTickGame(game: Game) {
|
||||
|
||||
const delta = now - gamePlaytime.lastTick;
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
const updatedGame: Game = {
|
||||
...game,
|
||||
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
|
||||
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
...gamePlaytime,
|
||||
lastTick: now,
|
||||
});
|
||||
|
||||
if (currentTick % TICKS_TO_UPDATE_API === 0) {
|
||||
if (currentTick % TICKS_TO_UPDATE_API === 0 && game.shop !== "custom") {
|
||||
const deltaToSync =
|
||||
now -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
const gamePromise = game.remoteId
|
||||
? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
? trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
: createGame(game);
|
||||
|
||||
gamePromise
|
||||
.then(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
...updatedGame,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
...updatedGame,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
...gamePlaytime,
|
||||
lastTick: now,
|
||||
lastSyncTick: now,
|
||||
});
|
||||
});
|
||||
@@ -288,11 +304,24 @@ function onTickGame(game: Game) {
|
||||
}
|
||||
|
||||
const onCloseGame = (game: Game) => {
|
||||
const now = performance.now();
|
||||
const gamePlaytime = gamesPlaytime.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
)!;
|
||||
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
const delta = now - gamePlaytime.lastTick;
|
||||
|
||||
const updatedGame: Game = {
|
||||
...game,
|
||||
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
};
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
|
||||
|
||||
if (game.shop === "custom") return;
|
||||
|
||||
if (game.remoteId) {
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
@@ -304,20 +333,20 @@ const onCloseGame = (game: Game) => {
|
||||
}
|
||||
|
||||
const deltaToSync =
|
||||
performance.now() -
|
||||
now -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
return trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
.then(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
...updatedGame,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
...updatedGame,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import { app } from "electron";
|
||||
import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
interface CachedResource<T = unknown> {
|
||||
data: T;
|
||||
etag: string | null;
|
||||
}
|
||||
|
||||
export class ResourceCache {
|
||||
private static cacheDir: string;
|
||||
|
||||
static initialize() {
|
||||
this.cacheDir = path.join(app.getPath("userData"), "resource-cache");
|
||||
|
||||
if (!fs.existsSync(this.cacheDir)) {
|
||||
fs.mkdirSync(this.cacheDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private static getCacheFilePath(resourceName: string): string {
|
||||
return path.join(this.cacheDir, `${resourceName}.json`);
|
||||
}
|
||||
|
||||
private static getEtagFilePath(resourceName: string): string {
|
||||
return path.join(this.cacheDir, `${resourceName}.etag`);
|
||||
}
|
||||
|
||||
private static readCachedResource<T = unknown>(
|
||||
resourceName: string
|
||||
): CachedResource<T> | null {
|
||||
const dataPath = this.getCacheFilePath(resourceName);
|
||||
const etagPath = this.getEtagFilePath(resourceName);
|
||||
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(dataPath, "utf-8")) as T;
|
||||
const etag = fs.existsSync(etagPath)
|
||||
? fs.readFileSync(etagPath, "utf-8")
|
||||
: null;
|
||||
|
||||
return { data, etag };
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read cached resource ${resourceName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static writeCachedResource<T = unknown>(
|
||||
resourceName: string,
|
||||
data: T,
|
||||
etag: string | null
|
||||
): void {
|
||||
const dataPath = this.getCacheFilePath(resourceName);
|
||||
const etagPath = this.getEtagFilePath(resourceName);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(dataPath, JSON.stringify(data), "utf-8");
|
||||
|
||||
if (etag) {
|
||||
fs.writeFileSync(etagPath, etag, "utf-8");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Cached resource ${resourceName} with etag: ${etag || "none"}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to write cached resource ${resourceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchAndCache<T = unknown>(
|
||||
resourceName: string,
|
||||
url: string,
|
||||
timeout: number = 10000
|
||||
): Promise<T> {
|
||||
const cached = this.readCachedResource<T>(resourceName);
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (cached?.etag) {
|
||||
headers["If-None-Match"] = cached.etag;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get<T>(url, {
|
||||
headers,
|
||||
timeout,
|
||||
});
|
||||
|
||||
const newEtag = response.headers["etag"] || null;
|
||||
this.writeCachedResource(resourceName, response.data, newEtag);
|
||||
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { status?: number };
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (axiosError.response?.status === 304 && cached) {
|
||||
logger.info(`Resource ${resourceName} not modified, using cache`);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
logger.warn(
|
||||
`Failed to fetch ${resourceName}, using cached version:`,
|
||||
axiosError.message || "Unknown error"
|
||||
);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Failed to fetch ${resourceName} and no cache available:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static getCachedData<T = unknown>(resourceName: string): T | null {
|
||||
const cached = this.readCachedResource<T>(resourceName);
|
||||
return cached?.data || null;
|
||||
}
|
||||
|
||||
static async updateResourcesOnStartup(): Promise<void> {
|
||||
logger.info("Starting background resource cache update...");
|
||||
|
||||
const resources = [
|
||||
{
|
||||
name: "steam-games-by-letter",
|
||||
url: `${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`,
|
||||
},
|
||||
{
|
||||
name: "sources-manifest",
|
||||
url: "https://cdn.losbroxas.org/sources-manifest.json",
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.allSettled(
|
||||
resources.map(async (resource) => {
|
||||
try {
|
||||
await this.fetchAndCache(resource.name, resource.url);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update ${resource.name} on startup:`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
logger.info("Resource cache update complete");
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const requestSteam250 = async (path: string) => {
|
||||
if (!steamGameUrl) return null;
|
||||
|
||||
return {
|
||||
title: $title.textContent,
|
||||
title: $title.getAttribute("data-title") || "",
|
||||
objectId: steamGameUrl.split("/").pop(),
|
||||
} as Steam250Game;
|
||||
})
|
||||
|
||||
2
src/main/services/user/index.ts
Normal file
2
src/main/services/user/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./get-user-data";
|
||||
export * from "./sync-download-sources";
|
||||
41
src/main/services/user/sync-download-sources.ts
Normal file
41
src/main/services/user/sync-download-sources.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { HydraApi, logger } from "../";
|
||||
import { downloadSourcesSublevel } from "@main/level";
|
||||
import type { DownloadSource } from "@types";
|
||||
|
||||
export const syncDownloadSourcesFromApi = async () => {
|
||||
if (!HydraApi.isLoggedIn() || !HydraApi.hasActiveSubscription()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileSources = await HydraApi.get<DownloadSource[]>(
|
||||
"/profile/download-sources"
|
||||
);
|
||||
|
||||
const existingSources = await downloadSourcesSublevel.values().all();
|
||||
const existingUrls = new Set(existingSources.map((source) => source.url));
|
||||
|
||||
for (const downloadSource of profileSources) {
|
||||
if (!existingUrls.has(downloadSource.url)) {
|
||||
try {
|
||||
await downloadSourcesSublevel.put(downloadSource.id, {
|
||||
...downloadSource,
|
||||
isRemote: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
logger.log(
|
||||
`Synced download source from profile: ${downloadSource.url}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to sync download source ${downloadSource.url}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to sync download sources from API:", error);
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
} from "@types";
|
||||
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
|
||||
import { isStaging } from "@main/constants";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
@@ -54,21 +55,25 @@ export class WindowManager {
|
||||
show: false,
|
||||
};
|
||||
|
||||
private static formatVersionNumber(version: string) {
|
||||
return version.replaceAll(".", "-");
|
||||
}
|
||||
|
||||
private static async loadWindowURL(window: BrowserWindow, hash: string = "") {
|
||||
// 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"]) {
|
||||
window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`);
|
||||
} else if (import.meta.env.MAIN_VITE_RENDERER_URL) {
|
||||
} else if (import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN) {
|
||||
// Try to load from remote URL in production
|
||||
try {
|
||||
await window.loadURL(
|
||||
`${import.meta.env.MAIN_VITE_RENDERER_URL}#/${hash}`
|
||||
`https://release-v${this.formatVersionNumber(app.getVersion())}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
|
||||
);
|
||||
} catch (error) {
|
||||
// Fall back to local file if remote URL fails
|
||||
console.error(
|
||||
"Failed to load from MAIN_VITE_RENDERER_URL, falling back to local file:",
|
||||
logger.error(
|
||||
"Failed to load from MAIN_VITE_LAUNCHER_SUBDOMAIN, falling back to local file:",
|
||||
error
|
||||
);
|
||||
window.loadFile(path.join(__dirname, "../renderer/index.html"), {
|
||||
@@ -284,12 +289,6 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static loadNotificationWindowURL() {
|
||||
if (this.notificationWindow) {
|
||||
this.loadWindowURL(this.notificationWindow, "achievement-notification");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
|
||||
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
|
||||
|
||||
@@ -297,46 +296,58 @@ export class WindowManager {
|
||||
position: AchievementCustomNotificationPosition | undefined
|
||||
) {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const { width, height } = display.workAreaSize;
|
||||
const {
|
||||
x: displayX,
|
||||
y: displayY,
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
} = display.bounds;
|
||||
|
||||
if (position === "bottom-left") {
|
||||
return {
|
||||
x: 0,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
x: displayX,
|
||||
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-left") {
|
||||
return {
|
||||
x: displayX,
|
||||
y: displayY,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: 0,
|
||||
x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: displayY,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: 0,
|
||||
x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: displayY,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: displayX,
|
||||
y: displayY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -382,7 +393,7 @@ export class WindowManager {
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
this.loadWindowURL(this.notificationWindow, "achievement-notification");
|
||||
|
||||
if (!app.isPackaged || isStaging) {
|
||||
this.notificationWindow.webContents.openDevTools();
|
||||
@@ -462,6 +473,7 @@ export class WindowManager {
|
||||
|
||||
editorWindow.once("ready-to-show", () => {
|
||||
editorWindow.show();
|
||||
this.mainWindow?.webContents.openDevTools();
|
||||
if (!app.isPackaged || isStaging) {
|
||||
editorWindow.webContents.openDevTools();
|
||||
}
|
||||
@@ -469,11 +481,12 @@ export class WindowManager {
|
||||
|
||||
editorWindow.webContents.on("before-input-event", (_event, input) => {
|
||||
if (input.key === "F12") {
|
||||
editorWindow.webContents.toggleDevTools();
|
||||
this.mainWindow?.webContents.toggleDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
editorWindow.on("close", () => {
|
||||
this.mainWindow?.webContents.closeDevTools();
|
||||
this.editorWindows.delete(themeId);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user