Feat: Custom Games

This commit is contained in:
Moyasee
2025-09-19 16:18:49 +03:00
parent 2604dfea22
commit 7e59e02d03
28 changed files with 1145 additions and 51 deletions

View File

@@ -14,6 +14,8 @@ import "./catalogue/get-developers";
import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library";
import "./library/add-custom-game-to-library";
import "./library/update-custom-game";
import "./library/add-game-to-favorites";
import "./library/remove-game-from-favorites";
import "./library/create-game-shortcut";

View File

@@ -0,0 +1,68 @@
import { registerEvent } from "../register-event";
import {
gamesSublevel,
gamesShopAssetsSublevel,
levelKeys,
} from "@main/level";
import { randomUUID } from "crypto";
import type { GameShop } from "@types";
const addCustomGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
title: string,
executablePath: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => {
const objectId = randomUUID();
const shop: GameShop = "custom";
const gameKey = levelKeys.game(shop, objectId);
const existingGames = await gamesSublevel.iterator().all();
const existingGame = existingGames.find(([_key, game]) =>
game.executablePath === executablePath && !game.isDeleted
);
if (existingGame) {
throw new Error("A game with this executable path already exists in your library");
}
const assets = {
objectId,
shop,
title,
iconUrl: iconUrl || null,
libraryHeroImageUrl: libraryHeroImageUrl || "",
libraryImageUrl: iconUrl || "",
logoImageUrl: logoImageUrl || "",
logoPosition: null,
coverImageUrl: iconUrl || "",
};
await gamesShopAssetsSublevel.put(gameKey, assets);
const game = {
title,
iconUrl: iconUrl || null,
logoImageUrl: logoImageUrl || null,
libraryHeroImageUrl: libraryHeroImageUrl || null,
objectId,
shop,
remoteId: null,
isDeleted: false,
playTimeInMilliseconds: 0,
lastTimePlayed: null,
executablePath,
launchOptions: null,
favorite: false,
automaticCloudSync: false,
hasManuallyUpdatedPlaytime: false,
};
await gamesSublevel.put(gameKey, game);
return game;
};
registerEvent("addCustomGameToLibrary", addCustomGameToLibrary);

View File

@@ -13,16 +13,20 @@ const changeGamePlaytime = async (
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
playTimeInSeconds,
});
if (game.remoteId) {
await HydraApi.put(`/profile/games/${shop}/${objectId}/playtime`, {
playTimeInSeconds,
});
}
await gamesSublevel.put(gameKey, {
...game,
playTimeInMilliseconds: playTimeInSeconds * 1000,
hasManuallyUpdatedPlaytime: true,
});
} catch (error) {
throw new Error(`Failed to update game favorite status: ${error}`);
throw new Error(`Failed to update game playtime: ${error}`);
}
};

View File

@@ -0,0 +1,53 @@
import { registerEvent } from "../register-event";
import {
gamesSublevel,
gamesShopAssetsSublevel,
levelKeys,
} from "@main/level";
import type { GameShop } from "@types";
const updateCustomGame = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
title: string,
iconUrl?: string,
logoImageUrl?: string,
libraryHeroImageUrl?: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const existingGame = await gamesSublevel.get(gameKey);
if (!existingGame) {
throw new Error("Game not found");
}
const updatedGame = {
...existingGame,
title,
iconUrl: iconUrl || null,
logoImageUrl: logoImageUrl || null,
libraryHeroImageUrl: libraryHeroImageUrl || null,
};
await gamesSublevel.put(gameKey, updatedGame);
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
if (existingAssets) {
const updatedAssets = {
...existingAssets,
title,
iconUrl: iconUrl || null,
libraryHeroImageUrl: libraryHeroImageUrl || "",
libraryImageUrl: iconUrl || "",
logoImageUrl: logoImageUrl || "",
coverImageUrl: iconUrl || "",
};
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
}
return updatedGame;
};
registerEvent("updateCustomGame", updateCustomGame);

View File

@@ -64,6 +64,54 @@ app.whenReady().then(async () => {
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
});
protocol.handle("gradient", (request) => {
const gradientCss = decodeURIComponent(request.url.slice("gradient:".length));
const match = gradientCss.match(/linear-gradient\(([^,]+),\s*([^,]+),\s*([^)]+)\)/);
let direction = "45deg";
let color1 = '#4a90e2';
let color2 = '#7b68ee';
if (match) {
direction = match[1].trim();
color1 = match[2].trim();
color2 = match[3].trim();
}
let x1 = "0%", y1 = "0%", x2 = "100%", y2 = "100%";
if (direction === "to right") {
x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "0%";
} else if (direction === "to bottom") {
x1 = "0%"; y1 = "0%"; x2 = "0%"; y2 = "100%";
} else if (direction === "45deg") {
x1 = "0%"; y1 = "100%"; x2 = "100%"; y2 = "0%";
} else if (direction === "135deg") {
x1 = "0%"; y1 = "0%"; x2 = "100%"; y2 = "100%";
} else if (direction === "225deg") {
x1 = "100%"; y1 = "0%"; x2 = "0%"; y2 = "100%";
} else if (direction === "315deg") {
x1 = "100%"; y1 = "100%"; x2 = "0%"; y2 = "0%";
}
const svgContent = `
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<defs>
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#grad)" />
</svg>
`;
return new Response(svgContent, {
headers: { 'Content-Type': 'image/svg+xml' }
});
});
await loadState();
const language = await db

View File

@@ -44,6 +44,8 @@ export const mergeWithRemoteGames = async () => {
remoteId: game.id,
shop: game.shop,
iconUrl: game.iconUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
logoImageUrl: game.logoImageUrl,
lastTimePlayed: game.lastTimePlayed,
playTimeInMilliseconds: game.playTimeInMilliseconds,
hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime,

View File

@@ -11,7 +11,7 @@ export const uploadGamesBatch = async () => {
.all()
.then((results) => {
return results.filter(
(game) => !game.isDeleted && game.remoteId === null
(game) => !game.isDeleted && game.remoteId === null && game.shop !== "custom"
);
});