Merge pull request #1447 from leandroperin/feat-favorites
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled

feat: favorites
This commit is contained in:
Chubby Granny Chaser 2025-02-05 23:54:05 +00:00 committed by GitHub
commit 110131f1d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 251 additions and 60 deletions

View file

@ -14,8 +14,10 @@
"paused": "{{title}} (Спынена)",
"downloading": "{{title}} ({{percentage}} - Сцягванне…)",
"filter": "Фільтар бібліятэкі",
"home": "Галоўная"
"home": "Галоўная",
"favorites": "Улюбленыя"
},
"header": {
"search": "Пошук",
"home": "Галоўная",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "Играта няма избран изпълним файл",
"sign_in": "Вписване",
"friends": "Приятели",
"need_help": "Имате нужда от помощ??"
"need_help": "Имате нужда от помощ??",
"favorites": "Любими игри"
},
"header": {
"search": "Търсене",

View file

@ -20,10 +20,12 @@
"home": "Inici",
"queued": "{{title}} (En espera)",
"game_has_no_executable": "El joc encara no té un executable seleccionat",
"sign_in": "Entra"
"sign_in": "Entra",
"favorites": "Favorits"
},
"header": {
"search": "Cerca jocs",
"home": "Inici",
"catalogue": "Catàleg",
"downloads": "Baixades",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
"sign_in": "Přihlásit se",
"friends": "Přátelé",
"need_help": "Potřebujete pomoc?"
"need_help": "Potřebujete pomoc?",
"favorites": "Oblíbené"
},
"header": {
"search": "Vyhledat hry",

View file

@ -24,10 +24,12 @@
"queued": "{{title}} (I køen)",
"game_has_no_executable": "Spillet har ikke nogen eksekverbar fil valgt",
"sign_in": "Log ind",
"friends": "Venner"
"friends": "Venner",
"favorites": "Favoritter"
},
"header": {
"search": "Søg efter spil",
"home": "Hjem",
"catalogue": "Katalog",
"downloads": "Downloads",

View file

@ -20,10 +20,12 @@
"home": "Home",
"queued": "{{title}} (In Warteschlange)",
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
"sign_in": "Anmelden"
"sign_in": "Anmelden",
"favorites": "Favoriten"
},
"header": {
"search": "Spiele suchen",
"home": "Home",
"catalogue": "Katalog",
"downloads": "Downloads",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in",
"friends": "Friends",
"need_help": "Need help?"
"need_help": "Need help?",
"favorites": "Favorites"
},
"header": {
"search": "Search games",
@ -190,6 +191,7 @@
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
},
"activation": {
"title": "Activate Hydra",
"installation_id": "Installation ID:",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar sesión",
"friends": "Amigos",
"need_help": "¿Necesitas ayuda?"
"need_help": "¿Necesitas ayuda?",
"favorites": "Favoritos"
},
"header": {
"search": "Buscar juegos",

View file

@ -25,7 +25,8 @@
"queued": "{{title}} (Järjekorras)",
"game_has_no_executable": "Mängul pole käivitusfaili valitud",
"sign_in": "Logi sisse",
"friends": "Sõbrad"
"friends": "Sõbrad",
"favorites": "Lemmikud"
},
"header": {
"search": "Otsi mänge",

View file

@ -14,8 +14,10 @@
"paused": "{{title}} (متوقف شده)",
"downloading": "{{title}} ({{percentage}} - در حال دانلود…)",
"filter": "فیلتر کردن کتابخانه",
"home": "خانه"
"home": "خانه",
"favorites": "علاقه‌مندی‌ها"
},
"header": {
"search": "جستجوی بازی‌ها",
"home": "خانه",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (En pause)",
"downloading": "{{title}} ({{percentage}} - Téléchargement en cours…)",
"filter": "Filtrer la bibliothèque",
"home": "Page daccueil"
"home": "Page daccueil",
"favorites": "Favoris"
},
"header": {
"search": "Recherche",
"catalogue": "Catalogue",
"downloads": "Téléchargements",
"search_results": "Résultats de la recherche",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (Szünet)",
"downloading": "{{title}} ({{percentage}} - Letöltés…)",
"filter": "Könyvtár szűrése",
"home": "Főoldal"
"home": "Főoldal",
"favorites": "Kedvenc játékok"
},
"header": {
"search": "Keresés",
"home": "Főoldal",
"catalogue": "Katalógus",
"downloads": "Letöltések",

View file

@ -20,10 +20,12 @@
"home": "Beranda",
"queued": "{{title}} (Antrian)",
"game_has_no_executable": "Game tidak punya file eksekusi yang dipilih",
"sign_in": "Masuk"
"sign_in": "Masuk",
"favorites": "Favorit"
},
"header": {
"search": "Cari game",
"home": "Beranda",
"catalogue": "Katalog",
"downloads": "Unduhan",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (In pausa)",
"downloading": "{{title}} ({{percentage}} - Download…)",
"filter": "Filtra libreria",
"home": "Home"
"home": "Home",
"favorites": "Preferiti"
},
"header": {
"search": "Cerca",
"home": "Home",
"catalogue": "Catalogo",
"downloads": "Download",

View file

@ -20,8 +20,10 @@
"home": "Басты бет",
"queued": "{{title}} (Кезекте)",
"game_has_no_executable": "Ойынды іске қосу файлы таңдалмаған",
"sign_in": "Кіру"
"sign_in": "Кіру",
"favorites": "Таңдаулылар"
},
"header": {
"search": "Іздеу",
"home": "Басты бет",

View file

@ -14,8 +14,10 @@
"paused": "{{title}} (일시 정지됨)",
"downloading": "{{title}} ({{percentage}} - 다운로드 중…)",
"filter": "라이브러리 정렬",
"home": "홈"
"home": "홈",
"favorites": "즐겨찾기"
},
"header": {
"search": "게임 검색하기",
"home": "홈",

View file

@ -24,10 +24,12 @@
"queued": "{{title}} (I køen)",
"game_has_no_executable": "Spillet har ikke noen kjørbar fil valgt",
"sign_in": "Logge inn",
"friends": "Venner"
"friends": "Venner",
"favorites": "Favoritter"
},
"header": {
"search": "Søk efter spill",
"home": "Hjem",
"catalogue": "Katalog",
"downloads": "Nedlastinger",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (Gepauzeerd)",
"downloading": "{{title}} ({{percentage}} - Downloading…)",
"filter": "Filter Bibliotheek",
"home": "Home"
"home": "Home",
"favorites": "Favorieten"
},
"header": {
"search": "Zoek spellen",
"home": "Home",
"catalogue": "Bibliotheek",
"downloads": "Downloads",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (Zatrzymano)",
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
"filter": "Filtruj biblioteke",
"home": "Główna"
"home": "Główna",
"favorites": "Ulubione"
},
"header": {
"search": "Szukaj",
"home": "Główna",
"catalogue": "Katalog",
"downloads": "Pobrane",

View file

@ -26,10 +26,12 @@
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos",
"need_help": "Precisa de ajuda?"
"need_help": "Precisa de ajuda?",
"favorites": "Favoritos"
},
"header": {
"search": "Buscar jogos",
"catalogue": "Catálogo",
"downloads": "Downloads",
"search_results": "Resultados da busca",
@ -179,6 +181,7 @@
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
},
"activation": {
"title": "Ativação",
"installation_id": "ID da instalação:",

View file

@ -25,10 +25,12 @@
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "O jogo não tem um executável selecionado",
"sign_in": "Iniciar sessão",
"friends": "Amigos"
"friends": "Amigos",
"favorites": "Favoritos"
},
"header": {
"search": "Procurar jogos",
"catalogue": "Catálogo",
"downloads": "Transferências",
"search_results": "Resultados da pesquisa",

View file

@ -14,10 +14,12 @@
"paused": "{{title}} (Pauzat)",
"downloading": "{{title}} ({{percentage}} - Se descarcă...)",
"filter": "Filtrează biblioteca",
"home": "Acasă"
"home": "Acasă",
"favorites": "Favorite"
},
"header": {
"search": "Caută jocuri",
"home": "Acasă",
"catalogue": "Catalog",
"downloads": "Descărcări",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья",
"need_help": "Нужна помощь?"
"need_help": "Нужна помощь?",
"favorites": "Избранное"
},
"header": {
"search": "Поиск",

View file

@ -26,7 +26,8 @@
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
"sign_in": "Giriş yap",
"friends": "Arkadaşlar",
"need_help": "Yardıma mı ihtiyacınız var?"
"need_help": "Yardıma mı ihtiyacınız var?",
"favorites": "Favoriler"
},
"header": {
"search": "Oyunları ara",

View file

@ -20,10 +20,12 @@
"home": "Головна",
"game_has_no_executable": "Не було вибрано файл для запуску гри",
"queued": "{{title}} в черзі",
"sign_in": "Увійти"
"sign_in": "Увійти",
"favorites": "Улюблені"
},
"header": {
"search": "Пошук",
"home": "Головна",
"catalogue": "Каталог",
"downloads": "Завантаження",

View file

@ -25,7 +25,8 @@
"queued": "{{title}} (已加入下载队列)",
"game_has_no_executable": "未选择游戏的可执行文件",
"sign_in": "登入",
"friends": "好友"
"friends": "好友",
"favorites": "收藏"
},
"header": {
"search": "搜索游戏",

View file

@ -13,6 +13,8 @@ import "./catalogue/get-developers";
import "./hardware/get-disk-free-space";
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/create-game-shortcut";
import "./library/close-game";
import "./library/delete-game-folder";

View file

@ -0,0 +1,25 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
const addGameToFavorites = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
try {
await gamesSublevel.put(gameKey, {
...game,
favorite: true,
});
} catch (error) {
throw new Error(`Failed to update game favorite status: ${error}`);
}
};
registerEvent("addGameToFavorites", addGameToFavorites);

