mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-25 20:01:03 +00:00
Merge branch 'main' of github.com:hydralauncher/hydra
This commit is contained in:
@@ -26,11 +26,10 @@ export const commonRedistPath = path.join(
|
||||
"CommonRedist"
|
||||
);
|
||||
|
||||
export const logsPath = path.join(SystemPath.getPath("userData"), "logs");
|
||||
|
||||
export const seedsPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "seeds")
|
||||
: path.join(__dirname, "..", "..", "seeds");
|
||||
export const logsPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
`logs${isStaging ? "-staging" : ""}`
|
||||
);
|
||||
|
||||
export const achievementSoundPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "achievement.wav")
|
||||
|
||||
@@ -20,6 +20,7 @@ import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
import "./library/delete-game-folder";
|
||||
import "./library/get-game-by-object-id";
|
||||
import "./library/sync-game-by-object-id";
|
||||
import "./library/get-library";
|
||||
import "./library/extract-game-download";
|
||||
import "./library/open-game";
|
||||
@@ -88,6 +89,8 @@ import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./cloud-save/toggle-artifact-freeze";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import "./notifications/update-achievement-notification-window";
|
||||
import "./notifications/show-achievement-test-notification";
|
||||
import "./themes/add-custom-theme";
|
||||
import "./themes/delete-custom-theme";
|
||||
import "./themes/get-all-custom-themes";
|
||||
|
||||
@@ -12,16 +12,14 @@ const openGameInstallerPath = async (
|
||||
) => {
|
||||
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||
|
||||
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||
if (!download?.folderName || !download.downloadPath) return;
|
||||
|
||||
const gamePath = path.join(
|
||||
download.downloadPath ?? (await getDownloadsPath()),
|
||||
download.folderName!
|
||||
download.folderName
|
||||
);
|
||||
|
||||
shell.showItemInFolder(gamePath);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
registerEvent("openGameInstallerPath", openGameInstallerPath);
|
||||
|
||||
28
src/main/events/library/sync-game-by-object-id.ts
Normal file
28
src/main/events/library/sync-game-by-object-id.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services";
|
||||
import type { GameShop, UserGameDetails } from "@types";
|
||||
|
||||
const syncGameByObjectId = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
return HydraApi.get<UserGameDetails>(
|
||||
`/profile/games/${shop}/${objectId}`
|
||||
).then(async (res) => {
|
||||
const { id, playTimeInSeconds, ...rest } = res;
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...rest,
|
||||
remoteId: id,
|
||||
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("syncGameByObjectId", syncGameByObjectId);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const showAchievementTestNotification = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
setTimeout(() => {
|
||||
WindowManager.showAchievementTestNotification();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"showAchievementTestNotification",
|
||||
showAchievementTestNotification
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
import { UserPreferences } from "@types";
|
||||
|
||||
const updateAchievementCustomNotificationWindow = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
WindowManager.closeNotificationWindow();
|
||||
|
||||
if (
|
||||
userPreferences.achievementNotificationsEnabled !== false &&
|
||||
userPreferences.achievementCustomNotificationsEnabled !== false
|
||||
) {
|
||||
WindowManager.createNotificationWindow();
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"updateAchievementCustomNotificationWindow",
|
||||
updateAchievementCustomNotificationWindow
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { themesSublevel } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const toggleCustomTheme = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -17,6 +18,8 @@ const toggleCustomTheme = async (
|
||||
isActive,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
WindowManager.notificationWindow?.webContents.send("on-custom-theme-updated");
|
||||
};
|
||||
|
||||
registerEvent("toggleCustomTheme", toggleCustomTheme);
|
||||
|
||||
@@ -20,7 +20,10 @@ const updateCustomTheme = async (
|
||||
});
|
||||
|
||||
if (theme.isActive) {
|
||||
WindowManager.mainWindow?.webContents.send("css-injected", code);
|
||||
WindowManager.mainWindow?.webContents.send("on-custom-theme-updated");
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-custom-theme-updated"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import i18n from "i18next";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||
import { logger, WindowManager } from "@main/services";
|
||||
import { logger, clearGamesPlaytime, WindowManager } from "@main/services";
|
||||
import resources from "@locales";
|
||||
import { PythonRPC } from "./services/python-rpc";
|
||||
import { db, levelKeys } from "./level";
|
||||
@@ -73,6 +73,7 @@ app.whenReady().then(async () => {
|
||||
WindowManager.createMainWindow();
|
||||
}
|
||||
|
||||
WindowManager.createNotificationWindow();
|
||||
WindowManager.createSystemTray(language || "en");
|
||||
});
|
||||
|
||||
@@ -142,9 +143,17 @@ app.on("window-all-closed", () => {
|
||||
WindowManager.mainWindow = null;
|
||||
});
|
||||
|
||||
app.on("before-quit", () => {
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
let canAppBeClosed = false;
|
||||
|
||||
app.on("before-quit", async (e) => {
|
||||
if (!canAppBeClosed) {
|
||||
e.preventDefault();
|
||||
/* Disconnects libtorrent */
|
||||
PythonRPC.kill();
|
||||
await clearGamesPlaytime();
|
||||
canAppBeClosed = true;
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const loadState = async () => {
|
||||
}
|
||||
|
||||
Ludusavi.copyConfigFileToUserData();
|
||||
Ludusavi.copyBinaryToUserData();
|
||||
|
||||
await HydraApi.setupApi().then(() => {
|
||||
uploadGamesBatch();
|
||||
|
||||
@@ -7,11 +7,18 @@ import {
|
||||
findAllAchievementFiles,
|
||||
getAlternativeObjectIds,
|
||||
} from "./find-achivement-files";
|
||||
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
|
||||
import type {
|
||||
AchievementFile,
|
||||
Game,
|
||||
UnlockedAchievement,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { sleep } from "@main/helpers";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
@@ -184,7 +191,7 @@ export class AchievementWatcherManager {
|
||||
return mergeAchievements(game, unlockedAchievements, false);
|
||||
}
|
||||
|
||||
private static preSearchAchievementsWindows = async () => {
|
||||
private static async getGameAchievementFilesWindows() {
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
@@ -194,24 +201,24 @@ export class AchievementWatcherManager {
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
const achievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(
|
||||
achievementFiles.push(
|
||||
...(gameAchievementFilesMap.get(objectId) || [])
|
||||
);
|
||||
|
||||
gameAchievementFiles.push(
|
||||
achievementFiles.push(
|
||||
...findAchievementFileInExecutableDirectory(game)
|
||||
);
|
||||
}
|
||||
|
||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static preSearchAchievementsWithWine = async () => {
|
||||
private static async getGameAchievementFilesLinux() {
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
@@ -219,37 +226,70 @@ export class AchievementWatcherManager {
|
||||
|
||||
return Promise.all(
|
||||
games.map((game) => {
|
||||
const gameAchievementFiles = findAchievementFiles(game);
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
const achievementFileInsideDirectory =
|
||||
findAchievementFileInExecutableDirectory(game);
|
||||
|
||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||
achievementFiles.push(...achievementFileInsideDirectory);
|
||||
|
||||
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
|
||||
return { game, achievementFiles };
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public static async preSearchAchievements() {
|
||||
await sleep(2000);
|
||||
|
||||
try {
|
||||
const newAchievementsCount =
|
||||
const gameAchievementFiles =
|
||||
process.platform === "win32"
|
||||
? await this.preSearchAchievementsWindows()
|
||||
: await this.preSearchAchievementsWithWine();
|
||||
? await this.getGameAchievementFilesWindows()
|
||||
: await this.getGameAchievementFilesLinux();
|
||||
|
||||
const newAchievementsCount: number[] = [];
|
||||
|
||||
for (const { game, achievementFiles } of gameAchievementFiles) {
|
||||
const result = await this.preProcessGameAchievementFiles(
|
||||
game,
|
||||
achievementFiles
|
||||
);
|
||||
|
||||
newAchievementsCount.push(result);
|
||||
}
|
||||
|
||||
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||
(achievements) => achievements
|
||||
).length;
|
||||
|
||||
const totalNewAchievements = newAchievementsCount.reduce(
|
||||
(acc, val) => acc + val,
|
||||
0
|
||||
);
|
||||
|
||||
if (totalNewAchievements > 0) {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences.achievementNotificationsEnabled !== false) {
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-combined-achievements-unlocked",
|
||||
totalNewGamesWithAchievements,
|
||||
totalNewAchievements,
|
||||
userPreferences.achievementCustomNotificationPosition ??
|
||||
"top-left"
|
||||
);
|
||||
} else {
|
||||
publishCombinedNewAchievementNotification(
|
||||
totalNewAchievements,
|
||||
totalNewGamesWithAchievements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
achievementsLogger.error("Error on preSearchAchievements", err);
|
||||
|
||||
@@ -38,7 +38,9 @@ export const getGameAchievementData = async (
|
||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||
achievements,
|
||||
cacheExpiresTimestamp: Date.now() + 1000 * 60 * 30, // 30 minutes
|
||||
cacheExpiresTimestamp: achievements.length
|
||||
? Date.now() + 1000 * 60 * 30 // 30 minutes
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return achievements;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AchievementNotificationInfo,
|
||||
Game,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
@@ -12,6 +13,13 @@ import { publishNewAchievementNotification } from "../notifications";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||
|
||||
const isRareAchievement = (points: number) => {
|
||||
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
||||
|
||||
return rawPercentage < 10;
|
||||
};
|
||||
|
||||
const saveAchievementsOnLocal = async (
|
||||
objectId: string,
|
||||
@@ -48,12 +56,22 @@ export const mergeAchievements = async (
|
||||
achievements: UnlockedAchievement[],
|
||||
publishNotification: boolean
|
||||
) => {
|
||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
|
||||
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||
let localGameAchievement = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
);
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if (!localGameAchievement) {
|
||||
await getGameAchievementData(game.objectId, game.shop, true);
|
||||
localGameAchievement = await gameAchievementsSublevel.get(
|
||||
levelKeys.game(game.shop, game.objectId)
|
||||
);
|
||||
}
|
||||
|
||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||
@@ -84,9 +102,9 @@ export const mergeAchievements = async (
|
||||
if (
|
||||
newAchievements.length &&
|
||||
publishNotification &&
|
||||
userPreferences?.achievementNotificationsEnabled
|
||||
userPreferences.achievementNotificationsEnabled !== false
|
||||
) {
|
||||
const achievementsInfo = newAchievements
|
||||
const filteredAchievements = newAchievements
|
||||
.toSorted((a, b) => {
|
||||
return a.unlockTime - b.unlockTime;
|
||||
})
|
||||
@@ -98,21 +116,41 @@ export const mergeAchievements = async (
|
||||
);
|
||||
});
|
||||
})
|
||||
.filter((achievement) => Boolean(achievement))
|
||||
.map((achievement) => {
|
||||
.filter((achievement) => !!achievement);
|
||||
|
||||
const achievementsInfo: AchievementNotificationInfo[] =
|
||||
filteredAchievements.map((achievement, index) => {
|
||||
return {
|
||||
displayName: achievement!.displayName,
|
||||
iconUrl: achievement!.icon,
|
||||
title: achievement.displayName,
|
||||
description: achievement.description,
|
||||
points: achievement.points,
|
||||
isHidden: achievement.hidden,
|
||||
isRare: achievement.points
|
||||
? isRareAchievement(achievement.points)
|
||||
: false,
|
||||
isPlatinum:
|
||||
index === filteredAchievements.length - 1 &&
|
||||
newAchievements.length + unlockedAchievements.length ===
|
||||
achievementsData.length,
|
||||
iconUrl: achievement.icon,
|
||||
};
|
||||
});
|
||||
|
||||
publishNewAchievementNotification({
|
||||
achievements: achievementsInfo,
|
||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||
totalAchievementCount: achievementsData.length,
|
||||
gameTitle: game.title,
|
||||
gameIcon: game.iconUrl,
|
||||
});
|
||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||
WindowManager.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left",
|
||||
achievementsInfo
|
||||
);
|
||||
} else {
|
||||
publishNewAchievementNotification({
|
||||
achievements: achievementsInfo,
|
||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
||||
totalAchievementCount: achievementsData.length,
|
||||
gameTitle: game.title,
|
||||
gameIcon: game.iconUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (game.remoteId) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export class HydraApi {
|
||||
subscription: null,
|
||||
};
|
||||
|
||||
private static isLoggedIn() {
|
||||
public static isLoggedIn() {
|
||||
return this.userAuth.authToken !== "";
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ export const uploadGamesBatch = async () => {
|
||||
|
||||
await mergeWithRemoteGames();
|
||||
|
||||
AchievementWatcherManager.preSearchAchievements();
|
||||
if (HydraApi.isLoggedIn()) {
|
||||
AchievementWatcherManager.preSearchAchievements();
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow)
|
||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||
|
||||
@@ -8,19 +8,19 @@ import cp from "node:child_process";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class Ludusavi {
|
||||
private static ludusaviPath = app.isPackaged
|
||||
private static ludusaviResourcesPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi");
|
||||
|
||||
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static binaryPath = path.join(this.configPath, "ludusavi");
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(path.join(this.ludusaviPath, "config.yaml"), "utf-8")
|
||||
fs.readFileSync(path.join(this.configPath, "config.yaml"), "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
@@ -29,13 +29,23 @@ export class Ludusavi {
|
||||
public static async copyConfigFileToUserData() {
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviPath, "config.yaml"),
|
||||
path.join(this.ludusaviResourcesPath, "config.yaml"),
|
||||
path.join(this.configPath, "config.yaml")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async copyBinaryToUserData() {
|
||||
if (!fs.existsSync(this.binaryPath)) {
|
||||
fs.cpSync(
|
||||
path.join(this.ludusaviResourcesPath, "ludusavi"),
|
||||
this.binaryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
|
||||
@@ -162,7 +162,7 @@ export const publishExtractionCompleteNotification = async (game: Game) => {
|
||||
};
|
||||
|
||||
export const publishNewAchievementNotification = async (info: {
|
||||
achievements: { displayName: string; iconUrl: string }[];
|
||||
achievements: { title: string; iconUrl: string }[];
|
||||
unlockedAchievementCount: number;
|
||||
totalAchievementCount: number;
|
||||
gameTitle: string;
|
||||
@@ -176,12 +176,12 @@ export const publishNewAchievementNotification = async (info: {
|
||||
gameTitle: info.gameTitle,
|
||||
achievementCount: info.achievements.length,
|
||||
}),
|
||||
body: info.achievements.map((a) => a.displayName).join(", "),
|
||||
body: info.achievements.map((a) => a.title).join(", "),
|
||||
icon: (await downloadImage(info.gameIcon)) ?? icon,
|
||||
}
|
||||
: {
|
||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
||||
body: info.achievements[0].displayName,
|
||||
body: info.achievements[0].title,
|
||||
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ interface GameExecutables {
|
||||
[key: string]: ExecutableInfo[];
|
||||
}
|
||||
|
||||
const TICKS_TO_UPDATE_API = 120;
|
||||
const TICKS_TO_UPDATE_API = 80;
|
||||
let currentTick = 1;
|
||||
|
||||
const isWindowsPlatform = process.platform === "win32";
|
||||
@@ -225,7 +225,18 @@ function onOpenGame(game: Game) {
|
||||
});
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
|
||||
new Date()
|
||||
)
|
||||
.then(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
@@ -250,13 +261,7 @@ function onTickGame(game: Game) {
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
@@ -266,22 +271,34 @@ function onTickGame(game: Game) {
|
||||
});
|
||||
|
||||
if (currentTick % TICKS_TO_UPDATE_API === 0) {
|
||||
const deltaToSync =
|
||||
now -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
const gamePromise = game.remoteId
|
||||
? updateGamePlaytime(
|
||||
game,
|
||||
now - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
)
|
||||
? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
: createGame(game);
|
||||
|
||||
gamePromise
|
||||
.then(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||
...gamePlaytime,
|
||||
lastSyncTick: now,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,12 +309,6 @@ const onCloseGame = (game: Game) => {
|
||||
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
game,
|
||||
performance.now() - gamePlaytime.lastSyncTick,
|
||||
game.lastTimePlayed!
|
||||
).catch(() => {});
|
||||
|
||||
if (game.automaticCloudSync) {
|
||||
CloudSync.uploadSaveGame(
|
||||
game.objectId,
|
||||
@@ -306,7 +317,38 @@ const onCloseGame = (game: Game) => {
|
||||
CloudSync.getBackupLabel(true)
|
||||
);
|
||||
}
|
||||
|
||||
const deltaToSync =
|
||||
performance.now() -
|
||||
gamePlaytime.lastSyncTick +
|
||||
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
|
||||
|
||||
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
|
||||
.then(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: 0,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
createGame(game).catch(() => {});
|
||||
return createGame(game).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
export const clearGamesPlaytime = async () => {
|
||||
for (const game of gamesPlaytime.keys()) {
|
||||
const gameData = await gamesSublevel.get(game);
|
||||
|
||||
if (gameData) {
|
||||
await onCloseGame(gameData);
|
||||
}
|
||||
}
|
||||
|
||||
gamesPlaytime.clear();
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Tray,
|
||||
app,
|
||||
nativeImage,
|
||||
screen,
|
||||
shell,
|
||||
} from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
@@ -17,12 +18,17 @@ import { HydraApi } from "./hydra-api";
|
||||
import UserAgent from "user-agents";
|
||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||
import { orderBy, slice } from "lodash-es";
|
||||
import type { ScreenState, UserPreferences } from "@types";
|
||||
import { AuthPage } from "@shared";
|
||||
import type {
|
||||
AchievementCustomNotificationPosition,
|
||||
ScreenState,
|
||||
UserPreferences,
|
||||
} from "@types";
|
||||
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
|
||||
import { isStaging } from "@main/constants";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||
|
||||
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
|
||||
|
||||
@@ -259,6 +265,156 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static loadNotificationWindowURL() {
|
||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.notificationWindow?.loadURL(
|
||||
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
||||
);
|
||||
} else {
|
||||
this.notificationWindow?.loadFile(
|
||||
path.join(__dirname, "../renderer/index.html"),
|
||||
{
|
||||
hash: "achievement-notification",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
|
||||
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
|
||||
|
||||
private static async getNotificationWindowPosition(
|
||||
position: AchievementCustomNotificationPosition | undefined
|
||||
) {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const { width, height } = display.workAreaSize;
|
||||
|
||||
if (position === "bottom-left") {
|
||||
return {
|
||||
x: 0,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "bottom-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-center") {
|
||||
return {
|
||||
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (position === "top-right") {
|
||||
return {
|
||||
x: width - this.NOTIFICATION_WINDOW_WIDTH,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public static async createNotificationWindow() {
|
||||
if (this.notificationWindow) return;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | undefined>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
userPreferences?.achievementNotificationsEnabled === false ||
|
||||
userPreferences?.achievementCustomNotificationsEnabled === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = await this.getNotificationWindowPosition(
|
||||
userPreferences?.achievementCustomNotificationPosition
|
||||
);
|
||||
|
||||
this.notificationWindow = new BrowserWindow({
|
||||
transparent: true,
|
||||
maximizable: false,
|
||||
autoHideMenuBar: true,
|
||||
minimizable: false,
|
||||
backgroundColor: "#00000000",
|
||||
focusable: false,
|
||||
skipTaskbar: true,
|
||||
frame: false,
|
||||
width: this.NOTIFICATION_WINDOW_WIDTH,
|
||||
height: this.NOTIFICATION_WINDOW_HEIGHT,
|
||||
x,
|
||||
y,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||
// visibleOnFullScreen: true,
|
||||
// });
|
||||
|
||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
this.loadNotificationWindowURL();
|
||||
|
||||
if (isStaging) {
|
||||
this.notificationWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
public static async showAchievementTestNotification() {
|
||||
const userPreferences = await db.get<string, UserPreferences>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
const language = userPreferences.language ?? "en";
|
||||
|
||||
this.notificationWindow?.webContents.send(
|
||||
"on-achievement-unlocked",
|
||||
userPreferences.achievementCustomNotificationPosition ?? "top-left",
|
||||
[
|
||||
generateAchievementCustomNotificationTest(t, language),
|
||||
generateAchievementCustomNotificationTest(t, language, {
|
||||
isRare: true,
|
||||
isHidden: true,
|
||||
}),
|
||||
generateAchievementCustomNotificationTest(t, language, {
|
||||
isPlatinum: true,
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static async closeNotificationWindow() {
|
||||
if (this.notificationWindow) {
|
||||
this.notificationWindow.close();
|
||||
this.notificationWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static openEditorWindow(themeId: string) {
|
||||
if (this.mainWindow) {
|
||||
const existingWindow = this.editorWindows.get(themeId);
|
||||
@@ -271,13 +427,13 @@ export class WindowManager {
|
||||
}
|
||||
|
||||
const editorWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
width: 720,
|
||||
height: 720,
|
||||
minWidth: 600,
|
||||
minHeight: 540,
|
||||
backgroundColor: "#1c1c1c",
|
||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
||||
...(process.platform === "linux" ? { icon } : {}),
|
||||
icon,
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
titleBarOverlay: {
|
||||
symbolColor: "#DADBE1",
|
||||
@@ -313,9 +469,8 @@ export class WindowManager {
|
||||
}
|
||||
});
|
||||
|
||||
editorWindow.webContents.on("before-input-event", (event, input) => {
|
||||
editorWindow.webContents.on("before-input-event", (_event, input) => {
|
||||
if (input.key === "F12") {
|
||||
event.preventDefault();
|
||||
this.mainWindow?.webContents.toggleDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import type { FriendGameSession } from "@main/generated/envelope";
|
||||
import { db, levelKeys } from "@main/level";
|
||||
import { HydraApi } from "@main/services/hydra-api";
|
||||
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
|
||||
import { GameStats } from "@types";
|
||||
import type { GameStats, UserPreferences, UserProfile } from "@types";
|
||||
|
||||
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
|
||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
valueEncoding: "json",
|
||||
}
|
||||
);
|
||||
|
||||
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
|
||||
|
||||
const [friend, gameStats] = await Promise.all([
|
||||
HydraApi.get(`/users/${payload.friendId}`),
|
||||
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
|
||||
HydraApi.get<GameStats>(
|
||||
`/games/stats?objectId=${payload.objectId}&shop=steam`
|
||||
),
|
||||
]);
|
||||
]).catch(() => [null, null]);
|
||||
|
||||
if (friend && gameStats) {
|
||||
publishFriendStartedPlayingGameNotification(friend, gameStats);
|
||||
|
||||
@@ -34,6 +34,10 @@ export class WSClient {
|
||||
});
|
||||
|
||||
this.ws.on("message", (message) => {
|
||||
if (message.toString() === "PONG") {
|
||||
return;
|
||||
}
|
||||
|
||||
const envelope = Envelope.fromBinary(
|
||||
new Uint8Array(Buffer.from(message.toString()))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user