mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-15 08:23:02 -03:00
refactor: remove game disk usage functionality and update related components
This commit is contained in:
@@ -775,7 +775,8 @@
|
||||
"recently_played": "Recently Played",
|
||||
"favorites": "Favorites",
|
||||
"disk_usage": "Disk usage",
|
||||
"disk_usage_tooltip": "Installed size on disk"
|
||||
"disk_usage_tooltip": "Installed size on disk",
|
||||
"installer_size_tooltip": "Installer size"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "Achievement unlocked",
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop, Game, Download } from "@types";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
// Directories that should never be scanned (too large or system-critical)
|
||||
const UNSAFE_DIRECTORIES = [
|
||||
"C:\\",
|
||||
"C:\\Users",
|
||||
"C:\\Program Files",
|
||||
"C:\\Program Files (x86)",
|
||||
"C:\\Windows",
|
||||
"C:\\ProgramData",
|
||||
"/",
|
||||
"/home",
|
||||
"/usr",
|
||||
"/var",
|
||||
"/opt",
|
||||
];
|
||||
|
||||
const isUnsafeDirectory = (dirPath: string): boolean => {
|
||||
const normalized = path.normalize(dirPath).replace(/[\\/]+$/, "");
|
||||
return UNSAFE_DIRECTORIES.some(
|
||||
(unsafe) => normalized.toLowerCase() === unsafe.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
const getDownloadsPath = async (): Promise<string> => {
|
||||
const { app } = await import("electron");
|
||||
return app.getPath("downloads");
|
||||
};
|
||||
|
||||
const getDirectorySize = async (dirPath: string): Promise<number> => {
|
||||
let totalSize = 0;
|
||||
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
try {
|
||||
if (entry.isDirectory()) {
|
||||
totalSize += await getDirectorySize(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const stat = await fs.promises.stat(fullPath);
|
||||
totalSize += stat.size;
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory can't be read
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
};
|
||||
|
||||
const getGameDiskUsage = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => {
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
let game: Game | undefined;
|
||||
let download: Download | undefined;
|
||||
|
||||
try {
|
||||
game = await gamesSublevel.get(gameKey);
|
||||
} catch {
|
||||
// Game not found
|
||||
}
|
||||
|
||||
try {
|
||||
download = await downloadsSublevel.get(gameKey);
|
||||
} catch {
|
||||
// Download not found
|
||||
}
|
||||
|
||||
let installerSize: number | null = null;
|
||||
let installedSize: number | null = null;
|
||||
|
||||
// Priority 1: Use Hydra-managed download folder
|
||||
if (download?.folderName) {
|
||||
const downloadsPath = download.downloadPath ?? (await getDownloadsPath());
|
||||
const gamePath = path.join(downloadsPath, download.folderName);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(gamePath)) {
|
||||
const stat = await fs.promises.stat(gamePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
installedSize = await getDirectorySize(gamePath);
|
||||
} else if (stat.isFile()) {
|
||||
installerSize = stat.size;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error getting game path stats:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Use executable directory (with safety check)
|
||||
if (game?.executablePath && !installedSize) {
|
||||
const executableDir = path.dirname(game.executablePath);
|
||||
|
||||
if (!isUnsafeDirectory(executableDir)) {
|
||||
try {
|
||||
if (fs.existsSync(executableDir)) {
|
||||
const stat = await fs.promises.stat(executableDir);
|
||||
if (stat.isDirectory()) {
|
||||
installedSize = await getDirectorySize(executableDir);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error getting executable directory stats:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Wine prefix for Linux/macOS
|
||||
if (game?.winePrefixPath && !installedSize) {
|
||||
if (!isUnsafeDirectory(game.winePrefixPath)) {
|
||||
try {
|
||||
if (fs.existsSync(game.winePrefixPath)) {
|
||||
const stat = await fs.promises.stat(game.winePrefixPath);
|
||||
if (stat.isDirectory()) {
|
||||
installedSize = await getDirectorySize(game.winePrefixPath);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error getting wine prefix stats:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { installerSize, installedSize };
|
||||
};
|
||||
|
||||
registerEvent("getGameDiskUsage", getGameDiskUsage);
|
||||
@@ -1,3 +1,2 @@
|
||||
import "./check-folder-write-permission";
|
||||
import "./get-disk-free-space";
|
||||
import "./get-game-disk-usage";
|
||||
|
||||
39
src/main/events/helpers/get-directory-size.ts
Normal file
39
src/main/events/helpers/get-directory-size.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
export const getDirectorySize = async (dirPath: string): Promise<number> => {
|
||||
let totalSize = 0;
|
||||
|
||||
try {
|
||||
const stat = await fs.promises.stat(dirPath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
return stat.size;
|
||||
}
|
||||
|
||||
if (!stat.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
try {
|
||||
if (entry.isDirectory()) {
|
||||
totalSize += await getDirectorySize(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const fileStat = await fs.promises.stat(fullPath);
|
||||
totalSize += fileStat.size;
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be accessed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Path doesn't exist or can't be read
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { logger } from "@main/services";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const deleteArchive = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -11,8 +13,33 @@ const deleteArchive = async (
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.unlink(filePath);
|
||||
logger.info(`Deleted archive: ${filePath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the game that has this archive and clear installer size
|
||||
const normalizedPath = path.normalize(filePath);
|
||||
const downloads = await downloadsSublevel.values().all();
|
||||
|
||||
for (const download of downloads) {
|
||||
if (!download.folderName) continue;
|
||||
|
||||
const downloadPath = path.normalize(
|
||||
path.join(download.downloadPath, download.folderName)
|
||||
);
|
||||
|
||||
if (downloadPath === normalizedPath) {
|
||||
const gameKey = levelKeys.game(download.shop, download.objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
installerSizeInBytes: null,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(`Failed to delete archive: ${filePath}`, err);
|
||||
|
||||
@@ -5,15 +5,15 @@ import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||
import { logger } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { GameShop } from "@types";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const deleteGameFolder = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): Promise<void> => {
|
||||
const downloadKey = levelKeys.game(shop, objectId);
|
||||
const download = await downloadsSublevel.get(downloadKey);
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const download = await downloadsSublevel.get(gameKey);
|
||||
|
||||
if (!download?.folderName) return;
|
||||
|
||||
@@ -49,7 +49,16 @@ const deleteGameFolder = async (
|
||||
|
||||
await deleteFile(folderPath, true);
|
||||
await deleteFile(metaPath);
|
||||
await downloadsSublevel.del(downloadKey);
|
||||
await downloadsSublevel.del(gameKey);
|
||||
|
||||
// Clear installer size from game record
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
installerSizeInBytes: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("deleteGameFolder", deleteGameFolder);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import type { LibraryGame } from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
@@ -28,9 +31,40 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||
achievements?.unlockedAchievements.length ?? 0;
|
||||
}
|
||||
|
||||
// Verify installer still exists, clear if deleted externally
|
||||
let installerSizeInBytes = game.installerSizeInBytes;
|
||||
if (installerSizeInBytes && download?.folderName) {
|
||||
const installerPath = path.join(
|
||||
download.downloadPath,
|
||||
download.folderName
|
||||
);
|
||||
|
||||
if (!fs.existsSync(installerPath)) {
|
||||
installerSizeInBytes = null;
|
||||
gamesSublevel.put(key, { ...game, installerSizeInBytes: null });
|
||||
}
|
||||
}
|
||||
|
||||
// Verify installed folder still exists, clear if deleted externally
|
||||
let installedSizeInBytes = game.installedSizeInBytes;
|
||||
if (installedSizeInBytes && game.executablePath) {
|
||||
const executableDir = path.dirname(game.executablePath);
|
||||
|
||||
if (!fs.existsSync(executableDir)) {
|
||||
installedSizeInBytes = null;
|
||||
gamesSublevel.put(key, {
|
||||
...game,
|
||||
installerSizeInBytes,
|
||||
installedSizeInBytes: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: key,
|
||||
...game,
|
||||
installerSizeInBytes,
|
||||
installedSizeInBytes,
|
||||
download: download ?? null,
|
||||
unlockedAchievementCount,
|
||||
achievementCount: game.achievementCount ?? 0,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||
import { getDirectorySize } from "../helpers/get-directory-size";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
@@ -18,12 +21,29 @@ const updateExecutablePath = async (
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
// Update immediately without size so UI responds fast
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: parsedPath,
|
||||
installedSizeInBytes: parsedPath ? game.installedSizeInBytes : null,
|
||||
automaticCloudSync:
|
||||
executablePath === null ? false : game.automaticCloudSync,
|
||||
});
|
||||
|
||||
// Calculate size in background and update later
|
||||
if (parsedPath) {
|
||||
const executableDir = path.dirname(parsedPath);
|
||||
|
||||
getDirectorySize(executableDir).then(async (installedSizeInBytes) => {
|
||||
const currentGame = await gamesSublevel.get(gameKey);
|
||||
if (!currentGame) return;
|
||||
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...currentGame,
|
||||
installedSizeInBytes,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("updateExecutablePath", updateExecutablePath);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { GameFilesManager } from "../game-files-manager";
|
||||
import { HydraDebridClient } from "./hydra-debrid";
|
||||
import { BuzzheavierApi, FuckingFastApi } from "@main/services/hosters";
|
||||
import { JsHttpDownloader } from "./js-http-downloader";
|
||||
import { getDirectorySize } from "@main/events/helpers/get-directory-size";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: string | null = null;
|
||||
@@ -360,6 +361,24 @@ export class DownloadManager {
|
||||
userPreferences?.seedAfterDownloadComplete
|
||||
);
|
||||
|
||||
// Calculate installer size in background
|
||||
if (download.folderName) {
|
||||
const installerPath = path.join(
|
||||
download.downloadPath,
|
||||
download.folderName
|
||||
);
|
||||
|
||||
getDirectorySize(installerPath).then(async (installerSizeInBytes) => {
|
||||
const currentGame = await gamesSublevel.get(gameId);
|
||||
if (!currentGame) return;
|
||||
|
||||
await gamesSublevel.put(gameId, {
|
||||
...currentGame,
|
||||
installerSizeInBytes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (download.automaticallyExtract) {
|
||||
this.handleExtraction(download, game);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SevenZip, ExtractionProgress } from "./7zip";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { publishExtractionCompleteNotification } from "./notifications";
|
||||
import { logger } from "./logger";
|
||||
import { getDirectorySize } from "@main/events/helpers/get-directory-size";
|
||||
|
||||
const PROGRESS_THROTTLE_MS = 1000;
|
||||
|
||||
@@ -142,6 +143,17 @@ export class GameFilesManager {
|
||||
extractionProgress: 0,
|
||||
});
|
||||
|
||||
// Calculate and store the installed size
|
||||
if (game && download.folderName) {
|
||||
const gamePath = path.join(download.downloadPath, download.folderName);
|
||||
const installedSizeInBytes = await getDirectorySize(gamePath);
|
||||
|
||||
await gamesSublevel.put(this.gameKey, {
|
||||
...game,
|
||||
installedSizeInBytes,
|
||||
});
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
"on-extraction-complete",
|
||||
this.shop,
|
||||
|
||||
@@ -299,8 +299,6 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getDiskFreeSpace", path),
|
||||
checkFolderWritePermission: (path: string) =>
|
||||
ipcRenderer.invoke("checkFolderWritePermission", path),
|
||||
getGameDiskUsage: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("getGameDiskUsage", shop, objectId),
|
||||
|
||||
/* Cloud save */
|
||||
uploadSaveGame: (
|
||||
|
||||
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@@ -238,13 +238,6 @@ declare global {
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
|
||||
checkFolderWritePermission: (path: string) => Promise<boolean>;
|
||||
getGameDiskUsage: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<{
|
||||
installerSize: number | null;
|
||||
installedSize: number | null;
|
||||
}>;
|
||||
|
||||
/* Cloud save */
|
||||
uploadSaveGame: (
|
||||
|
||||
@@ -8,7 +8,6 @@ export * from "./use-format";
|
||||
export * from "./use-feature";
|
||||
export * from "./use-download-options-listener";
|
||||
export * from "./use-game-card";
|
||||
export * from "./use-game-disk-usage";
|
||||
export * from "./use-search-history";
|
||||
export * from "./use-search-suggestions";
|
||||
export * from "./use-hls-video";
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { GameShop } from "@types";
|
||||
import { formatBytes } from "@renderer/utils/format-bytes";
|
||||
import { logger } from "@renderer/logger";
|
||||
|
||||
interface GameDiskUsageResult {
|
||||
installerSize: string | null;
|
||||
installedSize: string | null;
|
||||
totalSize: string | null;
|
||||
isLoading: boolean;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export function useGameDiskUsage(
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
): GameDiskUsageResult {
|
||||
const [installerSizeBytes, setInstallerSizeBytes] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [installedSizeBytes, setInstalledSizeBytes] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const fetchDiskUsage = useCallback(async () => {
|
||||
logger.log("useGameDiskUsage: fetching for", { shop, objectId });
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await window.electron.getGameDiskUsage(shop, objectId);
|
||||
logger.log("useGameDiskUsage: got result", result);
|
||||
setInstallerSizeBytes(result.installerSize);
|
||||
setInstalledSizeBytes(result.installedSize);
|
||||
} catch (err) {
|
||||
logger.error("Failed to fetch disk usage:", err);
|
||||
setInstallerSizeBytes(null);
|
||||
setInstalledSizeBytes(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [shop, objectId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiskUsage();
|
||||
}, [fetchDiskUsage]);
|
||||
|
||||
const installerSize =
|
||||
installerSizeBytes !== null ? formatBytes(installerSizeBytes) : null;
|
||||
const installedSize =
|
||||
installedSizeBytes !== null ? formatBytes(installedSizeBytes) : null;
|
||||
|
||||
const totalBytes =
|
||||
(installerSizeBytes ?? 0) + (installedSizeBytes ?? 0) || null;
|
||||
const totalSize = totalBytes !== null ? formatBytes(totalBytes) : null;
|
||||
|
||||
return {
|
||||
installerSize,
|
||||
installedSize,
|
||||
totalSize,
|
||||
isLoading,
|
||||
refetch: fetchDiskUsage,
|
||||
};
|
||||
}
|
||||
@@ -84,6 +84,45 @@
|
||||
gap: calc(globals.$spacing-unit);
|
||||
}
|
||||
|
||||
&__size-badges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: solid 1px rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
min-height: 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__size-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
&__size-bar-line {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.5),
|
||||
rgba(255, 255, 255, 0.8)
|
||||
);
|
||||
}
|
||||
|
||||
&__size-bar-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__logo-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -140,25 +179,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__disk-usage {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
border: solid 1px rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__disk-usage-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__manual-playtime {
|
||||
color: globals.$warning-color;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LibraryGame } from "@types";
|
||||
import { useGameCard, useGameDiskUsage } from "@renderer/hooks";
|
||||
import { useGameCard } from "@renderer/hooks";
|
||||
import { formatBytes } from "@renderer/utils";
|
||||
import {
|
||||
ClockIcon,
|
||||
AlertFillIcon,
|
||||
TrophyIcon,
|
||||
DatabaseIcon,
|
||||
FileZipIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -40,10 +42,48 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
|
||||
const { formatPlayTime, handleCardClick, handleContextMenuClick } =
|
||||
useGameCard(game, onContextMenu);
|
||||
|
||||
const { installedSize, isLoading: isDiskUsageLoading } = useGameDiskUsage(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
const sizeBars = useMemo(() => {
|
||||
const items: {
|
||||
type: "installer" | "installed";
|
||||
bytes: number;
|
||||
formatted: string;
|
||||
icon: typeof FileZipIcon;
|
||||
tooltipKey: string;
|
||||
}[] = [];
|
||||
|
||||
if (game.installerSizeInBytes) {
|
||||
items.push({
|
||||
type: "installer",
|
||||
bytes: game.installerSizeInBytes,
|
||||
formatted: formatBytes(game.installerSizeInBytes),
|
||||
icon: FileZipIcon,
|
||||
tooltipKey: "installer_size_tooltip",
|
||||
});
|
||||
}
|
||||
|
||||
if (game.installedSizeInBytes) {
|
||||
items.push({
|
||||
type: "installed",
|
||||
bytes: game.installedSizeInBytes,
|
||||
formatted: formatBytes(game.installedSizeInBytes),
|
||||
icon: DatabaseIcon,
|
||||
tooltipKey: "disk_usage_tooltip",
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) return [];
|
||||
|
||||
// Sort by size descending (larger first)
|
||||
items.sort((a, b) => b.bytes - a.bytes);
|
||||
|
||||
// Calculate proportional widths in pixels (max bar is 80px)
|
||||
const maxBytes = items[0].bytes;
|
||||
const maxWidth = 80;
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
widthPx: Math.round((item.bytes / maxBytes) * maxWidth),
|
||||
}));
|
||||
}, [game.installerSizeInBytes, game.installedSizeInBytes]);
|
||||
|
||||
const backgroundImage = useMemo(
|
||||
() =>
|
||||
@@ -106,15 +146,24 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
|
||||
|
||||
<div className="library-game-card-large__overlay">
|
||||
<div className="library-game-card-large__top-section">
|
||||
{installedSize && !isDiskUsageLoading && (
|
||||
<div
|
||||
className="library-game-card-large__disk-usage"
|
||||
title={t("disk_usage_tooltip")}
|
||||
>
|
||||
<DatabaseIcon size={11} />
|
||||
<span className="library-game-card-large__disk-usage-text">
|
||||
{installedSize}
|
||||
</span>
|
||||
{sizeBars.length > 0 && (
|
||||
<div className="library-game-card-large__size-badges">
|
||||
{sizeBars.map((bar) => (
|
||||
<div
|
||||
key={bar.type}
|
||||
className="library-game-card-large__size-bar"
|
||||
title={t(bar.tooltipKey)}
|
||||
>
|
||||
<bar.icon size={11} />
|
||||
<div
|
||||
className={`library-game-card-large__size-bar-line library-game-card-large__size-bar-line--${bar.type}`}
|
||||
style={{ width: `${bar.widthPx}px` }}
|
||||
/>
|
||||
<span className="library-game-card-large__size-bar-text">
|
||||
{bar.formatted}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface Game {
|
||||
automaticCloudSync?: boolean;
|
||||
hasManuallyUpdatedPlaytime?: boolean;
|
||||
newDownloadOptionsCount?: number;
|
||||
installedSizeInBytes?: number | null;
|
||||
installerSizeInBytes?: number | null;
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
|
||||
Reference in New Issue
Block a user