View file

@ -0,0 +1,25 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
const removeGameFromFavorites = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
try {
await gamesSublevel.put(gameKey, {
...game,
favorite: false,
});
} catch (error) {
throw new Error(`Failed to update game favorite status: ${error}`);
}
};
registerEvent("removeGameFromFavorites", removeGameFromFavorites);

View file

@ -110,11 +110,16 @@ contextBridge.exposeInMainWorld("electron", {
executablePath: string | null
) =>
ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath),
addGameToFavorites: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
updateLaunchOptions: (
shop: GameShop,
objectId: string,
launchOptions: string | null
) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions),
selectGameWinePrefix: (
shop: GameShop,
objectId: string,

View file

@ -0,0 +1,50 @@
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { LibraryGame } from "@types";
import cn from "classnames";
import { useLocation } from "react-router-dom";
interface SidebarGameItemProps {
game: LibraryGame;
handleSidebarGameClick: (event: React.MouseEvent, game: LibraryGame) => void;
getGameTitle: (game: LibraryGame) => string;
}
export function SidebarGameItem({
game,
handleSidebarGameClick,
getGameTitle,
}: Readonly<SidebarGameItemProps>) {
const location = useLocation();
return (
<li
key={game.id}
className={cn("sidebar__menu-item", {
"sidebar__menu-item--active":
location.pathname === `/game/${game.shop}/${game.objectId}`,
"sidebar__menu-item--muted": game.download?.status === "removed",
})}
>
<button
type="button"
className="sidebar__menu-item-button"
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className="sidebar__game-icon"
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className="sidebar__game-icon" />
)}
<span className="sidebar__menu-item-button-label">
{getGameTitle(game)}
</span>
</button>
</li>
);
}

