mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-28 05:11:02 +00:00
Merge branch 'main' of github.com:hydralauncher/hydra into feat/HYD-781
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
|
import { ShopAssets } from "@types";
|
||||||
|
|
||||||
const getCatalogue = async (
|
const getCatalogue = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -11,7 +12,7 @@ const getCatalogue = async (
|
|||||||
skip: "0",
|
skip: "0",
|
||||||
});
|
});
|
||||||
|
|
||||||
return HydraApi.get(
|
return HydraApi.get<ShopAssets[]>(
|
||||||
`/catalogue/${category}?${params.toString()}`,
|
`/catalogue/${category}?${params.toString()}`,
|
||||||
{},
|
{},
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { getSteamAppDetails, logger } from "@main/services";
|
import { getSteamAppDetails, logger } from "@main/services";
|
||||||
|
|
||||||
import type { ShopDetails, GameShop } from "@types";
|
import type { ShopDetails, GameShop, ShopDetailsWithAssets } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import {
|
||||||
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
gamesShopAssetsSublevel,
|
||||||
|
gamesShopCacheSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const getLocalizedSteamAppDetails = async (
|
const getLocalizedSteamAppDetails = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -14,22 +17,7 @@ const getLocalizedSteamAppDetails = async (
|
|||||||
return getSteamAppDetails(objectId, language);
|
return getSteamAppDetails(objectId, language);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSteamAppDetails(objectId, language).then(
|
return getSteamAppDetails(objectId, language);
|
||||||
async (localizedAppDetails) => {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (steamGame && localizedAppDetails) {
|
|
||||||
return {
|
|
||||||
...localizedAppDetails,
|
|
||||||
name: steamGame.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameShopDetails = async (
|
const getGameShopDetails = async (
|
||||||
@@ -37,34 +25,44 @@ const getGameShopDetails = async (
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
language: string
|
language: string
|
||||||
): Promise<ShopDetails | null> => {
|
): Promise<ShopDetailsWithAssets | null> => {
|
||||||
if (shop === "steam") {
|
if (shop === "steam") {
|
||||||
const cachedData = await gamesShopCacheSublevel.get(
|
const [cachedData, cachedAssets] = await Promise.all([
|
||||||
levelKeys.gameShopCacheItem(shop, objectId, language)
|
gamesShopCacheSublevel.get(
|
||||||
);
|
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||||
|
),
|
||||||
|
gamesShopAssetsSublevel.get(levelKeys.game(shop, objectId)),
|
||||||
|
]);
|
||||||
|
|
||||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
result.name = cachedAssets?.title ?? result.name;
|
||||||
|
|
||||||
gamesShopCacheSublevel
|
gamesShopCacheSublevel
|
||||||
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("Could not cache game details", err);
|
logger.error("Could not cache game details", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
assets: cachedAssets ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
return {
|
return {
|
||||||
...cachedData,
|
...cachedData,
|
||||||
objectId,
|
assets: cachedAssets ?? null,
|
||||||
} as ShopDetails;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(appDetails);
|
return appDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||||
"/games/trending",
|
"/games/featured",
|
||||||
{ language },
|
{ language },
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
).catch(() => []);
|
).catch(() => []);
|
||||||
|
|
||||||
return trendingGames;
|
return trendingGames.slice(0, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getTrendingGames", getTrendingGames);
|
registerEvent("getTrendingGames", getTrendingGames);
|
||||||
|
|||||||
14
src/main/events/catalogue/save-game-shop-assets.ts
Normal file
14
src/main/events/catalogue/save-game-shop-assets.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { GameShop, ShopAssets } from "@types";
|
||||||
|
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
|
const saveGameShopAssets = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
assets: ShopAssets
|
||||||
|
): Promise<void> => {
|
||||||
|
return gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), assets);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
||||||
@@ -3,6 +3,7 @@ import { ipcMain } from "electron";
|
|||||||
|
|
||||||
import "./catalogue/get-catalogue";
|
import "./catalogue/get-catalogue";
|
||||||
import "./catalogue/get-game-shop-details";
|
import "./catalogue/get-game-shop-details";
|
||||||
|
import "./catalogue/save-game-shop-assets";
|
||||||
import "./catalogue/get-how-long-to-beat";
|
import "./catalogue/get-how-long-to-beat";
|
||||||
import "./catalogue/get-random-game";
|
import "./catalogue/get-random-game";
|
||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -17,6 +18,8 @@ const addGameToLibrary = async (
|
|||||||
const gameKey = levelKeys.game(shop, objectId);
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
let game = await gamesSublevel.get(gameKey);
|
let game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
await downloadsSublevel.del(gameKey);
|
await downloadsSublevel.del(gameKey);
|
||||||
|
|
||||||
@@ -24,17 +27,9 @@ const addGameToLibrary = async (
|
|||||||
|
|
||||||
await gamesSublevel.put(gameKey, game);
|
await gamesSublevel.put(gameKey, game);
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
game = {
|
game = {
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
} from "@main/level";
|
||||||
|
|
||||||
const getLibrary = async (): Promise<LibraryGame[]> => {
|
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||||
return gamesSublevel
|
return gamesSublevel
|
||||||
@@ -12,11 +16,13 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
|||||||
.filter(([_key, game]) => game.isDeleted === false)
|
.filter(([_key, game]) => game.isDeleted === false)
|
||||||
.map(async ([key, game]) => {
|
.map(async ([key, game]) => {
|
||||||
const download = await downloadsSublevel.get(key);
|
const download = await downloadsSublevel.get(key);
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(key);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: key,
|
id: key,
|
||||||
...game,
|
...game,
|
||||||
download: download ?? null,
|
download: download ?? null,
|
||||||
|
...gameAssets,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { Download, StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
|
import { Downloader, DownloadError } from "@shared";
|
||||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
import {
|
||||||
|
downloadsSublevel,
|
||||||
|
gamesShopAssetsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
@@ -36,6 +39,7 @@ const startGameDownload = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const game = await gamesSublevel.get(gameKey);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||||
|
|
||||||
/* Delete any previous download */
|
/* Delete any previous download */
|
||||||
await downloadsSublevel.del(gameKey);
|
await downloadsSublevel.del(gameKey);
|
||||||
@@ -46,17 +50,9 @@ const startGameDownload = async (
|
|||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await gamesSublevel.put(gameKey, {
|
await gamesSublevel.put(gameKey, {
|
||||||
title,
|
title,
|
||||||
iconUrl,
|
iconUrl: gameAssets?.iconUrl ?? null,
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
remoteId: null,
|
remoteId: null,
|
||||||
|
|||||||
@@ -1,97 +1,12 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
|
||||||
import type { UserProfile } from "@types";
|
import type { UserProfile } from "@types";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
|
|
||||||
const getSteamGame = async (objectId: string) => {
|
|
||||||
try {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: steamGame.name as string,
|
|
||||||
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to get Steam game", err);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
return HydraApi.get<UserProfile>(`/users/${userId}`).catch(() => null);
|
||||||
const profile = await HydraApi.get<UserProfile | null>(`/users/${userId}`);
|
|
||||||
|
|
||||||
if (!profile) return null;
|
|
||||||
|
|
||||||
const recentGames = await Promise.all(
|
|
||||||
profile.recentGames
|
|
||||||
.map(async (game) => {
|
|
||||||
const steamGame = await getSteamGame(game.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((game) => game)
|
|
||||||
);
|
|
||||||
|
|
||||||
const libraryGames = await Promise.all(
|
|
||||||
profile.libraryGames
|
|
||||||
.map(async (game) => {
|
|
||||||
const steamGame = await getSteamGame(game.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...game,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((game) => game)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (profile.currentGame) {
|
|
||||||
const steamGame = await getSteamGame(profile.currentGame.objectId);
|
|
||||||
|
|
||||||
if (steamGame) {
|
|
||||||
profile.currentGame = {
|
|
||||||
...profile.currentGame,
|
|
||||||
...steamGame,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const friends = await Promise.all(
|
|
||||||
profile.friends.map(async (friend) => {
|
|
||||||
if (!friend.currentGame) return friend;
|
|
||||||
|
|
||||||
const currentGame = await getSteamGame(friend.currentGame.objectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...friend,
|
|
||||||
currentGame: {
|
|
||||||
...friend.currentGame,
|
|
||||||
...currentGame,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...profile,
|
|
||||||
friends,
|
|
||||||
libraryGames,
|
|
||||||
recentGames,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getUser", getUser);
|
registerEvent("getUser", getUser);
|
||||||
|
|||||||
11
src/main/level/sublevels/game-shop-assets.ts
Normal file
11
src/main/level/sublevels/game-shop-assets.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ShopAssets } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gamesShopAssetsSublevel = db.sublevel<string, ShopAssets>(
|
||||||
|
levelKeys.gameShopAssets,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./downloads";
|
export * from "./downloads";
|
||||||
export * from "./games";
|
export * from "./games";
|
||||||
|
export * from "./game-shop-assets";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const levelKeys = {
|
|||||||
user: "user",
|
user: "user",
|
||||||
auth: "auth",
|
auth: "auth",
|
||||||
themes: "themes",
|
themes: "themes",
|
||||||
|
gameShopAssets: "gameShopAssets",
|
||||||
gameShopCache: "gameShopCache",
|
gameShopCache: "gameShopCache",
|
||||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
`${shop}:${objectId}:${language}`,
|
`${shop}:${objectId}:${language}`,
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { ShopAssets } from "@types";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { gamesShopAssetsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
type ProfileGame = {
|
||||||
|
id: string;
|
||||||
|
lastTimePlayed: Date | null;
|
||||||
|
playTimeInMilliseconds: number;
|
||||||
|
} & ShopAssets;
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
return HydraApi.get("/profile/games")
|
return HydraApi.get<ProfileGame[]>("/profile/games")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
for (const game of response) {
|
for (const game of response) {
|
||||||
const localGame = await gamesSublevel.get(
|
const localGame = await gamesSublevel.get(
|
||||||
@@ -31,25 +36,32 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
playTimeInMilliseconds: updatedPlayTime,
|
playTimeInMilliseconds: updatedPlayTime,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: steamGame?.name,
|
title: game.title,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
iconUrl,
|
iconUrl: game.iconUrl,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await gamesShopAssetsSublevel.put(
|
||||||
|
levelKeys.game(game.shop, game.objectId),
|
||||||
|
{
|
||||||
|
shop: game.shop,
|
||||||
|
objectId: game.objectId,
|
||||||
|
title: game.title,
|
||||||
|
coverImageUrl: game.coverImageUrl,
|
||||||
|
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||||
|
libraryImageUrl: game.libraryImageUrl,
|
||||||
|
logoImageUrl: game.logoImageUrl,
|
||||||
|
iconUrl: game.iconUrl,
|
||||||
|
logoPosition: game.logoPosition,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
|
||||||
|
|
||||||
import Piscina from "piscina";
|
|
||||||
|
|
||||||
import { seedsPath } from "@main/constants";
|
|
||||||
|
|
||||||
export const steamGamesWorker = new Piscina({
|
|
||||||
filename: steamGamesWorkerPath,
|
|
||||||
workerData: {
|
|
||||||
steamGamesPath: path.join(seedsPath, "steam-games.json"),
|
|
||||||
},
|
|
||||||
maxThreads: 1,
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { SteamGame } from "@types";
|
|
||||||
import { slice } from "lodash-es";
|
|
||||||
import fs from "node:fs";
|
|
||||||
|
|
||||||
import { workerData } from "node:worker_threads";
|
|
||||||
|
|
||||||
const { steamGamesPath } = workerData;
|
|
||||||
|
|
||||||
const data = fs.readFileSync(steamGamesPath, "utf-8");
|
|
||||||
|
|
||||||
const steamGames = JSON.parse(data) as SteamGame[];
|
|
||||||
|
|
||||||
export const getById = (id: number) =>
|
|
||||||
steamGames.find((game) => game.id === id);
|
|
||||||
|
|
||||||
export const list = ({ limit, offset }: { limit: number; offset: number }) =>
|
|
||||||
slice(steamGames, offset, offset + limit);
|
|
||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
Theme,
|
Theme,
|
||||||
FriendRequestSync,
|
FriendRequestSync,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
|
ShopAssets,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AuthPage, CatalogueCategory } from "@shared";
|
import type { AuthPage, CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
@@ -64,6 +65,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("searchGames", payload, take, skip),
|
ipcRenderer.invoke("searchGames", payload, take, skip),
|
||||||
getCatalogue: (category: CatalogueCategory) =>
|
getCatalogue: (category: CatalogueCategory) =>
|
||||||
ipcRenderer.invoke("getCatalogue", category),
|
ipcRenderer.invoke("getCatalogue", category),
|
||||||
|
saveGameShopAssets: (objectId: string, shop: GameShop, assets: ShopAssets) =>
|
||||||
|
ipcRenderer.invoke("saveGameShopAssets", objectId, shop, assets),
|
||||||
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
||||||
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
|
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
|
||||||
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Badge } from "../badge/badge";
|
import { Badge } from "../badge/badge";
|
||||||
import { useCallback, useState, useMemo } from "react";
|
import { useCallback, useState, useMemo } from "react";
|
||||||
import { useFormat, useRepacks } from "@renderer/hooks";
|
import { useFormat, useRepacks } from "@renderer/hooks";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
|
|
||||||
export interface GameCardProps
|
export interface GameCardProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
@@ -63,7 +62,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
|||||||
>
|
>
|
||||||
<div className="game-card__backdrop">
|
<div className="game-card__backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={game.libraryImageUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className="game-card__cover"
|
className="game-card__cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -33,29 +33,27 @@ export function Hero() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (featuredGameDetails?.length) {
|
if (featuredGameDetails?.length) {
|
||||||
return featuredGameDetails.map((game, index) => (
|
return featuredGameDetails.map((game) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(game.uri)}
|
onClick={() => navigate(game.uri)}
|
||||||
className="hero"
|
className="hero"
|
||||||
key={index}
|
key={game.uri}
|
||||||
>
|
>
|
||||||
<div className="hero__backdrop">
|
<div className="hero__backdrop">
|
||||||
<img
|
<img
|
||||||
src={game.background}
|
src={game.libraryHeroImageUrl}
|
||||||
alt={game.description}
|
alt={game.description ?? ""}
|
||||||
className="hero__media"
|
className="hero__media"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="hero__content">
|
<div className="hero__content">
|
||||||
{game.logo && (
|
<img
|
||||||
<img
|
src={game.logoImageUrl}
|
||||||
src={game.logo}
|
width="250px"
|
||||||
width="250px"
|
alt={game.description ?? ""}
|
||||||
alt={game.description}
|
loading="eager"
|
||||||
loading="eager"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="hero__description">{game.description}</p>
|
<p className="hero__description">{game.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type {
|
|||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
ShopDetails,
|
ShopDetailsWithAssets,
|
||||||
UserAchievement,
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
@@ -69,7 +69,9 @@ export function GameDetailsContextProvider({
|
|||||||
gameTitle,
|
gameTitle,
|
||||||
shop,
|
shop,
|
||||||
}: Readonly<GameDetailsContextProps>) {
|
}: Readonly<GameDetailsContextProps>) {
|
||||||
const [shopDetails, setShopDetails] = useState<ShopDetails | null>(null);
|
const [shopDetails, setShopDetails] = useState<ShopDetailsWithAssets | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
const [achievements, setAchievements] = useState<UserAchievement[] | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -79,7 +81,7 @@ export function GameDetailsContextProvider({
|
|||||||
|
|
||||||
const [stats, setStats] = useState<GameStats | null>(null);
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [gameColor, setGameColor] = useState("");
|
const [gameColor, setGameColor] = useState("");
|
||||||
const [isGameRunning, setIsGameRunning] = useState(false);
|
const [isGameRunning, setIsGameRunning] = useState(false);
|
||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
@@ -120,7 +122,7 @@ export function GameDetailsContextProvider({
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
window.electron
|
const shopDetailsPromise = window.electron
|
||||||
.getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
|
.getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
@@ -135,15 +137,42 @@ export function GameDetailsContextProvider({
|
|||||||
) {
|
) {
|
||||||
setHasNSFWContentBlocked(true);
|
setHasNSFWContentBlocked(true);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.finally(() => {
|
if (result?.assets) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electron.getGameStats(objectId, shop).then((result) => {
|
const statsPromise = window.electron
|
||||||
if (abortController.signal.aborted) return;
|
.getGameStats(objectId, shop)
|
||||||
setStats(result);
|
.then((result) => {
|
||||||
});
|
if (abortController.signal.aborted) return null;
|
||||||
|
setStats(result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([shopDetailsPromise, statsPromise])
|
||||||
|
.then(([_, stats]) => {
|
||||||
|
if (stats) {
|
||||||
|
const assets = stats.assets;
|
||||||
|
if (assets) {
|
||||||
|
window.electron.saveGameShopAssets(objectId, shop, assets);
|
||||||
|
|
||||||
|
setShopDetails((prev) => {
|
||||||
|
if (!prev) return null;
|
||||||
|
console.log("assets", assets);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
assets,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
if (userDetails) {
|
if (userDetails) {
|
||||||
window.electron
|
window.electron
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import type {
|
|||||||
GameShop,
|
GameShop,
|
||||||
GameStats,
|
GameStats,
|
||||||
LibraryGame,
|
LibraryGame,
|
||||||
ShopDetails,
|
ShopDetailsWithAssets,
|
||||||
UserAchievement,
|
UserAchievement,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
export interface GameDetailsContext {
|
export interface GameDetailsContext {
|
||||||
game: LibraryGame | null;
|
game: LibraryGame | null;
|
||||||
shopDetails: ShopDetails | null;
|
shopDetails: ShopDetailsWithAssets | null;
|
||||||
repacks: GameRepack[];
|
repacks: GameRepack[];
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
|
|||||||
15
src/renderer/src/declaration.d.ts
vendored
15
src/renderer/src/declaration.d.ts
vendored
@@ -3,7 +3,6 @@ import type {
|
|||||||
AppUpdaterEvent,
|
AppUpdaterEvent,
|
||||||
GameShop,
|
GameShop,
|
||||||
HowLongToBeatCategory,
|
HowLongToBeatCategory,
|
||||||
ShopDetails,
|
|
||||||
Steam250Game,
|
Steam250Game,
|
||||||
DownloadProgress,
|
DownloadProgress,
|
||||||
SeedingStatus,
|
SeedingStatus,
|
||||||
@@ -33,6 +32,9 @@ import type {
|
|||||||
Badge,
|
Badge,
|
||||||
Auth,
|
Auth,
|
||||||
ShortcutLocation,
|
ShortcutLocation,
|
||||||
|
CatalogueSearchResult,
|
||||||
|
ShopAssets,
|
||||||
|
ShopDetailsWithAssets,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
@@ -69,13 +71,18 @@ declare global {
|
|||||||
payload: CatalogueSearchPayload,
|
payload: CatalogueSearchPayload,
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
) => Promise<{ edges: any[]; count: number }>;
|
) => Promise<{ edges: CatalogueSearchResult[]; count: number }>;
|
||||||
getCatalogue: (category: CatalogueCategory) => Promise<any[]>;
|
getCatalogue: (category: CatalogueCategory) => Promise<ShopAssets[]>;
|
||||||
|
saveGameShopAssets: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
assets: ShopAssets
|
||||||
|
) => Promise<void>;
|
||||||
getGameShopDetails: (
|
getGameShopDetails: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
language: string
|
language: string
|
||||||
) => Promise<ShopDetails | null>;
|
) => Promise<ShopDetailsWithAssets | null>;
|
||||||
getRandomGame: () => Promise<Steam250Game>;
|
getRandomGame: () => Promise<Steam250Game>;
|
||||||
getHowLongToBeat: (
|
getHowLongToBeat: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
@@ -120,8 +119,15 @@ export function AchievementsContent({
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||||
|
|
||||||
const { gameTitle, objectId, shop, achievements, gameColor, setGameColor } =
|
const {
|
||||||
useContext(gameDetailsContext);
|
gameTitle,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
shopDetails,
|
||||||
|
achievements,
|
||||||
|
gameColor,
|
||||||
|
setGameColor,
|
||||||
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -131,10 +137,13 @@ export function AchievementsContent({
|
|||||||
}, [dispatch, gameTitle]);
|
}, [dispatch, gameTitle]);
|
||||||
|
|
||||||
const handleHeroLoad = async () => {
|
const handleHeroLoad = async () => {
|
||||||
const output = await average(steamUrlBuilder.libraryHero(objectId!), {
|
const output = await average(
|
||||||
amount: 1,
|
shopDetails?.assets?.libraryHeroImageUrl ?? "",
|
||||||
format: "hex",
|
{
|
||||||
});
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const backgroundColor = output
|
const backgroundColor = output
|
||||||
? (new Color(output).darken(0.7).toString() as string)
|
? (new Color(output).darken(0.7).toString() as string)
|
||||||
@@ -179,7 +188,7 @@ export function AchievementsContent({
|
|||||||
return (
|
return (
|
||||||
<div className="achievements-content__achievements-list">
|
<div className="achievements-content__achievements-list">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId)}
|
src={shopDetails?.assets?.libraryHeroImageUrl ?? ""}
|
||||||
className="achievements-content__achievements-list__image"
|
className="achievements-content__achievements-list__image"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
@@ -205,7 +214,7 @@ export function AchievementsContent({
|
|||||||
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId)}
|
src={shopDetails?.assets?.logoImageUrl ?? ""}
|
||||||
className="achievements-content__achievements-list__section__container__hero__content__game-logo"
|
className="achievements-content__achievements-list__section__container__hero__content__game-logo"
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DownloadSource } from "@types";
|
import type { CatalogueSearchResult, DownloadSource } from "@types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
@@ -44,7 +44,7 @@ export default function Catalogue() {
|
|||||||
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [results, setResults] = useState<any[]>([]);
|
const [results, setResults] = useState<CatalogueSearchResult[]>([]);
|
||||||
|
|
||||||
const [itemsCount, setItemsCount] = useState(0);
|
const [itemsCount, setItemsCount] = useState(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Badge } from "@renderer/components";
|
import { Badge } from "@renderer/components";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { useAppSelector, useRepacks } from "@renderer/hooks";
|
import { useAppSelector, useRepacks } from "@renderer/hooks";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import "./game-item.scss";
|
import "./game-item.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CatalogueSearchResult } from "@types";
|
||||||
|
|
||||||
export interface GameItemProps {
|
export interface GameItemProps {
|
||||||
game: any;
|
game: CatalogueSearchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GameItem({ game }: GameItemProps) {
|
export function GameItem({ game }: GameItemProps) {
|
||||||
@@ -51,7 +51,7 @@ export function GameItem({ game }: GameItemProps) {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="game-item__cover"
|
className="game-item__cover"
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={game.libraryImageUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
formatDownloadProgress,
|
formatDownloadProgress,
|
||||||
} from "@renderer/helpers";
|
} from "@renderer/helpers";
|
||||||
|
|
||||||
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
import { Downloader, formatBytes } from "@shared";
|
||||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||||
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ export function DownloadGroup({
|
|||||||
<div className="download-group__cover">
|
<div className="download-group__cover">
|
||||||
<div className="download-group__cover-backdrop">
|
<div className="download-group__cover-backdrop">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.library(game.objectId)}
|
src={game.libraryImageUrl ?? ""}
|
||||||
className="download-group__cover-image"
|
className="download-group__cover-image"
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Sidebar } from "./sidebar/sidebar";
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { AuthPage, steamUrlBuilder } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
@@ -59,10 +59,13 @@ export function GameDetailsContent() {
|
|||||||
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
const [backdropOpacity, setBackdropOpacity] = useState(1);
|
||||||
|
|
||||||
const handleHeroLoad = async () => {
|
const handleHeroLoad = async () => {
|
||||||
const output = await average(steamUrlBuilder.libraryHero(objectId!), {
|
const output = await average(
|
||||||
amount: 1,
|
shopDetails?.assets?.libraryHeroImageUrl ?? "",
|
||||||
format: "hex",
|
{
|
||||||
});
|
amount: 1,
|
||||||
|
format: "hex",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const backgroundColor = output
|
const backgroundColor = output
|
||||||
? new Color(output).darken(0.7).toString()
|
? new Color(output).darken(0.7).toString()
|
||||||
@@ -100,7 +103,7 @@ export function GameDetailsContent() {
|
|||||||
<section className="game-details__container">
|
<section className="game-details__container">
|
||||||
<div ref={heroRef} className="game-details__hero">
|
<div ref={heroRef} className="game-details__hero">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.libraryHero(objectId!)}
|
src={shopDetails?.assets?.libraryHeroImageUrl ?? ""}
|
||||||
className="game-details__hero-image"
|
className="game-details__hero-image"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
@@ -119,7 +122,7 @@ export function GameDetailsContent() {
|
|||||||
>
|
>
|
||||||
<div className="game-details__hero-content">
|
<div className="game-details__hero-content">
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(objectId!)}
|
src={shopDetails?.assets?.logoImageUrl ?? ""}
|
||||||
className="game-details__game-logo"
|
className="game-details__game-logo"
|
||||||
alt={game?.title}
|
alt={game?.title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
import { Button, GameCard, Hero } from "@renderer/components";
|
import { Button, GameCard, Hero } from "@renderer/components";
|
||||||
import type { Steam250Game } from "@types";
|
import type { ShopAssets, Steam250Game } from "@types";
|
||||||
|
|
||||||
import flameIconStatic from "@renderer/assets/icons/flame-static.png";
|
import flameIconStatic from "@renderer/assets/icons/flame-static.png";
|
||||||
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
|
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
|
||||||
@@ -27,7 +27,9 @@ export default function Home() {
|
|||||||
CatalogueCategory.Hot
|
CatalogueCategory.Hot
|
||||||
);
|
);
|
||||||
|
|
||||||
const [catalogue, setCatalogue] = useState<Record<CatalogueCategory, any[]>>({
|
const [catalogue, setCatalogue] = useState<
|
||||||
|
Record<CatalogueCategory, ShopAssets[]>
|
||||||
|
>({
|
||||||
[CatalogueCategory.Hot]: [],
|
[CatalogueCategory.Hot]: [],
|
||||||
[CatalogueCategory.Weekly]: [],
|
[CatalogueCategory.Weekly]: [],
|
||||||
[CatalogueCategory.Achievements]: [],
|
[CatalogueCategory.Achievements]: [],
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { userProfileContext } from "@renderer/context";
|
|||||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { steamUrlBuilder } from "@shared";
|
|
||||||
import "./user-library-game-card.scss";
|
import "./user-library-game-card.scss";
|
||||||
|
|
||||||
interface UserLibraryGameCardProps {
|
interface UserLibraryGameCardProps {
|
||||||
@@ -150,7 +149,7 @@ export function UserLibraryGameCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.cover(game.objectId)}
|
src={game.coverImageUrl}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
className="user-library-game__game-image"
|
className="user-library-game__game-image"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,27 +36,41 @@ export interface DownloadSource {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShopAssets {
|
||||||
|
objectId: string;
|
||||||
|
shop: GameShop;
|
||||||
|
title: string;
|
||||||
|
iconUrl: string | null;
|
||||||
|
libraryHeroImageUrl: string;
|
||||||
|
libraryImageUrl: string;
|
||||||
|
logoImageUrl: string;
|
||||||
|
logoPosition: string | null;
|
||||||
|
coverImageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ShopDetails = SteamAppDetails & {
|
export type ShopDetails = SteamAppDetails & {
|
||||||
objectId: string;
|
objectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShopDetailsWithAssets = ShopDetails & {
|
||||||
|
assets: ShopAssets | null;
|
||||||
|
};
|
||||||
|
|
||||||
export interface TorrentFile {
|
export interface TorrentFile {
|
||||||
path: string;
|
path: string;
|
||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserGame {
|
export type UserGame = {
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
title: string;
|
title: string;
|
||||||
iconUrl: string | null;
|
|
||||||
cover: string;
|
|
||||||
playTimeInSeconds: number;
|
playTimeInSeconds: number;
|
||||||
lastTimePlayed: Date | null;
|
lastTimePlayed: Date | null;
|
||||||
unlockedAchievementCount: number;
|
unlockedAchievementCount: number;
|
||||||
achievementCount: number;
|
achievementCount: number;
|
||||||
achievementsPointsEarnedSum: number;
|
achievementsPointsEarnedSum: number;
|
||||||
}
|
} & ShopAssets;
|
||||||
|
|
||||||
export interface GameRunning {
|
export interface GameRunning {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -100,13 +114,11 @@ export interface UserFriend {
|
|||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
currentGame: {
|
currentGame:
|
||||||
title: string;
|
| (ShopAssets & {
|
||||||
iconUrl: string;
|
sessionDurationInSeconds: number;
|
||||||
objectId: string;
|
})
|
||||||
shop: GameShop;
|
| null;
|
||||||
sessionDurationInSeconds: number;
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserFriends {
|
export interface UserFriends {
|
||||||
@@ -138,10 +150,10 @@ export interface UserRelation {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
|
export type UserProfileCurrentGame = GameRunning &
|
||||||
objectId: string;
|
ShopAssets & {
|
||||||
sessionDurationInSeconds: number;
|
sessionDurationInSeconds: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||||
|
|
||||||
@@ -215,13 +227,12 @@ export interface DownloadSourceValidationResult {
|
|||||||
export interface GameStats {
|
export interface GameStats {
|
||||||
downloadCount: number;
|
downloadCount: number;
|
||||||
playerCount: number;
|
playerCount: number;
|
||||||
|
assets: ShopAssets | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrendingGame {
|
export interface TrendingGame extends ShopAssets {
|
||||||
|
description: string | null;
|
||||||
uri: string;
|
uri: string;
|
||||||
description: string;
|
|
||||||
background: string;
|
|
||||||
logo: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserStatsPercentile {
|
export interface UserStatsPercentile {
|
||||||
@@ -302,10 +313,25 @@ export interface CatalogueSearchPayload {
|
|||||||
developers: string[];
|
developers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibraryGame extends Game {
|
export type CatalogueSearchResult = {
|
||||||
id: string;
|
id: string;
|
||||||
download: Download | null;
|
tags: string[];
|
||||||
}
|
genres: string[];
|
||||||
|
objectId: string;
|
||||||
|
shop: GameShop;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
title: string;
|
||||||
|
installCount: number;
|
||||||
|
achievementCount: number;
|
||||||
|
shopData: string;
|
||||||
|
} & ShopAssets;
|
||||||
|
|
||||||
|
export type LibraryGame = Game &
|
||||||
|
Partial<ShopAssets> & {
|
||||||
|
id: string;
|
||||||
|
download: Download | null;
|
||||||
|
};
|
||||||
|
|
||||||
export * from "./game.types";
|
export * from "./game.types";
|
||||||
export * from "./steam.types";
|
export * from "./steam.types";
|
||||||
|
|||||||
Reference in New Issue
Block a user