mirror of
				https://github.com/hydralauncher/hydra.git
				synced 2025-03-09 15:40:26 +00:00 
			
		
		
		
	feat: adding change hero
This commit is contained in:
		
							parent
							
								
									586df616e8
								
							
						
					
					
						commit
						035e424a76
					
				
					 48 changed files with 520 additions and 500 deletions
				
			
		| 
						 | 
				
			
			@ -30,7 +30,7 @@ const getCatalogue = async (
 | 
			
		|||
        title: steamGame.name,
 | 
			
		||||
        shop: game.shop,
 | 
			
		||||
        cover: steamUrlBuilder.library(game.objectId),
 | 
			
		||||
        objectID: game.objectId,
 | 
			
		||||
        objectId: game.objectId,
 | 
			
		||||
      };
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,16 +7,16 @@ import { registerEvent } from "../register-event";
 | 
			
		|||
import { steamGamesWorker } from "@main/workers";
 | 
			
		||||
 | 
			
		||||
const getLocalizedSteamAppDetails = async (
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  language: string
 | 
			
		||||
): Promise<ShopDetails | null> => {
 | 
			
		||||
  if (language === "english") {
 | 
			
		||||
    return getSteamAppDetails(objectID, language);
 | 
			
		||||
    return getSteamAppDetails(objectId, language);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return getSteamAppDetails(objectID, language).then(
 | 
			
		||||
  return getSteamAppDetails(objectId, language).then(
 | 
			
		||||
    async (localizedAppDetails) => {
 | 
			
		||||
      const steamGame = await steamGamesWorker.run(Number(objectID), {
 | 
			
		||||
      const steamGame = await steamGamesWorker.run(Number(objectId), {
 | 
			
		||||
        name: "getById",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,21 +34,21 @@ const getLocalizedSteamAppDetails = async (
 | 
			
		|||
 | 
			
		||||
const getGameShopDetails = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  shop: GameShop,
 | 
			
		||||
  language: string
 | 
			
		||||
): Promise<ShopDetails | null> => {
 | 
			
		||||
  if (shop === "steam") {
 | 
			
		||||
    const cachedData = await gameShopCacheRepository.findOne({
 | 
			
		||||
      where: { objectID, language },
 | 
			
		||||
      where: { objectID: objectId, language },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
 | 
			
		||||
    const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
 | 
			
		||||
      (result) => {
 | 
			
		||||
        if (result) {
 | 
			
		||||
          gameShopCacheRepository.upsert(
 | 
			
		||||
            {
 | 
			
		||||
              objectID,
 | 
			
		||||
              objectID: objectId,
 | 
			
		||||
              shop: "steam",
 | 
			
		||||
              language,
 | 
			
		||||
              serializedData: JSON.stringify(result),
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ const getGameShopDetails = async (
 | 
			
		|||
    if (cachedGame) {
 | 
			
		||||
      return {
 | 
			
		||||
        ...cachedGame,
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectId,
 | 
			
		||||
      } as ShopDetails;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,28 +1,29 @@
 | 
			
		|||
import type { CatalogueEntry } from "@types";
 | 
			
		||||
 | 
			
		||||
import { registerEvent } from "../register-event";
 | 
			
		||||
import { steamGamesWorker } from "@main/workers";
 | 
			
		||||
import { HydraApi } from "@main/services";
 | 
			
		||||
import { steamUrlBuilder } from "@shared";
 | 
			
		||||
 | 
			
		||||
const getGames = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  take = 12,
 | 
			
		||||
  cursor = 0
 | 
			
		||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
 | 
			
		||||
  const steamGames = await steamGamesWorker.run(
 | 
			
		||||
    { limit: take, offset: cursor },
 | 
			
		||||
    { name: "list" }
 | 
			
		||||
  skip = 0
 | 
			
		||||
): Promise<CatalogueEntry[]> => {
 | 
			
		||||
  const searchParams = new URLSearchParams({
 | 
			
		||||
    take: take.toString(),
 | 
			
		||||
    skip: skip.toString(),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const games = await HydraApi.get<CatalogueEntry[]>(
 | 
			
		||||
    `/games/catalogue?${searchParams.toString()}`,
 | 
			
		||||
    undefined,
 | 
			
		||||
    { needsAuth: false }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    results: steamGames.map((steamGame) => ({
 | 
			
		||||
      title: steamGame.name,
 | 
			
		||||
      shop: "steam",
 | 
			
		||||
      cover: steamUrlBuilder.library(steamGame.id),
 | 
			
		||||
      objectID: steamGame.id,
 | 
			
		||||
    })),
 | 
			
		||||
    cursor: cursor + steamGames.length,
 | 
			
		||||
  };
 | 
			
		||||
  return games.map((game) => ({
 | 
			
		||||
    ...game,
 | 
			
		||||
    cover: steamUrlBuilder.library(game.objectId),
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
registerEvent("getGames", getGames);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,14 @@ import { gameShopCacheRepository } from "@main/repository";
 | 
			
		|||
 | 
			
		||||
const getHowLongToBeat = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  shop: GameShop,
 | 
			
		||||
  title: string
 | 
			
		||||
): Promise<HowLongToBeatCategory[] | null> => {
 | 
			
		||||
  const searchHowLongToBeatPromise = searchHowLongToBeat(title);
 | 
			
		||||
 | 
			
		||||
  const gameShopCache = await gameShopCacheRepository.findOne({
 | 
			
		||||
    where: { objectID, shop },
 | 
			
		||||
    where: { objectID: objectId, shop },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const howLongToBeatCachedData = gameShopCache?.howLongToBeatSerializedData
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ const getHowLongToBeat = async (
 | 
			
		|||
 | 
			
		||||
  return searchHowLongToBeatPromise.then(async (response) => {
 | 
			
		||||
    const game = response.data.find(
 | 
			
		||||
      (game) => game.profile_steam === Number(objectID)
 | 
			
		||||
      (game) => game.profile_steam === Number(objectId)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!game) return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ const getHowLongToBeat = async (
 | 
			
		|||
 | 
			
		||||
    gameShopCacheRepository.upsert(
 | 
			
		||||
      {
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
        shop,
 | 
			
		||||
        howLongToBeatSerializedData: JSON.stringify(howLongToBeat),
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,9 @@ import { registerEvent } from "../register-event";
 | 
			
		|||
const deleteGameArtifact = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  gameArtifactId: string
 | 
			
		||||
) => HydraApi.delete<{ ok: boolean }>(`/games/artifacts/${gameArtifactId}`);
 | 
			
		||||
) =>
 | 
			
		||||
  HydraApi.delete<{ ok: boolean }>(
 | 
			
		||||
    `/profile/games/artifacts/${gameArtifactId}`
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
registerEvent("deleteGameArtifact", deleteGameArtifact);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import AdmZip from "adm-zip";
 | 
			
		||||
import * as tar from "tar";
 | 
			
		||||
import { registerEvent } from "../register-event";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { app } from "electron";
 | 
			
		||||
| 
						 | 
				
			
			@ -8,16 +8,54 @@ import path from "node:path";
 | 
			
		|||
import { backupsPath } from "@main/constants";
 | 
			
		||||
import type { GameShop } from "@types";
 | 
			
		||||
 | 
			
		||||
import YAML from "yaml";
 | 
			
		||||
 | 
			
		||||
export interface LudusaviBackup {
 | 
			
		||||
  files: {
 | 
			
		||||
    [key: string]: {
 | 
			
		||||
      hash: string;
 | 
			
		||||
      size: number;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const replaceLudusaviBackupWithCurrentUser = (
 | 
			
		||||
  mappingPath: string,
 | 
			
		||||
  backupHomeDir: string
 | 
			
		||||
) => {
 | 
			
		||||
  const data = fs.readFileSync(mappingPath, "utf8");
 | 
			
		||||
  const manifest = YAML.parse(data);
 | 
			
		||||
 | 
			
		||||
  const currentHomeDir = app.getPath("home");
 | 
			
		||||
 | 
			
		||||
  const backups = manifest.backups.map((backup: LudusaviBackup) => {
 | 
			
		||||
    const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...prev,
 | 
			
		||||
        [key.replace(backupHomeDir, currentHomeDir)]: value,
 | 
			
		||||
      };
 | 
			
		||||
    }, {});
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      ...backup,
 | 
			
		||||
      files,
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(mappingPath, YAML.stringify({ ...manifest, backups }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const downloadGameArtifact = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  shop: GameShop,
 | 
			
		||||
  gameArtifactId: string
 | 
			
		||||
) => {
 | 
			
		||||
  const { downloadUrl, objectKey } = await HydraApi.post<{
 | 
			
		||||
  const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
 | 
			
		||||
    downloadUrl: string;
 | 
			
		||||
    objectKey: string;
 | 
			
		||||
  }>(`/games/artifacts/${gameArtifactId}/download`);
 | 
			
		||||
    homeDir: string;
 | 
			
		||||
  }>(`/profile/games/artifacts/${gameArtifactId}/download`);
 | 
			
		||||
 | 
			
		||||
  const zipLocation = path.join(app.getPath("userData"), objectKey);
 | 
			
		||||
  const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -42,20 +80,31 @@ const downloadGameArtifact = async (
 | 
			
		|||
  });
 | 
			
		||||
 | 
			
		||||
  writer.on("close", () => {
 | 
			
		||||
    const zip = new AdmZip(zipLocation);
 | 
			
		||||
    zip.extractAllToAsync(backupPath, true, true, (err) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        logger.error("Failed to extract zip", err);
 | 
			
		||||
        throw err;
 | 
			
		||||
      }
 | 
			
		||||
    tar
 | 
			
		||||
      .x({
 | 
			
		||||
        file: zipLocation,
 | 
			
		||||
        cwd: backupPath,
 | 
			
		||||
      })
 | 
			
		||||
      .then(async () => {
 | 
			
		||||
        const [game] = await Ludusavi.findGames(shop, objectId);
 | 
			
		||||
        if (!game) throw new Error("Game not found in Ludusavi manifest");
 | 
			
		||||
 | 
			
		||||
      Ludusavi.restoreBackup(backupPath).then(() => {
 | 
			
		||||
        WindowManager.mainWindow?.webContents.send(
 | 
			
		||||
          `on-backup-download-complete-${objectId}-${shop}`,
 | 
			
		||||
          true
 | 
			
		||||
        const mappingPath = path.join(
 | 
			
		||||
          backupsPath,
 | 
			
		||||
          `${shop}-${objectId}`,
 | 
			
		||||
          game,
 | 
			
		||||
          "mapping.yaml"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        replaceLudusaviBackupWithCurrentUser(mappingPath, homeDir);
 | 
			
		||||
 | 
			
		||||
        Ludusavi.restoreBackup(backupPath).then(() => {
 | 
			
		||||
          WindowManager.mainWindow?.webContents.send(
 | 
			
		||||
            `on-backup-download-complete-${objectId}-${shop}`,
 | 
			
		||||
            true
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,9 @@ const getGameArtifacts = async (
 | 
			
		|||
    shop,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return HydraApi.get<GameArtifact[]>(`/games/artifacts?${params.toString()}`);
 | 
			
		||||
  return HydraApi.get<GameArtifact[]>(
 | 
			
		||||
    `/profile/games/artifacts?${params.toString()}`
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
registerEvent("getGameArtifacts", getGameArtifacts);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,47 +2,31 @@ import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
 | 
			
		|||
import { registerEvent } from "../register-event";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import archiver from "archiver";
 | 
			
		||||
import * as tar from "tar";
 | 
			
		||||
import crypto from "node:crypto";
 | 
			
		||||
import { GameShop } from "@types";
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import os from "node:os";
 | 
			
		||||
import { app } from "electron";
 | 
			
		||||
import { backupsPath } from "@main/constants";
 | 
			
		||||
import { app } from "electron";
 | 
			
		||||
 | 
			
		||||
const compressBackupToArtifact = async (
 | 
			
		||||
  shop: GameShop,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  cb: (zipLocation: string) => void
 | 
			
		||||
) => {
 | 
			
		||||
const bundleBackup = async (shop: GameShop, objectId: string) => {
 | 
			
		||||
  const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
 | 
			
		||||
 | 
			
		||||
  await Ludusavi.backupGame(shop, objectId, backupPath);
 | 
			
		||||
 | 
			
		||||
  const archive = archiver("zip", {
 | 
			
		||||
    zlib: { level: 9 },
 | 
			
		||||
  });
 | 
			
		||||
  const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.zip`);
 | 
			
		||||
 | 
			
		||||
  const zipLocation = path.join(
 | 
			
		||||
    app.getPath("userData"),
 | 
			
		||||
    `${crypto.randomUUID()}.zip`
 | 
			
		||||
  await tar.create(
 | 
			
		||||
    {
 | 
			
		||||
      gzip: false,
 | 
			
		||||
      file: tarLocation,
 | 
			
		||||
      cwd: backupPath,
 | 
			
		||||
    },
 | 
			
		||||
    ["."]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const output = fs.createWriteStream(zipLocation);
 | 
			
		||||
 | 
			
		||||
  output.on("close", () => {
 | 
			
		||||
    cb(zipLocation);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  output.on("error", (err) => {
 | 
			
		||||
    logger.error("Failed to compress folder", err);
 | 
			
		||||
    throw err;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  archive.pipe(output);
 | 
			
		||||
 | 
			
		||||
  archive.directory(backupPath, false);
 | 
			
		||||
  archive.finalize();
 | 
			
		||||
  return tarLocation;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const uploadSaveGame = async (
 | 
			
		||||
| 
						 | 
				
			
			@ -50,49 +34,51 @@ const uploadSaveGame = async (
 | 
			
		|||
  objectId: string,
 | 
			
		||||
  shop: GameShop
 | 
			
		||||
) => {
 | 
			
		||||
  compressBackupToArtifact(shop, objectId, (zipLocation) => {
 | 
			
		||||
    fs.stat(zipLocation, async (err, stat) => {
 | 
			
		||||
  const bundleLocation = await bundleBackup(shop, objectId);
 | 
			
		||||
 | 
			
		||||
  fs.stat(bundleLocation, async (err, stat) => {
 | 
			
		||||
    if (err) {
 | 
			
		||||
      logger.error("Failed to get zip file stats", err);
 | 
			
		||||
      throw err;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { uploadUrl } = await HydraApi.post<{
 | 
			
		||||
      id: string;
 | 
			
		||||
      uploadUrl: string;
 | 
			
		||||
    }>("/profile/games/artifacts", {
 | 
			
		||||
      artifactLengthInBytes: stat.size,
 | 
			
		||||
      shop,
 | 
			
		||||
      objectId,
 | 
			
		||||
      hostname: os.hostname(),
 | 
			
		||||
      homeDir: app.getPath("home"),
 | 
			
		||||
      platform: os.platform(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    fs.readFile(bundleLocation, async (err, fileBuffer) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        logger.error("Failed to get zip file stats", err);
 | 
			
		||||
        logger.error("Failed to read zip file", err);
 | 
			
		||||
        throw err;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { uploadUrl } = await HydraApi.post<{
 | 
			
		||||
        id: string;
 | 
			
		||||
        uploadUrl: string;
 | 
			
		||||
      }>("/games/artifacts", {
 | 
			
		||||
        artifactLengthInBytes: stat.size,
 | 
			
		||||
        shop,
 | 
			
		||||
        objectId,
 | 
			
		||||
        hostname: os.hostname(),
 | 
			
		||||
      await axios.put(uploadUrl, fileBuffer, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          "Content-Type": "application/tar",
 | 
			
		||||
        },
 | 
			
		||||
        onUploadProgress: (progressEvent) => {
 | 
			
		||||
          console.log(progressEvent);
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      fs.readFile(zipLocation, async (err, fileBuffer) => {
 | 
			
		||||
      WindowManager.mainWindow?.webContents.send(
 | 
			
		||||
        `on-upload-complete-${objectId}-${shop}`,
 | 
			
		||||
        true
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      fs.rm(bundleLocation, (err) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          logger.error("Failed to read zip file", err);
 | 
			
		||||
          logger.error("Failed to remove tar file", err);
 | 
			
		||||
          throw err;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await axios.put(uploadUrl, fileBuffer, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            "Content-Type": "application/zip",
 | 
			
		||||
          },
 | 
			
		||||
          onUploadProgress: (progressEvent) => {
 | 
			
		||||
            console.log(progressEvent);
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        WindowManager.mainWindow?.webContents.send(
 | 
			
		||||
          `on-upload-complete-${objectId}-${shop}`,
 | 
			
		||||
          true
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fs.rm(zipLocation, (err) => {
 | 
			
		||||
          if (err) {
 | 
			
		||||
            logger.error("Failed to remove zip file", err);
 | 
			
		||||
            throw err;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ export interface SearchGamesArgs {
 | 
			
		|||
export const convertSteamGameToCatalogueEntry = (
 | 
			
		||||
  game: SteamGame
 | 
			
		||||
): CatalogueEntry => ({
 | 
			
		||||
  objectID: String(game.id),
 | 
			
		||||
  objectId: String(game.id),
 | 
			
		||||
  title: game.name,
 | 
			
		||||
  shop: "steam" as GameShop,
 | 
			
		||||
  cover: steamUrlBuilder.library(String(game.id)),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,14 +10,14 @@ import { steamUrlBuilder } from "@shared";
 | 
			
		|||
 | 
			
		||||
const addGameToLibrary = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  title: string,
 | 
			
		||||
  shop: GameShop
 | 
			
		||||
) => {
 | 
			
		||||
  return gameRepository
 | 
			
		||||
    .update(
 | 
			
		||||
      {
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        shop,
 | 
			
		||||
| 
						 | 
				
			
			@ -27,23 +27,25 @@ const addGameToLibrary = async (
 | 
			
		|||
    )
 | 
			
		||||
    .then(async ({ affected }) => {
 | 
			
		||||
      if (!affected) {
 | 
			
		||||
        const steamGame = await steamGamesWorker.run(Number(objectID), {
 | 
			
		||||
        const steamGame = await steamGamesWorker.run(Number(objectId), {
 | 
			
		||||
          name: "getById",
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const iconUrl = steamGame?.clientIcon
 | 
			
		||||
          ? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
 | 
			
		||||
          ? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
 | 
			
		||||
          : null;
 | 
			
		||||
 | 
			
		||||
        await gameRepository.insert({
 | 
			
		||||
          title,
 | 
			
		||||
          iconUrl,
 | 
			
		||||
          objectID,
 | 
			
		||||
          objectID: objectId,
 | 
			
		||||
          shop,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const game = await gameRepository.findOne({ where: { objectID } });
 | 
			
		||||
      const game = await gameRepository.findOne({
 | 
			
		||||
        where: { objectID: objectId },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      createGame(game!).catch(() => {});
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,15 +2,15 @@ import { gameRepository } from "@main/repository";
 | 
			
		|||
 | 
			
		||||
import { registerEvent } from "../register-event";
 | 
			
		||||
 | 
			
		||||
const getGameByObjectID = async (
 | 
			
		||||
const getGameByObjectId = async (
 | 
			
		||||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  objectID: string
 | 
			
		||||
  objectId: string
 | 
			
		||||
) =>
 | 
			
		||||
  gameRepository.findOne({
 | 
			
		||||
    where: {
 | 
			
		||||
      objectID,
 | 
			
		||||
      objectID: objectId,
 | 
			
		||||
      isDeleted: false,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
registerEvent("getGameByObjectID", getGameByObjectID);
 | 
			
		||||
registerEvent("getGameByObjectId", getGameByObjectId);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ const startGameDownload = async (
 | 
			
		|||
  _event: Electron.IpcMainInvokeEvent,
 | 
			
		||||
  payload: StartGameDownloadPayload
 | 
			
		||||
) => {
 | 
			
		||||
  const { objectID, title, shop, downloadPath, downloader, uri } = payload;
 | 
			
		||||
  const { objectId, title, shop, downloadPath, downloader, uri } = payload;
 | 
			
		||||
 | 
			
		||||
  return dataSource.transaction(async (transactionalEntityManager) => {
 | 
			
		||||
    const gameRepository = transactionalEntityManager.getRepository(Game);
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ const startGameDownload = async (
 | 
			
		|||
 | 
			
		||||
    const game = await gameRepository.findOne({
 | 
			
		||||
      where: {
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
        shop,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -51,18 +51,18 @@ const startGameDownload = async (
 | 
			
		|||
        }
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      const steamGame = await steamGamesWorker.run(Number(objectID), {
 | 
			
		||||
      const steamGame = await steamGamesWorker.run(Number(objectId), {
 | 
			
		||||
        name: "getById",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const iconUrl = steamGame?.clientIcon
 | 
			
		||||
        ? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
 | 
			
		||||
        ? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
 | 
			
		||||
        : null;
 | 
			
		||||
 | 
			
		||||
      await gameRepository.insert({
 | 
			
		||||
        title,
 | 
			
		||||
        iconUrl,
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
        downloader,
 | 
			
		||||
        shop,
 | 
			
		||||
        status: "active",
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ const startGameDownload = async (
 | 
			
		|||
 | 
			
		||||
    const updatedGame = await gameRepository.findOne({
 | 
			
		||||
      where: {
 | 
			
		||||
        objectID,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import { IsNull, Not } from "typeorm";
 | 
			
		|||
import { gameRepository } from "@main/repository";
 | 
			
		||||
import { WindowManager } from "./window-manager";
 | 
			
		||||
import { createGame, updateGamePlaytime } from "./library-sync";
 | 
			
		||||
import { GameRunning } from "@types";
 | 
			
		||||
import type { GameRunning } from "@types";
 | 
			
		||||
import { PythonInstance } from "./download";
 | 
			
		||||
import { Game } from "@main/entity";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ export const requestSteam250 = async (path: string) => {
 | 
			
		|||
 | 
			
		||||
          return {
 | 
			
		||||
            title: $title.textContent,
 | 
			
		||||
            objectID: steamGameUrl.split("/").pop(),
 | 
			
		||||
            objectId: steamGameUrl.split("/").pop(),
 | 
			
		||||
          } as Steam250Game;
 | 
			
		||||
        })
 | 
			
		||||
        .filter((game) => game != null);
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ export const getSteam250List = async () => {
 | 
			
		|||
  ).flat();
 | 
			
		||||
 | 
			
		||||
  const gamesMap: Map<string, Steam250Game> = gamesList.reduce((map, item) => {
 | 
			
		||||
    if (item) map.set(item.objectID, item);
 | 
			
		||||
    if (item) map.set(item.objectId, item);
 | 
			
		||||
 | 
			
		||||
    return map;
 | 
			
		||||
  }, new Map());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ export interface SteamGridGameResponse {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export const getSteamGridData = async (
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  path: string,
 | 
			
		||||
  shop: GameShop,
 | 
			
		||||
  params: Record<string, string> = {}
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ export const getSteamGridData = async (
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  const response = await axios.get(
 | 
			
		||||
    `https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectID}?${searchParams.toString()}`,
 | 
			
		||||
    `https://www.steamgriddb.com/api/v2/${path}/${shop}/${objectId}?${searchParams.toString()}`,
 | 
			
		||||
    {
 | 
			
		||||
      headers: {
 | 
			
		||||
        Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -59,10 +59,10 @@ export const getSteamGridGameById = async (
 | 
			
		|||
  return response.data;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getSteamGameClientIcon = async (objectID: string) => {
 | 
			
		||||
export const getSteamGameClientIcon = async (objectId: string) => {
 | 
			
		||||
  const {
 | 
			
		||||
    data: { id: steamGridGameId },
 | 
			
		||||
  } = await getSteamGridData(objectID, "games", "steam");
 | 
			
		||||
  } = await getSteamGridData(objectId, "games", "steam");
 | 
			
		||||
 | 
			
		||||
  const steamGridGame = await getSteamGridGameById(steamGridGameId);
 | 
			
		||||
  return steamGridGame.data.platforms.steam.metadata.clienticon;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,11 +12,11 @@ export interface SteamAppDetailsResponse {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export const getSteamAppDetails = async (
 | 
			
		||||
  objectID: string,
 | 
			
		||||
  objectId: string,
 | 
			
		||||
  language: string
 | 
			
		||||
) => {
 | 
			
		||||
  const searchParams = new URLSearchParams({
 | 
			
		||||
    appids: objectID,
 | 
			
		||||
    appids: objectId,
 | 
			
		||||
    l: language,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ export const getSteamAppDetails = async (
 | 
			
		|||
      `http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
 | 
			
		||||
    )
 | 
			
		||||
    .then((response) => {
 | 
			
		||||
      if (response.data[objectID].success) return response.data[objectID].data;
 | 
			
		||||
      if (response.data[objectId].success) return response.data[objectId].data;
 | 
			
		||||
      return null;
 | 
			
		||||
    })
 | 
			
		||||
    .catch((err) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,20 +28,18 @@ export const backupGame = ({
 | 
			
		|||
  title,
 | 
			
		||||
  backupPath,
 | 
			
		||||
  preview = false,
 | 
			
		||||
  winePrefix,
 | 
			
		||||
}: {
 | 
			
		||||
  title: string;
 | 
			
		||||
  backupPath: string;
 | 
			
		||||
  preview?: boolean;
 | 
			
		||||
  winePrefix?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  const args = ["backup", title, "--api", "--force"];
 | 
			
		||||
 | 
			
		||||
  if (preview) {
 | 
			
		||||
    args.push("--preview");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (backupPath) {
 | 
			
		||||
    args.push("--path", backupPath);
 | 
			
		||||
  }
 | 
			
		||||
  if (preview) args.push("--preview");
 | 
			
		||||
  if (backupPath) args.push("--path", backupPath);
 | 
			
		||||
  if (winePrefix) args.push("--wine-prefix", winePrefix);
 | 
			
		||||
 | 
			
		||||
  const result = cp.execFileSync(binaryPath, args);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,3 +57,5 @@ export const restoreBackup = (backupPath: string) => {
 | 
			
		|||
 | 
			
		||||
  return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// --wine-prefix
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,13 +38,13 @@ contextBridge.exposeInMainWorld("electron", {
 | 
			
		|||
  searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
 | 
			
		||||
  getCatalogue: (category: CatalogueCategory) =>
 | 
			
		||||
    ipcRenderer.invoke("getCatalogue", category),
 | 
			
		||||
  getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
 | 
			
		||||
  getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
 | 
			
		||||
  getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
 | 
			
		||||
  getHowLongToBeat: (objectID: string, shop: GameShop, title: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
 | 
			
		||||
  getGames: (take?: number, prevCursor?: number) =>
 | 
			
		||||
    ipcRenderer.invoke("getGames", take, prevCursor),
 | 
			
		||||
  getHowLongToBeat: (objectId: string, shop: GameShop, title: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getHowLongToBeat", objectId, shop, title),
 | 
			
		||||
  getGames: (take?: number, skip?: number) =>
 | 
			
		||||
    ipcRenderer.invoke("getGames", take, skip),
 | 
			
		||||
  searchGameRepacks: (query: string) =>
 | 
			
		||||
    ipcRenderer.invoke("searchGameRepacks", query),
 | 
			
		||||
  getGameStats: (objectId: string, shop: GameShop) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
 | 
			
		|||
    ipcRenderer.invoke("deleteDownloadSource", id),
 | 
			
		||||
 | 
			
		||||
  /* Library */
 | 
			
		||||
  addGameToLibrary: (objectID: string, title: string, shop: GameShop) =>
 | 
			
		||||
    ipcRenderer.invoke("addGameToLibrary", objectID, title, shop),
 | 
			
		||||
  addGameToLibrary: (objectId: string, title: string, shop: GameShop) =>
 | 
			
		||||
    ipcRenderer.invoke("addGameToLibrary", objectId, title, shop),
 | 
			
		||||
  createGameShortcut: (id: number) =>
 | 
			
		||||
    ipcRenderer.invoke("createGameShortcut", id),
 | 
			
		||||
  updateExecutablePath: (id: number, executablePath: string) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -88,8 +88,8 @@ contextBridge.exposeInMainWorld("electron", {
 | 
			
		|||
  removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId),
 | 
			
		||||
  deleteGameFolder: (gameId: number) =>
 | 
			
		||||
    ipcRenderer.invoke("deleteGameFolder", gameId),
 | 
			
		||||
  getGameByObjectID: (objectID: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getGameByObjectID", objectID),
 | 
			
		||||
  getGameByObjectId: (objectId: string) =>
 | 
			
		||||
    ipcRenderer.invoke("getGameByObjectId", objectId),
 | 
			
		||||
  onGamesRunning: (
 | 
			
		||||
    cb: (
 | 
			
		||||
      gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,10 @@ globalStyle("::-webkit-scrollbar-thumb", {
 | 
			
		|||
  borderRadius: "24px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
globalStyle("::-webkit-scrollbar-thumb:hover", {
 | 
			
		||||
  backgroundColor: "rgba(255, 255, 255, 0.16)",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
globalStyle("html, body, #root, main", {
 | 
			
		||||
  height: "100%",
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
 | 
			
		|||
 | 
			
		||||
  const handleHover = useCallback(() => {
 | 
			
		||||
    if (!stats) {
 | 
			
		||||
      window.electron.getGameStats(game.objectID, game.shop).then((stats) => {
 | 
			
		||||
      window.electron.getGameStats(game.objectId, game.shop).then((stats) => {
 | 
			
		||||
        setStats(stats);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -140,7 +140,10 @@ export function Sidebar() {
 | 
			
		|||
    event: React.MouseEvent,
 | 
			
		||||
    game: LibraryGame
 | 
			
		||||
  ) => {
 | 
			
		||||
    const path = buildGameDetailsPath(game);
 | 
			
		||||
    const path = buildGameDetailsPath({
 | 
			
		||||
      ...game,
 | 
			
		||||
      objectId: game.objectID,
 | 
			
		||||
    });
 | 
			
		||||
    if (path !== location.pathname) {
 | 
			
		||||
      navigate(path);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
 | 
			
		|||
  gameTitle: "",
 | 
			
		||||
  isGameRunning: false,
 | 
			
		||||
  isLoading: false,
 | 
			
		||||
  objectID: undefined,
 | 
			
		||||
  objectId: undefined,
 | 
			
		||||
  gameColor: "",
 | 
			
		||||
  showRepacksModal: false,
 | 
			
		||||
  showGameOptionsModal: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +97,7 @@ export function GameDetailsContextProvider({
 | 
			
		|||
 | 
			
		||||
  const updateGame = useCallback(async () => {
 | 
			
		||||
    return window.electron
 | 
			
		||||
      .getGameByObjectID(objectId!)
 | 
			
		||||
      .getGameByObjectId(objectId!)
 | 
			
		||||
      .then((result) => setGame(result));
 | 
			
		||||
  }, [setGame, objectId]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -199,7 +199,7 @@ export function GameDetailsContextProvider({
 | 
			
		|||
        gameTitle,
 | 
			
		||||
        isGameRunning,
 | 
			
		||||
        isLoading,
 | 
			
		||||
        objectID: objectId,
 | 
			
		||||
        objectId,
 | 
			
		||||
        gameColor,
 | 
			
		||||
        showGameOptionsModal,
 | 
			
		||||
        showRepacksModal,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ export interface GameDetailsContext {
 | 
			
		|||
  gameTitle: string;
 | 
			
		||||
  isGameRunning: boolean;
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
  objectID: string | undefined;
 | 
			
		||||
  objectId: string | undefined;
 | 
			
		||||
  gameColor: string;
 | 
			
		||||
  showRepacksModal: boolean;
 | 
			
		||||
  showGameOptionsModal: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,7 @@ export function RepacksContextProvider({ children }: RepacksContextProps) {
 | 
			
		|||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log("CALLED");
 | 
			
		||||
    indexRepacks();
 | 
			
		||||
  }, [indexRepacks]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										13
									
								
								src/renderer/src/declaration.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								src/renderer/src/declaration.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -51,27 +51,24 @@ declare global {
 | 
			
		|||
    searchGames: (query: string) => Promise<CatalogueEntry[]>;
 | 
			
		||||
    getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
 | 
			
		||||
    getGameShopDetails: (
 | 
			
		||||
      objectID: string,
 | 
			
		||||
      objectId: string,
 | 
			
		||||
      shop: GameShop,
 | 
			
		||||
      language: string
 | 
			
		||||
    ) => Promise<ShopDetails | null>;
 | 
			
		||||
    getRandomGame: () => Promise<Steam250Game>;
 | 
			
		||||
    getHowLongToBeat: (
 | 
			
		||||
      objectID: string,
 | 
			
		||||
      objectId: string,
 | 
			
		||||
      shop: GameShop,
 | 
			
		||||
      title: string
 | 
			
		||||
    ) => Promise<HowLongToBeatCategory[] | null>;
 | 
			
		||||
    getGames: (
 | 
			
		||||
      take?: number,
 | 
			
		||||
      prevCursor?: number
 | 
			
		||||
    ) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
 | 
			
		||||
    getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
 | 
			
		||||
    searchGameRepacks: (query: string) => Promise<GameRepack[]>;
 | 
			
		||||
    getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
 | 
			
		||||
    getTrendingGames: () => Promise<TrendingGame[]>;
 | 
			
		||||
 | 
			
		||||
    /* Library */
 | 
			
		||||
    addGameToLibrary: (
 | 
			
		||||
      objectID: string,
 | 
			
		||||
      objectId: string,
 | 
			
		||||
      title: string,
 | 
			
		||||
      shop: GameShop
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +84,7 @@ declare global {
 | 
			
		|||
    removeGameFromLibrary: (gameId: number) => Promise<void>;
 | 
			
		||||
    removeGame: (gameId: number) => Promise<void>;
 | 
			
		||||
    deleteGameFolder: (gameId: number) => Promise<unknown>;
 | 
			
		||||
    getGameByObjectID: (objectID: string) => Promise<Game | null>;
 | 
			
		||||
    getGameByObjectId: (objectId: string) => Promise<Game | null>;
 | 
			
		||||
    onGamesRunning: (
 | 
			
		||||
      cb: (
 | 
			
		||||
        gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,11 +27,11 @@ export const getSteamLanguage = (language: string) => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const buildGameDetailsPath = (
 | 
			
		||||
  game: { shop: GameShop; objectID: string; title: string },
 | 
			
		||||
  game: { shop: GameShop; objectId: string; title: string },
 | 
			
		||||
  params: Record<string, string> = {}
 | 
			
		||||
) => {
 | 
			
		||||
  const searchParams = new URLSearchParams({ title: game.title, ...params });
 | 
			
		||||
  return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
 | 
			
		||||
  return `/game/${game.shop}/${game.objectId}?${searchParams.toString()}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
 | 
			
		|||
              <Route path="/" Component={Home} />
 | 
			
		||||
              <Route path="/catalogue" Component={Catalogue} />
 | 
			
		||||
              <Route path="/downloads" Component={Downloads} />
 | 
			
		||||
              <Route path="/game/:shop/:objectID" Component={GameDetails} />
 | 
			
		||||
              <Route path="/game/:shop/:objectId" Component={GameDetails} />
 | 
			
		||||
              <Route path="/search" Component={SearchResults} />
 | 
			
		||||
              <Route path="/settings" Component={Settings} />
 | 
			
		||||
              <Route path="/profile/:userId" Component={Profile} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,12 +24,10 @@ export function Catalogue() {
 | 
			
		|||
 | 
			
		||||
  const contentRef = useRef<HTMLElement>(null);
 | 
			
		||||
 | 
			
		||||
  const cursorRef = useRef<number>(0);
 | 
			
		||||
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
  const cursor = Number(searchParams.get("cursor") ?? 0);
 | 
			
		||||
  const skip = Number(searchParams.get("skip") ?? 0);
 | 
			
		||||
 | 
			
		||||
  const handleGameClick = (game: CatalogueEntry) => {
 | 
			
		||||
    dispatch(clearSearch());
 | 
			
		||||
| 
						 | 
				
			
			@ -42,11 +40,10 @@ export function Catalogue() {
 | 
			
		|||
    setSearchResults([]);
 | 
			
		||||
 | 
			
		||||
    window.electron
 | 
			
		||||
      .getGames(24, cursor)
 | 
			
		||||
      .then(({ results, cursor }) => {
 | 
			
		||||
      .getGames(24, skip)
 | 
			
		||||
      .then((results) => {
 | 
			
		||||
        return new Promise((resolve) => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            cursorRef.current = cursor;
 | 
			
		||||
            setSearchResults(results);
 | 
			
		||||
            resolve(null);
 | 
			
		||||
          }, 500);
 | 
			
		||||
| 
						 | 
				
			
			@ -55,11 +52,11 @@ export function Catalogue() {
 | 
			
		|||
      .finally(() => {
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
      });
 | 
			
		||||
  }, [dispatch, cursor, searchParams]);
 | 
			
		||||
  }, [dispatch, skip, searchParams]);
 | 
			
		||||
 | 
			
		||||
  const handleNextPage = () => {
 | 
			
		||||
    const params = new URLSearchParams({
 | 
			
		||||
      cursor: cursorRef.current.toString(),
 | 
			
		||||
      skip: String(skip + 24),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    navigate(`/catalogue?${params.toString()}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +77,7 @@ export function Catalogue() {
 | 
			
		|||
        <Button
 | 
			
		||||
          onClick={() => navigate(-1)}
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          disabled={cursor === 0 || isLoading}
 | 
			
		||||
          disabled={skip === 0 || isLoading}
 | 
			
		||||
        >
 | 
			
		||||
          <ArrowLeftIcon />
 | 
			
		||||
          {t("previous_page")}
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +100,7 @@ export function Catalogue() {
 | 
			
		|||
            <>
 | 
			
		||||
              {searchResults.map((game) => (
 | 
			
		||||
                <GameCard
 | 
			
		||||
                  key={game.objectID}
 | 
			
		||||
                  key={game.objectId}
 | 
			
		||||
                  game={game}
 | 
			
		||||
                  onClick={() => handleGameClick(game)}
 | 
			
		||||
                />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,6 +93,7 @@ export const downloadRightContent = style({
 | 
			
		|||
  padding: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
  flex: "1",
 | 
			
		||||
  gap: `${SPACING_UNIT}px`,
 | 
			
		||||
  background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const downloadActions = style({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -227,7 +227,14 @@ export function DownloadGroup({
 | 
			
		|||
                    <button
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      className={styles.downloadTitle}
 | 
			
		||||
                      onClick={() => navigate(buildGameDetailsPath(game))}
 | 
			
		||||
                      onClick={() =>
 | 
			
		||||
                        navigate(
 | 
			
		||||
                          buildGameDetailsPath({
 | 
			
		||||
                            ...game,
 | 
			
		||||
                            objectId: game.objectID,
 | 
			
		||||
                          })
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      {game.title}
 | 
			
		||||
                    </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { Modal, ModalProps, TextField } from "@renderer/components";
 | 
			
		||||
import { Modal, ModalProps } from "@renderer/components";
 | 
			
		||||
import { useContext, useMemo } from "react";
 | 
			
		||||
import { cloudSyncContext } from "@renderer/context";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +11,8 @@ export function CloudSyncFilesModal({
 | 
			
		|||
}: CloudSyncFilesModalProps) {
 | 
			
		||||
  const { backupPreview } = useContext(cloudSyncContext);
 | 
			
		||||
 | 
			
		||||
  console.log(backupPreview);
 | 
			
		||||
 | 
			
		||||
  const files = useMemo(() => {
 | 
			
		||||
    if (!backupPreview) {
 | 
			
		||||
      return [];
 | 
			
		||||
| 
						 | 
				
			
			@ -51,22 +53,24 @@ export function CloudSyncFilesModal({
 | 
			
		|||
        ))}
 | 
			
		||||
      </div> */}
 | 
			
		||||
 | 
			
		||||
      <ul
 | 
			
		||||
        style={{
 | 
			
		||||
          margin: 0,
 | 
			
		||||
          padding: 0,
 | 
			
		||||
          listStyle: "none",
 | 
			
		||||
          gap: 16,
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          flexDirection: "column",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {files.map((file) => (
 | 
			
		||||
          <li key={file.path}>
 | 
			
		||||
            <TextField value={file.path} readOnly />
 | 
			
		||||
          </li>
 | 
			
		||||
        ))}
 | 
			
		||||
      </ul>
 | 
			
		||||
      <table>
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style={{ textAlign: "left" }}>Arquivo</th>
 | 
			
		||||
            <th style={{ textAlign: "left" }}>Hash</th>
 | 
			
		||||
            <th style={{ textAlign: "left" }}>Tamanho</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          {files.map((file) => (
 | 
			
		||||
            <tr key={file.path}>
 | 
			
		||||
              <td style={{ textAlign: "left" }}>{file.path}</td>
 | 
			
		||||
              <td style={{ textAlign: "left" }}>{file.change}</td>
 | 
			
		||||
              <td style={{ textAlign: "left" }}>{file.path}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          ))}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
    setShowCloudSyncFilesModal,
 | 
			
		||||
  } = useContext(cloudSyncContext);
 | 
			
		||||
 | 
			
		||||
  const { objectID, shop, gameTitle } = useContext(gameDetailsContext);
 | 
			
		||||
  const { objectId, shop, gameTitle } = useContext(gameDetailsContext);
 | 
			
		||||
 | 
			
		||||
  const { showSuccessToast, showErrorToast } = useToast();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,13 +63,13 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    gameBackupsTable
 | 
			
		||||
      .where({ shop: shop, objectId: objectID })
 | 
			
		||||
      .where({ shop: shop, objectId })
 | 
			
		||||
      .last()
 | 
			
		||||
      .then((lastBackup) => setLastBackup(lastBackup || null));
 | 
			
		||||
 | 
			
		||||
    const removeBackupDownloadProgressListener =
 | 
			
		||||
      window.electron.onBackupDownloadProgress(
 | 
			
		||||
        objectID!,
 | 
			
		||||
        objectId!,
 | 
			
		||||
        shop,
 | 
			
		||||
        (progressEvent) => {
 | 
			
		||||
          setBackupDownloadProgress(progressEvent);
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +79,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
 | 
			
		|||
    return () => {
 | 
			
		||||
      removeBackupDownloadProgressListener();
 | 
			
		||||
    };
 | 
			
		||||
  }, [backupPreview, objectID, shop]);
 | 
			
		||||
  }, [backupPreview, objectId, shop]);
 | 
			
		||||
 | 
			
		||||
  const handleBackupInstallClick = async (artifactId: string) => {
 | 
			
		||||
    setBackupDownloadProgress(null);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ export function GameDetailsContent() {
 | 
			
		|||
  const { t } = useTranslation("game_details");
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    objectID,
 | 
			
		||||
    objectId,
 | 
			
		||||
    shopDetails,
 | 
			
		||||
    game,
 | 
			
		||||
    gameColor,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ export function GameDetailsContent() {
 | 
			
		|||
  const [backdropOpactiy, setBackdropOpacity] = useState(1);
 | 
			
		||||
 | 
			
		||||
  const handleHeroLoad = async () => {
 | 
			
		||||
    const output = await average(steamUrlBuilder.libraryHero(objectID!), {
 | 
			
		||||
    const output = await average(steamUrlBuilder.libraryHero(objectId!), {
 | 
			
		||||
      amount: 1,
 | 
			
		||||
      format: "hex",
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ export function GameDetailsContent() {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setBackdropOpacity(1);
 | 
			
		||||
  }, [objectID]);
 | 
			
		||||
  }, [objectId]);
 | 
			
		||||
 | 
			
		||||
  const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
 | 
			
		||||
    const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ export function GameDetailsContent() {
 | 
			
		|||
  return (
 | 
			
		||||
    <div className={styles.wrapper({ blurredContent: hasNSFWContentBlocked })}>
 | 
			
		||||
      <img
 | 
			
		||||
        src={steamUrlBuilder.libraryHero(objectID!)}
 | 
			
		||||
        src={steamUrlBuilder.libraryHero(objectId!)}
 | 
			
		||||
        className={styles.heroImage}
 | 
			
		||||
        alt={game?.title}
 | 
			
		||||
        onLoad={handleHeroLoad}
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +116,7 @@ export function GameDetailsContent() {
 | 
			
		|||
          >
 | 
			
		||||
            <div className={styles.heroContent}>
 | 
			
		||||
              <img
 | 
			
		||||
                src={steamUrlBuilder.logo(objectID!)}
 | 
			
		||||
                src={steamUrlBuilder.logo(objectId!)}
 | 
			
		||||
                className={styles.gameLogo}
 | 
			
		||||
                alt={game?.title}
 | 
			
		||||
              />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ export function GameDetails() {
 | 
			
		|||
  const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
 | 
			
		||||
  const [randomizerLocked, setRandomizerLocked] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const { objectID, shop } = useParams();
 | 
			
		||||
  const { objectId, shop } = useParams();
 | 
			
		||||
  const [searchParams] = useSearchParams();
 | 
			
		||||
 | 
			
		||||
  const fromRandomizer = searchParams.get("fromRandomizer");
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ export function GameDetails() {
 | 
			
		|||
    window.electron.getRandomGame().then((randomGame) => {
 | 
			
		||||
      setRandomGame(randomGame);
 | 
			
		||||
    });
 | 
			
		||||
  }, [objectID]);
 | 
			
		||||
  }, [objectId]);
 | 
			
		||||
 | 
			
		||||
  const handleRandomizerClick = () => {
 | 
			
		||||
    if (randomGame) {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +82,7 @@ export function GameDetails() {
 | 
			
		|||
    <GameDetailsContextProvider
 | 
			
		||||
      gameTitle={gameTitle!}
 | 
			
		||||
      shop={shop! as GameShop}
 | 
			
		||||
      objectId={objectID!}
 | 
			
		||||
      objectId={objectId!}
 | 
			
		||||
    >
 | 
			
		||||
      <GameDetailsContextConsumer>
 | 
			
		||||
        {({
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +105,7 @@ export function GameDetails() {
 | 
			
		|||
          ) => {
 | 
			
		||||
            await startDownload({
 | 
			
		||||
              repackId: repack.id,
 | 
			
		||||
              objectID: objectID!,
 | 
			
		||||
              objectId: objectId!,
 | 
			
		||||
              title: gameTitle,
 | 
			
		||||
              downloader,
 | 
			
		||||
              shop: shop as GameShop,
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ export function GameDetails() {
 | 
			
		|||
 | 
			
		||||
          return (
 | 
			
		||||
            <CloudSyncContextProvider
 | 
			
		||||
              objectId={objectID!}
 | 
			
		||||
              objectId={objectId!}
 | 
			
		||||
              shop={shop! as GameShop}
 | 
			
		||||
            >
 | 
			
		||||
              <CloudSyncContextConsumer>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ export function HeroPanelActions() {
 | 
			
		|||
    game,
 | 
			
		||||
    repacks,
 | 
			
		||||
    isGameRunning,
 | 
			
		||||
    objectID,
 | 
			
		||||
    objectId,
 | 
			
		||||
    gameTitle,
 | 
			
		||||
    setShowGameOptionsModal,
 | 
			
		||||
    setShowRepacksModal,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ export function HeroPanelActions() {
 | 
			
		|||
    setToggleLibraryGameDisabled(true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await window.electron.addGameToLibrary(objectID!, gameTitle, "steam");
 | 
			
		||||
      await window.electron.addGameToLibrary(objectId!, gameTitle, "steam");
 | 
			
		||||
 | 
			
		||||
      updateLibrary();
 | 
			
		||||
      updateGame();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,11 +24,11 @@ export function Sidebar() {
 | 
			
		|||
  const { numberFormatter } = useFormat();
 | 
			
		||||
 | 
			
		||||
  // useEffect(() => {
 | 
			
		||||
  //   if (objectID) {
 | 
			
		||||
  //   if (objectId) {
 | 
			
		||||
  //     setHowLongToBeat({ isLoading: true, data: null });
 | 
			
		||||
 | 
			
		||||
  //     window.electron
 | 
			
		||||
  //       .getHowLongToBeat(objectID, "steam", gameTitle)
 | 
			
		||||
  //       .getHowLongToBeat(objectId, "steam", gameTitle)
 | 
			
		||||
  //       .then((howLongToBeat) => {
 | 
			
		||||
  //         setHowLongToBeat({ isLoading: false, data: howLongToBeat });
 | 
			
		||||
  //       })
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ export function Sidebar() {
 | 
			
		|||
  //         setHowLongToBeat({ isLoading: false, data: null });
 | 
			
		||||
  //       });
 | 
			
		||||
  //   }
 | 
			
		||||
  // }, [objectID, gameTitle]);
 | 
			
		||||
  // }, [objectId, gameTitle]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <aside className={styles.contentSidebar}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -186,7 +186,7 @@ export function Home() {
 | 
			
		|||
              ))
 | 
			
		||||
            : catalogue[currentCatalogueCategory].map((result) => (
 | 
			
		||||
                <GameCard
 | 
			
		||||
                  key={result.objectID}
 | 
			
		||||
                  key={result.objectId}
 | 
			
		||||
                  game={result}
 | 
			
		||||
                  onClick={() => navigate(buildGameDetailsPath(result))}
 | 
			
		||||
                />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,7 +115,7 @@ export function SearchResults() {
 | 
			
		|||
            <>
 | 
			
		||||
              {searchResults.map((game) => (
 | 
			
		||||
                <GameCard
 | 
			
		||||
                  key={game.objectID}
 | 
			
		||||
                  key={game.objectId}
 | 
			
		||||
                  game={game}
 | 
			
		||||
                  onClick={() => handleGameClick(game)}
 | 
			
		||||
                />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ export const gameCover = style({
 | 
			
		|||
    height: "172%",
 | 
			
		||||
    position: "absolute",
 | 
			
		||||
    background:
 | 
			
		||||
      "linear-gradient(35deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 51.5%, rgba(255, 255, 255, 0.15) 54%, rgba(255, 255, 255, 0.15) 100%);",
 | 
			
		||||
      "linear-gradient(35deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 51.5%, rgba(255, 255, 255, 0.15) 54%, rgba(255, 255, 255, 0.15) 100%)",
 | 
			
		||||
    transition: "all ease 0.3s",
 | 
			
		||||
    transform: "translateY(-36%)",
 | 
			
		||||
    opacity: "0.5",
 | 
			
		||||
| 
						 | 
				
			
			@ -189,3 +189,15 @@ export const defaultAvatarWrapper = style({
 | 
			
		|||
  border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
  borderRadius: "4px",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const achievementsProgressBar = style({
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  height: "4px",
 | 
			
		||||
  transition: "all ease 0.2s",
 | 
			
		||||
  "::-webkit-progress-bar": {
 | 
			
		||||
    backgroundColor: "rgba(255, 255, 255, 0.15)",
 | 
			
		||||
  },
 | 
			
		||||
  "::-webkit-progress-value": {
 | 
			
		||||
    backgroundColor: vars.color.muted,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import { steamUrlBuilder } from "@shared";
 | 
			
		|||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./profile-content.css";
 | 
			
		||||
import { ClockIcon, TelescopeIcon } from "@primer/octicons-react";
 | 
			
		||||
import { ClockIcon, TelescopeIcon, TrophyIcon } from "@primer/octicons-react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { LockedProfile } from "./locked-profile";
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,10 @@ import { ReportProfile } from "../report-profile/report-profile";
 | 
			
		|||
import { FriendsBox } from "./friends-box";
 | 
			
		||||
import { RecentGamesBox } from "./recent-games-box";
 | 
			
		||||
import { UserGame } from "@types";
 | 
			
		||||
import { buildGameDetailsPath } from "@renderer/helpers";
 | 
			
		||||
import {
 | 
			
		||||
  buildGameDetailsPath,
 | 
			
		||||
  formatDownloadProgress,
 | 
			
		||||
} from "@renderer/helpers";
 | 
			
		||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
 | 
			
		||||
 | 
			
		||||
export function ProfileContent() {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +47,7 @@ export function ProfileContent() {
 | 
			
		|||
  const buildUserGameDetailsPath = (game: UserGame) =>
 | 
			
		||||
    buildGameDetailsPath({
 | 
			
		||||
      ...game,
 | 
			
		||||
      objectID: game.objectId,
 | 
			
		||||
      objectId: game.objectId,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  const formatPlayTime = useCallback(
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +118,7 @@ export function ProfileContent() {
 | 
			
		|||
                      borderRadius: 4,
 | 
			
		||||
                      overflow: "hidden",
 | 
			
		||||
                      position: "relative",
 | 
			
		||||
                      display: "flex",
 | 
			
		||||
                    }}
 | 
			
		||||
                    className={styles.game}
 | 
			
		||||
                  >
 | 
			
		||||
| 
						 | 
				
			
			@ -126,23 +130,85 @@ export function ProfileContent() {
 | 
			
		|||
                      className={styles.gameCover}
 | 
			
		||||
                      onClick={() => navigate(buildUserGameDetailsPath(game))}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div style={{ position: "absolute", padding: 4 }}>
 | 
			
		||||
                      <div
 | 
			
		||||
                        style={{
 | 
			
		||||
                          position: "absolute",
 | 
			
		||||
                          display: "flex",
 | 
			
		||||
                          flexDirection: "column",
 | 
			
		||||
                          alignItems: "flex-start",
 | 
			
		||||
                          justifyContent: "space-between",
 | 
			
		||||
                          height: "100%",
 | 
			
		||||
                          width: "100%",
 | 
			
		||||
                          background:
 | 
			
		||||
                            "linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%)",
 | 
			
		||||
                          padding: 8,
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <small
 | 
			
		||||
                          style={{
 | 
			
		||||
                            backgroundColor: vars.color.background,
 | 
			
		||||
                            color: vars.color.muted,
 | 
			
		||||
                            // border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
                            border: `solid 1px ${vars.color.border}`,
 | 
			
		||||
                            borderRadius: 4,
 | 
			
		||||
                            display: "flex",
 | 
			
		||||
                            alignItems: "center",
 | 
			
		||||
                            gap: 4,
 | 
			
		||||
                            padding: "4px 4px",
 | 
			
		||||
                            padding: "4px",
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <ClockIcon size={11} />
 | 
			
		||||
                          {formatPlayTime(game.playTimeInSeconds)}
 | 
			
		||||
                        </small>
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                          style={{
 | 
			
		||||
                            color: "white",
 | 
			
		||||
                            width: "100%",
 | 
			
		||||
                            display: "flex",
 | 
			
		||||
                            flexDirection: "column",
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <div
 | 
			
		||||
                            style={{
 | 
			
		||||
                              display: "flex",
 | 
			
		||||
                              justifyContent: "space-between",
 | 
			
		||||
                              marginBottom: 8,
 | 
			
		||||
                              color: vars.color.muted,
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            <div
 | 
			
		||||
                              style={{
 | 
			
		||||
                                display: "flex",
 | 
			
		||||
                                alignItems: "center",
 | 
			
		||||
                                gap: 8,
 | 
			
		||||
                              }}
 | 
			
		||||
                            >
 | 
			
		||||
                              <TrophyIcon size={13} />
 | 
			
		||||
                              <span>
 | 
			
		||||
                                {game.unlockedAchievementCount} /{" "}
 | 
			
		||||
                                {game.achievementCount}
 | 
			
		||||
                              </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <span>
 | 
			
		||||
                              {formatDownloadProgress(
 | 
			
		||||
                                game.unlockedAchievementCount /
 | 
			
		||||
                                  game.achievementCount
 | 
			
		||||
                              )}
 | 
			
		||||
                            </span>
 | 
			
		||||
                          </div>
 | 
			
		||||
 | 
			
		||||
                          <progress
 | 
			
		||||
                            max={1}
 | 
			
		||||
                            value={
 | 
			
		||||
                              game.unlockedAchievementCount /
 | 
			
		||||
                              game.achievementCount
 | 
			
		||||
                            }
 | 
			
		||||
                            className={styles.achievementsProgressBar}
 | 
			
		||||
                          />
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
 | 
			
		||||
                      <img
 | 
			
		||||
                        src={steamUrlBuilder.cover(game.objectId)}
 | 
			
		||||
                        alt={game.title}
 | 
			
		||||
| 
						 | 
				
			
			@ -178,6 +244,7 @@ export function ProfileContent() {
 | 
			
		|||
    userStats,
 | 
			
		||||
    numberFormatter,
 | 
			
		||||
    t,
 | 
			
		||||
    formatPlayTime,
 | 
			
		||||
    navigate,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ export function RecentGamesBox() {
 | 
			
		|||
  const buildUserGameDetailsPath = (game: UserGame) =>
 | 
			
		||||
    buildGameDetailsPath({
 | 
			
		||||
      ...game,
 | 
			
		||||
      objectID: game.objectId,
 | 
			
		||||
      objectId: game.objectId,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  if (!userProfile?.recentGames.length) return null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,7 +70,7 @@ export const heroPanel = style({
 | 
			
		|||
 | 
			
		||||
export const userInformation = style({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
  padding: `${SPACING_UNIT * 6}px ${SPACING_UNIT * 3}px`,
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: `${SPACING_UNIT * 2}px`,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { SPACING_UNIT } from "@renderer/theme.css";
 | 
			
		||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
 | 
			
		||||
 | 
			
		||||
import * as styles from "./profile-hero.css";
 | 
			
		||||
import { useCallback, useContext, useMemo, useState } from "react";
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ import {
 | 
			
		|||
  PersonAddIcon,
 | 
			
		||||
  PersonIcon,
 | 
			
		||||
  SignOutIcon,
 | 
			
		||||
  UploadIcon,
 | 
			
		||||
  XCircleFillIcon,
 | 
			
		||||
} from "@primer/octicons-react";
 | 
			
		||||
import { buildGameDetailsPath } from "@renderer/helpers";
 | 
			
		||||
| 
						 | 
				
			
			@ -36,8 +37,7 @@ export function ProfileHero() {
 | 
			
		|||
  const [showEditProfileModal, setShowEditProfileModal] = useState(false);
 | 
			
		||||
  const [isPerformingAction, setIsPerformingAction] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const { isMe, heroBackground, getUserProfile, userProfile } =
 | 
			
		||||
    useContext(userProfileContext);
 | 
			
		||||
  const { isMe, getUserProfile, userProfile } = useContext(userProfileContext);
 | 
			
		||||
  const {
 | 
			
		||||
    signOut,
 | 
			
		||||
    updateFriendRequestState,
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +48,8 @@ export function ProfileHero() {
 | 
			
		|||
 | 
			
		||||
  const { gameRunning } = useAppSelector((state) => state.gameRunning);
 | 
			
		||||
 | 
			
		||||
  const [hero, setHero] = useState("");
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation("user_profile");
 | 
			
		||||
  const { formatDistance } = useDate();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +126,7 @@ export function ProfileHero() {
 | 
			
		|||
            theme="outline"
 | 
			
		||||
            onClick={() => setShowEditProfileModal(true)}
 | 
			
		||||
            disabled={isPerformingAction}
 | 
			
		||||
            style={{ borderColor: vars.color.body }}
 | 
			
		||||
          >
 | 
			
		||||
            <PencilIcon />
 | 
			
		||||
            {t("edit_profile")}
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +151,7 @@ export function ProfileHero() {
 | 
			
		|||
            theme="outline"
 | 
			
		||||
            onClick={() => handleFriendAction(userProfile.id, "SEND")}
 | 
			
		||||
            disabled={isPerformingAction}
 | 
			
		||||
            style={{ borderColor: vars.color.body }}
 | 
			
		||||
          >
 | 
			
		||||
            <PersonAddIcon />
 | 
			
		||||
            {t("add_friend")}
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +202,7 @@ export function ProfileHero() {
 | 
			
		|||
            handleFriendAction(userProfile.relation!.BId, "CANCEL")
 | 
			
		||||
          }
 | 
			
		||||
          disabled={isPerformingAction}
 | 
			
		||||
          style={{ borderColor: vars.color.body }}
 | 
			
		||||
        >
 | 
			
		||||
          <XCircleFillIcon /> {t("cancel_request")}
 | 
			
		||||
        </Button>
 | 
			
		||||
| 
						 | 
				
			
			@ -212,11 +217,12 @@ export function ProfileHero() {
 | 
			
		|||
            handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
 | 
			
		||||
          }
 | 
			
		||||
          disabled={isPerformingAction}
 | 
			
		||||
          style={{ borderColor: vars.color.body }}
 | 
			
		||||
        >
 | 
			
		||||
          <CheckCircleFillIcon /> {t("accept_request")}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          theme="outline"
 | 
			
		||||
          theme="danger"
 | 
			
		||||
          onClick={() =>
 | 
			
		||||
            handleFriendAction(userProfile.relation!.AId, "REFUSED")
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +252,6 @@ export function ProfileHero() {
 | 
			
		|||
      if (gameRunning)
 | 
			
		||||
        return {
 | 
			
		||||
          ...gameRunning,
 | 
			
		||||
          objectId: gameRunning.objectID,
 | 
			
		||||
          sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +260,43 @@ export function ProfileHero() {
 | 
			
		|||
    return userProfile?.currentGame;
 | 
			
		||||
  }, [isMe, userProfile, gameRunning]);
 | 
			
		||||
 | 
			
		||||
  const handleChangeCoverClick = async () => {
 | 
			
		||||
    const { filePaths } = await window.electron.showOpenDialog({
 | 
			
		||||
      properties: ["openFile"],
 | 
			
		||||
      filters: [
 | 
			
		||||
        {
 | 
			
		||||
          name: "Image",
 | 
			
		||||
          extensions: ["jpg", "jpeg", "png", "gif", "webp"],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (filePaths && filePaths.length > 0) {
 | 
			
		||||
      const path = filePaths[0];
 | 
			
		||||
 | 
			
		||||
      const { imagePath } = await window.electron
 | 
			
		||||
        .processProfileImage(path)
 | 
			
		||||
        .catch(() => {
 | 
			
		||||
          showErrorToast(t("image_process_failure"));
 | 
			
		||||
          return { imagePath: null };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      console.log("imagePath", imagePath);
 | 
			
		||||
      setHero(imagePath);
 | 
			
		||||
 | 
			
		||||
      // onChange(imagePath);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getImageUrl = () => {
 | 
			
		||||
    if (hero) return `local:${hero}`;
 | 
			
		||||
    // if (userDetails?.profileImageUrl) return userDetails.profileImageUrl;
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // const imageUrl = getImageUrl();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* <ConfirmationModal
 | 
			
		||||
| 
						 | 
				
			
			@ -270,12 +312,9 @@ export function ProfileHero() {
 | 
			
		|||
        onClose={() => setShowEditProfileModal(false)}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <section
 | 
			
		||||
        className={styles.profileContentBox}
 | 
			
		||||
        // style={{ background: heroBackground }}
 | 
			
		||||
      >
 | 
			
		||||
      <section className={styles.profileContentBox}>
 | 
			
		||||
        <img
 | 
			
		||||
          src="https://wallpapers.com/images/featured/cyberpunk-anime-dfyw8eb7bqkw278u.jpg"
 | 
			
		||||
          src={getImageUrl()}
 | 
			
		||||
          alt=""
 | 
			
		||||
          style={{
 | 
			
		||||
            position: "absolute",
 | 
			
		||||
| 
						 | 
				
			
			@ -286,7 +325,8 @@ export function ProfileHero() {
 | 
			
		|||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            background: heroBackground,
 | 
			
		||||
            background:
 | 
			
		||||
              "linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
 | 
			
		||||
            width: "100%",
 | 
			
		||||
            height: "100%",
 | 
			
		||||
            zIndex: 1,
 | 
			
		||||
| 
						 | 
				
			
			@ -324,7 +364,7 @@ export function ProfileHero() {
 | 
			
		|||
                    <Link
 | 
			
		||||
                      to={buildGameDetailsPath({
 | 
			
		||||
                        ...currentGame,
 | 
			
		||||
                        objectID: currentGame.objectId,
 | 
			
		||||
                        objectId: currentGame.objectID,
 | 
			
		||||
                      })}
 | 
			
		||||
                    >
 | 
			
		||||
                      {currentGame.title}
 | 
			
		||||
| 
						 | 
				
			
			@ -345,12 +385,30 @@ export function ProfileHero() {
 | 
			
		|||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              theme="outline"
 | 
			
		||||
              style={{
 | 
			
		||||
                position: "absolute",
 | 
			
		||||
                top: 16,
 | 
			
		||||
                right: 16,
 | 
			
		||||
                borderColor: vars.color.body,
 | 
			
		||||
              }}
 | 
			
		||||
              onClick={handleChangeCoverClick}
 | 
			
		||||
            >
 | 
			
		||||
              <UploadIcon />
 | 
			
		||||
              Upload cover
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          className={styles.heroPanel}
 | 
			
		||||
          style={{ background: heroBackground }}
 | 
			
		||||
          // style={{ background: heroBackground }}
 | 
			
		||||
          style={{
 | 
			
		||||
            background:
 | 
			
		||||
              "linear-gradient(135deg, rgb(0 0 0 / 70%), rgb(0 0 0 / 60%))",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,14 +5,14 @@ import flexSearch from "flexsearch";
 | 
			
		|||
 | 
			
		||||
const index = new flexSearch.Index();
 | 
			
		||||
 | 
			
		||||
const state = {
 | 
			
		||||
  repacks: [] as any[],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface SerializedGameRepack extends Omit<GameRepack, "uris"> {
 | 
			
		||||
  uris: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const state = {
 | 
			
		||||
  repacks: [] as SerializedGameRepack[],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
self.onmessage = async (
 | 
			
		||||
  event: MessageEvent<[string, string] | "INDEX_REPACKS">
 | 
			
		||||
) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,14 +91,14 @@ export const getDownloadersForUris = (uris: string[]) => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const steamUrlBuilder = {
 | 
			
		||||
  library: (objectID: string) =>
 | 
			
		||||
    `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
 | 
			
		||||
  libraryHero: (objectID: string) =>
 | 
			
		||||
    `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
 | 
			
		||||
  logo: (objectID: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
 | 
			
		||||
  cover: (objectID: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/library_600x900.jpg`,
 | 
			
		||||
  icon: (objectID: string, clientIcon: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectID}/${clientIcon}.ico`,
 | 
			
		||||
  library: (objectId: string) =>
 | 
			
		||||
    `https://steamcdn-a.akamaihd.net/steam/apps/${objectId}/header.jpg`,
 | 
			
		||||
  libraryHero: (objectId: string) =>
 | 
			
		||||
    `https://steamcdn-a.akamaihd.net/steam/apps/${objectId}/library_hero.jpg`,
 | 
			
		||||
  logo: (objectId: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectId}/logo.png`,
 | 
			
		||||
  cover: (objectId: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steam/apps/${objectId}/library_600x900.jpg`,
 | 
			
		||||
  icon: (objectId: string, clientIcon: string) =>
 | 
			
		||||
    `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectId}/${clientIcon}.ico`,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ export interface GameRepack {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export type ShopDetails = SteamAppDetails & {
 | 
			
		||||
  objectID: string;
 | 
			
		||||
  objectId: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface TorrentFile {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ export interface TorrentFile {
 | 
			
		|||
 | 
			
		||||
/* Used by the catalogue */
 | 
			
		||||
export interface CatalogueEntry {
 | 
			
		||||
  objectID: string;
 | 
			
		||||
  objectId: string;
 | 
			
		||||
  shop: GameShop;
 | 
			
		||||
  title: string;
 | 
			
		||||
  /* Epic Games covers cannot be guessed with objectID */
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +54,8 @@ export interface UserGame {
 | 
			
		|||
  cover: string;
 | 
			
		||||
  playTimeInSeconds: number;
 | 
			
		||||
  lastTimePlayed: Date | null;
 | 
			
		||||
  unlockedAchievementCount: number;
 | 
			
		||||
  achievementCount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DownloadQueue {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +128,7 @@ export interface HowLongToBeatCategory {
 | 
			
		|||
 | 
			
		||||
export interface Steam250Game {
 | 
			
		||||
  title: string;
 | 
			
		||||
  objectID: string;
 | 
			
		||||
  objectId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SteamGame {
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +144,7 @@ export type AppUpdaterEvent =
 | 
			
		|||
/* Events */
 | 
			
		||||
export interface StartGameDownloadPayload {
 | 
			
		||||
  repackId: number;
 | 
			
		||||
  objectID: string;
 | 
			
		||||
  objectId: string;
 | 
			
		||||
  title: string;
 | 
			
		||||
  shop: GameShop;
 | 
			
		||||
  uri: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +189,7 @@ export interface UserRelation {
 | 
			
		|||
  updatedAt: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserProfileCurrentGame extends Omit<GameRunning, "objectID"> {
 | 
			
		||||
export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
 | 
			
		||||
  objectId: string;
 | 
			
		||||
  sessionDurationInSeconds: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue