From ef4b9bc2cefe30d62c09c02a2abef768cacca82d Mon Sep 17 00:00:00 2001 From: skjsjhb Date: Sat, 1 Mar 2025 16:14:06 +0800 Subject: [PATCH] feat(ui): impl game pinning - Add entry to pin and unpin games. - Make game card slightly more compact. --- src/main/api/game.ts | 1 + src/main/game/spec.ts | 21 +++++++++++++- src/renderer/pages/games/GameCard.tsx | 12 ++++---- src/renderer/pages/games/GameCardActions.tsx | 30 ++++++++++++++++++-- src/renderer/pages/games/GamesView.tsx | 10 +++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/main/api/game.ts b/src/main/api/game.ts index 20d98260..2be9ef69 100644 --- a/src/main/api/game.ts +++ b/src/main/api/game.ts @@ -111,6 +111,7 @@ ipcMain.handle("addGame", async (_, init) => { versions: { game: p.id }, + user: {}, type }; diff --git a/src/main/game/spec.ts b/src/main/game/spec.ts index 668143fe..2de2728c 100644 --- a/src/main/game/spec.ts +++ b/src/main/game/spec.ts @@ -43,6 +43,16 @@ export interface GameProfile { */ launchHint: LaunchHint; + /** + * User preference object. + */ + user: { + /** + * The priority of the pinned game. + */ + pinTime?: number; + }; + /** * Type of the game core. */ @@ -60,7 +70,7 @@ export type GameCoreType = "neoforged" | "unknown" -export const GAME_REG_VERSION = 2; +export const GAME_REG_VERSION = 3; export const GAME_REG_TRANS: RegistryTransformer[] = [ // v1: patch the `installerProps` key (s) => { @@ -77,6 +87,15 @@ export const GAME_REG_TRANS: RegistryTransformer[] = [ s.installProps.gameVersion = s.launchHint.profileId; } + return s; + }, + + // v3: append `user` key + (s) => { + if (!s.user) { + s.user = {}; + } + return s; } ]; diff --git a/src/renderer/pages/games/GameCard.tsx b/src/renderer/pages/games/GameCard.tsx index 7dc8fe20..6df28c80 100644 --- a/src/renderer/pages/games/GameCard.tsx +++ b/src/renderer/pages/games/GameCard.tsx @@ -3,6 +3,7 @@ import { useInstallProgress } from "@/renderer/services/install"; import { GameTypeImage } from "@components/GameTypeImage"; import { Card, CardBody, Chip } from "@heroui/react"; import { GameCardActions } from "@pages/games/GameCardActions"; +import { clsx } from "clsx"; import { DotIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; @@ -14,21 +15,22 @@ export function GameCard({ game }: GameCardProps) { const { id, name, versions: { game: gameVersion }, installed, type } = game; const { t: tc } = useTranslation("common", { keyPrefix: "progress" }); const installProgress = useInstallProgress(id); + const pinned = game.user.pinTime && game.user.pinTime > 0; const isInstalling = installProgress !== null; const installStatus = isInstalling ? "installing" : installed ? "installed" : "not-installed"; const progressText = installProgress && tc(installProgress.state, { ...installProgress.value }); - return + return -
-
+
+
-
-
{name}
+
+
{name}
{id} { diff --git a/src/renderer/pages/games/GameCardActions.tsx b/src/renderer/pages/games/GameCardActions.tsx index 3454bcf1..32c37067 100644 --- a/src/renderer/pages/games/GameCardActions.tsx +++ b/src/renderer/pages/games/GameCardActions.tsx @@ -1,8 +1,11 @@ +import { alter } from "@/main/util/misc"; +import { useGameProfile } from "@/renderer/services/game"; import { remoteInstaller } from "@/renderer/services/install"; import { procService } from "@/renderer/services/proc"; import { useNav } from "@/renderer/util/nav"; import { Button } from "@heroui/react"; -import { CirclePlayIcon, DownloadIcon, EllipsisIcon, XIcon } from "lucide-react"; +import { clsx } from "clsx"; +import { CirclePlayIcon, DownloadIcon, EllipsisIcon, PinIcon, PinOffIcon, XIcon } from "lucide-react"; import { useState } from "react"; type InstallStatus = "installed" | "installing" | "not-installed"; @@ -14,8 +17,13 @@ interface GameActionsProps { export function GameCardActions({ installStatus, gameId }: GameActionsProps) { const [launching, setLaunching] = useState(false); + const game = useGameProfile(gameId); const nav = useNav(); + if (!game) throw "Game actions cannot be used without corresponding game profile"; + + const pinned = game.user.pinTime && game.user.pinTime > 0; + function handleShowDetails() { nav(`/game-detail/${gameId}`); } @@ -28,6 +36,16 @@ export function GameCardActions({ installStatus, gameId }: GameActionsProps) { void native.install.cancel(gameId); } + function togglePin() { + void native.game.update(alter(game!, g => { + if (g.user.pinTime && g.user.pinTime > 0) { + g.user.pinTime = undefined; + } else { + g.user.pinTime = Date.now(); + } + })); + } + async function launch() { try { setLaunching(true); @@ -65,7 +83,15 @@ export function GameCardActions({ installStatus, gameId }: GameActionsProps) { } - + +
; diff --git a/src/renderer/pages/games/GamesView.tsx b/src/renderer/pages/games/GamesView.tsx index 74c74dc4..68dd601f 100644 --- a/src/renderer/pages/games/GamesView.tsx +++ b/src/renderer/pages/games/GamesView.tsx @@ -56,6 +56,16 @@ export function GamesView() { function toSortedGames(games: GameProfile[], sortMethod: SortMethod): GameProfile[] { return games.toSorted((a, b) => { + const pa = a.user.pinTime; + const pb = b.user.pinTime; + + if (pa && pb) { + return pb - pa; + } + + if (pa) return -1; + if (pb) return 1; + switch (sortMethod) { case "az" : return a.name.localeCompare(b.name);