mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-20 01:33:56 +00:00
Merge pull request #1794 from hydralauncher/feat/revert-title-and-cleanup-fix
Feat: revert title in edit modal and proper cleanup of unused assets
This commit is contained in:
@@ -506,6 +506,8 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} hours",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"amount_hours_short": "{{amount}}h",
|
||||
"amount_minutes_short": "{{amount}}m",
|
||||
"last_time_played": "Last played {{period}}",
|
||||
"activity": "Recent Activity",
|
||||
"library": "Library",
|
||||
|
||||
@@ -486,6 +486,8 @@
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} часов",
|
||||
"amount_minutes": "{{amount}} минут",
|
||||
"amount_hours_short": "{{amount}}ч",
|
||||
"amount_minutes_short": "{{amount}}м",
|
||||
"last_time_played": "Последняя игра {{period}}",
|
||||
"activity": "Недавняя активность",
|
||||
"library": "Библиотека",
|
||||
|
||||
@@ -1,7 +1,70 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop, Game } from "@types";
|
||||
import fs from "node:fs";
|
||||
|
||||
const collectAssetPathsToDelete = (game: Game): string[] => {
|
||||
const assetPathsToDelete: string[] = [];
|
||||
|
||||
const assetUrls =
|
||||
game.shop === "custom"
|
||||
? [game.iconUrl, game.logoImageUrl, game.libraryHeroImageUrl]
|
||||
: [game.customIconUrl, game.customLogoImageUrl, game.customHeroImageUrl];
|
||||
|
||||
for (const url of assetUrls) {
|
||||
if (url?.startsWith("local:")) {
|
||||
assetPathsToDelete.push(url.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return assetPathsToDelete;
|
||||
};
|
||||
|
||||
const updateGameAsDeleted = async (
|
||||
game: Game,
|
||||
gameKey: string
|
||||
): Promise<void> => {
|
||||
const updatedGame = {
|
||||
...game,
|
||||
isDeleted: true,
|
||||
executablePath: null,
|
||||
...(game.shop !== "custom" && {
|
||||
customIconUrl: null,
|
||||
customLogoImageUrl: null,
|
||||
customHeroImageUrl: null,
|
||||
}),
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
};
|
||||
|
||||
const resetShopAssets = async (gameKey: string): Promise<void> => {
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const resetAssets = {
|
||||
...existingAssets,
|
||||
title: existingAssets.title,
|
||||
};
|
||||
await gamesShopAssetsSublevel.put(gameKey, resetAssets);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAssetFiles = async (
|
||||
assetPathsToDelete: string[]
|
||||
): Promise<void> => {
|
||||
if (assetPathsToDelete.length === 0) return;
|
||||
|
||||
for (const assetPath of assetPathsToDelete) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeGameFromLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -11,17 +74,21 @@ const removeGameFromLibrary = async (
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
isDeleted: true,
|
||||
executablePath: null,
|
||||
});
|
||||
if (!game) return;
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
const assetPathsToDelete = collectAssetPathsToDelete(game);
|
||||
|
||||
await updateGameAsDeleted(game, gameKey);
|
||||
|
||||
if (game.shop !== "custom") {
|
||||
await resetShopAssets(gameKey);
|
||||
}
|
||||
|
||||
if (game?.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
|
||||
await deleteAssetFiles(assetPathsToDelete);
|
||||
};
|
||||
|
||||
registerEvent("removeGameFromLibrary", removeGameFromLibrary);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
import fs from "node:fs";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
const updateCustomGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -18,6 +20,20 @@ const updateCustomGame = async (
|
||||
throw new Error("Game not found");
|
||||
}
|
||||
|
||||
const oldAssetPaths: string[] = [];
|
||||
|
||||
const assetPairs = [
|
||||
{ existing: existingGame.iconUrl, new: iconUrl },
|
||||
{ existing: existingGame.logoImageUrl, new: logoImageUrl },
|
||||
{ existing: existingGame.libraryHeroImageUrl, new: libraryHeroImageUrl },
|
||||
];
|
||||
|
||||
for (const { existing, new: newUrl } of assetPairs) {
|
||||
if (existing?.startsWith("local:") && (!newUrl || existing !== newUrl)) {
|
||||
oldAssetPaths.push(existing.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
const updatedGame = {
|
||||
...existingGame,
|
||||
title,
|
||||
@@ -43,6 +59,18 @@ const updateCustomGame = async (
|
||||
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||
}
|
||||
|
||||
if (oldAssetPaths.length > 0) {
|
||||
for (const assetPath of oldAssetPaths) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete old asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,84 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gamesSublevel, gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
import type { GameShop } from "@types";
|
||||
import type { GameShop, Game } from "@types";
|
||||
import fs from "node:fs";
|
||||
import { logger } from "@main/services";
|
||||
|
||||
const collectOldAssetPaths = (
|
||||
existingGame: Game,
|
||||
customIconUrl?: string | null,
|
||||
customLogoImageUrl?: string | null,
|
||||
customHeroImageUrl?: string | null
|
||||
): string[] => {
|
||||
const oldAssetPaths: string[] = [];
|
||||
|
||||
const assetPairs = [
|
||||
{ existing: existingGame.customIconUrl, new: customIconUrl },
|
||||
{ existing: existingGame.customLogoImageUrl, new: customLogoImageUrl },
|
||||
{ existing: existingGame.customHeroImageUrl, new: customHeroImageUrl },
|
||||
];
|
||||
|
||||
for (const { existing, new: newUrl } of assetPairs) {
|
||||
if (
|
||||
existing &&
|
||||
newUrl !== undefined &&
|
||||
existing !== newUrl &&
|
||||
existing.startsWith("local:")
|
||||
) {
|
||||
oldAssetPaths.push(existing.replace("local:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
return oldAssetPaths;
|
||||
};
|
||||
|
||||
const updateGameData = async (
|
||||
gameKey: string,
|
||||
existingGame: Game,
|
||||
title: string,
|
||||
customIconUrl?: string | null,
|
||||
customLogoImageUrl?: string | null,
|
||||
customHeroImageUrl?: string | null
|
||||
): Promise<Game> => {
|
||||
const updatedGame = {
|
||||
...existingGame,
|
||||
title,
|
||||
...(customIconUrl !== undefined && { customIconUrl }),
|
||||
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
|
||||
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
|
||||
};
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
const updateShopAssets = async (
|
||||
gameKey: string,
|
||||
title: string
|
||||
): Promise<void> => {
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const updatedAssets = {
|
||||
...existingAssets,
|
||||
title,
|
||||
};
|
||||
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteOldAssetFiles = async (oldAssetPaths: string[]): Promise<void> => {
|
||||
if (oldAssetPaths.length === 0) return;
|
||||
|
||||
for (const assetPath of oldAssetPaths) {
|
||||
try {
|
||||
if (fs.existsSync(assetPath)) {
|
||||
await fs.promises.unlink(assetPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete old custom asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateGameCustomAssets = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -18,26 +96,25 @@ const updateGameCustomAssets = async (
|
||||
throw new Error("Game not found");
|
||||
}
|
||||
|
||||
const updatedGame = {
|
||||
...existingGame,
|
||||
const oldAssetPaths = collectOldAssetPaths(
|
||||
existingGame,
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl
|
||||
);
|
||||
|
||||
const updatedGame = await updateGameData(
|
||||
gameKey,
|
||||
existingGame,
|
||||
title,
|
||||
...(customIconUrl !== undefined && { customIconUrl }),
|
||||
...(customLogoImageUrl !== undefined && { customLogoImageUrl }),
|
||||
...(customHeroImageUrl !== undefined && { customHeroImageUrl }),
|
||||
};
|
||||
customIconUrl,
|
||||
customLogoImageUrl,
|
||||
customHeroImageUrl
|
||||
);
|
||||
|
||||
await gamesSublevel.put(gameKey, updatedGame);
|
||||
await updateShopAssets(gameKey, title);
|
||||
|
||||
// Also update the shop assets for non-custom games
|
||||
const existingAssets = await gamesShopAssetsSublevel.get(gameKey);
|
||||
if (existingAssets) {
|
||||
const updatedAssets = {
|
||||
...existingAssets,
|
||||
title, // Update the title in shop assets as well
|
||||
};
|
||||
|
||||
await gamesShopAssetsSublevel.put(gameKey, updatedAssets);
|
||||
}
|
||||
await deleteOldAssetFiles(oldAssetPaths);
|
||||
|
||||
return updatedGame;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ImageIcon, XIcon } from "@primer/octicons-react";
|
||||
|
||||
@@ -29,16 +29,24 @@ export function EditGameModal({
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const [gameName, setGameName] = useState("");
|
||||
const [iconPath, setIconPath] = useState("");
|
||||
const [logoPath, setLogoPath] = useState("");
|
||||
const [heroPath, setHeroPath] = useState("");
|
||||
const [assetPaths, setAssetPaths] = useState({
|
||||
icon: "",
|
||||
logo: "",
|
||||
hero: "",
|
||||
});
|
||||
const [assetDisplayPaths, setAssetDisplayPaths] = useState({
|
||||
icon: "",
|
||||
logo: "",
|
||||
hero: "",
|
||||
});
|
||||
const [defaultUrls, setDefaultUrls] = useState({
|
||||
icon: null as string | null,
|
||||
logo: null as string | null,
|
||||
hero: null as string | null,
|
||||
});
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedAssetType, setSelectedAssetType] = useState<AssetType>("icon");
|
||||
|
||||
const [defaultIconUrl, setDefaultIconUrl] = useState<string | null>(null);
|
||||
const [defaultLogoUrl, setDefaultLogoUrl] = useState<string | null>(null);
|
||||
const [defaultHeroUrl, setDefaultHeroUrl] = useState<string | null>(null);
|
||||
|
||||
const isCustomGame = (game: LibraryGame | Game): boolean => {
|
||||
return game.shop === "custom";
|
||||
};
|
||||
@@ -48,26 +56,36 @@ export function EditGameModal({
|
||||
};
|
||||
|
||||
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
|
||||
setIconPath(extractLocalPath(game.iconUrl));
|
||||
setLogoPath(extractLocalPath(game.logoImageUrl));
|
||||
setHeroPath(extractLocalPath(game.libraryHeroImageUrl));
|
||||
setAssetPaths({
|
||||
icon: extractLocalPath(game.iconUrl),
|
||||
logo: extractLocalPath(game.logoImageUrl),
|
||||
hero: extractLocalPath(game.libraryHeroImageUrl),
|
||||
});
|
||||
setAssetDisplayPaths({
|
||||
icon: extractLocalPath(game.iconUrl),
|
||||
logo: extractLocalPath(game.logoImageUrl),
|
||||
hero: extractLocalPath(game.libraryHeroImageUrl),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setNonCustomGameAssets = useCallback(
|
||||
(game: LibraryGame) => {
|
||||
setIconPath(extractLocalPath(game.customIconUrl));
|
||||
setLogoPath(extractLocalPath(game.customLogoImageUrl));
|
||||
setHeroPath(extractLocalPath(game.customHeroImageUrl));
|
||||
setAssetPaths({
|
||||
icon: extractLocalPath(game.customIconUrl),
|
||||
logo: extractLocalPath(game.customLogoImageUrl),
|
||||
hero: extractLocalPath(game.customHeroImageUrl),
|
||||
});
|
||||
setAssetDisplayPaths({
|
||||
icon: extractLocalPath(game.customIconUrl),
|
||||
logo: extractLocalPath(game.customLogoImageUrl),
|
||||
hero: extractLocalPath(game.customHeroImageUrl),
|
||||
});
|
||||
|
||||
setDefaultIconUrl(shopDetails?.assets?.iconUrl || game.iconUrl || null);
|
||||
setDefaultLogoUrl(
|
||||
shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null
|
||||
);
|
||||
setDefaultHeroUrl(
|
||||
shopDetails?.assets?.libraryHeroImageUrl ||
|
||||
game.libraryHeroImageUrl ||
|
||||
null
|
||||
);
|
||||
setDefaultUrls({
|
||||
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
|
||||
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
|
||||
hero: shopDetails?.assets?.libraryHeroImageUrl || game.libraryHeroImageUrl || null,
|
||||
});
|
||||
},
|
||||
[shopDetails]
|
||||
);
|
||||
@@ -93,39 +111,23 @@ export function EditGameModal({
|
||||
};
|
||||
|
||||
const getAssetPath = (assetType: AssetType): string => {
|
||||
switch (assetType) {
|
||||
case "icon":
|
||||
return iconPath;
|
||||
case "logo":
|
||||
return logoPath;
|
||||
case "hero":
|
||||
return heroPath;
|
||||
}
|
||||
return assetPaths[assetType];
|
||||
};
|
||||
|
||||
const getAssetDisplayPath = (assetType: AssetType): string => {
|
||||
return assetDisplayPaths[assetType];
|
||||
};
|
||||
|
||||
const setAssetPath = (assetType: AssetType, path: string): void => {
|
||||
switch (assetType) {
|
||||
case "icon":
|
||||
setIconPath(path);
|
||||
break;
|
||||
case "logo":
|
||||
setLogoPath(path);
|
||||
break;
|
||||
case "hero":
|
||||
setHeroPath(path);
|
||||
break;
|
||||
}
|
||||
setAssetPaths(prev => ({ ...prev, [assetType]: path }));
|
||||
};
|
||||
|
||||
const setAssetDisplayPath = (assetType: AssetType, path: string): void => {
|
||||
setAssetDisplayPaths(prev => ({ ...prev, [assetType]: path }));
|
||||
};
|
||||
|
||||
const getDefaultUrl = (assetType: AssetType): string | null => {
|
||||
switch (assetType) {
|
||||
case "icon":
|
||||
return defaultIconUrl;
|
||||
case "logo":
|
||||
return defaultLogoUrl;
|
||||
case "hero":
|
||||
return defaultHeroUrl;
|
||||
}
|
||||
return defaultUrls[assetType];
|
||||
};
|
||||
|
||||
const handleSelectAsset = async (assetType: AssetType) => {
|
||||
@@ -140,23 +142,45 @@ export function EditGameModal({
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const originalPath = filePaths[0];
|
||||
try {
|
||||
const copiedAssetUrl = await window.electron.copyCustomGameAsset(
|
||||
filePaths[0],
|
||||
originalPath,
|
||||
assetType
|
||||
);
|
||||
setAssetPath(assetType, copiedAssetUrl.replace("local:", ""));
|
||||
setAssetDisplayPath(assetType, originalPath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy ${assetType} asset:`, error);
|
||||
setAssetPath(assetType, filePaths[0]);
|
||||
setAssetPath(assetType, originalPath);
|
||||
setAssetDisplayPath(assetType, originalPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreDefault = (assetType: AssetType) => {
|
||||
setAssetPath(assetType, "");
|
||||
setAssetDisplayPath(assetType, "");
|
||||
};
|
||||
|
||||
const getOriginalTitle = (): string => {
|
||||
if (!game) return "";
|
||||
|
||||
// For non-custom games, the original title is from shopDetails assets
|
||||
return shopDetails?.assets?.title || game.title || "";
|
||||
};
|
||||
|
||||
const handleRestoreDefaultTitle = () => {
|
||||
const originalTitle = getOriginalTitle();
|
||||
setGameName(originalTitle);
|
||||
};
|
||||
|
||||
const isTitleChanged = useMemo((): boolean => {
|
||||
if (!game || isCustomGame(game)) return false;
|
||||
const originalTitle = getOriginalTitle();
|
||||
return gameName.trim() !== originalTitle.trim();
|
||||
}, [game, gameName, shopDetails]);
|
||||
|
||||
const [dragOverTarget, setDragOverTarget] = useState<string | null>(null);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
@@ -232,6 +256,7 @@ export function EditGameModal({
|
||||
|
||||
const assetPath = copiedAssetUrl.replace("local:", "");
|
||||
setAssetPath(assetType, assetPath);
|
||||
setAssetDisplayPath(assetType, filePath);
|
||||
|
||||
showSuccessToast(
|
||||
`${assetType.charAt(0).toUpperCase() + assetType.slice(1)} updated successfully!`
|
||||
@@ -267,10 +292,10 @@ export function EditGameModal({
|
||||
|
||||
// Helper function to prepare custom game assets
|
||||
const prepareCustomGameAssets = (game: LibraryGame | Game) => {
|
||||
const iconUrl = iconPath ? `local:${iconPath}` : game.iconUrl;
|
||||
const logoImageUrl = logoPath ? `local:${logoPath}` : game.logoImageUrl;
|
||||
const libraryHeroImageUrl = heroPath
|
||||
? `local:${heroPath}`
|
||||
const iconUrl = assetPaths.icon ? `local:${assetPaths.icon}` : game.iconUrl;
|
||||
const logoImageUrl = assetPaths.logo ? `local:${assetPaths.logo}` : game.logoImageUrl;
|
||||
const libraryHeroImageUrl = assetPaths.hero
|
||||
? `local:${assetPaths.hero}`
|
||||
: game.libraryHeroImageUrl;
|
||||
|
||||
return { iconUrl, logoImageUrl, libraryHeroImageUrl };
|
||||
@@ -279,9 +304,9 @@ export function EditGameModal({
|
||||
// Helper function to prepare non-custom game assets
|
||||
const prepareNonCustomGameAssets = () => {
|
||||
return {
|
||||
customIconUrl: iconPath ? `local:${iconPath}` : null,
|
||||
customLogoImageUrl: logoPath ? `local:${logoPath}` : null,
|
||||
customHeroImageUrl: heroPath ? `local:${heroPath}` : null,
|
||||
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null,
|
||||
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null,
|
||||
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -343,19 +368,24 @@ export function EditGameModal({
|
||||
};
|
||||
|
||||
// Helper function to reset form to initial state
|
||||
const resetFormToInitialState = (game: LibraryGame | Game) => {
|
||||
setGameName(game.title || "");
|
||||
const resetFormToInitialState = useCallback(
|
||||
(game: LibraryGame | Game) => {
|
||||
setGameName(game.title || "");
|
||||
|
||||
if (isCustomGame(game)) {
|
||||
setCustomGameAssets(game);
|
||||
// Clear default URLs for custom games
|
||||
setDefaultIconUrl(null);
|
||||
setDefaultLogoUrl(null);
|
||||
setDefaultHeroUrl(null);
|
||||
} else {
|
||||
setNonCustomGameAssets(game as LibraryGame);
|
||||
}
|
||||
};
|
||||
if (isCustomGame(game)) {
|
||||
setCustomGameAssets(game);
|
||||
// Clear default URLs for custom games
|
||||
setDefaultUrls({
|
||||
icon: null,
|
||||
logo: null,
|
||||
hero: null,
|
||||
});
|
||||
} else {
|
||||
setNonCustomGameAssets(game as LibraryGame);
|
||||
}
|
||||
},
|
||||
[setCustomGameAssets, setNonCustomGameAssets]
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isUpdating && game) {
|
||||
@@ -378,6 +408,7 @@ export function EditGameModal({
|
||||
|
||||
const renderImageSection = (assetType: AssetType) => {
|
||||
const assetPath = getAssetPath(assetType);
|
||||
const assetDisplayPath = getAssetDisplayPath(assetType);
|
||||
const defaultUrl = getDefaultUrl(assetType);
|
||||
const hasImage = assetPath || (game && !isCustomGame(game) && defaultUrl);
|
||||
const isDragOver = dragOverTarget === assetType;
|
||||
@@ -390,7 +421,7 @@ export function EditGameModal({
|
||||
<div className="edit-game-modal__image-section">
|
||||
<TextField
|
||||
placeholder={t(`edit_game_modal_select_${assetType}`)}
|
||||
value={assetPath}
|
||||
value={assetDisplayPath}
|
||||
readOnly
|
||||
theme="dark"
|
||||
rightContent={
|
||||
@@ -489,6 +520,19 @@ export function EditGameModal({
|
||||
onChange={handleGameNameChange}
|
||||
theme="dark"
|
||||
disabled={isUpdating}
|
||||
rightContent={
|
||||
isTitleChanged && (
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleRestoreDefaultTitle}
|
||||
disabled={isUpdating}
|
||||
title="Restore default title"
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="edit-game-modal__asset-selector">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
display: flex;
|
||||
transition: all ease 0.2s;
|
||||
cursor: grab;
|
||||
container-type: inline-size;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
@@ -86,7 +87,7 @@
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -160,6 +161,25 @@
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&-long {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&-short {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// When the card is narrow (less than 180px), show short format
|
||||
@container (max-width: 180px) {
|
||||
&-long {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-short {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__manual-playtime {
|
||||
color: globals.$warning-color;
|
||||
|
||||
@@ -44,6 +44,7 @@ export function UserLibraryGameCard({
|
||||
const [isTooltipHovered, setIsTooltipHovered] = useState(false);
|
||||
const [isPinning, setIsPinning] = useState(false);
|
||||
|
||||
|
||||
const getStatsItemCount = useCallback(() => {
|
||||
let statsCount = 1;
|
||||
if (game.achievementsPointsEarnedSum > 0) statsCount++;
|
||||
@@ -79,21 +80,26 @@ export function UserLibraryGameCard({
|
||||
};
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds = 0) => {
|
||||
(playTimeInSeconds = 0, isShort = false) => {
|
||||
const minutes = playTimeInSeconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
return t(isShort ? "amount_minutes_short" : "amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
const hoursKey = isShort ? "amount_hours_short" : "amount_hours";
|
||||
const hoursAmount = isShort ? Math.floor(hours) : numberFormatter.format(hours);
|
||||
|
||||
return t(hoursKey, { amount: hoursAmount });
|
||||
},
|
||||
[numberFormatter, t]
|
||||
);
|
||||
|
||||
|
||||
|
||||
const toggleGamePinned = async () => {
|
||||
setIsPinning(true);
|
||||
|
||||
@@ -156,7 +162,7 @@ export function UserLibraryGameCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<small
|
||||
<div
|
||||
className="user-library-game__playtime"
|
||||
data-tooltip-place="top"
|
||||
data-tooltip-content={
|
||||
@@ -174,8 +180,13 @@ export function UserLibraryGameCard({
|
||||
) : (
|
||||
<ClockIcon size={11} />
|
||||
)}
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</small>
|
||||
<span className="user-library-game__playtime-long">
|
||||
{formatPlayTime(game.playTimeInSeconds)}
|
||||
</span>
|
||||
<span className="user-library-game__playtime-short">
|
||||
{formatPlayTime(game.playTimeInSeconds, true)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{userProfile?.hasActiveSubscription &&
|
||||
game.achievementCount > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user