View file

@ -18,11 +18,11 @@ import "./sidebar.scss";
import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import cn from "classnames";
import { CommentDiscussionIcon } from "@primer/octicons-react";
import { SidebarGameItem } from "./sidebar-game-item";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
@ -206,6 +206,23 @@ export function Sidebar() {
</ul>
</section>
<section className="sidebar__section">
<small className="sidebar__section-title">{t("favorites")}</small>
<ul className="sidebar__menu">
{sortedLibrary
.filter((game) => game.favorite)
.map((game) => (
<SidebarGameItem
key={game.id}
game={game}
handleSidebarGameClick={handleSidebarGameClick}
getGameTitle={getGameTitle}
/>
))}
</ul>
</section>
<section className="sidebar__section">
<small className="sidebar__section-title">{t("my_library")}</small>
@ -217,39 +234,16 @@ export function Sidebar() {
/>
<ul className="sidebar__menu">
{filteredLibrary.map((game) => (
<li
key={game.id}
className={cn("sidebar__menu-item", {
"sidebar__menu-item--active":
location.pathname ===
`/game/${game.shop}/${game.objectId}`,
"sidebar__menu-item--muted":
game.download?.status === "removed",
})}
>
<button
type="button"
className="sidebar__menu-item-button"
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className="sidebar__game-icon"
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className="sidebar__game-icon" />
)}
<span className="sidebar__menu-item-button-label">
{getGameTitle(game)}
</span>
</button>
</li>
))}
{filteredLibrary
.filter((game) => !game.favorite)
.map((game) => (
<SidebarGameItem
key={game.id}
game={game}
handleSidebarGameClick={handleSidebarGameClick}
getGameTitle={getGameTitle}
/>
))}
</ul>
</section>
</div>

