From 33c15baf0e05818cce20f106405f5c0ffd198aab Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 23 Sep 2025 15:21:32 +0300 Subject: [PATCH 1/8] feat: pinning and showing featuring games in profile --- src/locales/en/translation.json | 3 ++ src/main/events/index.ts | 2 ++ src/main/events/library/add-game-to-pinned.ts | 28 +++++++++++++++ .../events/library/remove-game-from-pinned.ts | 28 +++++++++++++++ .../library-sync/merge-with-remote-games.ts | 3 ++ .../library-sync/upload-games-batch.ts | 1 + src/preload/index.ts | 4 +++ src/renderer/src/declaration.d.ts | 5 +++ .../game-details/hero/hero-panel-actions.tsx | 36 +++++++++++++++++++ .../profile-content/profile-content.scss | 28 +++++++++++++++ .../profile-content/profile-content.tsx | 33 +++++++++++++++-- .../user-library-game-card.scss | 14 ++++++++ .../user-library-game-card.tsx | 7 +++- src/types/index.ts | 2 ++ src/types/level.types.ts | 1 + 15 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 src/main/events/library/add-game-to-pinned.ts create mode 100644 src/main/events/library/remove-game-from-pinned.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9f8de8f8..b5162431 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -210,6 +210,8 @@ "download_error_not_cached_on_hydra": "This download is not available on Nimbus.", "game_removed_from_favorites": "Game removed from favorites", "game_added_to_favorites": "Game added to favorites", + "game_removed_from_pinned": "Game removed from pinned", + "game_added_to_pinned": "Game added to pinned", "automatically_extract_downloaded_files": "Automatically extract downloaded files", "create_start_menu_shortcut": "Create Start Menu shortcut", "invalid_wine_prefix_path": "Invalid Wine prefix path", @@ -451,6 +453,7 @@ "last_time_played": "Last played {{period}}", "activity": "Recent Activity", "library": "Library", + "pinned": "Pinned", "total_play_time": "Total playtime", "manual_playtime_tooltip": "This playtime has been manually updated", "no_recent_activity_title": "Hmmm… nothing here", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 9765b517..733f63d7 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -16,6 +16,8 @@ import "./hardware/check-folder-write-permission"; import "./library/add-game-to-library"; import "./library/add-game-to-favorites"; import "./library/remove-game-from-favorites"; +import "./library/add-game-to-pinned"; +import "./library/remove-game-from-pinned"; import "./library/create-game-shortcut"; import "./library/close-game"; import "./library/delete-game-folder"; diff --git a/src/main/events/library/add-game-to-pinned.ts b/src/main/events/library/add-game-to-pinned.ts new file mode 100644 index 00000000..8ed5921f --- /dev/null +++ b/src/main/events/library/add-game-to-pinned.ts @@ -0,0 +1,28 @@ +import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const addGameToPinned = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +) => { + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + if (!game) return; + + HydraApi.put(`/profile/games/${shop}/${objectId}/pin`).catch(() => {}); + + try { + await gamesSublevel.put(gameKey, { + ...game, + pinned: true, + }); + } catch (error) { + throw new Error(`Failed to update game pinned status: ${error}`); + } +}; + +registerEvent("addGameToPinned", addGameToPinned); \ No newline at end of file diff --git a/src/main/events/library/remove-game-from-pinned.ts b/src/main/events/library/remove-game-from-pinned.ts new file mode 100644 index 00000000..613284bd --- /dev/null +++ b/src/main/events/library/remove-game-from-pinned.ts @@ -0,0 +1,28 @@ +import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { HydraApi } from "@main/services"; +import type { GameShop } from "@types"; + +const removeGameFromPinned = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +) => { + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + if (!game) return; + + HydraApi.put(`/profile/games/${shop}/${objectId}/unpin`).catch(() => {}); + + try { + await gamesSublevel.put(gameKey, { + ...game, + pinned: false, + }); + } catch (error) { + throw new Error(`Failed to update game pinned status: ${error}`); + } +}; + +registerEvent("removeGameFromPinned", removeGameFromPinned); \ No newline at end of file diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index a35414ac..0d5d92f8 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -8,6 +8,7 @@ type ProfileGame = { playTimeInMilliseconds: number; hasManuallyUpdatedPlaytime: boolean; isFavorite?: boolean; + isPinned?: boolean; } & ShopAssets; export const mergeWithRemoteGames = async () => { @@ -36,6 +37,7 @@ export const mergeWithRemoteGames = async () => { lastTimePlayed: updatedLastTimePlayed, playTimeInMilliseconds: updatedPlayTime, favorite: game.isFavorite ?? localGame.favorite, + pinned: game.isPinned ?? localGame.pinned, }); } else { await gamesSublevel.put(gameKey, { @@ -49,6 +51,7 @@ export const mergeWithRemoteGames = async () => { hasManuallyUpdatedPlaytime: game.hasManuallyUpdatedPlaytime, isDeleted: false, favorite: game.isFavorite ?? false, + pinned: game.isPinned ?? false, }); } diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 837fb48a..beab164f 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -27,6 +27,7 @@ export const uploadGamesBatch = async () => { shop: game.shop, lastTimePlayed: game.lastTimePlayed, isFavorite: game.favorite, + isPinned: game.pinned ?? false, }; }) ).catch(() => {}); diff --git a/src/preload/index.ts b/src/preload/index.ts index d29417b0..b3c4f400 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -143,6 +143,10 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("addGameToFavorites", shop, objectId), removeGameFromFavorites: (shop: GameShop, objectId: string) => ipcRenderer.invoke("removeGameFromFavorites", shop, objectId), + addGameToPinned: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("addGameToPinned", shop, objectId), + removeGameFromPinned: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("removeGameFromPinned", shop, objectId), updateLaunchOptions: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 0744884c..434c0adb 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -126,6 +126,11 @@ declare global { shop: GameShop, objectId: string ) => Promise; + addGameToPinned: (shop: GameShop, objectId: string) => Promise; + removeGameFromPinned: ( + shop: GameShop, + objectId: string + ) => Promise; updateLaunchOptions: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index a3b75d2e..bb3dfe98 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -3,6 +3,8 @@ import { GearIcon, HeartFillIcon, HeartIcon, + PinIcon, + PinSlashIcon, PlayIcon, PlusCircleIcon, } from "@primer/octicons-react"; @@ -82,6 +84,31 @@ export function HeroPanelActions() { } }; + const toggleGamePinned = async () => { + setToggleLibraryGameDisabled(true); + + try { + if (game?.pinned && objectId) { + await window.electron + .removeGameFromPinned(shop, objectId) + .then(() => { + showSuccessToast(t("game_removed_from_pinned")); + }); + } else { + if (!objectId) return; + + await window.electron.addGameToPinned(shop, objectId).then(() => { + showSuccessToast(t("game_added_to_pinned")); + }); + } + + updateLibrary(); + updateGame(); + } finally { + setToggleLibraryGameDisabled(false); + } + }; + const openGame = async () => { if (game) { if (game.executablePath) { @@ -198,6 +225,15 @@ export function HeroPanelActions() { {game.favorite ? : } + + - + {userDetails && ( + + )} - {userDetails && ( + {userDetails && shop !== "custom" && ( - {userDetails && shop !== "custom" && ( + {userDetails && (