View file

@ -96,6 +96,11 @@ declare global {
objectId: string,
executablePath: string | null
) => Promise<void>;
addGameToFavorites: (shop: GameShop, objectId: string) => Promise<void>;
removeGameFromFavorites: (
shop: GameShop,
objectId: string
) => Promise<void>;
updateLaunchOptions: (
shop: GameShop,
objectId: string,

View file

@ -1,6 +1,8 @@
import {
DownloadIcon,
GearIcon,
HeartFillIcon,
HeartIcon,
PlayIcon,
PlusCircleIcon,
} from "@primer/octicons-react";
@ -52,6 +54,32 @@ export function HeroPanelActions() {
}
};
const addGameToFavorites = async () => {
setToggleLibraryGameDisabled(true);
try {
if (!objectId) throw new Error("objectId is required");
await window.electron.addGameToFavorites(shop, objectId);
updateLibrary();
updateGame();
} finally {
setToggleLibraryGameDisabled(false);
}
};
const removeGameFromFavorites = async () => {
setToggleLibraryGameDisabled(true);
try {
if (!objectId) throw new Error("objectId is required");
await window.electron.removeGameFromFavorites(shop, objectId);
updateLibrary();
updateGame();
} finally {
setToggleLibraryGameDisabled(false);
}
};
const openGame = async () => {
if (game) {
if (game.executablePath) {
@ -159,6 +187,16 @@ export function HeroPanelActions() {
<div className="hero-panel-actions__container">
{gameActionButton()}
<div className="hero-panel-actions__separator" />
<Button
onClick={game.favorite ? removeGameFromFavorites : addGameToFavorites}
theme="outline"
disabled={deleting}
className="hero-panel-actions__action"
>
{game.favorite ? <HeartFillIcon /> : <HeartIcon />}
</Button>
<Button
onClick={() => setShowGameOptionsModal(true)}
theme="outline"

View file

@ -42,6 +42,7 @@ export interface Game {
winePrefixPath?: string | null;
executablePath?: string | null;
launchOptions?: string | null;
favorite?: boolean;
}
export interface Download {