From 8203399edada6c26cc70db1514f198665ef9de8d Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Sun, 12 Oct 2025 23:27:20 +0200
Subject: [PATCH 001/118] Matching to Latest Update
---
src/locales/hu/translation.json | 95 ++++++++++++++++++++++++++-------
1 file changed, 77 insertions(+), 18 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index 551c1363..b72811d3 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -76,7 +76,19 @@
"edit_game_modal_drop_hero_image_here": "Húzd ide a borítókép képét",
"edit_game_modal_drop_to_replace_icon": "Ikon kicserélése ráhúzással",
"edit_game_modal_drop_to_replace_logo": "Logó kicserélése ráhúzással",
- "edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással"
+ "edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással",
+ "install_decky_plugin": "Decky Plugin Telepítése",
+ "update_decky_plugin": "Decky Plugin Frissítése",
+ "decky_plugin_installed_version": "Decky Plugin (v{{version}})",
+ "install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint",
+ "install_decky_plugin_message": "Ez letölti és telepíteni fogja a Hydra plugint a Decky Loaderhez. Előfordulhat, hogy rendszergazdai jogosultságra lesz szükség. Folytatod?",
+ "update_decky_plugin_title": "Hydra Decky Plugin Frissítése",
+ "update_decky_plugin_message": "Egy új verzió elérhető a Hydra Decky Pluginhoz. Szeretnéd frissíteni?",
+ "decky_plugin_installed": "Decky plugin v{{version}} sikeresen telepítve",
+ "decky_plugin_installation_failed": "Decky plugin telepítése sikertelen: {{error}}",
+ "decky_plugin_installation_error": "Decky plugin telepítése hibával járt el: {{error}}",
+ "confirm": "Megerősít",
+ "cancel": "Mégse"
},
"header": {
"search": "Keresés",
@@ -126,7 +138,7 @@
"downloading_metadata": "Metaadat letöltése",
"filter": "Repackek szűrése",
"requirements": "Rendszerkövetelmények",
- "minimum": "Minimum",
+ "minimum": "Minimális",
"recommended": "Ajánlott",
"paused": "Szüneteltetve",
"release_date": "Megjelenés: {{date}}",
@@ -150,7 +162,7 @@
"playing_now": "Játékban: ",
"change": "Változtatás",
"repacks_modal_description": "Válaszd ki a repacket amit leszeretnél tölteni",
- "select_folder_hint": "Hogy megváltoztasd a letöltési mappát, menj a <0>Beállítások0> menüjébe",
+ "select_folder_hint": "A letöltési mappát a <0>Beállítások0> menüjében változtathatod meg",
"download_now": "Letöltés",
"no_shop_details": "A bolt adatai nem érhetőek el.",
"download_options": "Letöltési opciók",
@@ -193,20 +205,54 @@
"failed_remove_from_library": "Játék eltávolítása a könyvtárból sikertelen",
"files_removed_success": "Fájlok eltávolítása sikeres",
"failed_remove_files": "Fájlok eltávolítása sikertelen",
- "nsfw_content_title": "Ez a játék nem megfelelő tartalmat tartalmaz",
+ "nsfw_content_title": "Ez a játék tartalmaz nem megfelelő tartalmat",
"nsfw_content_description": "A(z) {{title}} tartalma lehetséges hogy nem megfelelő minden korosztály számára. Biztosan folytatni szeretnéd?",
"allow_nsfw_content": "Folytatás",
"refuse_nsfw_content": "Vissza",
"stats": "Statisztikák",
"download_count": "Letöltések",
"player_count": "Aktív játékosok",
- "download_error": "Ez a letöltési opció nem elérhető",
+ "rating_count": "Értékelés",
+ "download_error": "Ez a letöltési opció nem elérhető",
"download": "Letöltés",
"executable_path_in_use": "Ez a futtatható fájl már használatban van a(z) \"{{game}}\" által",
"warning": "Figyelmeztetés:",
"hydra_needs_to_remain_open": "ehhez a letöltéshez, a Hydrának muszáj nyitva maradnia hogy letöltődjön. Ha a Hydra bezáródik letöltés előtt, a letöltés elveszik.",
"achievements": "Achievementek",
"achievements_count": "Achievementek {{unlockedCount}}/{{achievementsCount}}",
+ "show_more": "Mutass többet",
+ "show_less": "Mutass kevesebbet",
+ "reviews": "Vélemények",
+ "leave_a_review": "Hagyd itt a véleményed",
+ "write_review_placeholder": "Oszd meg a gondolataid a játékról...",
+ "sort_newest": "Legújabb",
+ "no_reviews_yet": "Még nem lett vélemény megosztva",
+ "be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
+ "sort_oldest": "Legrégibb",
+ "sort_highest_score": "Legmagasabb Pontszám",
+ "sort_lowest_score": "Legalacsonyabb Pontszám",
+ "sort_most_voted": "Legszavazottabb",
+ "rating": "Értékelés",
+ "rating_stats": "Értékelés",
+ "rating_very_negative": "Nagyon Negatív",
+ "rating_negative": "Negatív",
+ "rating_neutral": "Átlagos",
+ "rating_positive": "Pozitív",
+ "rating_very_positive": "Nagyon Pozitív",
+ "submit_review": "Küldés",
+ "submitting": "Küldés alatt...",
+ "review_submitted_successfully": "Vélemény beküldve sikeresen!",
+ "review_submission_failed": "Vélemény beküldése sikertelen. Kérlek próbáld újra.",
+ "review_cannot_be_empty": "A vélemény mező nem lehet üres.",
+ "review_deleted_successfully": "Vélemény sikeresen törölve.",
+ "review_deletion_failed": "Vélemény törlése sikertelen. Kérlek próbáld újra.",
+ "loading_reviews": "Vélemények betöltése...",
+ "loading_more_reviews": "Több vélemény betöltése...",
+ "load_more_reviews": "Több vélemény betöltése",
+ "you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
+ "would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
+ "yes": "Igen",
+ "maybe_later": "Talán Később",
"cloud_save": "Mentés felhőben",
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
"backups": "Biztonsági másolatok",
@@ -219,6 +265,7 @@
"uploading_backup": "Biztonsági mentés feltöltése…",
"no_backups": "Még nem hoztál létre biztonsági másolatot ehhez a játékhoz",
"backup_uploaded": "Biztonsági mentés feltöltve",
+ "backup_failed": "Biztonsági mentés sikertelen",
"backup_deleted": "Biztonsági mentés törölve",
"backup_restored": "Biztonsági mentés helyreállítva",
"see_all_achievements": "Achievementlista megtekintése",
@@ -256,7 +303,7 @@
"download_error_real_debrid_account_not_authorized": "A Real-Debrid fiókod nem jogosult új letöltésekre. Kérlek, ellenőrízd a fiókbeállításaidat, majd próbáld újra.",
"download_error_not_cached_on_real_debrid": "Ez a letöltés nem elérhető a Real-Debriden, és lekérdezni letöltési állapotot még nem lehet.",
"update_playtime_title": "Játékidő frissítése",
- "update_playtime_description": "A(z) {{game}} játékidejének frissítése",
+ "update_playtime_description": "A(z) {{game}} játékidejének frissítése manuálisan",
"update_playtime": "Játékidő frissítése",
"update_playtime_success": "Játékidő sikeresen frissítve",
"update_playtime_error": "A Játékidőnek nem sikerült frissülnie",
@@ -303,11 +350,17 @@
"caption": "Felirat",
"audio": "Hang",
"filter_by_source": "Szűrés forrás szerint",
- "no_repacks_found": "Nem található forrás ehhez a játékhoz"
+ "no_repacks_found": "Nem található forrás ehhez a játékhoz" ,
+ "delete_review": "Vélemény törlése",
+ "remove_review": "Vélemény eltávolítása",
+ "delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
+ "delete_review_modal_description": "Ez a lépés nem vonható vissza.",
+ "delete_review_modal_delete_button": "Törlés",
+ "delete_review_modal_cancel_button": "Mégse"
},
"activation": {
- "title": "Hydra aktiválása",
- "installation_id": "Telepítési azonosító:",
+ "title": "Hydra Aktiválása",
+ "installation_id": "Telepítési Azonosító:",
"enter_activation_code": "Írd be az aktiválási kódod",
"message": "Ha nem tudod hol kérdezz efelől, akkor nem kéne ilyened legyen.",
"activate": "Aktiválás",
@@ -341,7 +394,7 @@
"stop_seeding": "Seedelés leállítása",
"resume_seeding": "Seedelés folytatása",
"options": "Kezelés",
- "alldebrid_size_not_supported": "Letöltési információ az AllDebridhez még nem támogatott",
+ "alldebrid_size_not_supported": "Letöltési információ az AllDebrid-hez még nem támogatott",
"extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…"
},
@@ -445,6 +498,8 @@
"delete_theme_description": "Ez törölni fogja a(z) {{theme}} témát",
"cancel": "Mégsem",
"appearance": "Megjelenés",
+ "debrid": "Debrid",
+ "debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
"enable_torbox": "TorBox bekapcsolása",
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
"torbox_account_linked": "TorBox fiók összekapcsolva",
@@ -520,11 +575,12 @@
"available_one": "Elérhető",
"available_other": "Elérhető",
"no_downloads": "Nincs elérhető letöltés"
+ "calculating": "Feldolgozás"
},
"binary_not_found_modal": {
"title": "A programok nincsenek telepítve",
"description": "Wine vagy Lutris futtatható fájlok nem találhatók a rendszereden",
- "instructions": "Ellenőrízd, hogy melyiket kell helyesen telepíteni a Linux disztribúcióra, hogy a játék normálisan fusson"
+ "instructions": "Ellenőrízd hogy melyiket kell helyesen telepíteni a Linux disztribúciódra, hogy a játék megfelelően fusson"
},
"modal": {
"close": "Bezárás gomb"
@@ -604,7 +660,7 @@
"report_reason_violence": "Fenyegető",
"report_reason_spam": "Spam",
"report_reason_other": "Egyéb",
- "profile_reported": "Profil jelentve",
+ "profile_reported": "Profil bejelentve",
"your_friend_code": "A barát kódod:",
"upload_banner": "Borítókép feltöltés",
"uploading_banner": "Borítókép feltöltése…",
@@ -622,17 +678,20 @@
"error_adding_friend": "Hiba, barátfelkérés sikertelen. Kérlek ellenőrízd a barát kódot",
"friend_code_length_error": "A barát kódnak 8 karakterből kell állnia",
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
- "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez"
+ "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez"
+ "karma": "Karma",
+ "karma_count": "karma",
+ "karma_description": "Pozitív értékelésekre kapott pontok alapján"
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
- "user_achievements": "{{displayName}} Achievementjei",
- "your_achievements": "A te Achievementjeid",
- "unlocked_at": "Feloldva ekkor: {{date}}",
+ "user_achievements": "{{displayName}} achievementjei",
+ "your_achievements": "A te achievementjeid",
+ "unlocked_at": "Feloldva: {{date}}",
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
- "new_achievements_unlocked": "{{achievementCount}} új achievementet oldottál fel {{gameCount}} játékban",
+ "new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
- "achievements_unlocked_for_game": "{{achievementCount}} új achievementet oldottál fel a(z) {{gameTitle}} játékban",
+ "achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
"earned_points": "Megszerzett pontok:",
From 3dc71a8d1fb203d1403c07abcd0fa4e1806f80bb Mon Sep 17 00:00:00 2001
From: whintersnow0
Date: Wed, 15 Oct 2025 19:19:08 +0200
Subject: [PATCH 002/118] refactor: remove unnecessary useMemo hooks
---
.../src/components/text-field/text-field.tsx | 52 +++++--------------
1 file changed, 14 insertions(+), 38 deletions(-)
diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx
index 1c4d54af..7c0cbb58 100644
--- a/src/renderer/src/components/text-field/text-field.tsx
+++ b/src/renderer/src/components/text-field/text-field.tsx
@@ -1,16 +1,13 @@
-import React, { useId, useMemo, useState } from "react";
+import React, { useId, useState } from "react";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
-
import cn from "classnames";
-
import "./text-field.scss";
-export interface TextFieldProps
- extends React.DetailedHTMLProps<
- React.InputHTMLAttributes,
- HTMLInputElement
- > {
+export interface TextFieldProps extends React.DetailedHTMLProps<
+ React.InputHTMLAttributes,
+ HTMLInputElement
+> {
theme?: "primary" | "dark";
label?: string | React.ReactNode;
hint?: string | React.ReactNode;
@@ -42,44 +39,27 @@ export const TextField = React.forwardRef(
) => {
const id = useId();
const [isFocused, setIsFocused] = useState(false);
-
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
-
const { t } = useTranslation("forms");
-
const showPasswordToggleButton = props.type === "password";
-
- const inputType = useMemo(() => {
- if (props.type === "password" && isPasswordVisible) return "text";
- return props.type ?? "text";
- }, [props.type, isPasswordVisible]);
-
- const hintContent = useMemo(() => {
- if (error)
- return (
- {error}
- );
-
- if (hint) return {hint};
- return null;
- }, [hint, error]);
-
+ const inputType = props.type === "password" && isPasswordVisible ? "text" : props.type ?? "text";
+ const hintContent = error ? (
+ {error}
+ ) : hint ? (
+ {hint}
+ ) : null;
const handleFocus: React.FocusEventHandler = (event) => {
setIsFocused(true);
- if (props.onFocus) props.onFocus(event);
+ props.onFocus?.(event);
};
-
const handleBlur: React.FocusEventHandler = (event) => {
setIsFocused(false);
- if (props.onBlur) props.onBlur(event);
+ props.onBlur?.(event);
};
-
const hasError = !!error;
-
return (
{label &&
}
-
(
onBlur={handleBlur}
type={inputType}
/>
-
{showPasswordToggleButton && (
)}
-
{rightContent}
-
{hintContent}
);
}
);
-
-TextField.displayName = "TextField";
+TextField.displayName = "TextField";
\ No newline at end of file
From c2273dbf712842ef7f6241d8a68b154816bbb64a Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sat, 18 Oct 2025 14:07:44 +0100
Subject: [PATCH 003/118] feat: moving sources to worker
---
src/main/events/download-sources/helpers.ts | 85 ++--------
.../download-sources/sync-download-sources.ts | 20 +--
src/main/main.ts | 4 +
.../services/game-matcher-worker-manager.ts | 145 ++++++++++++++++
src/main/services/index.ts | 1 +
src/main/workers/game-matcher-worker.ts | 158 ++++++++++++++++++
6 files changed, 329 insertions(+), 84 deletions(-)
create mode 100644 src/main/services/game-matcher-worker-manager.ts
create mode 100644 src/main/workers/game-matcher-worker.ts
diff --git a/src/main/events/download-sources/helpers.ts b/src/main/events/download-sources/helpers.ts
index 2e7489fd..edd3878e 100644
--- a/src/main/events/download-sources/helpers.ts
+++ b/src/main/events/download-sources/helpers.ts
@@ -192,83 +192,30 @@ export const addNewDownloads = async (
const batch = repacksSublevel.batch();
+ // Get title hash mapping and perform matching in worker thread
const titleHashMapping = await getTitleHashMapping();
- let hashMatchCount = 0;
- let fuzzyMatchCount = 0;
- let noMatchCount = 0;
- for (const download of downloads) {
- let objectIds: string[] = [];
- let usedHashMatch = false;
+ const { GameMatcherWorkerManager } = await import("@main/services");
+ const matchResult = await GameMatcherWorkerManager.matchDownloads(
+ downloads,
+ steamGames,
+ titleHashMapping
+ );
- const titleHash = hashTitle(download.title);
- const steamIdsFromHash = titleHashMapping[titleHash];
-
- if (steamIdsFromHash && steamIdsFromHash.length > 0) {
- hashMatchCount++;
- usedHashMatch = true;
-
- objectIds = steamIdsFromHash.map(String);
- }
-
- if (!usedHashMatch) {
- let gamesInSteam: FormattedSteamGame[] = [];
- const formattedTitle = formatRepackName(download.title);
-
- if (formattedTitle && formattedTitle.length > 0) {
- const [firstLetter] = formattedTitle;
- const games = steamGames[firstLetter] || [];
-
- gamesInSteam = games.filter((game) =>
- formattedTitle.startsWith(game.formattedName)
- );
-
- if (gamesInSteam.length === 0) {
- gamesInSteam = games.filter(
- (game) =>
- formattedTitle.includes(game.formattedName) ||
- game.formattedName.includes(formattedTitle)
- );
- }
-
- if (gamesInSteam.length === 0) {
- for (const letter of Object.keys(steamGames)) {
- const letterGames = steamGames[letter] || [];
- const matches = letterGames.filter(
- (game) =>
- formattedTitle.includes(game.formattedName) ||
- game.formattedName.includes(formattedTitle)
- );
- if (matches.length > 0) {
- gamesInSteam = matches;
- break;
- }
- }
- }
-
- if (gamesInSteam.length > 0) {
- fuzzyMatchCount++;
- objectIds = gamesInSteam.map((game) => String(game.id));
- } else {
- noMatchCount++;
- }
- } else {
- noMatchCount++;
- }
- }
-
- for (const id of objectIds) {
+ // Process matched results and write to database
+ for (const matchedDownload of matchResult.matchedDownloads) {
+ for (const id of matchedDownload.objectIds) {
objectIdsOnSource.add(id);
}
const repack = {
id: nextRepackId++,
- objectIds: objectIds,
- title: download.title,
- uris: download.uris,
- fileSize: download.fileSize,
+ objectIds: matchedDownload.objectIds,
+ title: matchedDownload.title,
+ uris: matchedDownload.uris,
+ fileSize: matchedDownload.fileSize,
repacker: downloadSource.name,
- uploadDate: download.uploadDate,
+ uploadDate: matchedDownload.uploadDate,
downloadSourceId: downloadSource.id,
createdAt: now,
updatedAt: now,
@@ -280,7 +227,7 @@ export const addNewDownloads = async (
await batch.write();
logger.info(
- `Matching stats for ${downloadSource.name}: Hash=${hashMatchCount}, Fuzzy=${fuzzyMatchCount}, None=${noMatchCount}`
+ `Matching stats for ${downloadSource.name}: Hash=${matchResult.stats.hashMatchCount}, Fuzzy=${matchResult.stats.fuzzyMatchCount}, None=${matchResult.stats.noMatchCount}`
);
const existingSource = await downloadSourcesSublevel.get(
diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts
index 88861074..3bb78f22 100644
--- a/src/main/events/download-sources/sync-download-sources.ts
+++ b/src/main/events/download-sources/sync-download-sources.ts
@@ -31,20 +31,10 @@ const syncDownloadSources = async (
downloadSources.push(source);
}
- const existingRepacks: Array<{
- id: number;
- title: string;
- uris: string[];
- repacker: string;
- fileSize: string | null;
- objectIds: string[];
- uploadDate: Date | string | null;
- downloadSourceId: number;
- createdAt: Date;
- updatedAt: Date;
- }> = [];
+ // Use a Set for O(1) lookups instead of O(n) with array.some()
+ const existingRepackTitles = new Set();
for await (const [, repack] of repacksSublevel.iterator()) {
- existingRepacks.push(repack);
+ existingRepackTitles.add(repack.title);
}
// Handle sources with missing fingerprints individually, don't delete all sources
@@ -77,9 +67,9 @@ const syncDownloadSources = async (
const source = downloadSourceSchema.parse(response.data);
const steamGames = await getSteamGames();
+ // O(1) lookup instead of O(n) - massive performance improvement
const repacks = source.downloads.filter(
- (download) =>
- !existingRepacks.some((repack) => repack.title === download.title)
+ (download) => !existingRepackTitles.has(download.title)
);
await downloadSourcesSublevel.put(`${downloadSource.id}`, {
diff --git a/src/main/main.ts b/src/main/main.ts
index 5eecb101..e9b6187c 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -17,6 +17,7 @@ import {
Lock,
DeckyPlugin,
ResourceCache,
+ GameMatcherWorkerManager,
} from "@main/services";
export const loadState = async () => {
@@ -25,6 +26,9 @@ export const loadState = async () => {
ResourceCache.initialize();
await ResourceCache.updateResourcesOnStartup();
+ // Initialize game matcher worker thread
+ GameMatcherWorkerManager.initialize();
+
const userPreferences = await db.get(
levelKeys.userPreferences,
{
diff --git a/src/main/services/game-matcher-worker-manager.ts b/src/main/services/game-matcher-worker-manager.ts
new file mode 100644
index 00000000..b5d306c7
--- /dev/null
+++ b/src/main/services/game-matcher-worker-manager.ts
@@ -0,0 +1,145 @@
+import { Worker } from "worker_threads";
+import workerPath from "../workers/game-matcher-worker?modulePath";
+
+interface WorkerMessage {
+ id: string;
+ data: unknown;
+}
+
+interface WorkerResponse {
+ id: string;
+ success: boolean;
+ result?: unknown;
+ error?: string;
+}
+
+export type TitleHashMapping = Record;
+
+export type FormattedSteamGame = {
+ id: string;
+ name: string;
+ formattedName: string;
+};
+export type FormattedSteamGamesByLetter = Record;
+
+interface DownloadToMatch {
+ title: string;
+ uris: string[];
+ uploadDate: string;
+ fileSize: string;
+}
+
+interface MatchedDownload {
+ title: string;
+ uris: string[];
+ uploadDate: string;
+ fileSize: string;
+ objectIds: string[];
+ usedHashMatch: boolean;
+}
+
+interface MatchResponse {
+ matchedDownloads: MatchedDownload[];
+ stats: {
+ hashMatchCount: number;
+ fuzzyMatchCount: number;
+ noMatchCount: number;
+ };
+}
+
+export class GameMatcherWorkerManager {
+ private static worker: Worker | null = null;
+ private static messageId = 0;
+ private static pendingMessages = new Map<
+ string,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ { resolve: (value: any) => void; reject: (error: Error) => void }
+ >();
+
+ public static initialize() {
+ if (this.worker) {
+ return;
+ }
+
+ try {
+ console.log(
+ "[GameMatcherWorker] Initializing worker with path:",
+ workerPath
+ );
+
+ this.worker = new Worker(workerPath);
+
+ this.worker.on("message", (response: WorkerResponse) => {
+ const pending = this.pendingMessages.get(response.id);
+ if (pending) {
+ if (response.success) {
+ pending.resolve(response.result);
+ } else {
+ pending.reject(new Error(response.error || "Unknown error"));
+ }
+ this.pendingMessages.delete(response.id);
+ }
+ });
+
+ this.worker.on("error", (error) => {
+ console.error("[GameMatcherWorker] Worker error:", error);
+ for (const [id, pending] of this.pendingMessages.entries()) {
+ pending.reject(error);
+ this.pendingMessages.delete(id);
+ }
+ });
+
+ this.worker.on("exit", (code) => {
+ if (code !== 0) {
+ console.error(
+ `[GameMatcherWorker] Worker stopped with exit code ${code}`
+ );
+ }
+ this.worker = null;
+ for (const [id, pending] of this.pendingMessages.entries()) {
+ pending.reject(new Error("Worker exited unexpectedly"));
+ this.pendingMessages.delete(id);
+ }
+ });
+
+ console.log("[GameMatcherWorker] Worker initialized successfully");
+ } catch (error) {
+ console.error("[GameMatcherWorker] Failed to initialize worker:", error);
+ throw error;
+ }
+ }
+
+ private static sendMessage(data: unknown): Promise {
+ if (!this.worker) {
+ return Promise.reject(new Error("Worker not initialized"));
+ }
+
+ const id = `msg_${++this.messageId}`;
+ const message: WorkerMessage = { id, data };
+
+ return new Promise((resolve, reject) => {
+ this.pendingMessages.set(id, { resolve, reject });
+ this.worker!.postMessage(message);
+ });
+ }
+
+ public static async matchDownloads(
+ downloads: DownloadToMatch[],
+ steamGames: FormattedSteamGamesByLetter,
+ titleHashMapping: TitleHashMapping
+ ): Promise {
+ return this.sendMessage({
+ downloads,
+ steamGames,
+ titleHashMapping,
+ });
+ }
+
+ public static terminate() {
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ this.pendingMessages.clear();
+ }
+ }
+}
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index c98f09e1..0853859f 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -19,3 +19,4 @@ export * from "./wine";
export * from "./lock";
export * from "./decky-plugin";
export * from "./resource-cache";
+export * from "./game-matcher-worker-manager";
diff --git a/src/main/workers/game-matcher-worker.ts b/src/main/workers/game-matcher-worker.ts
new file mode 100644
index 00000000..4930ada0
--- /dev/null
+++ b/src/main/workers/game-matcher-worker.ts
@@ -0,0 +1,158 @@
+import { parentPort } from "worker_threads";
+import crypto from "node:crypto";
+
+export type TitleHashMapping = Record;
+
+export type FormattedSteamGame = {
+ id: string;
+ name: string;
+ formattedName: string;
+};
+export type FormattedSteamGamesByLetter = Record;
+
+interface DownloadToMatch {
+ title: string;
+ uris: string[];
+ uploadDate: string;
+ fileSize: string;
+}
+
+interface MatchedDownload {
+ title: string;
+ uris: string[];
+ uploadDate: string;
+ fileSize: string;
+ objectIds: string[];
+ usedHashMatch: boolean;
+}
+
+interface MatchRequest {
+ downloads: DownloadToMatch[];
+ steamGames: FormattedSteamGamesByLetter;
+ titleHashMapping: TitleHashMapping;
+}
+
+interface MatchResponse {
+ matchedDownloads: MatchedDownload[];
+ stats: {
+ hashMatchCount: number;
+ fuzzyMatchCount: number;
+ noMatchCount: number;
+ };
+}
+
+const hashTitle = (title: string): string => {
+ return crypto.createHash("sha256").update(title).digest("hex");
+};
+
+const formatName = (name: string) => {
+ return name
+ .normalize("NFD")
+ .replaceAll(/[\u0300-\u036f]/g, "")
+ .toLowerCase()
+ .replaceAll(/[^a-z0-9]/g, "");
+};
+
+const formatRepackName = (name: string) => {
+ return formatName(name.replace("[DL]", ""));
+};
+
+const matchDownloads = (request: MatchRequest): MatchResponse => {
+ const { downloads, steamGames, titleHashMapping } = request;
+ const matchedDownloads: MatchedDownload[] = [];
+
+ let hashMatchCount = 0;
+ let fuzzyMatchCount = 0;
+ let noMatchCount = 0;
+
+ for (const download of downloads) {
+ let objectIds: string[] = [];
+ let usedHashMatch = false;
+
+ const titleHash = hashTitle(download.title);
+ const steamIdsFromHash = titleHashMapping[titleHash];
+
+ if (steamIdsFromHash && steamIdsFromHash.length > 0) {
+ hashMatchCount++;
+ usedHashMatch = true;
+ objectIds = steamIdsFromHash.map(String);
+ }
+
+ if (!usedHashMatch) {
+ let gamesInSteam: FormattedSteamGame[] = [];
+ const formattedTitle = formatRepackName(download.title);
+
+ if (formattedTitle && formattedTitle.length > 0) {
+ const [firstLetter] = formattedTitle;
+ const games = steamGames[firstLetter] || [];
+
+ gamesInSteam = games.filter((game) =>
+ formattedTitle.startsWith(game.formattedName)
+ );
+
+ if (gamesInSteam.length === 0) {
+ gamesInSteam = games.filter(
+ (game) =>
+ formattedTitle.includes(game.formattedName) ||
+ game.formattedName.includes(formattedTitle)
+ );
+ }
+
+ if (gamesInSteam.length === 0) {
+ for (const letter of Object.keys(steamGames)) {
+ const letterGames = steamGames[letter] || [];
+ const matches = letterGames.filter(
+ (game) =>
+ formattedTitle.includes(game.formattedName) ||
+ game.formattedName.includes(formattedTitle)
+ );
+ if (matches.length > 0) {
+ gamesInSteam = matches;
+ break;
+ }
+ }
+ }
+
+ if (gamesInSteam.length > 0) {
+ fuzzyMatchCount++;
+ objectIds = gamesInSteam.map((game) => String(game.id));
+ } else {
+ noMatchCount++;
+ }
+ } else {
+ noMatchCount++;
+ }
+ }
+
+ matchedDownloads.push({
+ ...download,
+ objectIds,
+ usedHashMatch,
+ });
+ }
+
+ return {
+ matchedDownloads,
+ stats: {
+ hashMatchCount,
+ fuzzyMatchCount,
+ noMatchCount,
+ },
+ };
+};
+
+// Message handler
+if (parentPort) {
+ parentPort.on("message", (message: { id: string; data: MatchRequest }) => {
+ try {
+ const result = matchDownloads(message.data);
+ parentPort!.postMessage({ id: message.id, success: true, result });
+ } catch (error) {
+ parentPort!.postMessage({
+ id: message.id,
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+ });
+}
From d168e203855e3592885721c30ab355ec99a54177 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Mon, 20 Oct 2025 23:43:47 +0530
Subject: [PATCH 004/118] feat(library): implement large game card and enhance
library UI
- Added `LibraryGameCardLarge` component for displaying games in a larger format with improved styling and animations.
- Introduced SCSS styles for the large game card, including hover effects and gradient overlays.
- Updated `LibraryGameCard` component to support mouse enter and leave events for better interaction.
- Enhanced the library view options with new styles and functionality for switching between grid, compact, and large views.
- Improved overall layout and responsiveness of the library page, ensuring a better user experience across different screen sizes.
- Added tooltips for playtime information and context menus for game actions.
---
src/locales/en/translation.json | 24 ++
src/main/events/index.ts | 1 +
src/main/events/library/get-library.ts | 8 +-
.../events/library/refresh-library-assets.ts | 8 +
.../library-sync/merge-with-remote-games.ts | 9 +-
src/preload/index.ts | 1 +
src/renderer/src/app.scss | 2 +-
.../src/components/header/header.scss | 4 +-
src/renderer/src/components/header/header.tsx | 3 +
.../src/components/sidebar/routes.tsx | 6 +
src/renderer/src/declaration.d.ts | 1 +
src/renderer/src/main.tsx | 2 +
.../src/pages/library/filter-options.scss | 58 +++++
.../src/pages/library/filter-options.tsx | 61 +++++
.../library/library-game-card-detailed.scss | 217 +++++++++++++++++
.../library/library-game-card-detailed.tsx | 209 ++++++++++++++++
.../library/library-game-card-large.scss | 227 ++++++++++++++++++
.../pages/library/library-game-card-large.tsx | 205 ++++++++++++++++
.../src/pages/library/library-game-card.scss | 206 ++++++++++++++++
.../src/pages/library/library-game-card.tsx | 212 ++++++++++++++++
src/renderer/src/pages/library/library.scss | 204 ++++++++++++++++
src/renderer/src/pages/library/library.tsx | 164 +++++++++++++
.../src/pages/library/view-options.scss | 50 ++++
.../src/pages/library/view-options.tsx | 45 ++++
24 files changed, 1920 insertions(+), 7 deletions(-)
create mode 100644 src/main/events/library/refresh-library-assets.ts
create mode 100644 src/renderer/src/pages/library/filter-options.scss
create mode 100644 src/renderer/src/pages/library/filter-options.tsx
create mode 100644 src/renderer/src/pages/library/library-game-card-detailed.scss
create mode 100644 src/renderer/src/pages/library/library-game-card-detailed.tsx
create mode 100644 src/renderer/src/pages/library/library-game-card-large.scss
create mode 100644 src/renderer/src/pages/library/library-game-card-large.tsx
create mode 100644 src/renderer/src/pages/library/library-game-card.scss
create mode 100644 src/renderer/src/pages/library/library-game-card.tsx
create mode 100644 src/renderer/src/pages/library/library.scss
create mode 100644 src/renderer/src/pages/library/library.tsx
create mode 100644 src/renderer/src/pages/library/view-options.scss
create mode 100644 src/renderer/src/pages/library/view-options.tsx
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 46bdb28c..97c1e42a 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -13,6 +13,7 @@
},
"sidebar": {
"catalogue": "Catalogue",
+ "library": "Library",
"downloads": "Downloads",
"settings": "Settings",
"my_library": "My library",
@@ -94,6 +95,7 @@
"search": "Search games",
"home": "Home",
"catalogue": "Catalogue",
+ "library": "Library",
"downloads": "Downloads",
"search_results": "Search results",
"settings": "Settings",
@@ -678,6 +680,28 @@
"karma_count": "karma",
"karma_description": "Earned from positive likes on reviews"
},
+ "library": {
+ "library": "Library",
+ "play": "Play",
+ "download": "Download",
+ "downloading": "Downloading",
+ "game": "game",
+ "games": "games",
+ "grid_view": "Grid view",
+ "compact_view": "Compact view",
+ "large_view": "Large view",
+ "no_games_title": "Your library is empty",
+ "no_games_description": "Add games from the catalogue or download them to get started",
+ "amount_hours": "{{amount}} hours",
+ "amount_minutes": "{{amount}} minutes",
+ "amount_hours_short": "{{amount}}h",
+ "amount_minutes_short": "{{amount}}m",
+ "manual_playtime_tooltip": "This playtime has been manually updated",
+ "all_games": "All Games",
+ "favourited_games": "Favourited",
+ "new_games": "New Games",
+ "top_10": "Top 10"
+ },
"achievement": {
"achievement_unlocked": "Achievement unlocked",
"user_achievements": "{{displayName}}'s Achievements",
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 8d21aa11..a533de1a 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -18,6 +18,7 @@ import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
+import "./library/refresh-library-assets";
import "./library/extract-game-download";
import "./library/open-game";
import "./library/open-game-executable-path";
diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts
index 6314f83d..8922802b 100644
--- a/src/main/events/library/get-library.ts
+++ b/src/main/events/library/get-library.ts
@@ -22,10 +22,12 @@ const getLibrary = async (): Promise => {
id: key,
...game,
download: download ?? null,
+ // Spread gameAssets last to ensure all image URLs are properly set
...gameAssets,
- // Ensure compatibility with LibraryGame type
- libraryHeroImageUrl:
- game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl,
+ // Preserve custom image URLs from game if they exist
+ customIconUrl: game.customIconUrl,
+ customLogoImageUrl: game.customLogoImageUrl,
+ customHeroImageUrl: game.customHeroImageUrl,
} as LibraryGame;
})
);
diff --git a/src/main/events/library/refresh-library-assets.ts b/src/main/events/library/refresh-library-assets.ts
new file mode 100644
index 00000000..d8578f1b
--- /dev/null
+++ b/src/main/events/library/refresh-library-assets.ts
@@ -0,0 +1,8 @@
+import { registerEvent } from "../register-event";
+import { mergeWithRemoteGames } from "@main/services";
+
+const refreshLibraryAssets = async () => {
+ await mergeWithRemoteGames();
+};
+
+registerEvent("refreshLibraryAssets", refreshLibraryAssets);
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 f7ea2744..33d3e3b5 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -60,13 +60,20 @@ export const mergeWithRemoteGames = async () => {
const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey);
+ // Construct coverImageUrl if not provided by backend (Steam games use predictable pattern)
+ const coverImageUrl =
+ game.coverImageUrl ||
+ (game.shop === "steam"
+ ? `https://shared.steamstatic.com/store_item_assets/steam/apps/${game.objectId}/library_600x900_2x.jpg`
+ : null);
+
await gamesShopAssetsSublevel.put(gameKey, {
updatedAt: Date.now(),
...localGameShopAsset,
shop: game.shop,
objectId: game.objectId,
title: localGame?.title || game.title, // Preserve local title if it exists
- coverImageUrl: game.coverImageUrl,
+ coverImageUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
libraryImageUrl: game.libraryImageUrl,
logoImageUrl: game.logoImageUrl,
diff --git a/src/preload/index.ts b/src/preload/index.ts
index da914b92..2b8816c9 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -208,6 +208,7 @@ contextBridge.exposeInMainWorld("electron", {
verifyExecutablePathInUse: (executablePath: string) =>
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
getLibrary: () => ipcRenderer.invoke("getLibrary"),
+ refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
openGameInstaller: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("openGameInstaller", shop, objectId),
openGameInstallerPath: (shop: GameShop, objectId: string) =>
diff --git a/src/renderer/src/app.scss b/src/renderer/src/app.scss
index 4c5374e8..ed7b9aa8 100644
--- a/src/renderer/src/app.scss
+++ b/src/renderer/src/app.scss
@@ -5,7 +5,7 @@
}
::-webkit-scrollbar {
- width: 9px;
+ width: 4px;
background-color: globals.$dark-background-color;
}
diff --git a/src/renderer/src/components/header/header.scss b/src/renderer/src/components/header/header.scss
index f0c72ce0..cd25d8e2 100644
--- a/src/renderer/src/components/header/header.scss
+++ b/src/renderer/src/components/header/header.scss
@@ -24,7 +24,7 @@
background-color: globals.$background-color;
display: inline-flex;
transition: all ease 0.2s;
- width: 200px;
+ width: 300px;
align-items: center;
border-radius: 8px;
border: solid 1px globals.$border-color;
@@ -35,7 +35,7 @@
}
&--focused {
- width: 250px;
+ width: 350px;
border-color: #dadbe1;
}
}
diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx
index e61f3954..328d7f64 100644
--- a/src/renderer/src/components/header/header.tsx
+++ b/src/renderer/src/components/header/header.tsx
@@ -13,6 +13,7 @@ import cn from "classnames";
const pathTitle: Record = {
"/": "home",
"/catalogue": "catalogue",
+ "/library": "library",
"/downloads": "downloads",
"/settings": "settings",
};
@@ -41,6 +42,8 @@ export function Header() {
if (location.pathname.startsWith("/game")) return headerTitle;
if (location.pathname.startsWith("/achievements")) return headerTitle;
if (location.pathname.startsWith("/profile")) return headerTitle;
+ if (location.pathname.startsWith("/library"))
+ return headerTitle || t("library");
if (location.pathname.startsWith("/search")) return t("search_results");
return t(pathTitle[location.pathname]);
diff --git a/src/renderer/src/components/sidebar/routes.tsx b/src/renderer/src/components/sidebar/routes.tsx
index 608718b6..f579329e 100644
--- a/src/renderer/src/components/sidebar/routes.tsx
+++ b/src/renderer/src/components/sidebar/routes.tsx
@@ -3,6 +3,7 @@ import {
DownloadIcon,
GearIcon,
HomeIcon,
+ BookIcon,
} from "@primer/octicons-react";
export const routes = [
@@ -16,6 +17,11 @@ export const routes = [
nameKey: "catalogue",
render: () => ,
},
+ {
+ path: "/library",
+ nameKey: "library",
+ render: () => ,
+ },
{
path: "/downloads",
nameKey: "downloads",
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 9f882aed..fc5a4b97 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -161,6 +161,7 @@ declare global {
) => Promise;
verifyExecutablePathInUse: (executablePath: string) => Promise;
getLibrary: () => Promise;
+ refreshLibraryAssets: () => Promise;
openGameInstaller: (shop: GameShop, objectId: string) => Promise;
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise;
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise;
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index a1b5f7d0..84c7f815 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -29,6 +29,7 @@ import Settings from "./pages/settings/settings";
import Profile from "./pages/profile/profile";
import Achievements from "./pages/achievements/achievements";
import ThemeEditor from "./pages/theme-editor/theme-editor";
+import Library from "./pages/library/library";
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
console.log = logger.log;
@@ -64,6 +65,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
}>
} />
} />
+ } />
} />
} />
} />
diff --git a/src/renderer/src/pages/library/filter-options.scss b/src/renderer/src/pages/library/filter-options.scss
new file mode 100644
index 00000000..e58e285b
--- /dev/null
+++ b/src/renderer/src/pages/library/filter-options.scss
@@ -0,0 +1,58 @@
+@use "../../scss/globals.scss";
+
+.library-filter-options {
+ &__container {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ flex-wrap: wrap;
+ }
+
+ &__option {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ padding: 8px 16px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.05);
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 500;
+ transition: all ease 0.2s;
+ white-space: nowrap; /* prevent label and count from wrapping */
+
+ &:hover {
+ color: rgba(255, 255, 255, 0.9);
+ background: rgba(255, 255, 255, 0.08);
+ }
+
+ &.active {
+ color: #c9aa71;
+ background: rgba(201, 170, 113, 0.15);
+
+ .library-filter-options__count {
+ background: rgba(201, 170, 113, 0.25);
+ color: #c9aa71;
+ }
+ }
+ }
+
+ &__label {
+ font-weight: 500;
+ white-space: nowrap;
+ }
+
+ &__count {
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.8);
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 600;
+ min-width: 24px;
+ text-align: center;
+ transition: all ease 0.2s;
+ }
+}
diff --git a/src/renderer/src/pages/library/filter-options.tsx b/src/renderer/src/pages/library/filter-options.tsx
new file mode 100644
index 00000000..07c935d9
--- /dev/null
+++ b/src/renderer/src/pages/library/filter-options.tsx
@@ -0,0 +1,61 @@
+import { useTranslation } from "react-i18next";
+import "./filter-options.scss";
+
+export type FilterOption = "all" | "favourited" | "new" | "top10";
+
+interface FilterOptionsProps {
+ filterBy: FilterOption;
+ onFilterChange: (filterBy: FilterOption) => void;
+ allGamesCount: number;
+ favouritedCount: number;
+ newGamesCount: number;
+ top10Count: number;
+}
+
+export function FilterOptions({
+ filterBy,
+ onFilterChange,
+ allGamesCount,
+ favouritedCount,
+ newGamesCount,
+ top10Count,
+}: FilterOptionsProps) {
+ const { t } = useTranslation("library");
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/src/pages/library/library-game-card-detailed.scss b/src/renderer/src/pages/library/library-game-card-detailed.scss
new file mode 100644
index 00000000..0038e918
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card-detailed.scss
@@ -0,0 +1,217 @@
+@use "../../scss/globals.scss";
+
+.library-game-card-detailed {
+ width: 100%;
+ height: 350px;
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ transition: all ease 0.2s;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ padding: 0;
+ text-align: left;
+
+ &:before {
+ content: "";
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 172%;
+ position: absolute;
+ background: linear-gradient(
+ 35deg,
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0.07) 51.5%,
+ rgba(255, 255, 255, 0.15) 74%,
+ rgba(255, 255, 255, 0.1) 100%
+ );
+ transition: all ease 0.3s;
+ transform: translateY(-36%);
+ opacity: 0.5;
+ z-index: 1;
+ }
+
+ &:hover::before {
+ opacity: 1;
+ transform: translateY(-20%);
+ }
+
+ &:hover {
+ transform: scale(1.05);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+ border-color: rgba(255, 255, 255, 0.1);
+ }
+
+ &__background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-size: cover;
+ background-position: top;
+ background-repeat: no-repeat;
+ z-index: 0;
+ }
+
+ &__gradient {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0.3) 50%,
+ rgba(0, 0, 0, 0.5) 100%
+ );
+ z-index: 1;
+ }
+
+ &__overlay {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: calc(globals.$spacing-unit * 3);
+ }
+
+ &__menu-button {
+ align-self: flex-end;
+ background: rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border: solid 1px rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all ease 0.2s;
+ color: rgba(255, 255, 255, 0.8);
+ padding: 0;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.6);
+ border-color: rgba(255, 255, 255, 0.25);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+ }
+
+ &__logo-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__logo {
+ max-height: 140px;
+ max-width: 450px;
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.6));
+ }
+
+ &__title {
+ font-size: 32px;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.95);
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
+ }
+
+ &__info-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: calc(globals.$spacing-unit * 2);
+ }
+
+ &__playtime {
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ color: rgba(255, 255, 255, 0.8);
+ border: solid 1px rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ font-size: 14px;
+ }
+
+ &__playtime-text {
+ font-weight: 500;
+ }
+
+ &__manual-playtime {
+ color: globals.$warning-color;
+ }
+
+ &__action-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 24px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.9);
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ transition: all ease 0.2s;
+ flex-shrink: 0;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+ }
+
+ &__action-icon--downloading {
+ animation: pulse 1.5s ease-in-out infinite;
+ }
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
diff --git a/src/renderer/src/pages/library/library-game-card-detailed.tsx b/src/renderer/src/pages/library/library-game-card-detailed.tsx
new file mode 100644
index 00000000..d07dad28
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card-detailed.tsx
@@ -0,0 +1,209 @@
+import { LibraryGame } from "@types";
+import { useDownload, useFormat } from "@renderer/hooks";
+import { useNavigate } from "react-router-dom";
+import { buildGameDetailsPath } from "@renderer/helpers";
+import {
+ PlayIcon,
+ DownloadIcon,
+ ClockIcon,
+ AlertFillIcon,
+ ThreeBarsIcon,
+} from "@primer/octicons-react";
+import { useTranslation } from "react-i18next";
+import { useCallback, useState } from "react";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
+import { GameContextMenu } from "@renderer/components";
+import "./library-game-card-detailed.scss";
+
+interface LibraryGameCardDetailedProps {
+ game: LibraryGame;
+}
+
+const getImageWithCustomPriority = (
+ customUrl: string | null | undefined,
+ originalUrl: string | null | undefined,
+ fallbackUrl?: string | null | undefined
+) => {
+ return customUrl || originalUrl || fallbackUrl || "";
+};
+
+export function LibraryGameCardDetailed({
+ game,
+}: LibraryGameCardDetailedProps) {
+ const { t } = useTranslation("library");
+ const { numberFormatter } = useFormat();
+ const navigate = useNavigate();
+ const { lastPacket } = useDownload();
+ const [contextMenu, setContextMenu] = useState<{
+ visible: boolean;
+ position: { x: number; y: number };
+ }>({ visible: false, position: { x: 0, y: 0 } });
+
+ const isGameDownloading =
+ game?.download?.status === "active" && lastPacket?.gameId === game?.id;
+
+ const formatPlayTime = useCallback(
+ (playTimeInMilliseconds = 0, isShort = false) => {
+ const minutes = playTimeInMilliseconds / 60000;
+
+ if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
+ return t(isShort ? "amount_minutes_short" : "amount_minutes", {
+ amount: minutes.toFixed(0),
+ });
+ }
+
+ const hours = minutes / 60;
+ 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 handleCardClick = () => {
+ navigate(buildGameDetailsPath(game));
+ };
+
+ const handleActionClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ if (game.executablePath) {
+ window.electron.openGame(
+ game.shop,
+ game.objectId,
+ game.executablePath,
+ game.launchOptions
+ );
+ } else {
+ navigate(buildGameDetailsPath(game));
+ }
+ };
+
+ const handleContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setContextMenu({
+ visible: true,
+ position: { x: e.clientX, y: e.clientY },
+ });
+ };
+
+ const handleMenuButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setContextMenu({
+ visible: true,
+ position: {
+ x: e.currentTarget.getBoundingClientRect().right,
+ y: e.currentTarget.getBoundingClientRect().bottom,
+ },
+ });
+ };
+
+ const handleCloseContextMenu = () => {
+ setContextMenu({ visible: false, position: { x: 0, y: 0 } });
+ };
+
+ // Use libraryHeroImageUrl as background, fallback to libraryImageUrl
+ const backgroundImage = getImageWithCustomPriority(
+ game.libraryHeroImageUrl,
+ game.libraryImageUrl,
+ game.iconUrl
+ );
+
+ // For logo, check if logoImageUrl exists (similar to game details page)
+ const logoImage = game.logoImageUrl;
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss
new file mode 100644
index 00000000..86a5a792
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card-large.scss
@@ -0,0 +1,227 @@
+@use "../../scss/globals.scss";
+
+.library-game-card-large {
+ width: 100%;
+ height: 300px;
+ position: relative;
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ transition: all ease 0.2s;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ padding: 0;
+ text-align: left;
+
+ &:before {
+ content: "";
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 172%;
+ position: absolute;
+ background: linear-gradient(
+ 35deg,
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0.07) 51.5%,
+ rgba(255, 255, 255, 0.15) 74%,
+ rgba(255, 255, 255, 0.1) 100%
+ );
+ transition: all ease 0.3s;
+ transform: translateY(-36%);
+ opacity: 0.5;
+ z-index: 1;
+ }
+
+ &:hover::before {
+ opacity: 1;
+ transform: translateY(-20%);
+ }
+
+ &:hover {
+ transform: scale(1.05);
+ transform: translateY(-2px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+ border-color: rgba(255, 255, 255, 0.1);
+ }
+
+ &__background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 0;
+ }
+
+ &__gradient {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0.3) 50%,
+ rgba(0, 0, 0, 0.5) 100%
+ );
+ z-index: 1;
+ }
+
+ &__overlay {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: calc(globals.$spacing-unit * 2.5);
+ }
+
+ &__menu-button {
+ align-self: flex-end;
+ background: rgba(0, 0, 0, 0.3);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border: solid 1px rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all ease 0.2s;
+ color: rgba(255, 255, 255, 0.8);
+ padding: 0;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ opacity: 0;
+ transform: scale(0.9);
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.6);
+ border-color: rgba(255, 255, 255, 0.25);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+ }
+
+ &__logo-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__logo {
+ max-height: 120px;
+ max-width: 400px;
+ width: auto;
+ height: auto;
+ object-fit: contain;
+ filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.6));
+ }
+
+ &__title {
+ font-size: 28px;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.95);
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
+ }
+
+ &__info-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: calc(globals.$spacing-unit * 2);
+ }
+
+ &__playtime {
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ color: rgba(255, 255, 255, 0.8);
+ border: solid 1px rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ font-size: 14px;
+ }
+
+ &__playtime-text {
+ font-weight: 500;
+ }
+
+ &__manual-playtime {
+ color: globals.$warning-color;
+ }
+
+ &__action-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 24px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ transition: all ease 0.2s;
+ flex-shrink: 0;
+ opacity: 0;
+ transform: translateY(10px);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+ }
+
+ &:hover &__menu-button,
+ &:hover &__action-button {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ &__action-icon--downloading {
+ animation: pulse 1.5s ease-in-out infinite;
+ }
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx
new file mode 100644
index 00000000..49d93048
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card-large.tsx
@@ -0,0 +1,205 @@
+import { LibraryGame } from "@types";
+import { useDownload, useFormat } from "@renderer/hooks";
+import { useNavigate } from "react-router-dom";
+import { buildGameDetailsPath } from "@renderer/helpers";
+import {
+ PlayIcon,
+ DownloadIcon,
+ ClockIcon,
+ AlertFillIcon,
+ ThreeBarsIcon,
+} from "@primer/octicons-react";
+import { useTranslation } from "react-i18next";
+import { useCallback, useState } from "react";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
+import { GameContextMenu } from "@renderer/components";
+import "./library-game-card-large.scss";
+
+interface LibraryGameCardLargeProps {
+ game: LibraryGame;
+}
+
+const getImageWithCustomPriority = (
+ customUrl: string | null | undefined,
+ originalUrl: string | null | undefined,
+ fallbackUrl?: string | null | undefined
+) => {
+ return customUrl || originalUrl || fallbackUrl || "";
+};
+
+export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
+ const { t } = useTranslation("library");
+ const { numberFormatter } = useFormat();
+ const navigate = useNavigate();
+ const { lastPacket } = useDownload();
+ const [contextMenu, setContextMenu] = useState<{
+ visible: boolean;
+ position: { x: number; y: number };
+ }>({ visible: false, position: { x: 0, y: 0 } });
+
+ const isGameDownloading =
+ game?.download?.status === "active" && lastPacket?.gameId === game?.id;
+
+ const formatPlayTime = useCallback(
+ (playTimeInMilliseconds = 0, isShort = false) => {
+ const minutes = playTimeInMilliseconds / 60000;
+
+ if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
+ return t(isShort ? "amount_minutes_short" : "amount_minutes", {
+ amount: minutes.toFixed(0),
+ });
+ }
+
+ const hours = minutes / 60;
+ 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 handleCardClick = () => {
+ navigate(buildGameDetailsPath(game));
+ };
+
+ const handleActionClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ if (game.executablePath) {
+ window.electron.openGame(
+ game.shop,
+ game.objectId,
+ game.executablePath,
+ game.launchOptions
+ );
+ } else {
+ navigate(buildGameDetailsPath(game));
+ }
+ };
+
+ const handleContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setContextMenu({
+ visible: true,
+ position: { x: e.clientX, y: e.clientY },
+ });
+ };
+
+ const handleMenuButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setContextMenu({
+ visible: true,
+ position: {
+ x: e.currentTarget.getBoundingClientRect().right,
+ y: e.currentTarget.getBoundingClientRect().bottom,
+ },
+ });
+ };
+
+ const handleCloseContextMenu = () => {
+ setContextMenu({ visible: false, position: { x: 0, y: 0 } });
+ };
+
+ // Use libraryHeroImageUrl as background, fallback to libraryImageUrl
+ const backgroundImage = getImageWithCustomPriority(
+ game.libraryHeroImageUrl,
+ game.libraryImageUrl,
+ game.iconUrl
+ );
+
+ // For logo, check if logoImageUrl exists (similar to game details page)
+ const logoImage = game.logoImageUrl;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {logoImage ? (
+

+ ) : (
+
{game.title}
+ )}
+
+
+
+
+ {game.hasManuallyUpdatedPlaytime ? (
+
+ ) : (
+
+ )}
+
+ {formatPlayTime(game.playTimeInMilliseconds)}
+
+
+
+
+ {isGameDownloading ? (
+ <>
+
+ {t("downloading")}
+ >
+ ) : game.executablePath ? (
+ <>
+
+ {t("play")}
+ >
+ ) : (
+ <>
+
+ {t("download")}
+ >
+ )}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss
new file mode 100644
index 00000000..30adacec
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card.scss
@@ -0,0 +1,206 @@
+@use "../../scss/globals.scss";
+
+.library-game-card {
+ &__wrapper {
+ cursor: pointer;
+ transition: all ease 0.2s;
+ box-shadow: 0 8px 10px -2px rgba(0, 0, 0, 0.5);
+ width: 100%;
+ aspect-ratio: 3 / 4;
+ position: relative;
+ border: none;
+ background: none;
+ padding: 0;
+ border-radius: 4px;
+ overflow: hidden;
+ display: block;
+ container-type: inline-size;
+
+ &:before {
+ content: "";
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 172%;
+ position: absolute;
+ background: linear-gradient(
+ 35deg,
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0.07) 51.5%,
+ rgba(255, 255, 255, 0.15) 64%,
+ rgba(255, 255, 255, 0.1) 100%
+ );
+ transition: all ease 0.3s;
+ transform: translateY(-36%);
+ opacity: 0.5;
+ z-index: 1;
+ }
+
+ &:hover {
+ transform: scale(1.05);
+ }
+
+ &:hover::before {
+ opacity: 1;
+ transform: translateY(-20%);
+ }
+ }
+
+ &__overlay {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ height: 100%;
+ width: 100%;
+ background: linear-gradient(0deg, rgba(0, 0, 0, 0.2) 20%, transparent 100%);
+ padding: 8px;
+ z-index: 2;
+ }
+
+ &__top-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ }
+
+ &__playtime {
+ background: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ color: rgba(255, 255, 255, 0.8);
+ border: solid 1px rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ transition: all ease 0.2s;
+
+ &-long {
+ display: inline;
+ font-size: 12px;
+ }
+
+ &-short {
+ display: none;
+ font-size: 12px;
+ }
+
+ // When the card is narrow (less than 140px), show short format
+ @container (max-width: 140px) {
+ &-long {
+ display: none;
+ }
+
+ &-short {
+ display: inline;
+ }
+ }
+ }
+
+ &__manual-playtime {
+ color: globals.$warning-color;
+ }
+
+ &__action-button {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border: solid 1px rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all ease 0.2s;
+ color: rgba(255, 255, 255, 0.9);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ transform: scale(0.9);
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.8);
+ border-color: rgba(255, 255, 255, 0.4);
+ transform: scale(1.1);
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+ }
+
+ &__menu-button {
+ 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;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all ease 0.2s;
+ color: rgba(255, 255, 255, 0.8);
+ padding: 0;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ opacity: 0;
+ transform: scale(0.9);
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.6);
+ border-color: rgba(255, 255, 255, 0.25);
+ transform: scale(1.05);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+ }
+
+ &__wrapper:hover &__action-button,
+ &__wrapper:hover &__menu-button {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ &__action-icon {
+ &--downloading {
+ animation: pulse 1.5s ease-in-out infinite;
+ }
+ }
+
+ &__game-image {
+ object-fit: cover;
+ border-radius: 4px;
+ width: 100%;
+ height: 100%;
+ min-width: 100%;
+ min-height: 100%;
+ display: block;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ }
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx
new file mode 100644
index 00000000..e18df1b0
--- /dev/null
+++ b/src/renderer/src/pages/library/library-game-card.tsx
@@ -0,0 +1,212 @@
+import { LibraryGame } from "@types";
+import { useFormat, useDownload } from "@renderer/hooks";
+import { useNavigate } from "react-router-dom";
+import { useCallback, useState } from "react";
+import { buildGameDetailsPath } from "@renderer/helpers";
+import {
+ ClockIcon,
+ PlayIcon,
+ DownloadIcon,
+ AlertFillIcon,
+ ThreeBarsIcon,
+} from "@primer/octicons-react";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
+import { Tooltip } from "react-tooltip";
+import { useTranslation } from "react-i18next";
+import { GameContextMenu } from "@renderer/components";
+import "./library-game-card.scss";
+
+interface LibraryGameCardProps {
+ game: LibraryGame;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+}
+
+export function LibraryGameCard({
+ game,
+ onMouseEnter,
+ onMouseLeave,
+}: LibraryGameCardProps) {
+ const { t } = useTranslation("library");
+ const { numberFormatter } = useFormat();
+ const navigate = useNavigate();
+ const [isTooltipHovered, setIsTooltipHovered] = useState(false);
+ const { lastPacket } = useDownload();
+ const [contextMenu, setContextMenu] = useState<{
+ visible: boolean;
+ position: { x: number; y: number };
+ }>({ visible: false, position: { x: 0, y: 0 } });
+
+ const isGameDownloading =
+ game?.download?.status === "active" && lastPacket?.gameId === game?.id;
+
+ const formatPlayTime = useCallback(
+ (playTimeInMilliseconds = 0, isShort = false) => {
+ const minutes = playTimeInMilliseconds / 60000;
+
+ if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
+ return t(isShort ? "amount_minutes_short" : "amount_minutes", {
+ amount: minutes.toFixed(0),
+ });
+ }
+
+ const hours = minutes / 60;
+ 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 handleCardClick = () => {
+ navigate(buildGameDetailsPath(game));
+ };
+
+ const handleActionClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ if (game.executablePath) {
+ // Game is installed, launch it
+ window.electron.openGame(
+ game.shop,
+ game.objectId,
+ game.executablePath,
+ game.launchOptions
+ );
+ } else {
+ // Game is not installed, navigate to download options
+ navigate(buildGameDetailsPath(game));
+ }
+ };
+
+ const handleContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ setContextMenu({
+ visible: true,
+ position: { x: e.clientX, y: e.clientY },
+ });
+ };
+
+ const handleMenuButtonClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setContextMenu({
+ visible: true,
+ position: {
+ x: e.currentTarget.getBoundingClientRect().right,
+ y: e.currentTarget.getBoundingClientRect().bottom,
+ },
+ });
+ };
+
+ const handleCloseContextMenu = () => {
+ setContextMenu({ visible: false, position: { x: 0, y: 0 } });
+ };
+
+ const coverImage =
+ game.coverImageUrl ??
+ game.libraryImageUrl ??
+ game.libraryHeroImageUrl ??
+ game.iconUrl ??
+ undefined;
+
+ return (
+ <>
+
+
+
+
+ {game.hasManuallyUpdatedPlaytime ? (
+
+ ) : (
+
+ )}
+
+ {formatPlayTime(game.playTimeInMilliseconds)}
+
+
+ {formatPlayTime(game.playTimeInMilliseconds, true)}
+
+
+
+
+
+
+
+
+ {/* Action button - Play or Download */}
+
+ {isGameDownloading ? (
+
+ ) : game.executablePath ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ setIsTooltipHovered(true)}
+ afterHide={() => setIsTooltipHovered(false)}
+ />
+
+ >
+ );
+}
diff --git a/src/renderer/src/pages/library/library.scss b/src/renderer/src/pages/library/library.scss
new file mode 100644
index 00000000..1e8038d9
--- /dev/null
+++ b/src/renderer/src/pages/library/library.scss
@@ -0,0 +1,204 @@
+@use "../../scss/globals.scss";
+
+.library {
+ &__content {
+ padding: calc(globals.$spacing-unit * 3);
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 3);
+ align-items: flex-start;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ }
+
+ &__page-header {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 1.5);
+ width: 100%;
+ }
+
+ &__page-title {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.95);
+ }
+
+ &__controls-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ gap: calc(globals.$spacing-unit * 2);
+ }
+
+ &__controls-left {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ }
+
+ &__controls-right {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ }
+
+ &__header-controls {
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+ gap: calc(globals.$spacing-unit * 1);
+ &__left {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 1);
+ }
+ }
+ &__header-title {
+ font-size: 20px;
+ font-weight: 700;
+ }
+ &__filter-label {
+ font-size: 14px;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.8);
+ white-space: nowrap;
+ }
+
+ &__separator {
+ width: 100%;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ margin: 0;
+ }
+
+ &__count {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 8px 16px;
+ }
+
+ &__count-label {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 13px;
+ font-weight: 500;
+ }
+
+ &__count-number {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ &__no-games {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: globals.$spacing-unit;
+ padding: calc(globals.$spacing-unit * 4);
+ }
+
+ &__telescope-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background-color: rgba(255, 255, 255, 0.06);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: calc(globals.$spacing-unit * 2);
+ }
+
+ &__games-grid {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: calc(globals.$spacing-unit * 2);
+ width: 100%;
+
+ // Grid view - larger cards
+ &--grid {
+ grid-template-columns: repeat(2, 1fr);
+
+ @container #{globals.$app-container} (min-width: 900px) {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 1300px) {
+ grid-template-columns: repeat(5, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 2000px) {
+ grid-template-columns: repeat(6, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 2600px) {
+ grid-template-columns: repeat(8, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 3000px) {
+ grid-template-columns: repeat(12, 1fr);
+ }
+ }
+
+ // Compact view - smaller cards
+ &--compact {
+ grid-template-columns: repeat(3, 1fr);
+
+ @container #{globals.$app-container} (min-width: 900px) {
+ grid-template-columns: repeat(5, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 1300px) {
+ grid-template-columns: repeat(7, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 2000px) {
+ grid-template-columns: repeat(9, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 2600px) {
+ grid-template-columns: repeat(12, 1fr);
+ }
+
+ @container #{globals.$app-container} (min-width: 3000px) {
+ grid-template-columns: repeat(16, 1fr);
+ }
+ }
+ }
+
+ &__games-list {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 2);
+ width: 100%;
+
+ // Large view - 2 columns grid
+ &--large {
+ display: grid;
+ grid-template-columns: repeat(1, 1fr);
+
+ @container #{globals.$app-container} (min-width: 900px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+ }
+}
diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx
new file mode 100644
index 00000000..0014f5a4
--- /dev/null
+++ b/src/renderer/src/pages/library/library.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useMemo, useState } from "react";
+import { useLibrary, useAppDispatch, useUserDetails } from "@renderer/hooks";
+import { setHeaderTitle } from "@renderer/features";
+import { TelescopeIcon } from "@primer/octicons-react";
+import { useTranslation } from "react-i18next";
+import { LibraryGameCard } from "./library-game-card";
+// detailed view removed — keep file if needed later
+import { LibraryGameCardLarge } from "./library-game-card-large";
+import { ViewOptions, ViewMode } from "./view-options";
+import { FilterOptions, FilterOption } from "./filter-options";
+import "./library.scss";
+
+export default function Library() {
+ const { library, updateLibrary } = useLibrary();
+
+ const [viewMode, setViewMode] = useState("grid");
+ const [filterBy, setFilterBy] = useState("all");
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation("library");
+ const { userDetails, fetchUserDetails } = useUserDetails();
+
+ useEffect(() => {
+ dispatch(setHeaderTitle(t("library")));
+
+ // Refresh library assets from cloud, then update library display
+ window.electron
+ .refreshLibraryAssets()
+ .then(() => updateLibrary())
+ .catch(() => updateLibrary()); // Fallback to local cache on error
+
+ // Listen for library sync completion to refresh cover images
+ const unsubscribe = window.electron.onLibraryBatchComplete(() => {
+ updateLibrary();
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [dispatch, t, updateLibrary]);
+
+ // Ensure we have the current user details available
+ useEffect(() => {
+ fetchUserDetails().catch(() => {
+ /* ignore errors - fallback to local state */
+ });
+ }, [fetchUserDetails]);
+
+ const handleOnMouseEnterGameCard = () => {
+ // Optional: pause animations if needed
+ };
+
+ const handleOnMouseLeaveGameCard = () => {
+ // Optional: resume animations if needed
+ };
+
+ const filteredLibrary = useMemo(() => {
+ switch (filterBy) {
+ case "favourited":
+ return library.filter((game) => game.favorite);
+ case "new":
+ return library.filter(
+ (game) => (game.playTimeInMilliseconds || 0) === 0
+ );
+ case "top10":
+ return library
+ .slice()
+ .sort(
+ (a, b) =>
+ (b.playTimeInMilliseconds || 0) - (a.playTimeInMilliseconds || 0)
+ )
+ .slice(0, 10);
+ case "all":
+ default:
+ return library;
+ }
+ }, [library, filterBy]);
+
+ // No sorting for now — rely on filteredLibrary
+ const sortedLibrary = filteredLibrary;
+
+ // Calculate counts for filters
+ const allGamesCount = library.length;
+ const favouritedCount = library.filter((game) => game.favorite).length;
+ const newGamesCount = library.filter(
+ (game) => (game.playTimeInMilliseconds || 0) === 0
+ ).length;
+ const top10Count = Math.min(10, library.length);
+
+ const hasGames = library.length > 0;
+
+ return (
+
+ {hasGames && (
+ <>
+
+
+ {`${t("Welcome", { defaultValue: "Welcome" })} ${
+ userDetails?.displayName || "John Doe"
+ }`}
+
+
+
+
+ >
+ )}
+
+ {!hasGames && (
+
+
+
+
+
{t("no_games_title")}
+
{t("no_games_description")}
+
+ )}
+
+ {hasGames && viewMode === "large" && (
+
+ {sortedLibrary.map((game) => (
+
+ ))}
+
+ )}
+
+ {hasGames && viewMode !== "large" && (
+
+ {sortedLibrary.map((game) => (
+ -
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/renderer/src/pages/library/view-options.scss b/src/renderer/src/pages/library/view-options.scss
new file mode 100644
index 00000000..a9b4e197
--- /dev/null
+++ b/src/renderer/src/pages/library/view-options.scss
@@ -0,0 +1,50 @@
+@use "../../scss/globals.scss";
+
+.library-view-options {
+ &__container {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ }
+
+ &__label {
+ font-size: 14px;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.8);
+ white-space: nowrap;
+ }
+
+ &__options {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ flex-wrap: wrap;
+ white-space: nowrap;
+ }
+
+ &__option {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ padding: 8px 16px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.04);
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 500;
+ transition: all ease 0.2s;
+ white-space: nowrap;
+
+ &:hover {
+ color: rgba(255, 255, 255, 0.95);
+ background: rgba(255, 255, 255, 0.06);
+ }
+
+ &.active {
+ color: #c9aa71;
+ background: rgba(201, 170, 113, 0.15);
+ }
+ }
+}
diff --git a/src/renderer/src/pages/library/view-options.tsx b/src/renderer/src/pages/library/view-options.tsx
new file mode 100644
index 00000000..3ea19cee
--- /dev/null
+++ b/src/renderer/src/pages/library/view-options.tsx
@@ -0,0 +1,45 @@
+import { AppsIcon, RowsIcon, SquareIcon } from "@primer/octicons-react";
+import { useTranslation } from "react-i18next";
+import "./view-options.scss";
+
+export type ViewMode = "grid" | "compact" | "large";
+
+interface ViewOptionsProps {
+ viewMode: ViewMode;
+ onViewModeChange: (viewMode: ViewMode) => void;
+}
+
+export function ViewOptions({ viewMode, onViewModeChange }: ViewOptionsProps) {
+ const { t } = useTranslation("library");
+
+ return (
+
+
+
onViewModeChange("grid")}
+ title={t("grid_view")}
+ >
+
+ {t("Grid View")}
+
+
onViewModeChange("compact")}
+ title={t("compact_view")}
+ >
+
+ {t("Compact")}
+
+
onViewModeChange("large")}
+ title={t("large_view")}
+ >
+
+ {t("Large")}
+
+
+
+ );
+}
From 48ce9a247640347f6667546ad8f60dcc75feaa08 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 21 Oct 2025 04:18:11 +0100
Subject: [PATCH 005/118] feat: using api download sources
---
.../download-sources/add-download-source.ts | 99 ++----
.../check-download-source-exists.ts | 17 -
.../delete-all-download-sources.ts | 13 -
.../delete-download-source.ts | 28 --
.../get-download-sources-list.ts | 19 --
.../download-sources/get-download-sources.ts | 4 +-
src/main/events/download-sources/helpers.ts | 314 ------------------
.../remove-download-source.ts | 17 +-
.../sync-download-sources-from-api.ts | 19 --
.../download-sources/sync-download-sources.ts | 113 +------
.../update-missing-fingerprints.ts | 67 ----
.../validate-download-source.ts | 32 --
src/main/events/index.ts | 7 -
src/main/events/repacks/get-all-repacks.ts | 16 -
src/main/level/sublevels/download-sources.ts | 14 +-
src/main/level/sublevels/index.ts | 1 -
src/main/level/sublevels/keys.ts | 1 -
src/main/level/sublevels/repacks.ts | 22 --
src/main/main.ts | 8 -
.../services/game-matcher-worker-manager.ts | 145 --------
src/main/services/hydra-api.ts | 5 -
src/main/services/index.ts | 2 -
src/main/services/resource-cache.ts | 157 ---------
src/main/workers/game-matcher-worker.ts | 158 ---------
src/preload/index.ts | 12 -
src/renderer/src/app.tsx | 34 --
.../src/components/game-card/game-card.tsx | 40 +--
.../game-details/game-details.context.tsx | 61 ++--
src/renderer/src/declaration.d.ts | 22 +-
.../src/features/download-sources-slice.ts | 21 --
src/renderer/src/features/index.ts | 2 -
src/renderer/src/hooks/index.ts | 1 -
src/renderer/src/hooks/use-catalogue.ts | 12 +-
src/renderer/src/hooks/use-repacks.ts | 26 --
.../src/pages/catalogue/catalogue.tsx | 73 ++--
.../src/pages/catalogue/game-item.tsx | 14 +-
.../src/pages/game-details/game-reviews.tsx | 4 -
.../game-details/modals/repacks-modal.tsx | 27 +-
src/renderer/src/pages/home/home.tsx | 18 +-
.../settings/add-download-source-modal.scss | 7 +
.../settings/add-download-source-modal.tsx | 128 ++-----
.../settings/settings-download-sources.tsx | 102 +++---
src/renderer/src/store.ts | 4 -
src/shared/constants.ts | 6 +-
src/types/index.ts | 28 +-
45 files changed, 295 insertions(+), 1625 deletions(-)
delete mode 100644 src/main/events/download-sources/check-download-source-exists.ts
delete mode 100644 src/main/events/download-sources/delete-all-download-sources.ts
delete mode 100644 src/main/events/download-sources/delete-download-source.ts
delete mode 100644 src/main/events/download-sources/get-download-sources-list.ts
delete mode 100644 src/main/events/download-sources/helpers.ts
delete mode 100644 src/main/events/download-sources/sync-download-sources-from-api.ts
delete mode 100644 src/main/events/download-sources/update-missing-fingerprints.ts
delete mode 100644 src/main/events/download-sources/validate-download-source.ts
delete mode 100644 src/main/events/repacks/get-all-repacks.ts
delete mode 100644 src/main/level/sublevels/repacks.ts
delete mode 100644 src/main/services/game-matcher-worker-manager.ts
delete mode 100644 src/main/services/resource-cache.ts
delete mode 100644 src/main/workers/game-matcher-worker.ts
delete mode 100644 src/renderer/src/features/download-sources-slice.ts
delete mode 100644 src/renderer/src/hooks/use-repacks.ts
diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts
index e51cae3e..45bcd27c 100644
--- a/src/main/events/download-sources/add-download-source.ts
+++ b/src/main/events/download-sources/add-download-source.ts
@@ -1,76 +1,45 @@
import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
-import { HydraApi, logger } from "@main/services";
-import { importDownloadSourceToLocal } from "./helpers";
+import { HydraApi } from "@main/services/hydra-api";
+import { downloadSourcesSublevel } from "@main/level";
+import type { DownloadSource } from "@types";
const addDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
url: string
) => {
- const result = await importDownloadSourceToLocal(url, true);
- if (!result) {
- throw new Error("Failed to import download source");
- }
-
- // Verify that repacks were actually written to the database (read-after-write)
- // This ensures all async operations are complete before proceeding
- let repackCount = 0;
- for await (const [, repack] of repacksSublevel.iterator()) {
- if (repack.downloadSourceId === result.id) {
- repackCount++;
- }
- }
-
- await HydraApi.post("/profile/download-sources", {
- urls: [url],
- });
-
- const { fingerprint } = await HydraApi.put<{ fingerprint: string }>(
- "/download-sources",
- {
- objectIds: result.objectIds,
- },
- { needsAuth: false }
- );
-
- // Update the source with fingerprint
- const updatedSource = await downloadSourcesSublevel.get(`${result.id}`);
- if (updatedSource) {
- await downloadSourcesSublevel.put(`${result.id}`, {
- ...updatedSource,
- fingerprint,
- updatedAt: new Date(),
- });
- }
-
- // Final verification: ensure the source with fingerprint is persisted
- const finalSource = await downloadSourcesSublevel.get(`${result.id}`);
- if (!finalSource || !finalSource.fingerprint) {
- throw new Error("Failed to persist download source with fingerprint");
- }
-
- // Verify repacks still exist after fingerprint update
- let finalRepackCount = 0;
- for await (const [, repack] of repacksSublevel.iterator()) {
- if (repack.downloadSourceId === result.id) {
- finalRepackCount++;
- }
- }
-
- if (finalRepackCount !== repackCount) {
- logger.warn(
- `Repack count mismatch! Before: ${repackCount}, After: ${finalRepackCount}`
+ try {
+ const downloadSource = await HydraApi.post(
+ "/download-sources",
+ {
+ url,
+ },
+ { needsAuth: false }
);
- } else {
- logger.info(
- `Final verification passed: ${finalRepackCount} repacks confirmed`
- );
- }
- return {
- ...result,
- fingerprint,
- };
+ if (HydraApi.isLoggedIn()) {
+ try {
+ await HydraApi.post("/profile/download-sources", {
+ urls: [url],
+ });
+ } catch (error) {
+ console.error("Failed to add download source to profile:", error);
+ }
+ }
+
+ const downloadSourceForStorage = {
+ ...downloadSource,
+ fingerprint: downloadSource.fingerprint || "",
+ };
+ await downloadSourcesSublevel.put(
+ downloadSource.id,
+ downloadSourceForStorage
+ );
+
+ return downloadSource;
+ } catch (error) {
+ console.error("Failed to add download source:", error);
+ throw error;
+ }
};
registerEvent("addDownloadSource", addDownloadSource);
diff --git a/src/main/events/download-sources/check-download-source-exists.ts b/src/main/events/download-sources/check-download-source-exists.ts
deleted file mode 100644
index 36dd88ce..00000000
--- a/src/main/events/download-sources/check-download-source-exists.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel } from "@main/level";
-
-const checkDownloadSourceExists = async (
- _event: Electron.IpcMainInvokeEvent,
- url: string
-): Promise => {
- for await (const [, source] of downloadSourcesSublevel.iterator()) {
- if (source.url === url) {
- return true;
- }
- }
-
- return false;
-};
-
-registerEvent("checkDownloadSourceExists", checkDownloadSourceExists);
diff --git a/src/main/events/download-sources/delete-all-download-sources.ts b/src/main/events/download-sources/delete-all-download-sources.ts
deleted file mode 100644
index cbf3958f..00000000
--- a/src/main/events/download-sources/delete-all-download-sources.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
-import { invalidateIdCaches } from "./helpers";
-
-const deleteAllDownloadSources = async (
- _event: Electron.IpcMainInvokeEvent
-) => {
- await Promise.all([repacksSublevel.clear(), downloadSourcesSublevel.clear()]);
-
- invalidateIdCaches();
-};
-
-registerEvent("deleteAllDownloadSources", deleteAllDownloadSources);
diff --git a/src/main/events/download-sources/delete-download-source.ts b/src/main/events/download-sources/delete-download-source.ts
deleted file mode 100644
index 5322b96c..00000000
--- a/src/main/events/download-sources/delete-download-source.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
-import { invalidateIdCaches } from "./helpers";
-
-const deleteDownloadSource = async (
- _event: Electron.IpcMainInvokeEvent,
- id: number
-) => {
- const repacksToDelete: string[] = [];
-
- for await (const [key, repack] of repacksSublevel.iterator()) {
- if (repack.downloadSourceId === id) {
- repacksToDelete.push(key);
- }
- }
-
- const batch = repacksSublevel.batch();
- for (const key of repacksToDelete) {
- batch.del(key);
- }
- await batch.write();
-
- await downloadSourcesSublevel.del(`${id}`);
-
- invalidateIdCaches();
-};
-
-registerEvent("deleteDownloadSource", deleteDownloadSource);
diff --git a/src/main/events/download-sources/get-download-sources-list.ts b/src/main/events/download-sources/get-download-sources-list.ts
deleted file mode 100644
index db26ad01..00000000
--- a/src/main/events/download-sources/get-download-sources-list.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel, DownloadSource } from "@main/level";
-
-const getDownloadSourcesList = async (_event: Electron.IpcMainInvokeEvent) => {
- const sources: DownloadSource[] = [];
-
- for await (const [, source] of downloadSourcesSublevel.iterator()) {
- sources.push(source);
- }
-
- // Sort by createdAt descending
- sources.sort(
- (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
- );
-
- return sources;
-};
-
-registerEvent("getDownloadSourcesList", getDownloadSourcesList);
diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts
index bbebd06c..cf7cd4d7 100644
--- a/src/main/events/download-sources/get-download-sources.ts
+++ b/src/main/events/download-sources/get-download-sources.ts
@@ -1,8 +1,8 @@
-import { HydraApi } from "@main/services";
+import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
- return HydraApi.get("/profile/download-sources");
+ return downloadSourcesSublevel.values().all();
};
registerEvent("getDownloadSources", getDownloadSources);
diff --git a/src/main/events/download-sources/helpers.ts b/src/main/events/download-sources/helpers.ts
deleted file mode 100644
index edd3878e..00000000
--- a/src/main/events/download-sources/helpers.ts
+++ /dev/null
@@ -1,314 +0,0 @@
-import axios from "axios";
-import { z } from "zod";
-import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
-import { DownloadSourceStatus } from "@shared";
-import crypto from "node:crypto";
-import { logger, ResourceCache } from "@main/services";
-
-export const downloadSourceSchema = z.object({
- name: z.string().max(255),
- downloads: z.array(
- z.object({
- title: z.string().max(255),
- uris: z.array(z.string()),
- uploadDate: z.string().max(255),
- fileSize: z.string().max(255),
- })
- ),
-});
-
-export type TitleHashMapping = Record;
-
-let titleHashMappingCache: TitleHashMapping | null = null;
-
-export const getTitleHashMapping = async (): Promise => {
- if (titleHashMappingCache) {
- return titleHashMappingCache;
- }
-
- try {
- const cached =
- ResourceCache.getCachedData("sources-manifest");
- if (cached) {
- titleHashMappingCache = cached;
- return cached;
- }
-
- const fetched = await ResourceCache.fetchAndCache(
- "sources-manifest",
- "https://cdn.losbroxas.org/sources-manifest.json",
- 10000
- );
- titleHashMappingCache = fetched;
- return fetched;
- } catch (error) {
- logger.error("Failed to fetch title hash mapping:", error);
- return {} as TitleHashMapping;
- }
-};
-
-export const hashTitle = (title: string): string => {
- return crypto.createHash("sha256").update(title).digest("hex");
-};
-
-export type SteamGamesByLetter = Record;
-export type FormattedSteamGame = {
- id: string;
- name: string;
- formattedName: string;
-};
-export type FormattedSteamGamesByLetter = Record;
-
-export const formatName = (name: string) => {
- return name
- .normalize("NFD")
- .replaceAll(/[\u0300-\u036f]/g, "")
- .toLowerCase()
- .replaceAll(/[^a-z0-9]/g, "");
-};
-
-export const formatRepackName = (name: string) => {
- return formatName(name.replace("[DL]", ""));
-};
-
-interface DownloadSource {
- id: number;
- url: string;
- name: string;
- etag: string | null;
- status: number;
- downloadCount: number;
- objectIds: string[];
- fingerprint?: string;
- createdAt: Date;
- updatedAt: Date;
-}
-
-const getDownloadSourcesMap = async (): Promise<
- Map
-> => {
- const map = new Map();
- for await (const [key, source] of downloadSourcesSublevel.iterator()) {
- map.set(key, source);
- }
-
- return map;
-};
-
-export const checkUrlExists = async (url: string): Promise => {
- const sources = await getDownloadSourcesMap();
- for (const source of sources.values()) {
- if (source.url === url) {
- return true;
- }
- }
- return false;
-};
-
-let steamGamesFormattedCache: FormattedSteamGamesByLetter | null = null;
-
-export const getSteamGames = async (): Promise => {
- if (steamGamesFormattedCache) {
- return steamGamesFormattedCache;
- }
-
- let steamGames: SteamGamesByLetter;
-
- const cached = ResourceCache.getCachedData(
- "steam-games-by-letter"
- );
- if (cached) {
- steamGames = cached;
- } else {
- steamGames = await ResourceCache.fetchAndCache(
- "steam-games-by-letter",
- `${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
- );
- }
-
- const formattedData: FormattedSteamGamesByLetter = {};
- for (const [letter, games] of Object.entries(steamGames)) {
- formattedData[letter] = games.map((game) => ({
- ...game,
- formattedName: formatName(game.name),
- }));
- }
-
- steamGamesFormattedCache = formattedData;
- return formattedData;
-};
-
-export type SublevelIterator = AsyncIterable<[string, { id: number }]>;
-
-export interface SublevelWithId {
- iterator: () => SublevelIterator;
-}
-
-let maxRepackId: number | null = null;
-let maxDownloadSourceId: number | null = null;
-
-export const getNextId = async (sublevel: SublevelWithId): Promise => {
- const isRepackSublevel = sublevel === repacksSublevel;
- const isDownloadSourceSublevel = sublevel === downloadSourcesSublevel;
-
- if (isRepackSublevel && maxRepackId !== null) {
- return ++maxRepackId;
- }
-
- if (isDownloadSourceSublevel && maxDownloadSourceId !== null) {
- return ++maxDownloadSourceId;
- }
-
- let maxId = 0;
- for await (const [, value] of sublevel.iterator()) {
- if (value.id > maxId) {
- maxId = value.id;
- }
- }
-
- if (isRepackSublevel) {
- maxRepackId = maxId;
- } else if (isDownloadSourceSublevel) {
- maxDownloadSourceId = maxId;
- }
-
- return maxId + 1;
-};
-
-export const invalidateIdCaches = () => {
- maxRepackId = null;
- maxDownloadSourceId = null;
-};
-
-export const addNewDownloads = async (
- downloadSource: { id: number; name: string },
- downloads: z.infer["downloads"],
- steamGames: FormattedSteamGamesByLetter
-) => {
- const now = new Date();
- const objectIdsOnSource = new Set();
-
- let nextRepackId = await getNextId(repacksSublevel);
-
- const batch = repacksSublevel.batch();
-
- // Get title hash mapping and perform matching in worker thread
- const titleHashMapping = await getTitleHashMapping();
-
- const { GameMatcherWorkerManager } = await import("@main/services");
- const matchResult = await GameMatcherWorkerManager.matchDownloads(
- downloads,
- steamGames,
- titleHashMapping
- );
-
- // Process matched results and write to database
- for (const matchedDownload of matchResult.matchedDownloads) {
- for (const id of matchedDownload.objectIds) {
- objectIdsOnSource.add(id);
- }
-
- const repack = {
- id: nextRepackId++,
- objectIds: matchedDownload.objectIds,
- title: matchedDownload.title,
- uris: matchedDownload.uris,
- fileSize: matchedDownload.fileSize,
- repacker: downloadSource.name,
- uploadDate: matchedDownload.uploadDate,
- downloadSourceId: downloadSource.id,
- createdAt: now,
- updatedAt: now,
- };
-
- batch.put(`${repack.id}`, repack);
- }
-
- await batch.write();
-
- logger.info(
- `Matching stats for ${downloadSource.name}: Hash=${matchResult.stats.hashMatchCount}, Fuzzy=${matchResult.stats.fuzzyMatchCount}, None=${matchResult.stats.noMatchCount}`
- );
-
- const existingSource = await downloadSourcesSublevel.get(
- `${downloadSource.id}`
- );
- if (existingSource) {
- await downloadSourcesSublevel.put(`${downloadSource.id}`, {
- ...existingSource,
- objectIds: Array.from(objectIdsOnSource),
- });
- }
-
- return Array.from(objectIdsOnSource);
-};
-
-export const importDownloadSourceToLocal = async (
- url: string,
- throwOnDuplicate = false
-) => {
- const urlExists = await checkUrlExists(url);
- if (urlExists) {
- if (throwOnDuplicate) {
- throw new Error("Download source with this URL already exists");
- }
- return null;
- }
-
- const response = await axios.get>(url);
-
- const steamGames = await getSteamGames();
-
- const now = new Date();
-
- const nextId = await getNextId(downloadSourcesSublevel);
-
- const downloadSource = {
- id: nextId,
- url,
- name: response.data.name,
- etag: response.headers["etag"] || null,
- status: DownloadSourceStatus.UpToDate,
- downloadCount: response.data.downloads.length,
- objectIds: [],
- createdAt: now,
- updatedAt: now,
- };
-
- await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
-
- const objectIds = await addNewDownloads(
- downloadSource,
- response.data.downloads,
- steamGames
- );
-
- // Invalidate ID caches after creating new repacks to prevent ID collisions
- invalidateIdCaches();
-
- return {
- ...downloadSource,
- objectIds,
- };
-};
-
-export const updateDownloadSourcePreservingTimestamp = async (
- existingSource: DownloadSource,
- url: string
-) => {
- const response = await axios.get>(url);
-
- const updatedSource = {
- ...existingSource,
- name: response.data.name,
- etag: response.headers["etag"] || null,
- status: DownloadSourceStatus.UpToDate,
- downloadCount: response.data.downloads.length,
- updatedAt: new Date(),
- // Preserve the original createdAt timestamp
- };
-
- await downloadSourcesSublevel.put(`${existingSource.id}`, updatedSource);
-
- return updatedSource;
-};
diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts
index bcc66998..8efe0072 100644
--- a/src/main/events/download-sources/remove-download-source.ts
+++ b/src/main/events/download-sources/remove-download-source.ts
@@ -1,18 +1,27 @@
import { HydraApi } from "@main/services";
+import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const removeDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
- url?: string,
- removeAll = false
+ removeAll = false,
+ downloadSourceId?: string
) => {
const params = new URLSearchParams({
all: removeAll.toString(),
});
- if (url) params.set("url", url);
+ if (downloadSourceId) params.set("downloadSourceId", downloadSourceId);
- return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
+ if (HydraApi.isLoggedIn()) {
+ void HydraApi.delete(`/profile/download-sources?${params.toString()}`);
+ }
+
+ if (removeAll) {
+ await downloadSourcesSublevel.clear();
+ } else if (downloadSourceId) {
+ await downloadSourcesSublevel.del(downloadSourceId);
+ }
};
registerEvent("removeDownloadSource", removeDownloadSource);
diff --git a/src/main/events/download-sources/sync-download-sources-from-api.ts b/src/main/events/download-sources/sync-download-sources-from-api.ts
deleted file mode 100644
index 3cac8819..00000000
--- a/src/main/events/download-sources/sync-download-sources-from-api.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { HydraApi, logger } from "@main/services";
-import { importDownloadSourceToLocal, checkUrlExists } from "./helpers";
-
-export const syncDownloadSourcesFromApi = async () => {
- try {
- const apiSources = await HydraApi.get<
- { url: string; createdAt: string; updatedAt: string }[]
- >("/profile/download-sources");
-
- for (const apiSource of apiSources) {
- const exists = await checkUrlExists(apiSource.url);
- if (!exists) {
- await importDownloadSourceToLocal(apiSource.url, false);
- }
- }
- } catch (error) {
- logger.error("Failed to sync download sources from API:", error);
- }
-};
diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts
index 3bb78f22..987ad1c1 100644
--- a/src/main/events/download-sources/sync-download-sources.ts
+++ b/src/main/events/download-sources/sync-download-sources.ts
@@ -1,105 +1,24 @@
+import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
-import axios, { AxiosError } from "axios";
-import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
-import { DownloadSourceStatus } from "@shared";
-import {
- invalidateIdCaches,
- downloadSourceSchema,
- getSteamGames,
- addNewDownloads,
-} from "./helpers";
+import { downloadSourcesSublevel } from "@main/level";
+import type { DownloadSource } from "@types";
-const syncDownloadSources = async (
- _event: Electron.IpcMainInvokeEvent
-): Promise => {
- let newRepacksCount = 0;
+const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
+ const downloadSources = await downloadSourcesSublevel.values().all();
- try {
- const downloadSources: Array<{
- id: number;
- url: string;
- name: string;
- etag: string | null;
- status: number;
- downloadCount: number;
- objectIds: string[];
- fingerprint?: string;
- createdAt: Date;
- updatedAt: Date;
- }> = [];
- for await (const [, source] of downloadSourcesSublevel.iterator()) {
- downloadSources.push(source);
- }
+ const response = await HydraApi.post(
+ "/download-sources/sync",
+ {
+ ids: downloadSources.map((downloadSource) => downloadSource.id),
+ },
+ { needsAuth: false }
+ );
- // Use a Set for O(1) lookups instead of O(n) with array.some()
- const existingRepackTitles = new Set();
- for await (const [, repack] of repacksSublevel.iterator()) {
- existingRepackTitles.add(repack.title);
- }
-
- // Handle sources with missing fingerprints individually, don't delete all sources
- const sourcesWithFingerprints = downloadSources.filter(
- (source) => source.fingerprint
- );
- const sourcesWithoutFingerprints = downloadSources.filter(
- (source) => !source.fingerprint
- );
-
- // For sources without fingerprints, just continue with normal sync
- // They will get fingerprints updated later by updateMissingFingerprints
- const allSourcesToSync = [
- ...sourcesWithFingerprints,
- ...sourcesWithoutFingerprints,
- ];
-
- for (const downloadSource of allSourcesToSync) {
- const headers: Record = {};
-
- if (downloadSource.etag) {
- headers["If-None-Match"] = downloadSource.etag;
- }
-
- try {
- const response = await axios.get(downloadSource.url, {
- headers,
- });
-
- const source = downloadSourceSchema.parse(response.data);
- const steamGames = await getSteamGames();
-
- // O(1) lookup instead of O(n) - massive performance improvement
- const repacks = source.downloads.filter(
- (download) => !existingRepackTitles.has(download.title)
- );
-
- await downloadSourcesSublevel.put(`${downloadSource.id}`, {
- ...downloadSource,
- etag: response.headers["etag"] || null,
- downloadCount: source.downloads.length,
- status: DownloadSourceStatus.UpToDate,
- });
-
- await addNewDownloads(downloadSource, repacks, steamGames);
-
- newRepacksCount += repacks.length;
- } catch (err: unknown) {
- const isNotModified = (err as AxiosError).response?.status === 304;
-
- await downloadSourcesSublevel.put(`${downloadSource.id}`, {
- ...downloadSource,
- status: isNotModified
- ? DownloadSourceStatus.UpToDate
- : DownloadSourceStatus.Errored,
- });
- }
- }
-
- invalidateIdCaches();
-
- return newRepacksCount;
- } catch (err) {
- return -1;
+ for (const downloadSource of response) {
+ await downloadSourcesSublevel.put(downloadSource.id, downloadSource);
}
+
+ return response;
};
registerEvent("syncDownloadSources", syncDownloadSources);
diff --git a/src/main/events/download-sources/update-missing-fingerprints.ts b/src/main/events/download-sources/update-missing-fingerprints.ts
deleted file mode 100644
index 7fd43c63..00000000
--- a/src/main/events/download-sources/update-missing-fingerprints.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { registerEvent } from "../register-event";
-import { downloadSourcesSublevel } from "@main/level";
-import { HydraApi, logger } from "@main/services";
-
-const updateMissingFingerprints = async (
- _event: Electron.IpcMainInvokeEvent
-): Promise => {
- const sourcesNeedingFingerprints: Array<{
- id: number;
- objectIds: string[];
- }> = [];
-
- for await (const [, source] of downloadSourcesSublevel.iterator()) {
- if (
- !source.fingerprint &&
- source.objectIds &&
- source.objectIds.length > 0
- ) {
- sourcesNeedingFingerprints.push({
- id: source.id,
- objectIds: source.objectIds,
- });
- }
- }
-
- if (sourcesNeedingFingerprints.length === 0) {
- return 0;
- }
-
- logger.info(
- `Updating fingerprints for ${sourcesNeedingFingerprints.length} sources`
- );
-
- await Promise.all(
- sourcesNeedingFingerprints.map(async (source) => {
- try {
- const { fingerprint } = await HydraApi.put<{ fingerprint: string }>(
- "/download-sources",
- {
- objectIds: source.objectIds,
- },
- { needsAuth: false }
- );
-
- const existingSource = await downloadSourcesSublevel.get(
- `${source.id}`
- );
- if (existingSource) {
- await downloadSourcesSublevel.put(`${source.id}`, {
- ...existingSource,
- fingerprint,
- updatedAt: new Date(),
- });
- }
- } catch (error) {
- logger.error(
- `Failed to update fingerprint for source ${source.id}:`,
- error
- );
- }
- })
- );
-
- return sourcesNeedingFingerprints.length;
-};
-
-registerEvent("updateMissingFingerprints", updateMissingFingerprints);
diff --git a/src/main/events/download-sources/validate-download-source.ts b/src/main/events/download-sources/validate-download-source.ts
deleted file mode 100644
index 2bc86df7..00000000
--- a/src/main/events/download-sources/validate-download-source.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { registerEvent } from "../register-event";
-import axios from "axios";
-import { z } from "zod";
-
-const downloadSourceSchema = z.object({
- name: z.string().max(255),
- downloads: z.array(
- z.object({
- title: z.string().max(255),
- uris: z.array(z.string()),
- uploadDate: z.string().max(255),
- fileSize: z.string().max(255),
- })
- ),
-});
-
-const validateDownloadSource = async (
- _event: Electron.IpcMainInvokeEvent,
- url: string
-) => {
- const response = await axios.get>(url);
-
- const { name } = downloadSourceSchema.parse(response.data);
-
- return {
- name,
- etag: response.headers["etag"] || null,
- downloadCount: response.data.downloads.length,
- };
-};
-
-registerEvent("validateDownloadSource", validateDownloadSource);
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 8d21aa11..0ab5499a 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -63,14 +63,7 @@ import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./user-preferences/authenticate-torbox";
import "./download-sources/add-download-source";
-import "./download-sources/update-missing-fingerprints";
-import "./download-sources/delete-download-source";
-import "./download-sources/delete-all-download-sources";
-import "./download-sources/validate-download-source";
import "./download-sources/sync-download-sources";
-import "./download-sources/get-download-sources-list";
-import "./download-sources/check-download-source-exists";
-import "./repacks/get-all-repacks";
import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
diff --git a/src/main/events/repacks/get-all-repacks.ts b/src/main/events/repacks/get-all-repacks.ts
deleted file mode 100644
index 6eb83a39..00000000
--- a/src/main/events/repacks/get-all-repacks.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { registerEvent } from "../register-event";
-import { repacksSublevel, GameRepack } from "@main/level";
-
-const getAllRepacks = async (_event: Electron.IpcMainInvokeEvent) => {
- const repacks: GameRepack[] = [];
-
- for await (const [, repack] of repacksSublevel.iterator()) {
- if (Array.isArray(repack.objectIds)) {
- repacks.push(repack);
- }
- }
-
- return repacks;
-};
-
-registerEvent("getAllRepacks", getAllRepacks);
diff --git a/src/main/level/sublevels/download-sources.ts b/src/main/level/sublevels/download-sources.ts
index 59104e3c..b6cdad0b 100644
--- a/src/main/level/sublevels/download-sources.ts
+++ b/src/main/level/sublevels/download-sources.ts
@@ -1,18 +1,6 @@
import { db } from "../level";
import { levelKeys } from "./keys";
-
-export interface DownloadSource {
- id: number;
- name: string;
- url: string;
- status: number;
- objectIds: string[];
- downloadCount: number;
- fingerprint?: string;
- etag: string | null;
- createdAt: Date;
- updatedAt: Date;
-}
+import type { DownloadSource } from "@types";
export const downloadSourcesSublevel = db.sublevel(
levelKeys.downloadSources,
diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts
index 7224fc64..3619ae26 100644
--- a/src/main/level/sublevels/index.ts
+++ b/src/main/level/sublevels/index.ts
@@ -7,4 +7,3 @@ export * from "./game-achievements";
export * from "./keys";
export * from "./themes";
export * from "./download-sources";
-export * from "./repacks";
diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts
index 6faacd52..a28690b2 100644
--- a/src/main/level/sublevels/keys.ts
+++ b/src/main/level/sublevels/keys.ts
@@ -18,5 +18,4 @@ export const levelKeys = {
screenState: "screenState",
rpcPassword: "rpcPassword",
downloadSources: "downloadSources",
- repacks: "repacks",
};
diff --git a/src/main/level/sublevels/repacks.ts b/src/main/level/sublevels/repacks.ts
deleted file mode 100644
index 6257665b..00000000
--- a/src/main/level/sublevels/repacks.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { db } from "../level";
-import { levelKeys } from "./keys";
-
-export interface GameRepack {
- id: number;
- title: string;
- uris: string[];
- repacker: string;
- fileSize: string | null;
- objectIds: string[];
- uploadDate: Date | string | null;
- downloadSourceId: number;
- createdAt: Date;
- updatedAt: Date;
-}
-
-export const repacksSublevel = db.sublevel(
- levelKeys.repacks,
- {
- valueEncoding: "json",
- }
-);
diff --git a/src/main/main.ts b/src/main/main.ts
index e9b6187c..617dd135 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -16,19 +16,11 @@ import {
Ludusavi,
Lock,
DeckyPlugin,
- ResourceCache,
- GameMatcherWorkerManager,
} from "@main/services";
export const loadState = async () => {
await Lock.acquireLock();
- ResourceCache.initialize();
- await ResourceCache.updateResourcesOnStartup();
-
- // Initialize game matcher worker thread
- GameMatcherWorkerManager.initialize();
-
const userPreferences = await db.get(
levelKeys.userPreferences,
{
diff --git a/src/main/services/game-matcher-worker-manager.ts b/src/main/services/game-matcher-worker-manager.ts
deleted file mode 100644
index b5d306c7..00000000
--- a/src/main/services/game-matcher-worker-manager.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { Worker } from "worker_threads";
-import workerPath from "../workers/game-matcher-worker?modulePath";
-
-interface WorkerMessage {
- id: string;
- data: unknown;
-}
-
-interface WorkerResponse {
- id: string;
- success: boolean;
- result?: unknown;
- error?: string;
-}
-
-export type TitleHashMapping = Record;
-
-export type FormattedSteamGame = {
- id: string;
- name: string;
- formattedName: string;
-};
-export type FormattedSteamGamesByLetter = Record;
-
-interface DownloadToMatch {
- title: string;
- uris: string[];
- uploadDate: string;
- fileSize: string;
-}
-
-interface MatchedDownload {
- title: string;
- uris: string[];
- uploadDate: string;
- fileSize: string;
- objectIds: string[];
- usedHashMatch: boolean;
-}
-
-interface MatchResponse {
- matchedDownloads: MatchedDownload[];
- stats: {
- hashMatchCount: number;
- fuzzyMatchCount: number;
- noMatchCount: number;
- };
-}
-
-export class GameMatcherWorkerManager {
- private static worker: Worker | null = null;
- private static messageId = 0;
- private static pendingMessages = new Map<
- string,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- { resolve: (value: any) => void; reject: (error: Error) => void }
- >();
-
- public static initialize() {
- if (this.worker) {
- return;
- }
-
- try {
- console.log(
- "[GameMatcherWorker] Initializing worker with path:",
- workerPath
- );
-
- this.worker = new Worker(workerPath);
-
- this.worker.on("message", (response: WorkerResponse) => {
- const pending = this.pendingMessages.get(response.id);
- if (pending) {
- if (response.success) {
- pending.resolve(response.result);
- } else {
- pending.reject(new Error(response.error || "Unknown error"));
- }
- this.pendingMessages.delete(response.id);
- }
- });
-
- this.worker.on("error", (error) => {
- console.error("[GameMatcherWorker] Worker error:", error);
- for (const [id, pending] of this.pendingMessages.entries()) {
- pending.reject(error);
- this.pendingMessages.delete(id);
- }
- });
-
- this.worker.on("exit", (code) => {
- if (code !== 0) {
- console.error(
- `[GameMatcherWorker] Worker stopped with exit code ${code}`
- );
- }
- this.worker = null;
- for (const [id, pending] of this.pendingMessages.entries()) {
- pending.reject(new Error("Worker exited unexpectedly"));
- this.pendingMessages.delete(id);
- }
- });
-
- console.log("[GameMatcherWorker] Worker initialized successfully");
- } catch (error) {
- console.error("[GameMatcherWorker] Failed to initialize worker:", error);
- throw error;
- }
- }
-
- private static sendMessage(data: unknown): Promise {
- if (!this.worker) {
- return Promise.reject(new Error("Worker not initialized"));
- }
-
- const id = `msg_${++this.messageId}`;
- const message: WorkerMessage = { id, data };
-
- return new Promise((resolve, reject) => {
- this.pendingMessages.set(id, { resolve, reject });
- this.worker!.postMessage(message);
- });
- }
-
- public static async matchDownloads(
- downloads: DownloadToMatch[],
- steamGames: FormattedSteamGamesByLetter,
- titleHashMapping: TitleHashMapping
- ): Promise {
- return this.sendMessage({
- downloads,
- steamGames,
- titleHashMapping,
- });
- }
-
- public static terminate() {
- if (this.worker) {
- this.worker.terminate();
- this.worker = null;
- this.pendingMessages.clear();
- }
- }
-}
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index dd26e6f0..ffc5756c 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -105,11 +105,6 @@ export class HydraApi {
// WSClient.close();
// WSClient.connect();
-
- const { syncDownloadSourcesFromApi } = await import(
- "../events/download-sources/sync-download-sources-from-api"
- );
- syncDownloadSourcesFromApi();
}
}
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 0853859f..88b39d1b 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -18,5 +18,3 @@ export * from "./library-sync";
export * from "./wine";
export * from "./lock";
export * from "./decky-plugin";
-export * from "./resource-cache";
-export * from "./game-matcher-worker-manager";
diff --git a/src/main/services/resource-cache.ts b/src/main/services/resource-cache.ts
deleted file mode 100644
index c59f873d..00000000
--- a/src/main/services/resource-cache.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { app } from "electron";
-import axios from "axios";
-import fs from "node:fs";
-import path from "node:path";
-import { logger } from "./logger";
-
-interface CachedResource {
- data: T;
- etag: string | null;
-}
-
-export class ResourceCache {
- private static cacheDir: string;
-
- static initialize() {
- this.cacheDir = path.join(app.getPath("userData"), "resource-cache");
-
- if (!fs.existsSync(this.cacheDir)) {
- fs.mkdirSync(this.cacheDir, { recursive: true });
- }
- }
-
- private static getCacheFilePath(resourceName: string): string {
- return path.join(this.cacheDir, `${resourceName}.json`);
- }
-
- private static getEtagFilePath(resourceName: string): string {
- return path.join(this.cacheDir, `${resourceName}.etag`);
- }
-
- private static readCachedResource(
- resourceName: string
- ): CachedResource | null {
- const dataPath = this.getCacheFilePath(resourceName);
- const etagPath = this.getEtagFilePath(resourceName);
-
- if (!fs.existsSync(dataPath)) {
- return null;
- }
-
- try {
- const data = JSON.parse(fs.readFileSync(dataPath, "utf-8")) as T;
- const etag = fs.existsSync(etagPath)
- ? fs.readFileSync(etagPath, "utf-8")
- : null;
-
- return { data, etag };
- } catch (error) {
- logger.error(`Failed to read cached resource ${resourceName}:`, error);
- return null;
- }
- }
-
- private static writeCachedResource(
- resourceName: string,
- data: T,
- etag: string | null
- ): void {
- const dataPath = this.getCacheFilePath(resourceName);
- const etagPath = this.getEtagFilePath(resourceName);
-
- try {
- fs.writeFileSync(dataPath, JSON.stringify(data), "utf-8");
-
- if (etag) {
- fs.writeFileSync(etagPath, etag, "utf-8");
- }
-
- logger.info(
- `Cached resource ${resourceName} with etag: ${etag || "none"}`
- );
- } catch (error) {
- logger.error(`Failed to write cached resource ${resourceName}:`, error);
- }
- }
-
- static async fetchAndCache(
- resourceName: string,
- url: string,
- timeout: number = 10000
- ): Promise {
- const cached = this.readCachedResource(resourceName);
- const headers: Record = {};
-
- if (cached?.etag) {
- headers["If-None-Match"] = cached.etag;
- }
-
- try {
- const response = await axios.get(url, {
- headers,
- timeout,
- });
-
- const newEtag = response.headers["etag"] || null;
- this.writeCachedResource(resourceName, response.data, newEtag);
-
- return response.data;
- } catch (error: unknown) {
- const axiosError = error as {
- response?: { status?: number };
- message?: string;
- };
-
- if (axiosError.response?.status === 304 && cached) {
- logger.info(`Resource ${resourceName} not modified, using cache`);
- return cached.data;
- }
-
- if (cached) {
- logger.warn(
- `Failed to fetch ${resourceName}, using cached version:`,
- axiosError.message || "Unknown error"
- );
- return cached.data;
- }
-
- logger.error(
- `Failed to fetch ${resourceName} and no cache available:`,
- error
- );
- throw error;
- }
- }
-
- static getCachedData(resourceName: string): T | null {
- const cached = this.readCachedResource(resourceName);
- return cached?.data || null;
- }
-
- static async updateResourcesOnStartup(): Promise {
- logger.info("Starting background resource cache update...");
-
- const resources = [
- {
- name: "steam-games-by-letter",
- url: `${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`,
- },
- {
- name: "sources-manifest",
- url: "https://cdn.losbroxas.org/sources-manifest.json",
- },
- ];
-
- await Promise.allSettled(
- resources.map(async (resource) => {
- try {
- await this.fetchAndCache(resource.name, resource.url);
- } catch (error) {
- logger.error(`Failed to update ${resource.name} on startup:`, error);
- }
- })
- );
-
- logger.info("Resource cache update complete");
- }
-}
diff --git a/src/main/workers/game-matcher-worker.ts b/src/main/workers/game-matcher-worker.ts
deleted file mode 100644
index 4930ada0..00000000
--- a/src/main/workers/game-matcher-worker.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { parentPort } from "worker_threads";
-import crypto from "node:crypto";
-
-export type TitleHashMapping = Record;
-
-export type FormattedSteamGame = {
- id: string;
- name: string;
- formattedName: string;
-};
-export type FormattedSteamGamesByLetter = Record;
-
-interface DownloadToMatch {
- title: string;
- uris: string[];
- uploadDate: string;
- fileSize: string;
-}
-
-interface MatchedDownload {
- title: string;
- uris: string[];
- uploadDate: string;
- fileSize: string;
- objectIds: string[];
- usedHashMatch: boolean;
-}
-
-interface MatchRequest {
- downloads: DownloadToMatch[];
- steamGames: FormattedSteamGamesByLetter;
- titleHashMapping: TitleHashMapping;
-}
-
-interface MatchResponse {
- matchedDownloads: MatchedDownload[];
- stats: {
- hashMatchCount: number;
- fuzzyMatchCount: number;
- noMatchCount: number;
- };
-}
-
-const hashTitle = (title: string): string => {
- return crypto.createHash("sha256").update(title).digest("hex");
-};
-
-const formatName = (name: string) => {
- return name
- .normalize("NFD")
- .replaceAll(/[\u0300-\u036f]/g, "")
- .toLowerCase()
- .replaceAll(/[^a-z0-9]/g, "");
-};
-
-const formatRepackName = (name: string) => {
- return formatName(name.replace("[DL]", ""));
-};
-
-const matchDownloads = (request: MatchRequest): MatchResponse => {
- const { downloads, steamGames, titleHashMapping } = request;
- const matchedDownloads: MatchedDownload[] = [];
-
- let hashMatchCount = 0;
- let fuzzyMatchCount = 0;
- let noMatchCount = 0;
-
- for (const download of downloads) {
- let objectIds: string[] = [];
- let usedHashMatch = false;
-
- const titleHash = hashTitle(download.title);
- const steamIdsFromHash = titleHashMapping[titleHash];
-
- if (steamIdsFromHash && steamIdsFromHash.length > 0) {
- hashMatchCount++;
- usedHashMatch = true;
- objectIds = steamIdsFromHash.map(String);
- }
-
- if (!usedHashMatch) {
- let gamesInSteam: FormattedSteamGame[] = [];
- const formattedTitle = formatRepackName(download.title);
-
- if (formattedTitle && formattedTitle.length > 0) {
- const [firstLetter] = formattedTitle;
- const games = steamGames[firstLetter] || [];
-
- gamesInSteam = games.filter((game) =>
- formattedTitle.startsWith(game.formattedName)
- );
-
- if (gamesInSteam.length === 0) {
- gamesInSteam = games.filter(
- (game) =>
- formattedTitle.includes(game.formattedName) ||
- game.formattedName.includes(formattedTitle)
- );
- }
-
- if (gamesInSteam.length === 0) {
- for (const letter of Object.keys(steamGames)) {
- const letterGames = steamGames[letter] || [];
- const matches = letterGames.filter(
- (game) =>
- formattedTitle.includes(game.formattedName) ||
- game.formattedName.includes(formattedTitle)
- );
- if (matches.length > 0) {
- gamesInSteam = matches;
- break;
- }
- }
- }
-
- if (gamesInSteam.length > 0) {
- fuzzyMatchCount++;
- objectIds = gamesInSteam.map((game) => String(game.id));
- } else {
- noMatchCount++;
- }
- } else {
- noMatchCount++;
- }
- }
-
- matchedDownloads.push({
- ...download,
- objectIds,
- usedHashMatch,
- });
- }
-
- return {
- matchedDownloads,
- stats: {
- hashMatchCount,
- fuzzyMatchCount,
- noMatchCount,
- },
- };
-};
-
-// Message handler
-if (parentPort) {
- parentPort.on("message", (message: { id: string; data: MatchRequest }) => {
- try {
- const result = matchDownloads(message.data);
- parentPort!.postMessage({ id: message.id, success: true, result });
- } catch (error) {
- parentPort!.postMessage({
- id: message.id,
- success: false,
- error: error instanceof Error ? error.message : String(error),
- });
- }
- });
-}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index da914b92..f89ec4db 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -99,22 +99,10 @@ contextBridge.exposeInMainWorld("electron", {
/* Download sources */
addDownloadSource: (url: string) =>
ipcRenderer.invoke("addDownloadSource", url),
- updateMissingFingerprints: () =>
- ipcRenderer.invoke("updateMissingFingerprints"),
removeDownloadSource: (url: string, removeAll?: boolean) =>
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
- deleteDownloadSource: (id: number) =>
- ipcRenderer.invoke("deleteDownloadSource", id),
- deleteAllDownloadSources: () =>
- ipcRenderer.invoke("deleteAllDownloadSources"),
- validateDownloadSource: (url: string) =>
- ipcRenderer.invoke("validateDownloadSource", url),
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
- getDownloadSourcesList: () => ipcRenderer.invoke("getDownloadSourcesList"),
- checkDownloadSourceExists: (url: string) =>
- ipcRenderer.invoke("checkDownloadSourceExists", url),
- getAllRepacks: () => ipcRenderer.invoke("getAllRepacks"),
/* Library */
toggleAutomaticCloudSync: (
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index 74a2a97e..168a4435 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -7,7 +7,6 @@ import {
useAppSelector,
useDownload,
useLibrary,
- useRepacks,
useToast,
useUserDetails,
} from "@renderer/hooks";
@@ -20,7 +19,6 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
- setIsImportingSources,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
@@ -40,8 +38,6 @@ export function App() {
const { t } = useTranslation("app");
- const { updateRepacks } = useRepacks();
-
const { clearDownload, setLastPacket } = useDownload();
const {
@@ -199,36 +195,6 @@ export function App() {
});
}, [dispatch, draggingDisabled]);
- useEffect(() => {
- (async () => {
- dispatch(setIsImportingSources(true));
-
- try {
- // Initial repacks load
- await updateRepacks();
-
- // Sync all local sources (check for updates)
- const newRepacksCount = await window.electron.syncDownloadSources();
-
- if (newRepacksCount > 0) {
- window.electron.publishNewRepacksNotification(newRepacksCount);
- }
-
- // Update fingerprints for sources that don't have them
- await window.electron.updateMissingFingerprints();
-
- // Update repacks AFTER all syncing and fingerprint updates are complete
- await updateRepacks();
- } catch (error) {
- console.error("Error syncing download sources:", error);
- // Still update repacks even if sync fails
- await updateRepacks();
- } finally {
- dispatch(setIsImportingSources(false));
- }
- })();
- }, [updateRepacks, dispatch]);
-
const loadAndApplyTheme = useCallback(async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx
index 5752ba19..598874b5 100644
--- a/src/renderer/src/components/game-card/game-card.tsx
+++ b/src/renderer/src/components/game-card/game-card.tsx
@@ -1,5 +1,5 @@
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
-import type { GameStats } from "@types";
+import type { GameStats, ShopAssets } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -8,15 +8,15 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { StarRating } from "../star-rating/star-rating";
-import { useCallback, useState, useMemo } from "react";
-import { useFormat, useRepacks } from "@renderer/hooks";
+import { useCallback, useState } from "react";
+import { useFormat } from "@renderer/hooks";
export interface GameCardProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes,
HTMLButtonElement
> {
- game: any;
+ game: ShopAssets;
}
const shopIcon = {
@@ -28,13 +28,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
const [stats, setStats] = useState(null);
- const { getRepacksForObjectId } = useRepacks();
- const repacks = getRepacksForObjectId(game.objectId);
-
- const uniqueRepackers = Array.from(
- new Set(repacks.map((repack) => repack.repacker))
- );
-
const handleHover = useCallback(() => {
if (!stats) {
window.electron.getGameStats(game.objectId, game.shop).then((stats) => {
@@ -45,14 +38,7 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { numberFormatter } = useFormat();
- const firstThreeRepackers = useMemo(
- () => uniqueRepackers.slice(0, 3),
- [uniqueRepackers]
- );
- const remainingCount = useMemo(
- () => uniqueRepackers.length - 3,
- [uniqueRepackers]
- );
+ console.log("game", game);
return (
{game.title}
- {uniqueRepackers.length > 0 ? (
+ {game.downloadSources.length > 0 ? (
- {firstThreeRepackers.map((repacker) => (
- -
- {repacker}
+ {game.downloadSources.slice(0, 3).map((sourceName) => (
+
-
+ {sourceName}
))}
- {remainingCount > 0 && (
+ {game.downloadSources.length > 3 && (
-
- +{remainingCount}{" "}
- {t("game_card:available", { count: remainingCount })}
+ +{game.downloadSources.length - 3}{" "}
+ {t("game_card:available", {
+ count: game.downloadSources.length - 3,
+ })}
)}
diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx
index 14e5d587..2b8e8bf7 100644
--- a/src/renderer/src/context/game-details/game-details.context.tsx
+++ b/src/renderer/src/context/game-details/game-details.context.tsx
@@ -1,11 +1,4 @@
-import {
- createContext,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
+import { createContext, useCallback, useEffect, useRef, useState } from "react";
import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage } from "@renderer/helpers";
@@ -13,11 +6,11 @@ import {
useAppDispatch,
useAppSelector,
useDownload,
- useRepacks,
useUserDetails,
} from "@renderer/hooks";
import type {
+ GameRepack,
GameShop,
GameStats,
LibraryGame,
@@ -84,12 +77,7 @@ export function GameDetailsContextProvider({
const [isGameRunning, setIsGameRunning] = useState(false);
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
-
- const { getRepacksForObjectId } = useRepacks();
-
- const repacks = useMemo(() => {
- return getRepacksForObjectId(objectId);
- }, [getRepacksForObjectId, objectId]);
+ const [repacks, setRepacks] = useState([]);
const { i18n } = useTranslation("game_details");
const location = useLocation();
@@ -287,19 +275,6 @@ export function GameDetailsContextProvider({
}
}, [location]);
- const lastDownloadedOption = useMemo(() => {
- if (game?.download) {
- const repack = repacks.find((repack) =>
- repack.uris.some((uri) => uri.includes(game.download!.uri))
- );
-
- if (!repack) return null;
- return repack;
- }
-
- return null;
- }, [game?.download, repacks]);
-
useEffect(() => {
const unsubscribe = window.electron.onUpdateAchievements(
objectId,
@@ -315,6 +290,34 @@ export function GameDetailsContextProvider({
};
}, [objectId, shop, userDetails]);
+ useEffect(() => {
+ const fetchDownloadSources = async () => {
+ try {
+ const sources = await window.electron.getDownloadSources();
+
+ const params = {
+ take: 100,
+ skip: 0,
+ downloadSourceIds: sources.map((source) => source.id),
+ };
+
+ const downloads = await window.electron.hydraApi.get(
+ `/games/${shop}/${objectId}/download-sources`,
+ {
+ params,
+ needsAuth: false,
+ }
+ );
+
+ setRepacks(downloads);
+ } catch (error) {
+ console.error("Failed to fetch download sources:", error);
+ }
+ };
+
+ fetchDownloadSources();
+ }, [shop, objectId]);
+
const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
return window.electron.getDefaultDownloadsPath();
@@ -359,7 +362,7 @@ export function GameDetailsContextProvider({
stats,
achievements,
hasNSFWContentBlocked,
- lastDownloadedOption,
+ lastDownloadedOption: null,
setHasNSFWContentBlocked,
selectGameExecutable,
updateGame,
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 9f882aed..4e004e2b 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -31,8 +31,6 @@ import type {
Game,
DiskUsage,
DownloadSource,
- DownloadSourceValidationResult,
- GameRepack,
} from "@types";
import type { AxiosProgressEvent } from "axios";
@@ -210,20 +208,12 @@ declare global {
/* Download sources */
addDownloadSource: (url: string) => Promise;
- updateMissingFingerprints: () => Promise;
- removeDownloadSource: (url: string, removeAll?: boolean) => Promise;
- getDownloadSources: () => Promise<
- Pick[]
- >;
- deleteDownloadSource: (id: number) => Promise;
- deleteAllDownloadSources: () => Promise;
- validateDownloadSource: (
- url: string
- ) => Promise;
- syncDownloadSources: () => Promise;
- getDownloadSourcesList: () => Promise;
- checkDownloadSourceExists: (url: string) => Promise;
- getAllRepacks: () => Promise;
+ removeDownloadSource: (
+ removeAll = false,
+ downloadSourceId?: string
+ ) => Promise;
+ getDownloadSources: () => Promise;
+ syncDownloadSources: () => Promise;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise;
diff --git a/src/renderer/src/features/download-sources-slice.ts b/src/renderer/src/features/download-sources-slice.ts
deleted file mode 100644
index 52e58d26..00000000
--- a/src/renderer/src/features/download-sources-slice.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-
-export interface DownloadSourcesState {
- isImporting: boolean;
-}
-
-const initialState: DownloadSourcesState = {
- isImporting: false,
-};
-
-export const downloadSourcesSlice = createSlice({
- name: "downloadSources",
- initialState,
- reducers: {
- setIsImportingSources: (state, action) => {
- state.isImporting = action.payload;
- },
- },
-});
-
-export const { setIsImportingSources } = downloadSourcesSlice.actions;
diff --git a/src/renderer/src/features/index.ts b/src/renderer/src/features/index.ts
index 3b602cff..a7e64e1f 100644
--- a/src/renderer/src/features/index.ts
+++ b/src/renderer/src/features/index.ts
@@ -6,6 +6,4 @@ export * from "./toast-slice";
export * from "./user-details-slice";
export * from "./game-running.slice";
export * from "./subscription-slice";
-export * from "./repacks-slice";
-export * from "./download-sources-slice";
export * from "./catalogue-search";
diff --git a/src/renderer/src/hooks/index.ts b/src/renderer/src/hooks/index.ts
index 8140e0cd..73733e2b 100644
--- a/src/renderer/src/hooks/index.ts
+++ b/src/renderer/src/hooks/index.ts
@@ -5,5 +5,4 @@ export * from "./use-toast";
export * from "./redux";
export * from "./use-user-details";
export * from "./use-format";
-export * from "./use-repacks";
export * from "./use-feature";
diff --git a/src/renderer/src/hooks/use-catalogue.ts b/src/renderer/src/hooks/use-catalogue.ts
index 1d0aeb57..675f5013 100644
--- a/src/renderer/src/hooks/use-catalogue.ts
+++ b/src/renderer/src/hooks/use-catalogue.ts
@@ -2,6 +2,7 @@ import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useAppDispatch } from "./redux";
import { setGenres, setTags } from "@renderer/features";
+import type { DownloadSource } from "@types";
export const externalResourcesInstance = axios.create({
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
@@ -12,6 +13,7 @@ export function useCatalogue() {
const [steamPublishers, setSteamPublishers] = useState([]);
const [steamDevelopers, setSteamDevelopers] = useState([]);
+ const [downloadSources, setDownloadSources] = useState([]);
const getSteamUserTags = useCallback(() => {
externalResourcesInstance.get("/steam-user-tags.json").then((response) => {
@@ -37,17 +39,25 @@ export function useCatalogue() {
});
}, []);
+ const getDownloadSources = useCallback(() => {
+ window.electron.getDownloadSources().then((results) => {
+ setDownloadSources(results.filter((source) => !!source.fingerprint));
+ });
+ }, []);
+
useEffect(() => {
getSteamUserTags();
getSteamGenres();
getSteamPublishers();
getSteamDevelopers();
+ getDownloadSources();
}, [
getSteamUserTags,
getSteamGenres,
getSteamPublishers,
getSteamDevelopers,
+ getDownloadSources,
]);
- return { steamPublishers, steamDevelopers };
+ return { steamPublishers, downloadSources, steamDevelopers };
}
diff --git a/src/renderer/src/hooks/use-repacks.ts b/src/renderer/src/hooks/use-repacks.ts
deleted file mode 100644
index c024aaa4..00000000
--- a/src/renderer/src/hooks/use-repacks.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { setRepacks } from "@renderer/features";
-import { useCallback } from "react";
-import { RootState } from "@renderer/store";
-import { useSelector } from "react-redux";
-import { useAppDispatch } from "./redux";
-
-export function useRepacks() {
- const dispatch = useAppDispatch();
- const repacks = useSelector((state: RootState) => state.repacks.value);
-
- const getRepacksForObjectId = useCallback(
- (objectId: string) => {
- return repacks.filter((repack) => repack.objectIds.includes(objectId));
- },
- [repacks]
- );
-
- const updateRepacks = useCallback(async () => {
- const repacks = await window.electron.getAllRepacks();
- dispatch(
- setRepacks(repacks.filter((repack) => Array.isArray(repack.objectIds)))
- );
- }, [dispatch]);
-
- return { getRepacksForObjectId, updateRepacks };
-}
diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx
index 07bcf3ff..bbeda906 100644
--- a/src/renderer/src/pages/catalogue/catalogue.tsx
+++ b/src/renderer/src/pages/catalogue/catalogue.tsx
@@ -1,4 +1,8 @@
-import type { CatalogueSearchResult, DownloadSource } from "@types";
+import type {
+ CatalogueSearchResult,
+ CatalogueSearchPayload,
+ DownloadSource,
+} from "@types";
import { useAppDispatch, useAppSelector, useFormat } from "@renderer/hooks";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -29,13 +33,12 @@ export default function Catalogue() {
const abortControllerRef = useRef(null);
const cataloguePageRef = useRef(null);
- const { steamDevelopers, steamPublishers } = useCatalogue();
+ const { steamDevelopers, steamPublishers, downloadSources } = useCatalogue();
const { steamGenres, steamUserTags } = useAppSelector(
(state) => state.catalogueSearch
);
- const [downloadSources, setDownloadSources] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [results, setResults] = useState([]);
@@ -51,24 +54,41 @@ export default function Catalogue() {
const { t, i18n } = useTranslation("catalogue");
const debouncedSearch = useRef(
- debounce(async (filters, pageSize, offset) => {
- const abortController = new AbortController();
- abortControllerRef.current = abortController;
+ debounce(
+ async (
+ filters: CatalogueSearchPayload,
+ downloadSources: DownloadSource[],
+ pageSize: number,
+ offset: number
+ ) => {
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
- const response = await window.electron.hydraApi.post<{
- edges: CatalogueSearchResult[];
- count: number;
- }>("/catalogue/search", {
- data: { ...filters, take: pageSize, skip: offset },
- needsAuth: false,
- });
+ const requestData = {
+ ...filters,
+ take: pageSize,
+ skip: offset,
+ downloadSourceIds: downloadSources.map(
+ (downloadSource) => downloadSource.id
+ ),
+ };
- if (abortController.signal.aborted) return;
+ const response = await window.electron.hydraApi.post<{
+ edges: CatalogueSearchResult[];
+ count: number;
+ }>("/catalogue/search", {
+ data: requestData,
+ needsAuth: false,
+ });
- setResults(response.edges);
- setItemsCount(response.count);
- setIsLoading(false);
- }, 500)
+ if (abortController.signal.aborted) return;
+
+ setResults(response.edges);
+ setItemsCount(response.count);
+ setIsLoading(false);
+ },
+ 500
+ )
).current;
const decodeHTML = (s: string) =>
@@ -79,18 +99,17 @@ export default function Catalogue() {
setIsLoading(true);
abortControllerRef.current?.abort();
- debouncedSearch(filters, PAGE_SIZE, (page - 1) * PAGE_SIZE);
+ debouncedSearch(
+ filters,
+ downloadSources,
+ PAGE_SIZE,
+ (page - 1) * PAGE_SIZE
+ );
return () => {
debouncedSearch.cancel();
};
- }, [filters, page, debouncedSearch]);
-
- useEffect(() => {
- window.electron.getDownloadSourcesList().then((sources) => {
- setDownloadSources(sources.filter((source) => !!source.fingerprint));
- });
- }, []);
+ }, [filters, downloadSources, page, debouncedSearch]);
const language = i18n.language.split("-")[0];
@@ -168,7 +187,7 @@ export default function Catalogue() {
value: publisher,
})),
];
- }, [filters, steamUserTags, steamGenresMapping, language, downloadSources]);
+ }, [filters, steamUserTags, downloadSources, steamGenresMapping, language]);
const filterSections = useMemo(() => {
return [
diff --git a/src/renderer/src/pages/catalogue/game-item.tsx b/src/renderer/src/pages/catalogue/game-item.tsx
index ecfe0f73..4583afd3 100644
--- a/src/renderer/src/pages/catalogue/game-item.tsx
+++ b/src/renderer/src/pages/catalogue/game-item.tsx
@@ -1,6 +1,6 @@
import { Badge } from "@renderer/components";
import { buildGameDetailsPath } from "@renderer/helpers";
-import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
+import { useAppSelector, useLibrary } from "@renderer/hooks";
import { useMemo, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
@@ -23,10 +23,6 @@ export function GameItem({ game }: GameItemProps) {
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
- const { getRepacksForObjectId } = useRepacks();
-
- const repacks = getRepacksForObjectId(game.objectId);
-
const [isAddingToLibrary, setIsAddingToLibrary] = useState(false);
const [added, setAdded] = useState(false);
@@ -63,10 +59,6 @@ export function GameItem({ game }: GameItemProps) {
}
};
- const uniqueRepackers = useMemo(() => {
- return Array.from(new Set(repacks.map((repack) => repack.repacker)));
- }, [repacks]);
-
const genres = useMemo(() => {
return game.genres?.map((genre) => {
const index = steamGenres["en"]?.findIndex(
@@ -117,8 +109,8 @@ export function GameItem({ game }: GameItemProps) {
{genres.join(", ")}
- {uniqueRepackers.map((repacker) => (
- {repacker}
+ {game.downloadSources.map((sourceName) => (
+ {sourceName}
))}
diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx
index f8117f43..1ce44550 100644
--- a/src/renderer/src/pages/game-details/game-reviews.tsx
+++ b/src/renderer/src/pages/game-details/game-reviews.tsx
@@ -144,8 +144,6 @@ export function GameReviews({
}
}, [objectId, userDetailsId, shop, game, onUserReviewedChange]);
- console.log("reviews", reviews);
-
const loadReviews = useCallback(
async (reset = false) => {
if (!objectId) return;
@@ -440,8 +438,6 @@ export function GameReviews({
});
}, [reviews]);
- console.log("reviews", reviews);
-
return (
{showReviewPrompt &&
diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
index 7551a31e..306e8647 100644
--- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
+++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx
@@ -54,7 +54,7 @@ export function RepacksModal({
{}
);
- const { repacks, game } = useContext(gameDetailsContext);
+ const { game, repacks } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
@@ -88,6 +88,15 @@ export function RepacksModal({
});
}, [repacks, isFeatureEnabled, Feature]);
+ useEffect(() => {
+ const fetchDownloadSources = async () => {
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources);
+ };
+
+ fetchDownloadSources();
+ }, []);
+
const sortedRepacks = useMemo(() => {
return orderBy(
repacks,
@@ -103,23 +112,13 @@ export function RepacksModal({
);
}, [repacks, hashesInDebrid]);
- useEffect(() => {
- window.electron.getDownloadSourcesList().then((sources) => {
- const uniqueRepackers = new Set(sortedRepacks.map((r) => r.repacker));
- const filteredSources = sources.filter(
- (s) => s.name && uniqueRepackers.has(s.name) && !!s.fingerprint
- );
- setDownloadSources(filteredSources);
- });
- }, [sortedRepacks]);
-
useEffect(() => {
const term = filterTerm.trim().toLowerCase();
const byTerm = sortedRepacks.filter((repack) => {
if (!term) return true;
const lowerTitle = repack.title.toLowerCase();
- const lowerRepacker = repack.repacker.toLowerCase();
+ const lowerRepacker = repack.downloadSourceName.toLowerCase();
return lowerTitle.includes(term) || lowerRepacker.includes(term);
});
@@ -130,7 +129,7 @@ export function RepacksModal({
(src) =>
src.fingerprint &&
selectedFingerprints.includes(src.fingerprint) &&
- src.name === repack.repacker
+ src.name === repack.downloadSourceName
);
});
@@ -281,7 +280,7 @@ export function RepacksModal({
)}
- {repack.fileSize} - {repack.repacker} -{" "}
+ {repack.fileSize} - {repack.downloadSourceName} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx
index 40bf181d..b8f632a6 100644
--- a/src/renderer/src/pages/home/home.tsx
+++ b/src/renderer/src/pages/home/home.tsx
@@ -40,14 +40,20 @@ export default function Home() {
setCurrentCatalogueCategory(category);
setIsLoading(true);
- const params = new URLSearchParams({
- take: "12",
- skip: "0",
- });
+ const downloadSources = await window.electron.getDownloadSources();
+
+ const params = {
+ take: 12,
+ skip: 0,
+ downloadSourceIds: downloadSources.map((source) => source.id),
+ };
const catalogue = await window.electron.hydraApi.get
(
- `/catalogue/${category}?${params.toString()}`,
- { needsAuth: false }
+ `/catalogue/${category}`,
+ {
+ params,
+ needsAuth: false,
+ }
);
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.scss b/src/renderer/src/pages/settings/add-download-source-modal.scss
index ea92ca71..d938f7f0 100644
--- a/src/renderer/src/pages/settings/add-download-source-modal.scss
+++ b/src/renderer/src/pages/settings/add-download-source-modal.scss
@@ -38,4 +38,11 @@
animation: spin 1s linear infinite;
margin-right: calc(globals.$spacing-unit / 2);
}
+
+ &__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: globals.$spacing-unit;
+ margin-top: calc(globals.$spacing-unit * 2);
+ }
}
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx
index c2b47513..2b45ed72 100644
--- a/src/renderer/src/pages/settings/add-download-source-modal.tsx
+++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx
@@ -1,15 +1,12 @@
-import { useCallback, useContext, useEffect, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import { settingsContext } from "@renderer/context";
import { useForm } from "react-hook-form";
-import { useAppDispatch } from "@renderer/hooks";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
-import type { DownloadSourceValidationResult } from "@types";
-import { setIsImportingSources } from "@renderer/features";
import { SyncIcon } from "@primer/octicons-react";
import "./add-download-source-modal.scss";
@@ -28,7 +25,6 @@ export function AddDownloadSourceModal({
onClose,
onAddDownloadSource,
}: Readonly) {
- const [url, setUrl] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation("settings");
@@ -48,77 +44,38 @@ export function AddDownloadSourceModal({
resolver: yupResolver(schema),
});
- const [validationResult, setValidationResult] =
- useState(null);
-
const { sourceUrl } = useContext(settingsContext);
- const dispatch = useAppDispatch();
+ const onSubmit = async (values: FormValues) => {
+ setIsLoading(true);
- const onSubmit = useCallback(
- async (values: FormValues) => {
- const exists = await window.electron.checkDownloadSourceExists(
- values.url
- );
+ try {
+ await window.electron.addDownloadSource(values.url);
- if (exists) {
- setError("url", {
- type: "server",
- message: t("source_already_exists"),
- });
-
- return;
- }
-
- const validationResult = await window.electron.validateDownloadSource(
- values.url
- );
-
- setValidationResult(validationResult);
- setUrl(values.url);
- },
- [setError, t]
- );
+ onClose();
+ onAddDownloadSource();
+ } catch (error) {
+ console.error("Failed to add download source:", error);
+ setError("url", {
+ type: "server",
+ message: "Failed to add download source. Please try again.",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
useEffect(() => {
setValue("url", "");
clearErrors();
setIsLoading(false);
- setValidationResult(null);
if (sourceUrl) {
setValue("url", sourceUrl);
- handleSubmit(onSubmit)();
}
- }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]);
-
- const handleAddDownloadSource = async () => {
- if (validationResult) {
- setIsLoading(true);
- dispatch(setIsImportingSources(true));
-
- try {
- // Single call that handles: import → API sync → fingerprint
- await window.electron.addDownloadSource(url);
-
- // Close modal and update UI
- onClose();
- onAddDownloadSource();
- } catch (error) {
- console.error("Failed to add download source:", error);
- setError("url", {
- type: "server",
- message: "Failed to import source. Please try again.",
- });
- } finally {
- setIsLoading(false);
- dispatch(setIsImportingSources(false));
- }
- }
- };
+ }, [visible, clearErrors, setValue, sourceUrl]);
const handleClose = () => {
- // Prevent closing while importing
if (isLoading) return;
onClose();
};
@@ -132,49 +89,32 @@ export function AddDownloadSourceModal({
clickOutsideToClose={!isLoading}
>
-
+
+
+
- {t("validate_download_source")}
-
- }
- />
-
- {validationResult && (
-
-
-
{validationResult?.name}
-
- {t("found_download_option", {
- count: validationResult?.downloadCount,
- countFormatted:
- validationResult?.downloadCount.toLocaleString(),
- })}
-
-
-
-
+ {t("cancel")}
+
+
+
{isLoading && (
)}
- {isLoading ? t("importing") : t("import")}
+ {isLoading ? t("adding") : t("add_download_source")}
- )}
+
);
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index f873b321..85c0569a 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -16,7 +16,7 @@ import {
TrashIcon,
} from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
-import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks";
+import { useAppDispatch, useToast } from "@renderer/hooks";
import { DownloadSourceStatus } from "@shared";
import { settingsContext } from "@renderer/context";
import { useNavigate } from "react-router-dom";
@@ -35,7 +35,6 @@ export function SettingsDownloadSources() {
useState(false);
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
useState(false);
- const [isFetchingSources, setIsFetchingSources] = useState(true);
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
@@ -46,37 +45,29 @@ export function SettingsDownloadSources() {
const navigate = useNavigate();
- const { updateRepacks } = useRepacks();
-
- const getDownloadSources = async () => {
- await window.electron
- .getDownloadSourcesList()
- .then((sources) => {
- setDownloadSources(sources);
- })
- .finally(() => {
- setIsFetchingSources(false);
- });
- };
-
- useEffect(() => {
- getDownloadSources();
- }, []);
-
useEffect(() => {
if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]);
+ useEffect(() => {
+ const fetchDownloadSources = async () => {
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources);
+ };
+
+ fetchDownloadSources();
+ }, []);
+
const handleRemoveSource = async (downloadSource: DownloadSource) => {
setIsRemovingDownloadSource(true);
try {
- await window.electron.deleteDownloadSource(downloadSource.id);
- await window.electron.removeDownloadSource(downloadSource.url);
-
+ await window.electron.removeDownloadSource(false, downloadSource.id);
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("removed_download_source"));
- await getDownloadSources();
- updateRepacks();
+ } catch (error) {
+ console.error("Failed to remove download source:", error);
} finally {
setIsRemovingDownloadSource(false);
}
@@ -86,53 +77,44 @@ export function SettingsDownloadSources() {
setIsRemovingDownloadSource(true);
try {
- await window.electron.deleteAllDownloadSources();
- await window.electron.removeDownloadSource("", true);
-
- showSuccessToast(t("removed_download_sources"));
- await getDownloadSources();
- setShowConfirmationDeleteAllSourcesModal(false);
- updateRepacks();
+ await window.electron.removeDownloadSource(true);
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources as DownloadSource[]);
+ showSuccessToast(t("removed_all_download_sources"));
+ } catch (error) {
+ console.error("Failed to remove all download sources:", error);
} finally {
setIsRemovingDownloadSource(false);
+ setShowConfirmationDeleteAllSourcesModal(false);
}
};
const handleAddDownloadSource = async () => {
- // Refresh sources list and repacks after import completes
- await getDownloadSources();
-
- // Force repacks update to ensure UI reflects new data
- await updateRepacks();
-
- showSuccessToast(t("added_download_source"));
+ try {
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources as DownloadSource[]);
+ } catch (error) {
+ console.error("Failed to refresh download sources:", error);
+ }
};
const syncDownloadSources = async () => {
setIsSyncingDownloadSources(true);
-
try {
- // Sync local sources (check for updates)
- await window.electron.syncDownloadSources();
-
- // Refresh sources and repacks AFTER sync completes
- await getDownloadSources();
- await updateRepacks();
-
- showSuccessToast(t("download_sources_synced"));
- } catch (error) {
- console.error("Error syncing download sources:", error);
- // Still refresh the UI even if sync fails
- await getDownloadSources();
- await updateRepacks();
+ const sources = await window.electron.syncDownloadSources();
+ setDownloadSources(sources);
} finally {
setIsSyncingDownloadSources(false);
}
};
const statusTitle = {
- [DownloadSourceStatus.UpToDate]: t("download_source_up_to_date"),
- [DownloadSourceStatus.Errored]: t("download_source_errored"),
+ [DownloadSourceStatus.PendingMatching]: t(
+ "download_source_pending_matching"
+ ),
+ [DownloadSourceStatus.Matched]: t("download_source_matched"),
+ [DownloadSourceStatus.Matching]: t("download_source_matching"),
+ [DownloadSourceStatus.Failed]: t("download_source_failed"),
};
const handleModalClose = () => {
@@ -180,8 +162,7 @@ export function SettingsDownloadSources() {
disabled={
!downloadSources.length ||
isSyncingDownloadSources ||
- isRemovingDownloadSource ||
- isFetchingSources
+ isRemovingDownloadSource
}
onClick={syncDownloadSources}
>
@@ -197,8 +178,7 @@ export function SettingsDownloadSources() {
disabled={
isRemovingDownloadSource ||
isSyncingDownloadSources ||
- !downloadSources.length ||
- isFetchingSources
+ !downloadSources.length
}
>
@@ -209,11 +189,7 @@ export function SettingsDownloadSources() {
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
- disabled={
- isSyncingDownloadSources ||
- isFetchingSources ||
- isRemovingDownloadSource
- }
+ disabled={isSyncingDownloadSources || isRemovingDownloadSource}
>
{t("add_download_source")}
diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts
index 264b1296..9903271c 100644
--- a/src/renderer/src/store.ts
+++ b/src/renderer/src/store.ts
@@ -8,8 +8,6 @@ import {
userDetailsSlice,
gameRunningSlice,
subscriptionSlice,
- repacksSlice,
- downloadSourcesSlice,
catalogueSearchSlice,
} from "@renderer/features";
@@ -23,8 +21,6 @@ export const store = configureStore({
userDetails: userDetailsSlice.reducer,
gameRunning: gameRunningSlice.reducer,
subscription: subscriptionSlice.reducer,
- repacks: repacksSlice.reducer,
- downloadSources: downloadSourcesSlice.reducer,
catalogueSearch: catalogueSearchSlice.reducer,
},
});
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
index 851aec49..619dca65 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.ts
@@ -11,8 +11,10 @@ export enum Downloader {
}
export enum DownloadSourceStatus {
- UpToDate,
- Errored,
+ PendingMatching = "PENDING_MATCHING",
+ Matched = "MATCHED",
+ Matching = "MATCHING",
+ Failed = "FAILED",
}
export enum CatalogueCategory {
diff --git a/src/types/index.ts b/src/types/index.ts
index 63b18645..092adaf8 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -16,29 +16,22 @@ export interface DiskUsage {
}
export interface GameRepack {
- id: number;
+ id: string;
title: string;
- uris: string[];
- repacker: string;
fileSize: string | null;
- objectIds: string[];
- uploadDate: Date | string | null;
- createdAt: Date;
- updatedAt: Date;
+ uris: string[];
+ uploadDate: string | null;
+ downloadSourceId: string;
+ downloadSourceName: string;
}
export interface DownloadSource {
- id: number;
+ id: string;
name: string;
url: string;
- repackCount: number;
status: DownloadSourceStatus;
- objectIds: string[];
downloadCount: number;
fingerprint?: string;
- etag: string | null;
- createdAt: Date;
- updatedAt: Date;
}
export interface ShopAssets {
@@ -51,6 +44,7 @@ export interface ShopAssets {
logoImageUrl: string;
logoPosition: string | null;
coverImageUrl: string | null;
+ downloadSources: string[];
}
export type ShopDetails = SteamAppDetails & {
@@ -231,12 +225,6 @@ export interface DownloadSourceDownload {
fileSize: string;
}
-export interface DownloadSourceValidationResult {
- name: string;
- etag: string;
- downloadCount: number;
-}
-
export interface GameStats {
downloadCount: number;
playerCount: number;
@@ -366,7 +354,7 @@ export type CatalogueSearchResult = {
title: string;
shop: GameShop;
genres: string[];
-} & Pick;
+} & Pick;
export type LibraryGame = Game &
Partial & {
From e1ce5bc6cb8e6d2cbc14a7e7615c0b95619d9cf2 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 21 Oct 2025 04:20:11 +0100
Subject: [PATCH 006/118] feat: using api download sources
---
src/main/events/library/add-custom-game-to-library.ts | 1 +
src/main/services/library-sync/merge-with-remote-games.ts | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts
index f2f2dd40..6a90087e 100644
--- a/src/main/events/library/add-custom-game-to-library.ts
+++ b/src/main/events/library/add-custom-game-to-library.ts
@@ -37,6 +37,7 @@ const addCustomGameToLibrary = async (
logoImageUrl: logoImageUrl || "",
logoPosition: null,
coverImageUrl: iconUrl || "",
+ downloadSources: [],
};
await gamesShopAssetsSublevel.put(gameKey, assets);
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 f7ea2744..c00e4961 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -72,6 +72,7 @@ export const mergeWithRemoteGames = async () => {
logoImageUrl: game.logoImageUrl,
iconUrl: game.iconUrl,
logoPosition: game.logoPosition,
+ downloadSources: game.downloadSources,
});
}
})
From 8a40c678f7c2db1d4558cf1cca018359a592e3d7 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 21 Oct 2025 04:21:56 +0100
Subject: [PATCH 007/118] feat: using api download sources
---
src/renderer/src/pages/game-details/game-details.tsx | 1 -
src/types/index.ts | 1 -
2 files changed, 2 deletions(-)
diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx
index f0778494..04b78aa4 100644
--- a/src/renderer/src/pages/game-details/game-details.tsx
+++ b/src/renderer/src/pages/game-details/game-details.tsx
@@ -102,7 +102,6 @@ export default function GameDetails() {
automaticallyExtract: boolean
) => {
const response = await startDownload({
- repackId: repack.id,
objectId: objectId!,
title: gameTitle,
downloader,
diff --git a/src/types/index.ts b/src/types/index.ts
index 092adaf8..7d11171b 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -107,7 +107,6 @@ export type AppUpdaterEvent =
/* Events */
export interface StartGameDownloadPayload {
- repackId: number;
objectId: string;
title: string;
shop: GameShop;
From 33e0d509668c4eb24dd09593c332324c6380b032 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Wed, 22 Oct 2025 14:24:04 +0530
Subject: [PATCH 008/118] feat: add achievements tracking to game library
- Updated `get-library.ts` to include unlocked and total achievement counts for each game.
- Removed `library-game-card-detailed.tsx` and its associated styles as part of the refactor.
- Enhanced `library-game-card-large.tsx` to display achievements with progress bars.
- Modified `library-game-card.scss` and `library-game-card-large.scss` to style the achievements section.
- Introduced a new `search-bar` component for filtering the game library.
- Implemented fuzzy search functionality in the library view.
- Updated `view-options` to improve UI consistency.
- Added achievement-related properties to the `LibraryGame` type in `index.ts`.
- Created a new `copilot-instructions.md` for project guidelines.
---
src/main/events/library/get-library.ts | 17 ++
.../library/library-game-card-detailed.scss | 217 ------------------
.../library/library-game-card-detailed.tsx | 209 -----------------
.../library/library-game-card-large.scss | 75 +++++-
.../pages/library/library-game-card-large.tsx | 78 +++++--
.../src/pages/library/library-game-card.scss | 67 +++++-
.../src/pages/library/library-game-card.tsx | 59 +++--
src/renderer/src/pages/library/library.tsx | 46 +++-
.../src/pages/library/search-bar.scss | 75 ++++++
src/renderer/src/pages/library/search-bar.tsx | 44 ++++
.../src/pages/library/view-options.scss | 4 +-
.../src/pages/library/view-options.tsx | 3 -
src/types/index.ts | 2 +
13 files changed, 405 insertions(+), 491 deletions(-)
delete mode 100644 src/renderer/src/pages/library/library-game-card-detailed.scss
delete mode 100644 src/renderer/src/pages/library/library-game-card-detailed.tsx
create mode 100644 src/renderer/src/pages/library/search-bar.scss
create mode 100644 src/renderer/src/pages/library/search-bar.tsx
diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts
index 8922802b..f62c60e7 100644
--- a/src/main/events/library/get-library.ts
+++ b/src/main/events/library/get-library.ts
@@ -4,6 +4,7 @@ import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
+ gameAchievementsSublevel,
} from "@main/level";
const getLibrary = async (): Promise => {
@@ -18,10 +19,26 @@ const getLibrary = async (): Promise => {
const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.get(key);
+ let unlockedAchievementCount = 0;
+ let achievementCount = 0;
+
+ try {
+ const achievements = await gameAchievementsSublevel.get(key);
+ if (achievements) {
+ achievementCount = achievements.achievements.length;
+ unlockedAchievementCount =
+ achievements.unlockedAchievements.length;
+ }
+ } catch {
+ // No achievements data for this game
+ }
+
return {
id: key,
...game,
download: download ?? null,
+ unlockedAchievementCount,
+ achievementCount,
// Spread gameAssets last to ensure all image URLs are properly set
...gameAssets,
// Preserve custom image URLs from game if they exist
diff --git a/src/renderer/src/pages/library/library-game-card-detailed.scss b/src/renderer/src/pages/library/library-game-card-detailed.scss
deleted file mode 100644
index 0038e918..00000000
--- a/src/renderer/src/pages/library/library-game-card-detailed.scss
+++ /dev/null
@@ -1,217 +0,0 @@
-@use "../../scss/globals.scss";
-
-.library-game-card-detailed {
- width: 100%;
- height: 350px;
- position: relative;
- border-radius: 8px;
- overflow: hidden;
- border: 1px solid rgba(255, 255, 255, 0.05);
- transition: all ease 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- padding: 0;
- text-align: left;
-
- &:before {
- content: "";
- top: 0;
- left: 0;
- width: 100%;
- height: 172%;
- position: absolute;
- background: linear-gradient(
- 35deg,
- rgba(0, 0, 0, 0.1) 0%,
- rgba(0, 0, 0, 0.07) 51.5%,
- rgba(255, 255, 255, 0.15) 74%,
- rgba(255, 255, 255, 0.1) 100%
- );
- transition: all ease 0.3s;
- transform: translateY(-36%);
- opacity: 0.5;
- z-index: 1;
- }
-
- &:hover::before {
- opacity: 1;
- transform: translateY(-20%);
- }
-
- &:hover {
- transform: scale(1.05);
- transform: translateY(-2px);
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
- border-color: rgba(255, 255, 255, 0.1);
- }
-
- &__background {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-size: cover;
- background-position: top;
- background-repeat: no-repeat;
- z-index: 0;
- }
-
- &__gradient {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(
- 90deg,
- rgba(0, 0, 0, 0.1) 0%,
- rgba(0, 0, 0, 0.3) 50%,
- rgba(0, 0, 0, 0.5) 100%
- );
- z-index: 1;
- }
-
- &__overlay {
- position: relative;
- z-index: 2;
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: calc(globals.$spacing-unit * 3);
- }
-
- &__menu-button {
- align-self: flex-end;
- background: rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- border: solid 1px rgba(255, 255, 255, 0.15);
- border-radius: 4px;
- width: 36px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all ease 0.2s;
- color: rgba(255, 255, 255, 0.8);
- padding: 0;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-
- &:hover {
- background: rgba(0, 0, 0, 0.6);
- border-color: rgba(255, 255, 255, 0.25);
- transform: scale(1.05);
- }
-
- &:active {
- transform: scale(0.95);
- }
- }
-
- &__logo-container {
- flex: 1;
- display: flex;
- align-items: center;
- min-width: 0;
- }
-
- &__logo {
- max-height: 140px;
- max-width: 450px;
- width: auto;
- height: auto;
- object-fit: contain;
- filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.6));
- }
-
- &__title {
- font-size: 32px;
- font-weight: 700;
- color: rgba(255, 255, 255, 0.95);
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9);
- }
-
- &__info-bar {
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- gap: calc(globals.$spacing-unit * 2);
- }
-
- &__playtime {
- background: rgba(0, 0, 0, 0.4);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- color: rgba(255, 255, 255, 0.8);
- border: solid 1px rgba(255, 255, 255, 0.15);
- border-radius: 4px;
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 12px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
- font-size: 14px;
- }
-
- &__playtime-text {
- font-weight: 500;
- }
-
- &__manual-playtime {
- color: globals.$warning-color;
- }
-
- &__action-button {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 12px 24px;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(4px);
- -webkit-backdrop-filter: blur(4px);
- border: 1px solid rgba(255, 255, 255, 0.2);
- color: rgba(255, 255, 255, 0.9);
- cursor: pointer;
- font-size: 14px;
- font-weight: 600;
- transition: all ease 0.2s;
- flex-shrink: 0;
-
- &:hover {
- background: rgba(255, 255, 255, 0.15);
- border-color: rgba(255, 255, 255, 0.3);
- transform: scale(1.05);
- }
-
- &:active {
- transform: scale(0.98);
- }
- }
-
- &__action-icon--downloading {
- animation: pulse 1.5s ease-in-out infinite;
- }
-}
-
-@keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
-}
diff --git a/src/renderer/src/pages/library/library-game-card-detailed.tsx b/src/renderer/src/pages/library/library-game-card-detailed.tsx
deleted file mode 100644
index d07dad28..00000000
--- a/src/renderer/src/pages/library/library-game-card-detailed.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { LibraryGame } from "@types";
-import { useDownload, useFormat } from "@renderer/hooks";
-import { useNavigate } from "react-router-dom";
-import { buildGameDetailsPath } from "@renderer/helpers";
-import {
- PlayIcon,
- DownloadIcon,
- ClockIcon,
- AlertFillIcon,
- ThreeBarsIcon,
-} from "@primer/octicons-react";
-import { useTranslation } from "react-i18next";
-import { useCallback, useState } from "react";
-import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
-import { GameContextMenu } from "@renderer/components";
-import "./library-game-card-detailed.scss";
-
-interface LibraryGameCardDetailedProps {
- game: LibraryGame;
-}
-
-const getImageWithCustomPriority = (
- customUrl: string | null | undefined,
- originalUrl: string | null | undefined,
- fallbackUrl?: string | null | undefined
-) => {
- return customUrl || originalUrl || fallbackUrl || "";
-};
-
-export function LibraryGameCardDetailed({
- game,
-}: LibraryGameCardDetailedProps) {
- const { t } = useTranslation("library");
- const { numberFormatter } = useFormat();
- const navigate = useNavigate();
- const { lastPacket } = useDownload();
- const [contextMenu, setContextMenu] = useState<{
- visible: boolean;
- position: { x: number; y: number };
- }>({ visible: false, position: { x: 0, y: 0 } });
-
- const isGameDownloading =
- game?.download?.status === "active" && lastPacket?.gameId === game?.id;
-
- const formatPlayTime = useCallback(
- (playTimeInMilliseconds = 0, isShort = false) => {
- const minutes = playTimeInMilliseconds / 60000;
-
- if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
- return t(isShort ? "amount_minutes_short" : "amount_minutes", {
- amount: minutes.toFixed(0),
- });
- }
-
- const hours = minutes / 60;
- 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 handleCardClick = () => {
- navigate(buildGameDetailsPath(game));
- };
-
- const handleActionClick = async (e: React.MouseEvent) => {
- e.stopPropagation();
-
- if (game.executablePath) {
- window.electron.openGame(
- game.shop,
- game.objectId,
- game.executablePath,
- game.launchOptions
- );
- } else {
- navigate(buildGameDetailsPath(game));
- }
- };
-
- const handleContextMenu = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
-
- setContextMenu({
- visible: true,
- position: { x: e.clientX, y: e.clientY },
- });
- };
-
- const handleMenuButtonClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- setContextMenu({
- visible: true,
- position: {
- x: e.currentTarget.getBoundingClientRect().right,
- y: e.currentTarget.getBoundingClientRect().bottom,
- },
- });
- };
-
- const handleCloseContextMenu = () => {
- setContextMenu({ visible: false, position: { x: 0, y: 0 } });
- };
-
- // Use libraryHeroImageUrl as background, fallback to libraryImageUrl
- const backgroundImage = getImageWithCustomPriority(
- game.libraryHeroImageUrl,
- game.libraryImageUrl,
- game.iconUrl
- );
-
- // For logo, check if logoImageUrl exists (similar to game details page)
- const logoImage = game.logoImageUrl;
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {logoImage ? (
-

- ) : (
-
- {game.title}
-
- )}
-
-
-
-
- {game.hasManuallyUpdatedPlaytime ? (
-
- ) : (
-
- )}
-
- {formatPlayTime(game.playTimeInMilliseconds)}
-
-
-
-
- {isGameDownloading ? (
- <>
-
- {t("downloading")}
- >
- ) : game.executablePath ? (
- <>
-
- {t("play")}
- >
- ) : (
- <>
-
- {t("download")}
- >
- )}
-
-
-
-
-
- >
- );
-}
diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss
index 86a5a792..10e8beb5 100644
--- a/src/renderer/src/pages/library/library-game-card-large.scss
+++ b/src/renderer/src/pages/library/library-game-card-large.scss
@@ -65,9 +65,9 @@
right: 0;
bottom: 0;
background: linear-gradient(
- 90deg,
- rgba(0, 0, 0, 0.1) 0%,
- rgba(0, 0, 0, 0.3) 50%,
+ 0deg,
+ rgba(0, 0, 0, 1) 0%,
+ rgba(0, 0, 0, 0.2) 50%,
rgba(0, 0, 0, 0.5) 100%
);
z-index: 1;
@@ -81,11 +81,18 @@
display: flex;
flex-direction: column;
justify-content: space-between;
- padding: calc(globals.$spacing-unit * 2.5);
+ padding: calc(globals.$spacing-unit * 2);
+ }
+
+ &__top-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: calc(globals.$spacing-unit);
}
&__menu-button {
- align-self: flex-end;
+ align-self: flex-start;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
@@ -175,6 +182,59 @@
color: globals.$warning-color;
}
+ &__achievements {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 6px 12px;
+ width: 100%;
+ }
+
+ &__achievement-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ justify-content: space-between;
+ }
+ &__achievements-gap {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ &__achievement-trophy {
+ color: #ffd700;
+ flex-shrink: 0;
+ }
+
+ &__achievement-progress {
+ width: 100%;
+ height: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ overflow: hidden;
+ }
+
+ &__achievement-bar {
+ height: 100%;
+ background: linear-gradient(90deg, #ffd700, #ffed4e);
+ border-radius: 4px;
+ transition: width 0.3s ease;
+ }
+
+ &__achievement-count {
+ font-size: 14px;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.9);
+ white-space: nowrap;
+ }
+
+ &__achievement-percentage {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.7);
+ white-space: nowrap;
+ }
+
&__action-button {
display: flex;
align-items: center;
@@ -191,8 +251,6 @@
font-weight: 600;
transition: all ease 0.2s;
flex-shrink: 0;
- opacity: 0;
- transform: translateY(10px);
&:hover {
background: rgba(255, 255, 255, 0.15);
@@ -205,8 +263,7 @@
}
}
- &:hover &__menu-button,
- &:hover &__action-button {
+ &:hover &__menu-button {
opacity: 1;
transform: scale(1);
}
diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx
index 49d93048..cc1c659d 100644
--- a/src/renderer/src/pages/library/library-game-card-large.tsx
+++ b/src/renderer/src/pages/library/library-game-card-large.tsx
@@ -8,6 +8,7 @@ import {
ClockIcon,
AlertFillIcon,
ThreeBarsIcon,
+ TrophyIcon,
} from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { useCallback, useState } from "react";
@@ -130,14 +131,29 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
-
-
-
+
+
+ {game.hasManuallyUpdatedPlaytime ? (
+
+ ) : (
+
+ )}
+
+ {formatPlayTime(game.playTimeInMilliseconds)}
+
+
+
+
+
+
{logoImage ? (
@@ -152,19 +168,39 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
-
- {game.hasManuallyUpdatedPlaytime ? (
-
- ) : (
-
- )}
-
- {formatPlayTime(game.playTimeInMilliseconds)}
-
-
+ {/* Achievements section */}
+ {(game.achievementCount ?? 0) > 0 && (
+
+
+
+
+
+ {game.unlockedAchievementCount ?? 0} /{" "}
+ {game.achievementCount ?? 0}
+
+
+
+ {Math.round(
+ ((game.unlockedAchievementCount ?? 0) /
+ (game.achievementCount ?? 1)) *
+ 100
+ )}
+ %
+
+
+
+
+ )}
- {/* Action button - Play or Download */}
-
- {isGameDownloading ? (
-
- ) : game.executablePath ? (
-
- ) : (
-
- )}
-
+ {/* Achievements section - shown on hover */}
+ {(game.achievementCount ?? 0) > 0 && (
+
+
+
+
+
+ {game.unlockedAchievementCount ?? 0} /{" "}
+ {game.achievementCount ?? 0}
+
+
+
+ {Math.round(
+ ((game.unlockedAchievementCount ?? 0) /
+ (game.achievementCount ?? 1)) *
+ 100
+ )}
+ %
+
+
+
+
+ )}
("grid");
const [filterBy, setFilterBy] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
const dispatch = useAppDispatch();
const { t } = useTranslation("library");
const { userDetails, fetchUserDetails } = useUserDetails();
@@ -53,27 +55,60 @@ export default function Library() {
// Optional: resume animations if needed
};
+ // Simple fuzzy search function
+ const fuzzySearch = (query: string, items: typeof library) => {
+ if (!query.trim()) return items;
+
+ const queryLower = query.toLowerCase();
+ return items.filter((game) => {
+ const titleLower = game.title.toLowerCase();
+ let matches = 0;
+ let queryIndex = 0;
+
+ for (
+ let i = 0;
+ i < titleLower.length && queryIndex < queryLower.length;
+ i++
+ ) {
+ if (titleLower[i] === queryLower[queryIndex]) {
+ matches++;
+ queryIndex++;
+ }
+ }
+
+ return queryIndex === queryLower.length;
+ });
+ };
+
const filteredLibrary = useMemo(() => {
+ let filtered;
+
switch (filterBy) {
case "favourited":
- return library.filter((game) => game.favorite);
+ filtered = library.filter((game) => game.favorite);
+ break;
case "new":
- return library.filter(
+ filtered = library.filter(
(game) => (game.playTimeInMilliseconds || 0) === 0
);
+ break;
case "top10":
- return library
+ filtered = library
.slice()
.sort(
(a, b) =>
(b.playTimeInMilliseconds || 0) - (a.playTimeInMilliseconds || 0)
)
.slice(0, 10);
+ break;
case "all":
default:
- return library;
+ filtered = library;
}
- }, [library, filterBy]);
+
+ // Apply search filter
+ return fuzzySearch(searchQuery, filtered);
+ }, [library, filterBy, searchQuery]);
// No sorting for now — rely on filteredLibrary
const sortedLibrary = filteredLibrary;
@@ -112,6 +147,7 @@ export default function Library() {
+
void;
+}
+
+export const SearchBar: FC = ({ value, onChange }) => {
+ const { t } = useTranslation();
+ const inputRef = useRef(null);
+
+ const handleClear = () => {
+ onChange("");
+ inputRef.current?.focus();
+ };
+
+ return (
+
+
+
+ onChange(e.target.value)}
+ />
+ {value && (
+
+ ×
+
+ )}
+
+
+ );
+};
diff --git a/src/renderer/src/pages/library/view-options.scss b/src/renderer/src/pages/library/view-options.scss
index a9b4e197..3f49851c 100644
--- a/src/renderer/src/pages/library/view-options.scss
+++ b/src/renderer/src/pages/library/view-options.scss
@@ -26,13 +26,13 @@
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
- padding: 8px 16px;
+ padding: 8px 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.04);
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
- font-size: 13px;
+ font-size: 14px;
font-weight: 500;
transition: all ease 0.2s;
white-space: nowrap;
diff --git a/src/renderer/src/pages/library/view-options.tsx b/src/renderer/src/pages/library/view-options.tsx
index 3ea19cee..788eefa9 100644
--- a/src/renderer/src/pages/library/view-options.tsx
+++ b/src/renderer/src/pages/library/view-options.tsx
@@ -21,7 +21,6 @@ export function ViewOptions({ viewMode, onViewModeChange }: ViewOptionsProps) {
title={t("grid_view")}
>
- {t("Grid View")}
- {t("Compact")}
- {t("Large")}
diff --git a/src/types/index.ts b/src/types/index.ts
index 63b18645..f714a938 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -372,6 +372,8 @@ export type LibraryGame = Game &
Partial & {
id: string;
download: Download | null;
+ unlockedAchievementCount?: number;
+ achievementCount?: number;
};
export type UserGameDetails = ShopAssets & {
From 811a6ad9557ba74505d58f0948aaf71269b0058a Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Wed, 22 Oct 2025 14:42:47 +0530
Subject: [PATCH 009/118] refactor: remove unused imports and download logic
from LibraryGameCard
---
.../src/pages/library/library-game-card.tsx | 25 +------------------
1 file changed, 1 insertion(+), 24 deletions(-)
diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx
index 0eecc4a6..be0a5a73 100644
--- a/src/renderer/src/pages/library/library-game-card.tsx
+++ b/src/renderer/src/pages/library/library-game-card.tsx
@@ -1,5 +1,5 @@
import { LibraryGame } from "@types";
-import { useFormat, useDownload } from "@renderer/hooks";
+import { useFormat } from "@renderer/hooks";
import { useNavigate } from "react-router-dom";
import { useCallback, useState } from "react";
import { buildGameDetailsPath } from "@renderer/helpers";
@@ -8,8 +8,6 @@ import {
AlertFillIcon,
ThreeBarsIcon,
TrophyIcon,
- PlayIcon,
- DownloadIcon,
} from "@primer/octicons-react";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { Tooltip } from "react-tooltip";
@@ -32,15 +30,11 @@ export function LibraryGameCard({
const { numberFormatter } = useFormat();
const navigate = useNavigate();
const [isTooltipHovered, setIsTooltipHovered] = useState(false);
- const { lastPacket } = useDownload();
const [contextMenu, setContextMenu] = useState<{
visible: boolean;
position: { x: number; y: number };
}>({ visible: false, position: { x: 0, y: 0 } });
- const isGameDownloading =
- game?.download?.status === "active" && lastPacket?.gameId === game?.id;
-
const formatPlayTime = useCallback(
(playTimeInMilliseconds = 0, isShort = false) => {
const minutes = playTimeInMilliseconds / 60000;
@@ -66,23 +60,6 @@ export function LibraryGameCard({
navigate(buildGameDetailsPath(game));
};
- const handleActionClick = async (e: React.MouseEvent) => {
- e.stopPropagation();
-
- if (game.executablePath) {
- // Game is installed, launch it
- window.electron.openGame(
- game.shop,
- game.objectId,
- game.executablePath,
- game.launchOptions
- );
- } else {
- // Game is not installed, navigate to download options
- navigate(buildGameDetailsPath(game));
- }
- };
-
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
From 107b61f663fd4a8a7b8e8a2f6f7188bfada34ac1 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Wed, 22 Oct 2025 14:46:25 +0530
Subject: [PATCH 010/118] style: update active state colors for filter and view
options
---
src/renderer/src/pages/library/filter-options.scss | 10 ++++++----
src/renderer/src/pages/library/view-options.scss | 4 ++--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/renderer/src/pages/library/filter-options.scss b/src/renderer/src/pages/library/filter-options.scss
index e58e285b..377527a1 100644
--- a/src/renderer/src/pages/library/filter-options.scss
+++ b/src/renderer/src/pages/library/filter-options.scss
@@ -29,12 +29,14 @@
}
&.active {
- color: #c9aa71;
- background: rgba(201, 170, 113, 0.15);
+ &.active {
+ color: rgba(255, 255, 255, 0.95);
+ background: rgba(255, 255, 255, 0.15);
+ }
.library-filter-options__count {
- background: rgba(201, 170, 113, 0.25);
- color: #c9aa71;
+ background: rgba(255, 255, 255, 0.25);
+ color: rgba(255, 255, 255, 0.95);
}
}
}
diff --git a/src/renderer/src/pages/library/view-options.scss b/src/renderer/src/pages/library/view-options.scss
index 3f49851c..6815f625 100644
--- a/src/renderer/src/pages/library/view-options.scss
+++ b/src/renderer/src/pages/library/view-options.scss
@@ -43,8 +43,8 @@
}
&.active {
- color: #c9aa71;
- background: rgba(201, 170, 113, 0.15);
+ color: rgba(255, 255, 255, 0.95);
+ background: rgba(255, 255, 255, 0.15);
}
}
}
From e19102ea66d8adad2ab94f73cfacc7faf3c4fbc2 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Wed, 22 Oct 2025 16:12:12 +0530
Subject: [PATCH 011/118] style: update active state styles for filter and view
options; adjust achievement progress bar styles
---
.../src/pages/library/filter-options.scss | 14 ++++++----
.../library/library-game-card-large.scss | 23 +++++++++++-----
.../src/pages/library/library-game-card.scss | 27 ++++++++++++++-----
src/renderer/src/pages/library/library.tsx | 16 +----------
.../src/pages/library/view-options.scss | 9 +++++--
5 files changed, 54 insertions(+), 35 deletions(-)
diff --git a/src/renderer/src/pages/library/filter-options.scss b/src/renderer/src/pages/library/filter-options.scss
index 377527a1..cc899d56 100644
--- a/src/renderer/src/pages/library/filter-options.scss
+++ b/src/renderer/src/pages/library/filter-options.scss
@@ -22,6 +22,7 @@
font-weight: 500;
transition: all ease 0.2s;
white-space: nowrap; /* prevent label and count from wrapping */
+ border: 1px solid rgba(0, 0, 0, 0.06);
&:hover {
color: rgba(255, 255, 255, 0.9);
@@ -29,14 +30,17 @@
}
&.active {
- &.active {
- color: rgba(255, 255, 255, 0.95);
- background: rgba(255, 255, 255, 0.15);
+ color: #000;
+ background: #fff;
+ svg,
+ svg * {
+ fill: currentColor;
+ color: currentColor;
}
.library-filter-options__count {
- background: rgba(255, 255, 255, 0.25);
- color: rgba(255, 255, 255, 0.95);
+ background: #ebebeb;
+ color: rgba(0, 0, 0, 0.9);
}
}
}
diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss
index 10e8beb5..0be1e907 100644
--- a/src/renderer/src/pages/library/library-game-card-large.scss
+++ b/src/renderer/src/pages/library/library-game-card-large.scss
@@ -203,28 +203,39 @@
}
&__achievement-trophy {
- color: #ffd700;
+ color: #fff;
flex-shrink: 0;
}
&__achievement-progress {
width: 100%;
- height: 6px;
- background: rgba(255, 255, 255, 0.1);
+ height: 4px;
+ transition: all ease 0.2s;
+ background-color: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
+
+ &::-webkit-progress-bar {
+ background-color: transparent;
+ border-radius: 4px;
+ }
+
+ &::-webkit-progress-value {
+ background-color: globals.$muted-color;
+ border-radius: 4px;
+ }
}
&__achievement-bar {
height: 100%;
- background: linear-gradient(90deg, #ffd700, #ffed4e);
+ background-color: globals.$muted-color;
border-radius: 4px;
transition: width 0.3s ease;
}
&__achievement-count {
font-size: 14px;
- font-weight: 600;
+ font-weight: 500;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
@@ -239,7 +250,7 @@
display: flex;
align-items: center;
gap: 8px;
- padding: 12px 24px;
+ padding: 10px 20px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss
index ae4db0c8..b763ae2c 100644
--- a/src/renderer/src/pages/library/library-game-card.scss
+++ b/src/renderer/src/pages/library/library-game-card.scss
@@ -131,28 +131,41 @@
}
&__achievement-trophy {
- color: #ffd700;
+ color: #fff;
flex-shrink: 0;
}
&__achievement-progress {
+ margin-top: 8px;
width: 100%;
- height: 6px;
- background: rgba(255, 255, 255, 0.1);
- border-radius: 3px;
+ height: 4px;
+ transition: all ease 0.2s;
+ background-color: rgba(255, 255, 255, 0.08);
+ border-radius: 4px;
overflow: hidden;
+
+ &::-webkit-progress-bar {
+ background-color: transparent;
+ border-radius: 4px;
+ }
+
+ &::-webkit-progress-value {
+ background-color: globals.$muted-color;
+ border-radius: 4px;
+ }
}
&__achievement-bar {
height: 100%;
- background: linear-gradient(90deg, #ffd700, #ffed4e);
- border-radius: 3px;
+ background-color: globals.$muted-color;
+ border-radius: 4px;
transition: width 0.3s ease;
+ position: relative;
}
&__achievement-count {
font-size: 12px;
- font-weight: 600;
+ font-weight: 500;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx
index 1c6baf17..748aba4f 100644
--- a/src/renderer/src/pages/library/library.tsx
+++ b/src/renderer/src/pages/library/library.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
-import { useLibrary, useAppDispatch, useUserDetails } from "@renderer/hooks";
+import { useLibrary, useAppDispatch } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
@@ -19,7 +19,6 @@ export default function Library() {
const [searchQuery, setSearchQuery] = useState("");
const dispatch = useAppDispatch();
const { t } = useTranslation("library");
- const { userDetails, fetchUserDetails } = useUserDetails();
useEffect(() => {
dispatch(setHeaderTitle(t("library")));
@@ -40,13 +39,6 @@ export default function Library() {
};
}, [dispatch, t, updateLibrary]);
- // Ensure we have the current user details available
- useEffect(() => {
- fetchUserDetails().catch(() => {
- /* ignore errors - fallback to local state */
- });
- }, [fetchUserDetails]);
-
const handleOnMouseEnterGameCard = () => {
// Optional: pause animations if needed
};
@@ -128,12 +120,6 @@ export default function Library() {
{hasGames && (
<>
-
- {`${t("Welcome", { defaultValue: "Welcome" })} ${
- userDetails?.displayName || "John Doe"
- }`}
-
-
Date: Wed, 22 Oct 2025 18:28:24 +0530
Subject: [PATCH 012/118] style: update compact view styles for game cards;
adjust grid layout and add button order
---
.../src/pages/library/library-game-card.scss | 7 +++++++
src/renderer/src/pages/library/library.scss | 15 +++++++++------
src/renderer/src/pages/library/library.tsx | 2 +-
src/renderer/src/pages/library/view-options.tsx | 14 +++++++-------
4 files changed, 24 insertions(+), 14 deletions(-)
diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss
index b763ae2c..d6bf8c6d 100644
--- a/src/renderer/src/pages/library/library-game-card.scss
+++ b/src/renderer/src/pages/library/library-game-card.scss
@@ -280,3 +280,10 @@
opacity: 0.5;
}
}
+
+/* Force fixed size for compact grid cells so cards render at 220x320 */
+.library__games-grid--compact .library-game-card__wrapper {
+ width: 215px;
+ height: 320px;
+ aspect-ratio: unset;
+}
diff --git a/src/renderer/src/pages/library/library.scss b/src/renderer/src/pages/library/library.scss
index 1e8038d9..9b660a45 100644
--- a/src/renderer/src/pages/library/library.scss
+++ b/src/renderer/src/pages/library/library.scss
@@ -161,26 +161,29 @@
// Compact view - smaller cards
&--compact {
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: repeat(auto-fill, 215px);
+ grid-auto-rows: 320px;
+ justify-content: start;
@container #{globals.$app-container} (min-width: 900px) {
- grid-template-columns: repeat(5, 1fr);
+ grid-template-columns: repeat(auto-fill, 215px);
}
@container #{globals.$app-container} (min-width: 1300px) {
- grid-template-columns: repeat(7, 1fr);
+ grid-template-columns: repeat(auto-fill, 215px);
}
+ /* keep same pattern for very large screens */
@container #{globals.$app-container} (min-width: 2000px) {
- grid-template-columns: repeat(9, 1fr);
+ grid-template-columns: repeat(auto-fill, 215px);
}
@container #{globals.$app-container} (min-width: 2600px) {
- grid-template-columns: repeat(12, 1fr);
+ grid-template-columns: repeat(auto-fill, 215px);
}
@container #{globals.$app-container} (min-width: 3000px) {
- grid-template-columns: repeat(16, 1fr);
+ grid-template-columns: repeat(auto-fill, 210px);
}
}
}
diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx
index 748aba4f..2d58bb08 100644
--- a/src/renderer/src/pages/library/library.tsx
+++ b/src/renderer/src/pages/library/library.tsx
@@ -14,7 +14,7 @@ import "./library.scss";
export default function Library() {
const { library, updateLibrary } = useLibrary();
- const [viewMode, setViewMode] = useState("grid");
+ const [viewMode, setViewMode] = useState("compact");
const [filterBy, setFilterBy] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const dispatch = useAppDispatch();
diff --git a/src/renderer/src/pages/library/view-options.tsx b/src/renderer/src/pages/library/view-options.tsx
index 788eefa9..662bab65 100644
--- a/src/renderer/src/pages/library/view-options.tsx
+++ b/src/renderer/src/pages/library/view-options.tsx
@@ -15,13 +15,6 @@ export function ViewOptions({ viewMode, onViewModeChange }: ViewOptionsProps) {
return (
-
onViewModeChange("grid")}
- title={t("grid_view")}
- >
-
-
onViewModeChange("compact")}
@@ -29,6 +22,13 @@ export function ViewOptions({ viewMode, onViewModeChange }: ViewOptionsProps) {
>
+
onViewModeChange("grid")}
+ title={t("grid_view")}
+ >
+
+
onViewModeChange("large")}
From 362d6b634e74b1577d0d8cdb8e04788a109c0f19 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 22 Oct 2025 21:13:05 +0300
Subject: [PATCH 013/118] feat: added reviews in profile and tabs
---
src/locales/en/translation.json | 5 +-
.../src/hooks/use-section-collapse.ts | 3 +
.../profile-content/profile-content.scss | 156 ++++-
.../profile-content/profile-content.tsx | 551 ++++++++++++++----
4 files changed, 610 insertions(+), 105 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 46bdb28c..e00501c0 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -676,7 +676,10 @@
"game_added_to_pinned": "Game added to pinned",
"karma": "Karma",
"karma_count": "karma",
- "karma_description": "Earned from positive likes on reviews"
+ "karma_description": "Earned from positive likes on reviews",
+ "user_reviews": "User's Reviews",
+ "delete_review": "Delete Review",
+ "loading_reviews": "Loading reviews..."
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
diff --git a/src/renderer/src/hooks/use-section-collapse.ts b/src/renderer/src/hooks/use-section-collapse.ts
index 7cd22224..59ffd6af 100644
--- a/src/renderer/src/hooks/use-section-collapse.ts
+++ b/src/renderer/src/hooks/use-section-collapse.ts
@@ -3,12 +3,14 @@ import { useState, useCallback } from "react";
interface SectionCollapseState {
pinned: boolean;
library: boolean;
+ reviews: boolean;
}
export function useSectionCollapse() {
const [collapseState, setCollapseState] = useState({
pinned: false,
library: false,
+ reviews: false,
});
const toggleSection = useCallback((section: keyof SectionCollapseState) => {
@@ -23,5 +25,6 @@ export function useSectionCollapse() {
toggleSection,
isPinnedCollapsed: collapseState.pinned,
isLibraryCollapsed: collapseState.library,
+ isReviewsCollapsed: collapseState.reviews,
};
}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index c3c71d9a..a9aad402 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -120,7 +120,7 @@
&--active {
color: white;
- border-bottom-color: #c9aa71;
+ border-bottom-color: white;
}
}
@@ -177,3 +177,157 @@
}
}
}
+
+ // Reviews minimal styles
+ .user-reviews__loading {
+ padding: calc(globals.$spacing-unit * 2);
+ color: rgba(255, 255, 255, 0.8);
+ }
+
+ .user-reviews__empty {
+ text-align: center;
+ padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
+ color: rgba(255, 255, 255, 0.6);
+ }
+
+ .user-reviews__list {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 2);
+ }
+
+ .user-reviews__review-item {
+ border-radius: 8px;
+ padding: calc(globals.$spacing-unit * 2);
+ }
+
+ .user-reviews__review-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: calc(globals.$spacing-unit * 1.5);
+ }
+
+ .user-reviews__review-game {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+ }
+
+ .user-reviews__game-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ object-fit: cover;
+ }
+
+ .user-reviews__game-info {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 0.25);
+ }
+
+ .user-reviews__game-title {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.9);
+ font-weight: 600;
+ cursor: pointer;
+ text-align: left;
+
+ &--clickable:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .user-reviews__review-date {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: globals.$small-font-size;
+ }
+
+ .user-reviews__review-score-stars {
+ display: flex;
+ gap: calc(globals.$spacing-unit * 0.5);
+ }
+
+ .user-reviews__review-star-container {
+ display: flex;
+ align-items: center;
+ }
+
+ .user-reviews__review-content {
+ color: rgba(255, 255, 255, 0.85);
+ line-height: 1.5;
+ }
+
+ .user-reviews__review-actions {
+ margin-top: calc(globals.$spacing-unit * 2);
+ padding-top: calc(globals.$spacing-unit);
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .user-reviews__review-votes {
+ display: flex;
+ gap: calc(globals.$spacing-unit);
+ }
+
+ .user-reviews__vote-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 6px;
+ padding: 6px 12px;
+ color: #ccc;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.2);
+ color: #ffffff;
+ }
+
+ &--active {
+ color: #ffffff;
+ border-color: rgba(255, 255, 255, 0.3);
+
+ svg {
+ fill: white;
+ }
+ }
+ }
+
+ .user-reviews__delete-review-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(244, 67, 54, 0.1);
+ border: 1px solid rgba(244, 67, 54, 0.3);
+ border-radius: 6px;
+ padding: 6px 10px;
+ color: #f44336;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(244, 67, 54, 0.2);
+ border-color: rgba(244, 67, 54, 0.4);
+ color: #ff7961;
+ }
+ }
+
+ .profile-content {
+ &__tab-panels {
+ display: block;
+ }
+
+ &__tab-panel[hidden] {
+ display: none;
+ }
+ }
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index d2f1f074..bb4e0025 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -1,9 +1,9 @@
import { userProfileContext } from "@renderer/context";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { ProfileHero } from "../profile-hero/profile-hero";
-import { useAppDispatch, useFormat } from "@renderer/hooks";
+import { useAppDispatch, useFormat, useDate, useUserDetails } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
-import { TelescopeIcon, ChevronRightIcon } from "@primer/octicons-react";
+import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { LockedProfile } from "./locked-profile";
import { ReportProfile } from "../report-profile/report-profile";
@@ -13,17 +13,56 @@ import { UserStatsBox } from "./user-stats-box";
import { UserKarmaBox } from "./user-karma-box";
import { UserLibraryGameCard } from "./user-library-game-card";
import { SortOptions } from "./sort-options";
-import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
+
import { motion, AnimatePresence } from "framer-motion";
+import { useNavigate } from "react-router-dom";
+import { buildGameDetailsPath } from "@renderer/helpers";
+import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
+import type { GameShop } from "@types";
+import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
import {
- sectionVariants,
- chevronVariants,
+ // removed: sectionVariants,
+ // removed: chevronVariants,
GAME_STATS_ANIMATION_DURATION_IN_MS,
} from "./profile-animations";
import "./profile-content.scss";
+
type SortOption = "playtime" | "achievementCount" | "playedRecently";
+interface UserReview {
+ id: string;
+ reviewHtml: string;
+ score: number;
+ upvotes: number;
+ downvotes: number;
+ hasUpvoted: boolean;
+ hasDownvoted: boolean;
+ createdAt: string;
+ updatedAt: string;
+ user: {
+ id: string;
+ };
+ game: {
+ title: string;
+ iconUrl: string;
+ objectId: string;
+ shop: GameShop;
+ };
+}
+
+interface UserReviewsResponse {
+ totalCount: number;
+ reviews: UserReview[];
+}
+
+const getScoreColorClass = (score: number) => {
+ if (score >= 1 && score <= 2) return "game-details__review-score--red";
+ if (score === 3) return "game-details__review-score--yellow";
+ if (score >= 4 && score <= 5) return "game-details__review-score--green";
+ return "";
+};
+
export function ProfileContent() {
const {
userProfile,
@@ -33,11 +72,23 @@ export function ProfileContent() {
pinnedGames,
getUserLibraryGames,
} = useContext(userProfileContext);
+ const { userDetails } = useUserDetails();
+ const { formatDistance } = useDate();
+ const navigate = useNavigate();
const [statsIndex, setStatsIndex] = useState(0);
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
const [sortBy, setSortBy] = useState("playedRecently");
const statsAnimation = useRef(-1);
- const { toggleSection, isPinnedCollapsed } = useSectionCollapse();
+
+ const [activeTab, setActiveTab] = useState<"library" | "reviews">("library");
+
+ // User reviews state
+ const [reviews, setReviews] = useState([]);
+ const [reviewsTotalCount, setReviewsTotalCount] = useState(0);
+ const [isLoadingReviews, setIsLoadingReviews] = useState(false);
+ const [votingReviews, setVotingReviews] = useState>(new Set());
+ const [deleteModalVisible, setDeleteModalVisible] = useState(false);
+ const [reviewToDelete, setReviewToDelete] = useState(null);
const dispatch = useAppDispatch();
@@ -57,6 +108,152 @@ export function ProfileContent() {
}
}, [sortBy, getUserLibraryGames, userProfile]);
+ useEffect(() => {
+ if (userProfile?.id) {
+ fetchUserReviews();
+ }
+ }, [userProfile?.id]);
+
+ const fetchUserReviews = async () => {
+ if (!userProfile?.id) return;
+
+ setIsLoadingReviews(true);
+ try {
+ const response = await window.electron.hydraApi.get(
+ `/users/${userProfile.id}/reviews`,
+ { needsAuth: true }
+ );
+ setReviews(response.reviews);
+ setReviewsTotalCount(response.totalCount);
+ } catch (error) {
+ console.error("Failed to fetch user reviews:", error);
+ } finally {
+ setIsLoadingReviews(false);
+ }
+ };
+
+ const handleDeleteReview = async (reviewId: string) => {
+ try {
+ const reviewToDeleteObj = reviews.find(review => review.id === reviewId);
+ if (!reviewToDeleteObj) return;
+
+ await window.electron.hydraApi.delete(
+ `/games/${reviewToDeleteObj.game.shop}/${reviewToDeleteObj.game.objectId}/reviews/${reviewId}`
+ );
+ // Remove the review from the local state
+ setReviews(prev => prev.filter(review => review.id !== reviewId));
+ setReviewsTotalCount(prev => prev - 1);
+ } catch (error) {
+ console.error("Failed to delete review:", error);
+ }
+ };
+
+ const handleDeleteClick = (reviewId: string) => {
+ setReviewToDelete(reviewId);
+ setDeleteModalVisible(true);
+ };
+
+ const handleDeleteConfirm = () => {
+ if (reviewToDelete) {
+ handleDeleteReview(reviewToDelete);
+ setReviewToDelete(null);
+ }
+ };
+
+ const handleDeleteCancel = () => {
+ setDeleteModalVisible(false);
+ setReviewToDelete(null);
+ };
+
+ const handleVoteReview = async (reviewId: string, isUpvote: boolean) => {
+ if (votingReviews.has(reviewId)) return;
+
+ setVotingReviews(prev => new Set(prev).add(reviewId));
+
+ const review = reviews.find(r => r.id === reviewId);
+ if (!review) return;
+
+ const wasUpvoted = review.hasUpvoted;
+ const wasDownvoted = review.hasDownvoted;
+
+ // Optimistic update
+ setReviews(prev => prev.map(r => {
+ if (r.id !== reviewId) return r;
+
+ let newUpvotes = r.upvotes;
+ let newDownvotes = r.downvotes;
+ let newHasUpvoted = r.hasUpvoted;
+ let newHasDownvoted = r.hasDownvoted;
+
+ if (isUpvote) {
+ if (wasUpvoted) {
+ // Remove upvote
+ newUpvotes--;
+ newHasUpvoted = false;
+ } else {
+ // Add upvote
+ newUpvotes++;
+ newHasUpvoted = true;
+ if (wasDownvoted) {
+ // Remove downvote if it was downvoted
+ newDownvotes--;
+ newHasDownvoted = false;
+ }
+ }
+ } else {
+ if (wasDownvoted) {
+ // Remove downvote
+ newDownvotes--;
+ newHasDownvoted = false;
+ } else {
+ // Add downvote
+ newDownvotes++;
+ newHasDownvoted = true;
+ if (wasUpvoted) {
+ // Remove upvote if it was upvoted
+ newUpvotes--;
+ newHasUpvoted = false;
+ }
+ }
+ }
+
+ return {
+ ...r,
+ upvotes: newUpvotes,
+ downvotes: newDownvotes,
+ hasUpvoted: newHasUpvoted,
+ hasDownvoted: newHasDownvoted,
+ };
+ }));
+
+ try {
+ const endpoint = isUpvote ? 'upvote' : 'downvote';
+ await window.electron.hydraApi.put(
+ `/games/${review.game.shop}/${review.game.objectId}/reviews/${reviewId}/${endpoint}`
+ );
+ } catch (error) {
+ console.error("Failed to vote on review:", error);
+
+ // Rollback optimistic update on error
+ setReviews(prev => prev.map(r => {
+ if (r.id !== reviewId) return r;
+ return {
+ ...r,
+ upvotes: review.upvotes,
+ downvotes: review.downvotes,
+ hasUpvoted: review.hasUpvoted,
+ hasDownvoted: review.hasDownvoted,
+ };
+ }));
+ } finally {
+ setVotingReviews(prev => {
+ const newSet = new Set(prev);
+ newSet.delete(reviewId);
+ return newSet;
+ });
+ }
+ };
+
const handleOnMouseEnterGameCard = () => {
setIsAnimationRunning(false);
};
@@ -113,112 +310,253 @@ export function ProfileContent() {
return (
- {hasAnyGames && (
-
- )}
+
+ setActiveTab("library")}
+ >
+ {t("library")}
+
+ setActiveTab("reviews")}
+ >
+ {t("user_reviews")}
+
+
- {!hasAnyGames && (
-
-
-
-
-
{t("no_recent_activity_title")}
- {isMe &&
{t("no_recent_activity_description")}
}
-
- )}
+
+
+ {hasAnyGames && (
+
+ )}
- {hasAnyGames && (
-
- {hasPinnedGames && (
-
-
-
- toggleSection("pinned")}
- aria-label={
- isPinnedCollapsed
- ? "Expand pinned section"
- : "Collapse pinned section"
- }
- >
-
-
-
-
-
{t("pinned")}
-
- {pinnedGames.length}
-
-
+ {!hasAnyGames && (
+
+
+
-
-
- {!isPinnedCollapsed && (
-
-
- {pinnedGames?.map((game) => (
- -
-
-
- ))}
-
-
- )}
-
+
{t("no_recent_activity_title")}
+ {isMe &&
{t("no_recent_activity_description")}
}
)}
- {hasGames && (
+ {hasAnyGames && (
-
-
-
{t("library")}
- {userStats && (
-
- {numberFormatter.format(userStats.libraryCount)}
-
- )}
-
-
+ {hasPinnedGames && (
+
+
+
+ {/* removed collapse button */}
+
{t("pinned")}
+
+ {pinnedGames.length}
+
+
+
-
- {libraryGames?.map((game) => (
- -
-
-
- ))}
-
+ {/* render pinned games unconditionally */}
+
+ {pinnedGames?.map((game) => (
+ -
+
+
+ ))}
+
+
+ )}
+
+ {hasGames && (
+
+
+
+
{t("library")}
+ {userStats && (
+
+ {numberFormatter.format(userStats.libraryCount)}
+
+ )}
+
+
+
+
+ {libraryGames?.map((game) => (
+ -
+
+
+ ))}
+
+
+ )}
)}
- )}
+
+
+
+
+
+ {/* removed collapse button */}
+
{t("user_reviews")}
+ {reviewsTotalCount > 0 && (
+
+ {reviewsTotalCount}
+
+ )}
+
+
+
+ {/* render reviews content unconditionally */}
+ {isLoadingReviews ? (
+
{t("loading_reviews")}
+ ) : reviews.length === 0 ? (
+
+
{t("no_reviews", "No reviews yet")}
+
+ ) : (
+
+ {reviews.map((review) => {
+ const isOwnReview = userDetails?.id === review.user.id;
+
+ return (
+
+
+
+

+
+
navigate(buildGameDetailsPath(review.game))}
+ >
+ {review.game.title}
+
+
+ {formatDistance(new Date(review.createdAt), new Date(), { addSuffix: true })}
+
+
+
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
handleVoteReview(review.id, true)}
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.upvotes}
+
+
+
+
+
handleVoteReview(review.id, false)}
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.downvotes}
+
+
+
+
+
+ {isOwnReview && (
+
handleDeleteClick(review.id)}
+ title={t("delete_review")}
+ >
+
+ {t("delete_review")}
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+
{shouldShowRightContent && (
@@ -230,6 +568,12 @@ export function ProfileContent() {
)}
+
+
);
}, [
@@ -242,9 +586,10 @@ export function ProfileContent() {
statsIndex,
libraryGames,
pinnedGames,
- isPinnedCollapsed,
- toggleSection,
+ // removed isPinnedCollapsed,
+ // removed toggleSection,
sortBy,
+ activeTab,
]);
return (
From 035f6e8d24c478d6a6df1df35f669ccb1b6957c4 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 22 Oct 2025 22:05:05 +0300
Subject: [PATCH 014/118] ci: formatting
---
.../profile-content/profile-content.scss | 288 +++++++++---------
.../profile-content/profile-content.tsx | 178 ++++++-----
2 files changed, 247 insertions(+), 219 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index a9aad402..4cdea61b 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -178,156 +178,156 @@
}
}
- // Reviews minimal styles
- .user-reviews__loading {
- padding: calc(globals.$spacing-unit * 2);
- color: rgba(255, 255, 255, 0.8);
+// Reviews minimal styles
+.user-reviews__loading {
+ padding: calc(globals.$spacing-unit * 2);
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.user-reviews__empty {
+ text-align: center;
+ padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.user-reviews__list {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 2);
+}
+
+.user-reviews__review-item {
+ border-radius: 8px;
+ padding: calc(globals.$spacing-unit * 2);
+}
+
+.user-reviews__review-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: calc(globals.$spacing-unit * 1.5);
+}
+
+.user-reviews__review-game {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit);
+}
+
+.user-reviews__game-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ object-fit: cover;
+}
+
+.user-reviews__game-info {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 0.25);
+}
+
+.user-reviews__game-title {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.9);
+ font-weight: 600;
+ cursor: pointer;
+ text-align: left;
+
+ &--clickable:hover {
+ text-decoration: underline;
+ }
+}
+
+.user-reviews__review-date {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: globals.$small-font-size;
+}
+
+.user-reviews__review-score-stars {
+ display: flex;
+ gap: calc(globals.$spacing-unit * 0.5);
+}
+
+.user-reviews__review-star-container {
+ display: flex;
+ align-items: center;
+}
+
+.user-reviews__review-content {
+ color: rgba(255, 255, 255, 0.85);
+ line-height: 1.5;
+}
+
+.user-reviews__review-actions {
+ margin-top: calc(globals.$spacing-unit * 2);
+ padding-top: calc(globals.$spacing-unit);
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.user-reviews__review-votes {
+ display: flex;
+ gap: calc(globals.$spacing-unit);
+}
+
+.user-reviews__vote-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 6px;
+ padding: 6px 12px;
+ color: #ccc;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.2);
+ color: #ffffff;
}
- .user-reviews__empty {
- text-align: center;
- padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
- color: rgba(255, 255, 255, 0.6);
- }
+ &--active {
+ color: #ffffff;
+ border-color: rgba(255, 255, 255, 0.3);
- .user-reviews__list {
- display: flex;
- flex-direction: column;
- gap: calc(globals.$spacing-unit * 2);
- }
-
- .user-reviews__review-item {
- border-radius: 8px;
- padding: calc(globals.$spacing-unit * 2);
- }
-
- .user-reviews__review-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: calc(globals.$spacing-unit * 1.5);
- }
-
- .user-reviews__review-game {
- display: flex;
- align-items: center;
- gap: calc(globals.$spacing-unit);
- }
-
- .user-reviews__game-icon {
- width: 40px;
- height: 40px;
- border-radius: 8px;
- object-fit: cover;
- }
-
- .user-reviews__game-info {
- display: flex;
- flex-direction: column;
- gap: calc(globals.$spacing-unit * 0.25);
- }
-
- .user-reviews__game-title {
- background: none;
- border: none;
- color: rgba(255, 255, 255, 0.9);
- font-weight: 600;
- cursor: pointer;
- text-align: left;
-
- &--clickable:hover {
- text-decoration: underline;
+ svg {
+ fill: white;
}
}
+}
- .user-reviews__review-date {
- color: rgba(255, 255, 255, 0.6);
- font-size: globals.$small-font-size;
+.user-reviews__delete-review-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(244, 67, 54, 0.1);
+ border: 1px solid rgba(244, 67, 54, 0.3);
+ border-radius: 6px;
+ padding: 6px 10px;
+ color: #f44336;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(244, 67, 54, 0.2);
+ border-color: rgba(244, 67, 54, 0.4);
+ color: #ff7961;
+ }
+}
+
+.profile-content {
+ &__tab-panels {
+ display: block;
}
- .user-reviews__review-score-stars {
- display: flex;
- gap: calc(globals.$spacing-unit * 0.5);
- }
-
- .user-reviews__review-star-container {
- display: flex;
- align-items: center;
- }
-
- .user-reviews__review-content {
- color: rgba(255, 255, 255, 0.85);
- line-height: 1.5;
- }
-
- .user-reviews__review-actions {
- margin-top: calc(globals.$spacing-unit * 2);
- padding-top: calc(globals.$spacing-unit);
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .user-reviews__review-votes {
- display: flex;
- gap: calc(globals.$spacing-unit);
- }
-
- .user-reviews__vote-button {
- display: flex;
- align-items: center;
- gap: 6px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 6px;
- padding: 6px 12px;
- color: #ccc;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
-
- &:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.2);
- color: #ffffff;
- }
-
- &--active {
- color: #ffffff;
- border-color: rgba(255, 255, 255, 0.3);
-
- svg {
- fill: white;
- }
- }
- }
-
- .user-reviews__delete-review-button {
- display: flex;
- align-items: center;
- gap: 6px;
- background: rgba(244, 67, 54, 0.1);
- border: 1px solid rgba(244, 67, 54, 0.3);
- border-radius: 6px;
- padding: 6px 10px;
- color: #f44336;
- cursor: pointer;
- transition: all 0.2s ease;
-
- &:hover {
- background: rgba(244, 67, 54, 0.2);
- border-color: rgba(244, 67, 54, 0.4);
- color: #ff7961;
- }
- }
-
- .profile-content {
- &__tab-panels {
- display: block;
- }
-
- &__tab-panel[hidden] {
- display: none;
- }
+ &__tab-panel[hidden] {
+ display: none;
}
+}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index bb4e0025..d245aa5e 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -1,7 +1,12 @@
import { userProfileContext } from "@renderer/context";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { ProfileHero } from "../profile-hero/profile-hero";
-import { useAppDispatch, useFormat, useDate, useUserDetails } from "@renderer/hooks";
+import {
+ useAppDispatch,
+ useFormat,
+ useDate,
+ useUserDetails,
+} from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
@@ -27,7 +32,6 @@ import {
} from "./profile-animations";
import "./profile-content.scss";
-
type SortOption = "playtime" | "achievementCount" | "playedRecently";
interface UserReview {
@@ -79,7 +83,7 @@ export function ProfileContent() {
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
const [sortBy, setSortBy] = useState("playedRecently");
const statsAnimation = useRef(-1);
-
+
const [activeTab, setActiveTab] = useState<"library" | "reviews">("library");
// User reviews state
@@ -116,7 +120,7 @@ export function ProfileContent() {
const fetchUserReviews = async () => {
if (!userProfile?.id) return;
-
+
setIsLoadingReviews(true);
try {
const response = await window.electron.hydraApi.get(
@@ -134,15 +138,17 @@ export function ProfileContent() {
const handleDeleteReview = async (reviewId: string) => {
try {
- const reviewToDeleteObj = reviews.find(review => review.id === reviewId);
+ const reviewToDeleteObj = reviews.find(
+ (review) => review.id === reviewId
+ );
if (!reviewToDeleteObj) return;
-
+
await window.electron.hydraApi.delete(
`/games/${reviewToDeleteObj.game.shop}/${reviewToDeleteObj.game.objectId}/reviews/${reviewId}`
);
// Remove the review from the local state
- setReviews(prev => prev.filter(review => review.id !== reviewId));
- setReviewsTotalCount(prev => prev - 1);
+ setReviews((prev) => prev.filter((review) => review.id !== reviewId));
+ setReviewsTotalCount((prev) => prev - 1);
} catch (error) {
console.error("Failed to delete review:", error);
}
@@ -168,85 +174,89 @@ export function ProfileContent() {
const handleVoteReview = async (reviewId: string, isUpvote: boolean) => {
if (votingReviews.has(reviewId)) return;
- setVotingReviews(prev => new Set(prev).add(reviewId));
+ setVotingReviews((prev) => new Set(prev).add(reviewId));
- const review = reviews.find(r => r.id === reviewId);
+ const review = reviews.find((r) => r.id === reviewId);
if (!review) return;
const wasUpvoted = review.hasUpvoted;
const wasDownvoted = review.hasDownvoted;
// Optimistic update
- setReviews(prev => prev.map(r => {
- if (r.id !== reviewId) return r;
+ setReviews((prev) =>
+ prev.map((r) => {
+ if (r.id !== reviewId) return r;
- let newUpvotes = r.upvotes;
- let newDownvotes = r.downvotes;
- let newHasUpvoted = r.hasUpvoted;
- let newHasDownvoted = r.hasDownvoted;
+ let newUpvotes = r.upvotes;
+ let newDownvotes = r.downvotes;
+ let newHasUpvoted = r.hasUpvoted;
+ let newHasDownvoted = r.hasDownvoted;
- if (isUpvote) {
- if (wasUpvoted) {
- // Remove upvote
- newUpvotes--;
- newHasUpvoted = false;
- } else {
- // Add upvote
- newUpvotes++;
- newHasUpvoted = true;
- if (wasDownvoted) {
- // Remove downvote if it was downvoted
- newDownvotes--;
- newHasDownvoted = false;
- }
- }
- } else {
- if (wasDownvoted) {
- // Remove downvote
- newDownvotes--;
- newHasDownvoted = false;
- } else {
- // Add downvote
- newDownvotes++;
- newHasDownvoted = true;
+ if (isUpvote) {
if (wasUpvoted) {
- // Remove upvote if it was upvoted
+ // Remove upvote
newUpvotes--;
newHasUpvoted = false;
+ } else {
+ // Add upvote
+ newUpvotes++;
+ newHasUpvoted = true;
+ if (wasDownvoted) {
+ // Remove downvote if it was downvoted
+ newDownvotes--;
+ newHasDownvoted = false;
+ }
+ }
+ } else {
+ if (wasDownvoted) {
+ // Remove downvote
+ newDownvotes--;
+ newHasDownvoted = false;
+ } else {
+ // Add downvote
+ newDownvotes++;
+ newHasDownvoted = true;
+ if (wasUpvoted) {
+ // Remove upvote if it was upvoted
+ newUpvotes--;
+ newHasUpvoted = false;
+ }
}
}
- }
- return {
- ...r,
- upvotes: newUpvotes,
- downvotes: newDownvotes,
- hasUpvoted: newHasUpvoted,
- hasDownvoted: newHasDownvoted,
- };
- }));
+ return {
+ ...r,
+ upvotes: newUpvotes,
+ downvotes: newDownvotes,
+ hasUpvoted: newHasUpvoted,
+ hasDownvoted: newHasDownvoted,
+ };
+ })
+ );
try {
- const endpoint = isUpvote ? 'upvote' : 'downvote';
+ const endpoint = isUpvote ? "upvote" : "downvote";
await window.electron.hydraApi.put(
`/games/${review.game.shop}/${review.game.objectId}/reviews/${reviewId}/${endpoint}`
);
} catch (error) {
console.error("Failed to vote on review:", error);
-
+
// Rollback optimistic update on error
- setReviews(prev => prev.map(r => {
- if (r.id !== reviewId) return r;
- return {
- ...r,
- upvotes: review.upvotes,
- downvotes: review.downvotes,
- hasUpvoted: review.hasUpvoted,
- hasDownvoted: review.hasDownvoted,
- };
- }));
+ setReviews((prev) =>
+ prev.map((r) => {
+ if (r.id !== reviewId) return r;
+ return {
+ ...r,
+ upvotes: review.upvotes,
+ downvotes: review.downvotes,
+ hasUpvoted: review.hasUpvoted,
+ hasDownvoted: review.hasDownvoted,
+ };
+ })
+ );
} finally {
- setVotingReviews(prev => {
+ setVotingReviews((prev) => {
const newSet = new Set(prev);
newSet.delete(reviewId);
return newSet;
@@ -364,10 +374,7 @@ export function ProfileContent() {
{/* render pinned games unconditionally */}
{pinnedGames?.map((game) => (
- -
+
-
{t("loading_reviews")}
+
+ {t("loading_reviews")}
+
) : reviews.length === 0 ? (
{t("no_reviews", "No reviews yet")}
@@ -461,22 +470,35 @@ export function ProfileContent() {
navigate(buildGameDetailsPath(review.game))}
+ onClick={() =>
+ navigate(buildGameDetailsPath(review.game))
+ }
>
{review.game.title}
- {formatDistance(new Date(review.createdAt), new Date(), { addSuffix: true })}
+ {formatDistance(
+ new Date(review.createdAt),
+ new Date(),
+ { addSuffix: true }
+ )}
{Array.from({ length: 5 }, (_, index) => (
-
+
handleVoteReview(review.id, true)}
+ onClick={() =>
+ handleVoteReview(review.id, true)
+ }
disabled={votingReviews.has(review.id)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
@@ -518,7 +544,9 @@ export function ProfileContent() {
handleVoteReview(review.id, false)}
+ onClick={() =>
+ handleVoteReview(review.id, false)
+ }
disabled={votingReviews.has(review.id)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
From f5470b29c065bf110c5fcefd408ae3ca5ed86330 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Thu, 23 Oct 2025 10:58:31 +0530
Subject: [PATCH 015/118] style: adjust hover effects and dimensions for game
cards; refine context menu actions
---
.../game-context-menu/game-context-menu.tsx | 4 +-
.../library/library-game-card-large.scss | 16 ++++----
.../pages/library/library-game-card-large.tsx | 39 ++++++++++++++-----
.../src/pages/library/library-game-card.scss | 6 +--
4 files changed, 44 insertions(+), 21 deletions(-)
diff --git a/src/renderer/src/components/game-context-menu/game-context-menu.tsx b/src/renderer/src/components/game-context-menu/game-context-menu.tsx
index 694012b7..782857a9 100644
--- a/src/renderer/src/components/game-context-menu/game-context-menu.tsx
+++ b/src/renderer/src/components/game-context-menu/game-context-menu.tsx
@@ -70,8 +70,10 @@ export function GameContextMenu({
onClick: () => {
if (isGameRunning) {
void handleCloseGame();
- } else {
+ } else if (canPlay) {
void handlePlayGame();
+ } else {
+ handleOpenDownloadOptions();
}
},
disabled: isDeleting,
diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss
index 0be1e907..700c7d0b 100644
--- a/src/renderer/src/pages/library/library-game-card-large.scss
+++ b/src/renderer/src/pages/library/library-game-card-large.scss
@@ -40,8 +40,7 @@
}
&:hover {
- transform: scale(1.05);
- transform: translateY(-2px);
+ transform: scale(1.01);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.1);
}
@@ -66,9 +65,9 @@
bottom: 0;
background: linear-gradient(
0deg,
- rgba(0, 0, 0, 1) 0%,
+ rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.2) 50%,
- rgba(0, 0, 0, 0.5) 100%
+ rgba(0, 0, 0, 0.3) 100%
);
z-index: 1;
}
@@ -154,9 +153,9 @@
&__info-bar {
display: flex;
- justify-content: space-between;
- align-items: flex-end;
+ align-items: center;
gap: calc(globals.$spacing-unit * 2);
+ justify-content: flex-end;
}
&__playtime {
@@ -187,7 +186,8 @@
flex-direction: column;
gap: 6px;
padding: 6px 12px;
- width: 100%;
+ flex: 1 1 auto;
+ min-width: 0;
}
&__achievement-header {
@@ -261,7 +261,7 @@
font-size: 14px;
font-weight: 600;
transition: all ease 0.2s;
- flex-shrink: 0;
+ flex: 0 0 auto;
&:hover {
background: rgba(255, 255, 255, 0.15);
diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx
index cc1c659d..3c9285e9 100644
--- a/src/renderer/src/pages/library/library-game-card-large.tsx
+++ b/src/renderer/src/pages/library/library-game-card-large.tsx
@@ -9,9 +9,11 @@ import {
AlertFillIcon,
ThreeBarsIcon,
TrophyIcon,
+ XIcon,
} from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { useCallback, useState } from "react";
+import { useGameActions } from "@renderer/components/game-context-menu/use-game-actions";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { GameContextMenu } from "@renderer/components";
import "./library-game-card-large.scss";
@@ -66,18 +68,32 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
navigate(buildGameDetailsPath(game));
};
+ const {
+ handlePlayGame,
+ handleOpenDownloadOptions,
+ handleCloseGame,
+ isGameRunning,
+ } = useGameActions(game);
+
const handleActionClick = async (e: React.MouseEvent) => {
e.stopPropagation();
- if (game.executablePath) {
- window.electron.openGame(
- game.shop,
- game.objectId,
- game.executablePath,
- game.launchOptions
- );
- } else {
- navigate(buildGameDetailsPath(game));
+ if (isGameRunning) {
+ try {
+ await handleCloseGame();
+ } catch (e) {
+ void e;
+ }
+ return;
+ }
+ try {
+ await handlePlayGame();
+ } catch (err) {
+ try {
+ handleOpenDownloadOptions();
+ } catch (e) {
+ void e;
+ }
}
};
@@ -215,6 +231,11 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
/>
{t("downloading")}
>
+ ) : isGameRunning ? (
+ <>
+
+ {t("close")}
+ >
) : game.executablePath ? (
<>
diff --git a/src/renderer/src/pages/library/library-game-card.scss b/src/renderer/src/pages/library/library-game-card.scss
index d6bf8c6d..aa957d12 100644
--- a/src/renderer/src/pages/library/library-game-card.scss
+++ b/src/renderer/src/pages/library/library-game-card.scss
@@ -37,7 +37,7 @@
}
&:hover {
- transform: scale(1.05);
+ transform: scale(1.02);
}
&:hover::before {
@@ -215,8 +215,8 @@
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
- width: 32px;
- height: 32px;
+ width: 28px;
+ height: 28px;
display: flex;
align-items: center;
justify-content: center;
From acf8f340dd171c64b819c95c37045acf1fd314ae Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 10:33:29 +0300
Subject: [PATCH 016/118] ci: review message ui change and fix loading reviews
positioning
---
.../profile-content/profile-content.scss | 22 +++++++-
.../profile-content/profile-content.tsx | 51 ++++++++++---------
2 files changed, 48 insertions(+), 25 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index 4cdea61b..21acfa47 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -182,6 +182,10 @@
.user-reviews__loading {
padding: calc(globals.$spacing-unit * 2);
color: rgba(255, 255, 255, 0.8);
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.user-reviews__empty {
@@ -208,6 +212,14 @@
margin-bottom: calc(globals.$spacing-unit * 1.5);
}
+.user-reviews__review-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: calc(globals.$spacing-unit * 1.5);
+ margin-bottom: calc(globals.$spacing-unit * 1.5);
+}
+
.user-reviews__review-game {
display: flex;
align-items: center;
@@ -215,8 +227,8 @@
}
.user-reviews__game-icon {
- width: 40px;
- height: 40px;
+ width: 24px;
+ height: 24px;
border-radius: 8px;
object-fit: cover;
}
@@ -227,6 +239,12 @@
gap: calc(globals.$spacing-unit * 0.25);
}
+.user-reviews__game-details {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit * 0.5);
+}
+
.user-reviews__game-title {
background: none;
border: none;
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index d245aa5e..97d9b1a9 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -461,29 +461,12 @@ export function ProfileContent() {
transition={{ duration: 0.3 }}
>
-
-

-
-
- navigate(buildGameDetailsPath(review.game))
- }
- >
- {review.game.title}
-
-
- {formatDistance(
- new Date(review.createdAt),
- new Date(),
- { addSuffix: true }
- )}
-
-
+
+ {formatDistance(
+ new Date(review.createdAt),
+ new Date(),
+ { addSuffix: true }
+ )}
@@ -517,6 +500,28 @@ export function ProfileContent() {
}}
/>
+
+
+
+
+

+
+ navigate(buildGameDetailsPath(review.game))
+ }
+ >
+ {review.game.title}
+
+
+
+
+
+
Date: Thu, 23 Oct 2025 10:34:15 +0300
Subject: [PATCH 017/118] ci: formatting
---
.../src/pages/profile/profile-content/profile-content.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 97d9b1a9..b37f7517 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -512,7 +512,9 @@ export function ProfileContent() {
- navigate(buildGameDetailsPath(review.game))
+ navigate(
+ buildGameDetailsPath(review.game)
+ )
}
>
{review.game.title}
From ab27f3295e9554f744740f2b5ddf2021a353fcb8 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 11:26:29 +0300
Subject: [PATCH 018/118] fix: duplicate selectors and if statements should not
be the only statement
---
.../profile-content/profile-content.scss | 60 +++++++++----------
.../profile-content/profile-content.tsx | 24 ++++----
2 files changed, 40 insertions(+), 44 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index 21acfa47..06d1e05f 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -152,30 +152,38 @@
grid-template-columns: repeat(12, 1fr);
}
- &--drag-over {
- background: rgba(255, 255, 255, 0.05);
- border: 2px dashed rgba(255, 255, 255, 0.3);
- position: relative;
- transition: all ease 0.2s;
+ &--drag-over {
+ background: rgba(255, 255, 255, 0.05);
+ border: 2px dashed rgba(255, 255, 255, 0.3);
+ position: relative;
+ transition: all ease 0.2s;
- &::before {
- content: "Drop here to " attr(data-action);
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- color: globals.$muted-color;
- font-size: 14px;
- font-weight: 500;
- z-index: 10;
- pointer-events: none;
- background: rgba(0, 0, 0, 0.8);
- padding: 8px 16px;
- border-radius: 4px;
- backdrop-filter: blur(10px);
- }
+ &::before {
+ content: "Drop here to " attr(data-action);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: globals.$muted-color;
+ font-size: 14px;
+ font-weight: 500;
+ z-index: 10;
+ pointer-events: none;
+ background: rgba(0, 0, 0, 0.8);
+ padding: 8px 16px;
+ border-radius: 4px;
+ backdrop-filter: blur(10px);
}
}
+
+ &__tab-panels {
+ display: block;
+ }
+
+ &__tab-panel[hidden] {
+ display: none;
+ }
+}
}
// Reviews minimal styles
@@ -339,13 +347,3 @@
color: #ff7961;
}
}
-
-.profile-content {
- &__tab-panels {
- display: block;
- }
-
- &__tab-panel[hidden] {
- display: none;
- }
-}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index b37f7517..642ab25d 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -207,20 +207,18 @@ export function ProfileContent() {
newHasDownvoted = false;
}
}
+ } else if (wasDownvoted) {
+ // Remove downvote
+ newDownvotes--;
+ newHasDownvoted = false;
} else {
- if (wasDownvoted) {
- // Remove downvote
- newDownvotes--;
- newHasDownvoted = false;
- } else {
- // Add downvote
- newDownvotes++;
- newHasDownvoted = true;
- if (wasUpvoted) {
- // Remove upvote if it was upvoted
- newUpvotes--;
- newHasUpvoted = false;
- }
+ // Add downvote
+ newDownvotes++;
+ newHasDownvoted = true;
+ if (wasUpvoted) {
+ // Remove upvote if it was upvoted
+ newUpvotes--;
+ newHasUpvoted = false;
}
}
From 65f83399f5fc63fea78be4dd55b3d109bd8f2c94 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 11:29:27 +0300
Subject: [PATCH 019/118] ci: merge
---
.../profile-content/profile-content.scss | 58 +++++++++----------
1 file changed, 29 insertions(+), 29 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index 06d1e05f..45a7f119 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -152,38 +152,38 @@
grid-template-columns: repeat(12, 1fr);
}
- &--drag-over {
- background: rgba(255, 255, 255, 0.05);
- border: 2px dashed rgba(255, 255, 255, 0.3);
- position: relative;
- transition: all ease 0.2s;
+ &--drag-over {
+ background: rgba(255, 255, 255, 0.05);
+ border: 2px dashed rgba(255, 255, 255, 0.3);
+ position: relative;
+ transition: all ease 0.2s;
- &::before {
- content: "Drop here to " attr(data-action);
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- color: globals.$muted-color;
- font-size: 14px;
- font-weight: 500;
- z-index: 10;
- pointer-events: none;
- background: rgba(0, 0, 0, 0.8);
- padding: 8px 16px;
- border-radius: 4px;
- backdrop-filter: blur(10px);
+ &::before {
+ content: "Drop here to " attr(data-action);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: globals.$muted-color;
+ font-size: 14px;
+ font-weight: 500;
+ z-index: 10;
+ pointer-events: none;
+ background: rgba(0, 0, 0, 0.8);
+ padding: 8px 16px;
+ border-radius: 4px;
+ backdrop-filter: blur(10px);
+ }
+ }
+
+ &__tab-panels {
+ display: block;
+ }
+
+ &__tab-panel[hidden] {
+ display: none;
}
}
-
- &__tab-panels {
- display: block;
- }
-
- &__tab-panel[hidden] {
- display: none;
- }
-}
}
// Reviews minimal styles
From 3ff20417d54caf6fbabdef7bdd87fe8588e9dbf8 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 11:37:50 +0300
Subject: [PATCH 020/118] fix: extracted ternary operation
---
.../profile-content/profile-content.tsx | 34 ++++++++++++-------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 642ab25d..679556db 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -437,17 +437,26 @@ export function ProfileContent() {
{/* render reviews content unconditionally */}
- {isLoadingReviews ? (
-
- {t("loading_reviews")}
-
- ) : reviews.length === 0 ? (
-
-
{t("no_reviews", "No reviews yet")}
-
- ) : (
-
- {reviews.map((review) => {
+ {(() => {
+ if (isLoadingReviews) {
+ return (
+
+ {t("loading_reviews")}
+
+ );
+ }
+
+ if (reviews.length === 0) {
+ return (
+
+
{t("no_reviews", "No reviews yet")}
+
+ );
+ }
+
+ return (
+
+ {reviews.map((review) => {
const isOwnReview = userDetails?.id === review.user.id;
return (
@@ -586,7 +595,8 @@ export function ProfileContent() {
);
})}
- )}
+ );
+ })()}
From f5399774316da2250c7049c3049f2900406f4e58 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 11:53:35 +0300
Subject: [PATCH 021/118] fix: refactoring functions to prevent nesting more
than 4 lvls
---
.../profile-content/profile-content.tsx | 294 +++++++++---------
1 file changed, 143 insertions(+), 151 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 679556db..f5511a1e 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -437,166 +437,158 @@ export function ProfileContent() {
{/* render reviews content unconditionally */}
- {(() => {
- if (isLoadingReviews) {
- return (
-
- {t("loading_reviews")}
-
- );
- }
-
- if (reviews.length === 0) {
- return (
-
-
{t("no_reviews", "No reviews yet")}
-
- );
- }
-
- return (
-
- {reviews.map((review) => {
- const isOwnReview = userDetails?.id === review.user.id;
+ {isLoadingReviews && (
+
+ {t("loading_reviews")}
+
+ )}
+ {!isLoadingReviews && reviews.length === 0 && (
+
+
{t("no_reviews", "No reviews yet")}
+
+ )}
+ {!isLoadingReviews && reviews.length > 0 && (
+
+ {reviews.map((review) => {
+ const isOwnReview = userDetails?.id === review.user.id;
- return (
-
-
-
- {formatDistance(
- new Date(review.createdAt),
- new Date(),
- { addSuffix: true }
- )}
-
+ return (
+
+
+
+ {formatDistance(
+ new Date(review.createdAt),
+ new Date(),
+ { addSuffix: true }
+ )}
+
-
- {Array.from({ length: 5 }, (_, index) => (
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-

-
- navigate(
- buildGameDetailsPath(review.game)
- )
- }
+
+ {Array.from({ length: 5 }, (_, index) => (
+
- {review.game.title}
-
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+

+
+ navigate(
+ buildGameDetailsPath(review.game)
+ )
+ }
+ >
+ {review.game.title}
+
+
-
-
-
-
- handleVoteReview(review.id, true)
- }
- disabled={votingReviews.has(review.id)}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.upvotes}
-
-
-
+
+
+
+ handleVoteReview(review.id, true)
+ }
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.upvotes}
+
+
+
-
- handleVoteReview(review.id, false)
- }
- disabled={votingReviews.has(review.id)}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.downvotes}
-
-
-
+
+ handleVoteReview(review.id, false)
+ }
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.downvotes}
+
+
+
+
+
+ {isOwnReview && (
+
handleDeleteClick(review.id)}
+ title={t("delete_review")}
+ >
+
+ {t("delete_review")}
+
+ )}
-
- {isOwnReview && (
-
handleDeleteClick(review.id)}
- title={t("delete_review")}
- >
-
- {t("delete_review")}
-
- )}
-
-
- );
- })}
-
- );
- })()}
+
+ );
+ })}
+
+ )}
From d21ec52814e17d657360bfba75443aa1cc3bae45 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 12:06:23 +0300
Subject: [PATCH 022/118] ci: deleted comments
---
.../profile-content/profile-content.tsx | 264 +++++++++---------
1 file changed, 131 insertions(+), 133 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index f5511a1e..dd09ed0b 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -26,8 +26,7 @@ import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
import type { GameShop } from "@types";
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
import {
- // removed: sectionVariants,
- // removed: chevronVariants,
+
GAME_STATS_ANIMATION_DURATION_IN_MS,
} from "./profile-animations";
import "./profile-content.scss";
@@ -450,145 +449,145 @@ export function ProfileContent() {
{!isLoadingReviews && reviews.length > 0 && (
{reviews.map((review) => {
- const isOwnReview = userDetails?.id === review.user.id;
+ const isOwnReview = userDetails?.id === review.user.id;
- return (
-
-
-
- {formatDistance(
- new Date(review.createdAt),
- new Date(),
- { addSuffix: true }
- )}
-
-
-
- {Array.from({ length: 5 }, (_, index) => (
-
-
-
- ))}
-
+ return (
+
+
+
+ {formatDistance(
+ new Date(review.createdAt),
+ new Date(),
+ { addSuffix: true }
+ )}
-
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+
+
+ ))}
+
+
-
-
-
-
-

-
- navigate(
- buildGameDetailsPath(review.game)
- )
- }
- >
- {review.game.title}
-
-
+
+
+
+
+
+
+

+
+ navigate(
+ buildGameDetailsPath(review.game)
+ )
+ }
+ >
+ {review.game.title}
+
+
-
-
-
- handleVoteReview(review.id, true)
- }
- disabled={votingReviews.has(review.id)}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.upvotes}
-
-
-
+
+
+
+ handleVoteReview(review.id, true)
+ }
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.upvotes}
+
+
+
-
- handleVoteReview(review.id, false)
- }
- disabled={votingReviews.has(review.id)}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.downvotes}
-
-
-
-
-
- {isOwnReview && (
-
handleDeleteClick(review.id)}
- title={t("delete_review")}
- >
-
- {t("delete_review")}
-
- )}
+
+ handleVoteReview(review.id, false)
+ }
+ disabled={votingReviews.has(review.id)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.downvotes}
+
+
+
-
- );
- })}
-
- )}
+
+ {isOwnReview && (
+
handleDeleteClick(review.id)}
+ title={t("delete_review")}
+ >
+
+ {t("delete_review")}
+
+ )}
+
+
+ );
+ })}
+
+ )}
@@ -621,8 +620,7 @@ export function ProfileContent() {
statsIndex,
libraryGames,
pinnedGames,
- // removed isPinnedCollapsed,
- // removed toggleSection,
+
sortBy,
activeTab,
]);
From daf9751cf6ebf1532c600f86be3b9dde2ab05931 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 14:27:03 +0300
Subject: [PATCH 023/118] ci: import formatting
---
.../src/pages/profile/profile-content/profile-content.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index dd09ed0b..cd833c6c 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -25,10 +25,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
import type { GameShop } from "@types";
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
-import {
-
- GAME_STATS_ANIMATION_DURATION_IN_MS,
-} from "./profile-animations";
+import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
import "./profile-content.scss";
type SortOption = "playtime" | "achievementCount" | "playedRecently";
From cc95deb709c78d0d9621ca8ad37ea0280873381b Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 14:40:02 +0300
Subject: [PATCH 024/118] fix: proreply reseting user reviews on profile
changing
---
.../src/pages/profile/profile-content/profile-content.tsx | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index cd833c6c..9955640b 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -108,6 +108,14 @@ export function ProfileContent() {
}
}, [sortBy, getUserLibraryGames, userProfile]);
+ // Clear reviews state and reset tab when switching users
+ useEffect(() => {
+ setReviews([]);
+ setReviewsTotalCount(0);
+ setIsLoadingReviews(false);
+ setActiveTab("library");
+ }, [userProfile?.id]);
+
useEffect(() => {
if (userProfile?.id) {
fetchUserReviews();
From 81a77411ccd325692395d79f188fee5f6d2911df Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 16:54:18 +0300
Subject: [PATCH 025/118] ci: fix gap between game image and game name in
reviews
---
.../src/pages/profile/profile-content/profile-content.scss | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index 45a7f119..15b9de6d 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -230,27 +230,24 @@
.user-reviews__review-game {
display: flex;
- align-items: center;
gap: calc(globals.$spacing-unit);
}
.user-reviews__game-icon {
width: 24px;
height: 24px;
- border-radius: 8px;
object-fit: cover;
}
.user-reviews__game-info {
display: flex;
flex-direction: column;
- gap: calc(globals.$spacing-unit * 0.25);
}
.user-reviews__game-details {
display: flex;
align-items: center;
- gap: calc(globals.$spacing-unit * 0.5);
+ gap: calc(globals.$spacing-unit * 0.75);
}
.user-reviews__game-title {
From 29e1713824856a4f455dd06361214b5fc69786ab Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 23 Oct 2025 20:06:37 +0300
Subject: [PATCH 026/118] fix: upvote/downvote button arent being disabled
after click
---
.../profile-content/profile-content.tsx | 35 +++++++++++++++----
1 file changed, 29 insertions(+), 6 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 9955640b..44af9c1d 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -181,7 +181,14 @@ export function ProfileContent() {
setVotingReviews((prev) => new Set(prev).add(reviewId));
const review = reviews.find((r) => r.id === reviewId);
- if (!review) return;
+ if (!review) {
+ setVotingReviews((prev) => {
+ const next = new Set(prev);
+ next.delete(reviewId);
+ return next;
+ });
+ return;
+ }
const wasUpvoted = review.hasUpvoted;
const wasDownvoted = review.hasDownvoted;
@@ -258,11 +265,13 @@ export function ProfileContent() {
})
);
} finally {
- setVotingReviews((prev) => {
- const newSet = new Set(prev);
- newSet.delete(reviewId);
- return newSet;
- });
+ setTimeout(() => {
+ setVotingReviews((prev) => {
+ const newSet = new Set(prev);
+ newSet.delete(reviewId);
+ return newSet;
+ });
+ }, 500);
}
};
@@ -536,6 +545,10 @@ export function ProfileContent() {
handleVoteReview(review.id, true)
}
disabled={votingReviews.has(review.id)}
+ style={{
+ opacity: votingReviews.has(review.id) ? 0.5 : 1,
+ cursor: votingReviews.has(review.id) ? "not-allowed" : "pointer",
+ }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
@@ -559,6 +572,10 @@ export function ProfileContent() {
handleVoteReview(review.id, false)
}
disabled={votingReviews.has(review.id)}
+ style={{
+ opacity: votingReviews.has(review.id) ? 0.5 : 1,
+ cursor: votingReviews.has(review.id) ? "not-allowed" : "pointer",
+ }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
@@ -628,6 +645,12 @@ export function ProfileContent() {
sortBy,
activeTab,
+ // ensure reviews UI updates correctly
+ reviews,
+ reviewsTotalCount,
+ isLoadingReviews,
+ votingReviews,
+ deleteModalVisible,
]);
return (
From 40f7e6e2ad210d56bddd15b2b59828358f3919fb Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 23 Oct 2025 17:47:54 -0300
Subject: [PATCH 027/118] chore: bump electron version to 35
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 342b078a..59497aad 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
"@types/winreg": "^1.2.36",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.2.1",
- "electron": "^33.4.11",
+ "electron": "^35.7.5",
"electron-builder": "^26.0.12",
"electron-vite": "^3.0.0",
"eslint": "^8.56.0",
From a388acf9481ac1dcdff964b7c24749d084ba6247 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 23 Oct 2025 17:51:15 -0300
Subject: [PATCH 028/118] chore: update node version on gh actions
---
.github/workflows/build-renderer.yml | 4 ++--
.github/workflows/build.yml | 2 +-
.github/workflows/lint.yml | 2 +-
.github/workflows/release.yml | 2 +-
yarn.lock | 19 +++++++++++++------
5 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/build-renderer.yml b/.github/workflows/build-renderer.yml
index 6aefac43..ed7a99ab 100644
--- a/.github/workflows/build-renderer.yml
+++ b/.github/workflows/build-renderer.yml
@@ -6,7 +6,7 @@ concurrency:
on:
push:
- branches: main
+ branches: [main]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.18.0
+ node-version: 22.19.5
- name: Install dependencies
run: yarn --frozen-lockfile --ignore-scripts
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5062c7ad..32688379 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.18.3
+ node-version: 22.19.5
- name: Install dependencies
run: yarn --frozen-lockfile
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ac359364..6d08525c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.18.3
+ node-version: 22.19.5
- name: Install dependencies
run: yarn --frozen-lockfile
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3ceb42c7..a06eeb21 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.18.3
+ node-version: 22.19.5
- name: Install dependencies
run: yarn --frozen-lockfile
diff --git a/yarn.lock b/yarn.lock
index 0337a77b..5ffc3f03 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3206,13 +3206,20 @@
dependencies:
undici-types "~7.14.0"
-"@types/node@^20.12.7", "@types/node@^20.9.0":
+"@types/node@^20.12.7":
version "20.19.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.21.tgz#6e5378e04993c40395473b13baf94a09875157b8"
integrity sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==
dependencies:
undici-types "~6.21.0"
+"@types/node@^22.7.7":
+ version "22.18.12"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.12.tgz#e165d87bc25d7bf6d3657035c914db7485de84fb"
+ integrity sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==
+ dependencies:
+ undici-types "~6.21.0"
+
"@types/parse-torrent-file@*":
version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.6.tgz#11801dfd5b0a017302a164b72c8869f2bcba15b1"
@@ -4651,13 +4658,13 @@ electron-vite@^3.0.0:
magic-string "^0.30.17"
picocolors "^1.1.1"
-electron@^33.4.11:
- version "33.4.11"
- resolved "https://registry.yarnpkg.com/electron/-/electron-33.4.11.tgz#225d7f106ed3edf788ced318c63858d8b8a446dc"
- integrity sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==
+electron@^35.7.5:
+ version "35.7.5"
+ resolved "https://registry.yarnpkg.com/electron/-/electron-35.7.5.tgz#294a4aebb2ad2a884de730c410f2358d061e8d53"
+ integrity sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==
dependencies:
"@electron/get" "^2.0.0"
- "@types/node" "^20.9.0"
+ "@types/node" "^22.7.7"
extract-zip "^2.0.1"
embla-carousel-autoplay@^8.6.0:
From 29e822f2f110a3ae83d0f18862a37d5614112fbe Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 23 Oct 2025 17:56:45 -0300
Subject: [PATCH 029/118] fix: node version on gh actions files
---
.github/workflows/build-renderer.yml | 2 +-
.github/workflows/build.yml | 2 +-
.github/workflows/lint.yml | 2 +-
.github/workflows/release.yml | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/build-renderer.yml b/.github/workflows/build-renderer.yml
index ed7a99ab..f7361883 100644
--- a/.github/workflows/build-renderer.yml
+++ b/.github/workflows/build-renderer.yml
@@ -19,7 +19,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.19.5
+ node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile --ignore-scripts
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 32688379..86fce350 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.19.5
+ node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6d08525c..89e8b59f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.19.5
+ node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a06eeb21..11df9b9f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.19.5
+ node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile
From 8de6c92d28ef63d26217d85a82880292d5d90c90 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Fri, 24 Oct 2025 08:19:55 +0300
Subject: [PATCH 030/118] ci: formatting
---
.../profile/profile-content/profile-content.tsx | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 44af9c1d..e284cb88 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -546,8 +546,12 @@ export function ProfileContent() {
}
disabled={votingReviews.has(review.id)}
style={{
- opacity: votingReviews.has(review.id) ? 0.5 : 1,
- cursor: votingReviews.has(review.id) ? "not-allowed" : "pointer",
+ opacity: votingReviews.has(review.id)
+ ? 0.5
+ : 1,
+ cursor: votingReviews.has(review.id)
+ ? "not-allowed"
+ : "pointer",
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
@@ -573,8 +577,12 @@ export function ProfileContent() {
}
disabled={votingReviews.has(review.id)}
style={{
- opacity: votingReviews.has(review.id) ? 0.5 : 1,
- cursor: votingReviews.has(review.id) ? "not-allowed" : "pointer",
+ opacity: votingReviews.has(review.id)
+ ? 0.5
+ : 1,
+ cursor: votingReviews.has(review.id)
+ ? "not-allowed"
+ : "pointer",
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
From 11c19f5fe5bd49e7fb5560f65e383e98fb3000fa Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 24 Oct 2025 20:20:51 -0300
Subject: [PATCH 031/118] chore: downgrade to latest of 34
---
package.json | 2 +-
src/renderer/src/pages/game-details/game-reviews.tsx | 4 ----
yarn.lock | 2 +-
3 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/package.json b/package.json
index 59497aad..f74825a1 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
"@types/winreg": "^1.2.36",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.2.1",
- "electron": "^35.7.5",
+ "electron": "^34.5.8",
"electron-builder": "^26.0.12",
"electron-vite": "^3.0.0",
"eslint": "^8.56.0",
diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx
index f8117f43..1ce44550 100644
--- a/src/renderer/src/pages/game-details/game-reviews.tsx
+++ b/src/renderer/src/pages/game-details/game-reviews.tsx
@@ -144,8 +144,6 @@ export function GameReviews({
}
}, [objectId, userDetailsId, shop, game, onUserReviewedChange]);
- console.log("reviews", reviews);
-
const loadReviews = useCallback(
async (reset = false) => {
if (!objectId) return;
@@ -440,8 +438,6 @@ export function GameReviews({
});
}, [reviews]);
- console.log("reviews", reviews);
-
return (
{showReviewPrompt &&
diff --git a/yarn.lock b/yarn.lock
index 5ffc3f03..d936ff61 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4658,7 +4658,7 @@ electron-vite@^3.0.0:
magic-string "^0.30.17"
picocolors "^1.1.1"
-electron@^35.7.5:
+electron@^35.2.1:
version "35.7.5"
resolved "https://registry.yarnpkg.com/electron/-/electron-35.7.5.tgz#294a4aebb2ad2a884de730c410f2358d061e8d53"
integrity sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==
From 4471bf0f8bc9ee7e27a48dbfc2c9a3be3299f6a7 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 24 Oct 2025 21:05:40 -0300
Subject: [PATCH 032/118] chore: bump to electron 37
---
package.json | 4 ++--
yarn.lock | 10 +++++-----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/package.json b/package.json
index f74825a1..b34425a0 100644
--- a/package.json
+++ b/package.json
@@ -116,9 +116,9 @@
"@types/winreg": "^1.2.36",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.2.1",
- "electron": "^34.5.8",
+ "electron": "^37.7.1",
"electron-builder": "^26.0.12",
- "electron-vite": "^3.0.0",
+ "electron-vite": "^3.1.0",
"eslint": "^8.56.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
diff --git a/yarn.lock b/yarn.lock
index d936ff61..c362ada8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4646,7 +4646,7 @@ electron-updater@^6.6.2:
semver "^7.6.3"
tiny-typed-emitter "^2.1.0"
-electron-vite@^3.0.0:
+electron-vite@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/electron-vite/-/electron-vite-3.1.0.tgz#1784907a83d23c6c8093ec68b8e414a74d814385"
integrity sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==
@@ -4658,10 +4658,10 @@ electron-vite@^3.0.0:
magic-string "^0.30.17"
picocolors "^1.1.1"
-electron@^35.2.1:
- version "35.7.5"
- resolved "https://registry.yarnpkg.com/electron/-/electron-35.7.5.tgz#294a4aebb2ad2a884de730c410f2358d061e8d53"
- integrity sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==
+electron@^37.7.1:
+ version "37.7.1"
+ resolved "https://registry.yarnpkg.com/electron/-/electron-37.7.1.tgz#7d771b3d3365b5458f8bc758385defee14387034"
+ integrity sha512-2EmIqWv4T8BtgFQosB3/0Fezs09X3l0wXhIzes/cNt/GI+UDljbQr3NiF2J9WnqP0aFSbUEfztGUQMiX+qDvsw==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^22.7.7"
From ee35bc24b27c05458f888cbf2e477bdb80f7c1df Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 24 Oct 2025 21:11:56 -0300
Subject: [PATCH 033/118] chore: undo remove hydra api logs
---
src/main/services/hydra-api.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 07f81d68..dd26e6f0 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -29,7 +29,7 @@ export class HydraApi {
private static instance: AxiosInstance;
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
- private static readonly ADD_LOG_INTERCEPTOR = false;
+ private static readonly ADD_LOG_INTERCEPTOR = true;
private static secondsToMilliseconds(seconds: number) {
return seconds * 1000;
From 7f2343413efc80cb9ba894b77162b96ee1aa34dc Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Sun, 26 Oct 2025 17:26:25 +0200
Subject: [PATCH 034/118] feat: added manual page selection and changed
functionality of pagination
---
.../src/pages/catalogue/pagination.scss | 29 +++++
.../src/pages/catalogue/pagination.tsx | 112 ++++++++++++++----
2 files changed, 121 insertions(+), 20 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.scss b/src/renderer/src/pages/catalogue/pagination.scss
index 141dfe54..cac10211 100644
--- a/src/renderer/src/pages/catalogue/pagination.scss
+++ b/src/renderer/src/pages/catalogue/pagination.scss
@@ -1,3 +1,5 @@
+@use "../../scss/globals.scss";
+
.pagination {
display: flex;
gap: 4px;
@@ -18,4 +20,31 @@
font-size: 16px;
}
}
+
+ &__page-input {
+ box-sizing: border-box;
+ width: 40px;
+ min-width: 40px;
+ max-width: 40px;
+ min-height: 40px;
+ border-radius: 8px;
+ border: solid 1px globals.$border-color;
+ background-color: transparent;
+ color: globals.$muted-color;
+ text-align: center;
+ font-size: 12px;
+ padding: 0 6px;
+ outline: none;
+ }
+
+ &__double-chevron {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0; // remove whitespace node width between SVGs
+ }
+
+ &__double-chevron > svg + svg {
+ margin-left: -8px; // pull the second chevron closer
+ }
}
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index dfae6164..4040c4b5 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -1,6 +1,7 @@
import { Button } from "@renderer/components/button/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
import { useFormat } from "@renderer/hooks/use-format";
+import { useEffect, useRef, useState } from "react";
import "./pagination.scss";
interface PaginationProps {
@@ -16,6 +17,17 @@ export function Pagination({
}: PaginationProps) {
const { formatNumber } = useFormat();
+ const [isJumpOpen, setIsJumpOpen] = useState(false);
+ const [jumpValue, setJumpValue] = useState("");
+ const jumpInputRef = useRef(null);
+
+ useEffect(() => {
+ if (isJumpOpen) {
+ setJumpValue("");
+ setTimeout(() => jumpInputRef.current?.focus(), 0);
+ }
+ }, [isJumpOpen, page]);
+
if (totalPages <= 1) return null;
const visiblePages = 3;
@@ -30,6 +42,19 @@ export function Pagination({
return (
+ {startPage > 1 && (
+
onPageChange(1)}
+ className="pagination__button"
+ >
+
+
+
+
+
+ )}
+
onPageChange(page - 1)}
@@ -39,23 +64,6 @@ export function Pagination({
- {page > 2 && (
- <>
-
onPageChange(1)}
- className="pagination__button"
- disabled={page === 1}
- >
- {1}
-
-
-
- ...
-
- >
- )}
-
{Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i
@@ -72,9 +80,60 @@ export function Pagination({
{page < totalPages - 1 && (
<>
-
- ...
-
+ {isJumpOpen ? (
+
{
+ const val = e.target.value;
+ if (val === "") {
+ setJumpValue("");
+ return;
+ }
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ return;
+ }
+ if (num < 1) {
+ setJumpValue("1");
+ return;
+ }
+ if (num > totalPages) {
+ setJumpValue(String(totalPages));
+ return;
+ }
+ setJumpValue(val);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (jumpValue.trim() === "") return;
+ const parsed = Number(jumpValue);
+ if (Number.isNaN(parsed)) return;
+ const target = Math.max(1, Math.min(totalPages, parsed));
+ onPageChange(target);
+ setIsJumpOpen(false);
+ } else if (e.key === "Escape") {
+ setIsJumpOpen(false);
+ }
+ }}
+ onBlur={() => {
+ setIsJumpOpen(false);
+ }}
+ aria-label="Go to page"
+ />
+ ) : (
+
setIsJumpOpen(true)}
+ >
+ ...
+
+ )}
+
+ {endPage < totalPages && (
+
onPageChange(totalPages)}
+ className="pagination__button"
+ >
+
+
+
+
+
+ )}
);
}
From cb3e52de34be44bf731aa2f5a04b477d7454d5c8 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Sun, 26 Oct 2025 19:37:57 +0200
Subject: [PATCH 035/118] fix: go to page button did not appear correctly for
the last pages
---
.../src/pages/catalogue/pagination.tsx | 74 ++++++++++++++++++-
1 file changed, 72 insertions(+), 2 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index 4040c4b5..eaaa97a8 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -31,11 +31,15 @@ export function Pagination({
if (totalPages <= 1) return null;
const visiblePages = 3;
+ const isLastThree = totalPages > 3 && page >= totalPages - 2;
let startPage = Math.max(1, page - 1);
let endPage = startPage + visiblePages - 1;
- if (endPage > totalPages) {
+ if (isLastThree) {
+ startPage = Math.max(1, totalPages - 2);
+ endPage = totalPages;
+ } else if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - visiblePages + 1);
}
@@ -64,6 +68,72 @@ export function Pagination({
+ {isLastThree && startPage > 1 && (
+ <>
+ onPageChange(1)}
+ >
+ {formatNumber(1)}
+
+ {isJumpOpen ? (
+ {
+ const val = e.target.value;
+ if (val === "") {
+ setJumpValue("");
+ return;
+ }
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ return;
+ }
+ if (num < 1) {
+ setJumpValue("1");
+ return;
+ }
+ if (num > totalPages) {
+ setJumpValue(String(totalPages));
+ return;
+ }
+ setJumpValue(val);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (jumpValue.trim() === "") return;
+ const parsed = Number(jumpValue);
+ if (Number.isNaN(parsed)) return;
+ const target = Math.max(1, Math.min(totalPages, parsed));
+ onPageChange(target);
+ setIsJumpOpen(false);
+ } else if (e.key === "Escape") {
+ setIsJumpOpen(false);
+ }
+ }}
+ onBlur={() => {
+ setIsJumpOpen(false);
+ }}
+ aria-label="Go to page"
+ />
+ ) : (
+ setIsJumpOpen(true)}
+ >
+ ...
+
+ )}
+ >
+ )}
+
{Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i
@@ -78,7 +148,7 @@ export function Pagination({
))}
- {page < totalPages - 1 && (
+ {!isLastThree && page < totalPages - 1 && (
<>
{isJumpOpen ? (
Date: Sun, 26 Oct 2025 19:49:15 +0200
Subject: [PATCH 036/118] fix: duplications
---
.../src/pages/catalogue/pagination.tsx | 171 +++++++-----------
1 file changed, 63 insertions(+), 108 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index eaaa97a8..1ba02d06 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -2,6 +2,7 @@ import { Button } from "@renderer/components/button/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
import { useFormat } from "@renderer/hooks/use-format";
import { useEffect, useRef, useState } from "react";
+import type { ChangeEvent, KeyboardEvent } from "react";
import "./pagination.scss";
interface PaginationProps {
@@ -44,6 +45,66 @@ export function Pagination({
startPage = Math.max(1, endPage - visiblePages + 1);
}
+ const onJumpChange = (e: ChangeEvent) => {
+ const val = e.target.value;
+ if (val === "") {
+ setJumpValue("");
+ return;
+ }
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ return;
+ }
+ if (num < 1) {
+ setJumpValue("1");
+ return;
+ }
+ if (num > totalPages) {
+ setJumpValue(String(totalPages));
+ return;
+ }
+ setJumpValue(val);
+ };
+
+ const onJumpKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ if (jumpValue.trim() === "") return;
+ const parsed = Number(jumpValue);
+ if (Number.isNaN(parsed)) return;
+ const target = Math.max(1, Math.min(totalPages, parsed));
+ onPageChange(target);
+ setIsJumpOpen(false);
+ } else if (e.key === "Escape") {
+ setIsJumpOpen(false);
+ }
+ };
+
+ const JumpControl = () =>
+ isJumpOpen ? (
+ {
+ setIsJumpOpen(false);
+ }}
+ aria-label="Go to page"
+ />
+ ) : (
+ setIsJumpOpen(true)}
+ >
+ ...
+
+ );
+
return (
{startPage > 1 && (
@@ -77,60 +138,7 @@ export function Pagination({
>
{formatNumber(1)}
- {isJumpOpen ? (
-
{
- const val = e.target.value;
- if (val === "") {
- setJumpValue("");
- return;
- }
- const num = Number(val);
- if (Number.isNaN(num)) {
- return;
- }
- if (num < 1) {
- setJumpValue("1");
- return;
- }
- if (num > totalPages) {
- setJumpValue(String(totalPages));
- return;
- }
- setJumpValue(val);
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- if (jumpValue.trim() === "") return;
- const parsed = Number(jumpValue);
- if (Number.isNaN(parsed)) return;
- const target = Math.max(1, Math.min(totalPages, parsed));
- onPageChange(target);
- setIsJumpOpen(false);
- } else if (e.key === "Escape") {
- setIsJumpOpen(false);
- }
- }}
- onBlur={() => {
- setIsJumpOpen(false);
- }}
- aria-label="Go to page"
- />
- ) : (
-
setIsJumpOpen(true)}
- >
- ...
-
- )}
+
>
)}
@@ -150,60 +158,7 @@ export function Pagination({
{!isLastThree && page < totalPages - 1 && (
<>
- {isJumpOpen ? (
-
{
- const val = e.target.value;
- if (val === "") {
- setJumpValue("");
- return;
- }
- const num = Number(val);
- if (Number.isNaN(num)) {
- return;
- }
- if (num < 1) {
- setJumpValue("1");
- return;
- }
- if (num > totalPages) {
- setJumpValue(String(totalPages));
- return;
- }
- setJumpValue(val);
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- if (jumpValue.trim() === "") return;
- const parsed = Number(jumpValue);
- if (Number.isNaN(parsed)) return;
- const target = Math.max(1, Math.min(totalPages, parsed));
- onPageChange(target);
- setIsJumpOpen(false);
- } else if (e.key === "Escape") {
- setIsJumpOpen(false);
- }
- }}
- onBlur={() => {
- setIsJumpOpen(false);
- }}
- aria-label="Go to page"
- />
- ) : (
-
setIsJumpOpen(true)}
- >
- ...
-
- )}
+
Date: Sun, 26 Oct 2025 19:59:43 +0200
Subject: [PATCH 037/118] fix: moved component out of parent component
---
.../src/pages/catalogue/pagination.tsx | 91 +++++++++++++------
1 file changed, 62 insertions(+), 29 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index 1ba02d06..9febc8f8 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -2,9 +2,50 @@ import { Button } from "@renderer/components/button/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
import { useFormat } from "@renderer/hooks/use-format";
import { useEffect, useRef, useState } from "react";
-import type { ChangeEvent, KeyboardEvent } from "react";
+import type { ChangeEvent, KeyboardEvent, RefObject } from "react";
import "./pagination.scss";
+interface JumpControlProps {
+ isOpen: boolean;
+ value: string;
+ totalPages: number;
+ inputRef: RefObject;
+ onOpen: () => void;
+ onClose: () => void;
+ onChange: (e: ChangeEvent) => void;
+ onKeyDown: (e: KeyboardEvent) => void;
+}
+
+function JumpControl({
+ isOpen,
+ value,
+ totalPages,
+ inputRef,
+ onOpen,
+ onClose,
+ onChange,
+ onKeyDown,
+}: JumpControlProps) {
+ return isOpen ? (
+
+ ) : (
+
+ ...
+
+ );
+}
+
interface PaginationProps {
page: number;
totalPages: number;
@@ -79,32 +120,6 @@ export function Pagination({
}
};
- const JumpControl = () =>
- isJumpOpen ? (
- {
- setIsJumpOpen(false);
- }}
- aria-label="Go to page"
- />
- ) : (
- setIsJumpOpen(true)}
- >
- ...
-
- );
-
return (
{startPage > 1 && (
@@ -138,7 +153,16 @@ export function Pagination({
>
{formatNumber(1)}
-
+
setIsJumpOpen(true)}
+ onClose={() => setIsJumpOpen(false)}
+ onChange={onJumpChange}
+ onKeyDown={onJumpKeyDown}
+ />
>
)}
@@ -158,7 +182,16 @@ export function Pagination({
{!isLastThree && page < totalPages - 1 && (
<>
-
+ setIsJumpOpen(true)}
+ onClose={() => setIsJumpOpen(false)}
+ onChange={onJumpChange}
+ onKeyDown={onJumpKeyDown}
+ />
Date: Sun, 26 Oct 2025 23:22:20 +0000
Subject: [PATCH 038/118] feat: adding sources migration
---
.../download-sources/add-download-source.ts | 16 +++++------
.../download-sources/get-download-sources.ts | 4 ++-
src/main/helpers/migrate-download-sources.ts | 27 +++++++++++++++++++
src/main/main.ts | 2 ++
.../src/components/game-card/game-card.tsx | 2 --
.../settings/settings-download-sources.tsx | 2 ++
src/types/index.ts | 2 ++
7 files changed, 43 insertions(+), 12 deletions(-)
create mode 100644 src/main/helpers/migrate-download-sources.ts
diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts
index 45bcd27c..d4e65ef3 100644
--- a/src/main/events/download-sources/add-download-source.ts
+++ b/src/main/events/download-sources/add-download-source.ts
@@ -2,6 +2,7 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services/hydra-api";
import { downloadSourcesSublevel } from "@main/level";
import type { DownloadSource } from "@types";
+import { logger } from "@main/services";
const addDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
@@ -22,22 +23,19 @@ const addDownloadSource = async (
urls: [url],
});
} catch (error) {
- console.error("Failed to add download source to profile:", error);
+ logger.error("Failed to add download source to profile:", error);
}
}
- const downloadSourceForStorage = {
+ await downloadSourcesSublevel.put(downloadSource.id, {
...downloadSource,
- fingerprint: downloadSource.fingerprint || "",
- };
- await downloadSourcesSublevel.put(
- downloadSource.id,
- downloadSourceForStorage
- );
+ isRemote: true,
+ createdAt: new Date().toISOString(),
+ });
return downloadSource;
} catch (error) {
- console.error("Failed to add download source:", error);
+ logger.error("Failed to add download source:", error);
throw error;
}
};
diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts
index cf7cd4d7..48583d9e 100644
--- a/src/main/events/download-sources/get-download-sources.ts
+++ b/src/main/events/download-sources/get-download-sources.ts
@@ -1,8 +1,10 @@
import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
+import { orderBy } from "lodash-es";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
- return downloadSourcesSublevel.values().all();
+ const allSources = await downloadSourcesSublevel.values().all();
+ return orderBy(allSources, "createdAt", "desc");
};
registerEvent("getDownloadSources", getDownloadSources);
diff --git a/src/main/helpers/migrate-download-sources.ts b/src/main/helpers/migrate-download-sources.ts
new file mode 100644
index 00000000..fd627f20
--- /dev/null
+++ b/src/main/helpers/migrate-download-sources.ts
@@ -0,0 +1,27 @@
+import { downloadSourcesSublevel } from "@main/level";
+import { HydraApi } from "@main/services/hydra-api";
+import { DownloadSource } from "@types";
+
+export const migrateDownloadSources = async () => {
+ const downloadSources = downloadSourcesSublevel.iterator();
+
+ for await (const [key, value] of downloadSources) {
+ if (!value.isRemote) {
+ const downloadSource = await HydraApi.post(
+ "/download-sources",
+ {
+ url: value.url,
+ },
+ { needsAuth: false }
+ );
+
+ await downloadSourcesSublevel.put(downloadSource.id, {
+ ...downloadSource,
+ isRemote: true,
+ createdAt: new Date().toISOString(),
+ });
+
+ await downloadSourcesSublevel.del(key);
+ }
+ }
+};
diff --git a/src/main/main.ts b/src/main/main.ts
index 617dd135..6e477a18 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -17,6 +17,7 @@ import {
Lock,
DeckyPlugin,
} from "@main/services";
+import { migrateDownloadSources } from "./helpers/migrate-download-sources";
export const loadState = async () => {
await Lock.acquireLock();
@@ -51,6 +52,7 @@ export const loadState = async () => {
await HydraApi.setupApi().then(() => {
uploadGamesBatch();
+ void migrateDownloadSources();
// WSClient.connect();
});
diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx
index 598874b5..edea8d50 100644
--- a/src/renderer/src/components/game-card/game-card.tsx
+++ b/src/renderer/src/components/game-card/game-card.tsx
@@ -38,8 +38,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { numberFormatter } = useFormat();
- console.log("game", game);
-
return (
Date: Mon, 27 Oct 2025 01:42:02 +0000
Subject: [PATCH 039/118] fix: persisting is remote
---
.../events/download-sources/sync-download-sources.ts | 11 ++++++++---
src/renderer/src/declaration.d.ts | 2 +-
.../src/pages/settings/settings-download-sources.tsx | 5 +++--
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts
index 987ad1c1..68a6be3f 100644
--- a/src/main/events/download-sources/sync-download-sources.ts
+++ b/src/main/events/download-sources/sync-download-sources.ts
@@ -15,10 +15,15 @@ const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
);
for (const downloadSource of response) {
- await downloadSourcesSublevel.put(downloadSource.id, downloadSource);
- }
+ const existingDownloadSource = downloadSources.find(
+ (source) => source.id === downloadSource.id
+ );
- return response;
+ await downloadSourcesSublevel.put(downloadSource.id, {
+ ...existingDownloadSource,
+ ...downloadSource,
+ });
+ }
};
registerEvent("syncDownloadSources", syncDownloadSources);
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 4e004e2b..fa4ab3d6 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -213,7 +213,7 @@ declare global {
downloadSourceId?: string
) => Promise;
getDownloadSources: () => Promise;
- syncDownloadSources: () => Promise;
+ syncDownloadSources: () => Promise;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise;
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index 52d3bd97..08670145 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -101,8 +101,9 @@ export function SettingsDownloadSources() {
const syncDownloadSources = async () => {
setIsSyncingDownloadSources(true);
try {
- const sources = await window.electron.syncDownloadSources();
- setDownloadSources(sources);
+ await window.electron.syncDownloadSources();
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("download_sources_synced_successfully"));
} finally {
From 2835207d793b3aca86eadec5303e16a67b7316f8 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 04:12:19 +0000
Subject: [PATCH 040/118] fix: fixing download sources import status
---
.cursorrules | 29 ++++++++++
src/locales/en/translation.json | 10 ++++
src/locales/pt-BR/translation.json | 6 ++
src/locales/pt-PT/translation.json | 6 ++
.../settings/add-download-source-modal.tsx | 5 +-
.../src/pages/settings/settings-account.tsx | 2 +-
.../settings/settings-download-sources.scss | 20 +++++++
.../settings/settings-download-sources.tsx | 58 +++++++++++++++----
.../pages/settings/settings-real-debrid.tsx | 2 +-
.../src/pages/settings/settings-torbox.tsx | 2 +-
10 files changed, 124 insertions(+), 16 deletions(-)
create mode 100644 .cursorrules
diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 00000000..0b0c009c
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,29 @@
+# Hydra Project Rules
+
+## Logging
+
+- **Always use `logger` instead of `console` for logging** in both main and renderer processes
+- In main process: `import { logger } from "@main/services";`
+- In renderer process: `import { logger } from "@renderer/logger";`
+- Replace all instances of:
+ - `console.log()` → `logger.log()`
+ - `console.error()` → `logger.error()`
+ - `console.warn()` → `logger.warn()`
+ - `console.info()` → `logger.info()`
+ - `console.debug()` → `logger.debug()`
+- Do not use `console` for any logging purposes
+
+## Internationalization (i18n)
+
+- All user-facing strings must be translated using i18next
+- Use the `useTranslation` hook in React components: `const { t } = useTranslation("namespace");`
+- Add new translation keys to `src/locales/en/translation.json`
+- Never hardcode English strings in the UI code
+- Placeholder text in form fields must also be translated
+
+## Code Style
+
+- Use ESLint and Prettier for code formatting
+- Follow TypeScript strict mode conventions
+- Use async/await instead of promises when possible
+- Prefer named exports over default exports for utilities and services
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 46bdb28c..bfc4a379 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -428,6 +428,8 @@
"validate_download_source": "Validate",
"remove_download_source": "Remove",
"add_download_source": "Add source",
+ "adding": "Adding…",
+ "failed_add_download_source": "Failed to add download source. Please try again.",
"download_count_zero": "No download options",
"download_count_one": "{{countFormatted}} download option",
"download_count_other": "{{countFormatted}} download options",
@@ -435,9 +437,16 @@
"add_download_source_description": "Insert the URL of the .json file",
"download_source_up_to_date": "Up-to-date",
"download_source_errored": "Errored",
+ "download_source_pending_matching": "Updating soon",
+ "download_source_matched": "Up to date",
+ "download_source_matching": "Updating",
+ "download_source_failed": "Error",
+ "download_source_no_information": "No information available",
"sync_download_sources": "Sync sources",
"removed_download_source": "Download source removed",
"removed_download_sources": "Download sources removed",
+ "removed_all_download_sources": "All download sources removed",
+ "download_sources_synced_successfully": "All download sources are synced",
"cancel_button_confirmation_delete_all_sources": "No",
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
"title_confirmation_delete_all_sources": "Delete all download sources",
@@ -468,6 +477,7 @@
"seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"account": "Account",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "You have no blocked users",
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
"manage_subscription": "Manage subscription",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 4ea77015..968483a6 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -423,7 +423,13 @@
"add_download_source_description": "Insira a URL contendo o arquivo .json",
"download_source_up_to_date": "Sincronizada",
"download_source_errored": "Falhou",
+ "download_source_pending_matching": "Importando em breve",
+ "download_source_matched": "Sincronizada",
+ "download_source_matching": "Sincronizando",
+ "download_source_failed": "Erro",
+ "download_source_no_information": "Sem informações",
"sync_download_sources": "Sincronizar",
+ "download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas",
"cancel_button_confirmation_delete_all_sources": "Não",
diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json
index 962504d4..2894cf65 100644
--- a/src/locales/pt-PT/translation.json
+++ b/src/locales/pt-PT/translation.json
@@ -252,7 +252,13 @@
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
"download_source_up_to_date": "Sincronizada",
"download_source_errored": "Falhou",
+ "download_source_pending_matching": "A atualizar em breve",
+ "download_source_matched": "Atualizado",
+ "download_source_matching": "A atualizar",
+ "download_source_failed": "Erro",
+ "download_source_no_information": "Sem informações",
"sync_download_sources": "Sincronizar",
+ "download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, apague tudo",
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx
index 2b45ed72..d7071391 100644
--- a/src/renderer/src/pages/settings/add-download-source-modal.tsx
+++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import { settingsContext } from "@renderer/context";
import { useForm } from "react-hook-form";
+import { logger } from "@renderer/logger";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
@@ -55,10 +56,10 @@ export function AddDownloadSourceModal({
onClose();
onAddDownloadSource();
} catch (error) {
- console.error("Failed to add download source:", error);
+ logger.error("Failed to add download source:", error);
setError("url", {
type: "server",
- message: "Failed to add download source. Please try again.",
+ message: t("failed_add_download_source"),
});
} finally {
setIsLoading(false);
diff --git a/src/renderer/src/pages/settings/settings-account.tsx b/src/renderer/src/pages/settings/settings-account.tsx
index 9cf35541..f2825cca 100644
--- a/src/renderer/src/pages/settings/settings-account.tsx
+++ b/src/renderer/src/pages/settings/settings-account.tsx
@@ -201,7 +201,7 @@ export function SettingsAccount() {
- Hydra Cloud
+ {t("hydra_cloud")}
{getHydraCloudSectionContent().description}
diff --git a/src/renderer/src/pages/settings/settings-download-sources.scss b/src/renderer/src/pages/settings/settings-download-sources.scss
index a12bdff3..df0f5c8b 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.scss
+++ b/src/renderer/src/pages/settings/settings-download-sources.scss
@@ -1,5 +1,14 @@
@use "../../scss/globals.scss";
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
.settings-download-sources {
&__list {
padding: 0;
@@ -22,6 +31,17 @@
&--syncing {
opacity: globals.$disabled-opacity;
}
+
+ &--pending {
+ opacity: 0.6;
+ }
+ }
+
+ &__spinner {
+ animation: spin 1s linear infinite;
+ margin-right: calc(globals.$spacing-unit / 2);
+ width: 12px;
+ height: 12px;
}
&__item-header {
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index 08670145..75f0cc73 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -22,6 +22,7 @@ import { settingsContext } from "@renderer/context";
import { useNavigate } from "react-router-dom";
import { setFilters, clearFilters } from "@renderer/features";
import "./settings-download-sources.scss";
+import { logger } from "@renderer/logger";
export function SettingsDownloadSources() {
const [
@@ -58,6 +59,30 @@ export function SettingsDownloadSources() {
fetchDownloadSources();
}, []);
+ useEffect(() => {
+ const hasPendingOrMatchingSource = downloadSources.some(
+ (source) =>
+ source.status === DownloadSourceStatus.PendingMatching ||
+ source.status === DownloadSourceStatus.Matching
+ );
+
+ if (!hasPendingOrMatchingSource || !downloadSources.length) {
+ return;
+ }
+
+ const intervalId = setInterval(async () => {
+ try {
+ await window.electron.syncDownloadSources();
+ const sources = await window.electron.getDownloadSources();
+ setDownloadSources(sources);
+ } catch (error) {
+ logger.error("Failed to fetch download sources:", error);
+ }
+ }, 5000);
+
+ return () => clearInterval(intervalId);
+ }, [downloadSources]);
+
const handleRemoveSource = async (downloadSource: DownloadSource) => {
setIsRemovingDownloadSource(true);
@@ -67,7 +92,7 @@ export function SettingsDownloadSources() {
setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("removed_download_source"));
} catch (error) {
- console.error("Failed to remove download source:", error);
+ logger.error("Failed to remove download source:", error);
} finally {
setIsRemovingDownloadSource(false);
}
@@ -82,7 +107,7 @@ export function SettingsDownloadSources() {
setDownloadSources(sources as DownloadSource[]);
showSuccessToast(t("removed_all_download_sources"));
} catch (error) {
- console.error("Failed to remove all download sources:", error);
+ logger.error("Failed to remove all download sources:", error);
} finally {
setIsRemovingDownloadSource(false);
setShowConfirmationDeleteAllSourcesModal(false);
@@ -94,7 +119,7 @@ export function SettingsDownloadSources() {
const sources = await window.electron.getDownloadSources();
setDownloadSources(sources as DownloadSource[]);
} catch (error) {
- console.error("Failed to refresh download sources:", error);
+ logger.error("Failed to refresh download sources:", error);
}
};
@@ -127,7 +152,7 @@ export function SettingsDownloadSources() {
const navigateToCatalogue = (fingerprint?: string) => {
if (!fingerprint) {
- console.error("Cannot navigate: fingerprint is undefined");
+ logger.error("Cannot navigate: fingerprint is undefined");
return;
}
@@ -202,16 +227,25 @@ export function SettingsDownloadSources() {
{downloadSources.map((downloadSource) => {
+ const isPendingOrMatching =
+ downloadSource.status === DownloadSourceStatus.PendingMatching ||
+ downloadSource.status === DownloadSourceStatus.Matching;
+
return (
-
{downloadSource.name}
- {statusTitle[downloadSource.status]}
+
+ {isPendingOrMatching && (
+
+ )}
+ {statusTitle[downloadSource.status]}
+
- {t("download_count", {
- count: downloadSource.downloadCount,
- countFormatted:
- downloadSource.downloadCount.toLocaleString(),
- })}
+ {isPendingOrMatching
+ ? t("download_source_no_information")
+ : t("download_count", {
+ count: downloadSource.downloadCount,
+ countFormatted:
+ downloadSource.downloadCount.toLocaleString(),
+ })}
diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx
index 42ba6ad9..db3a29a3 100644
--- a/src/renderer/src/pages/settings/settings-real-debrid.tsx
+++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx
@@ -133,7 +133,7 @@ export function SettingsRealDebrid() {
{t("save_changes")}
}
- placeholder="API Token"
+ placeholder={t("api_token")}
hint={
diff --git a/src/renderer/src/pages/settings/settings-torbox.tsx b/src/renderer/src/pages/settings/settings-torbox.tsx
index 610dc942..46c8e2f9 100644
--- a/src/renderer/src/pages/settings/settings-torbox.tsx
+++ b/src/renderer/src/pages/settings/settings-torbox.tsx
@@ -116,7 +116,7 @@ export function SettingsTorBox() {
onChange={(event) =>
setForm({ ...form, torBoxApiToken: event.target.value })
}
- placeholder="API Token"
+ placeholder={t("api_token")}
rightContent={
{t("save_changes")}
From 6921bfa3ff0870c1e16ec787032eed000f8ec5c0 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 07:41:58 +0000
Subject: [PATCH 041/118] ci: testing windows 2019
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5062c7ad..c269359b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
- os: [windows-2022, ubuntu-latest]
+ os: [windows-2019, ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
From a073cf7f8cff712a40963c06b4cd827a5cc60783 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Mon, 27 Oct 2025 04:50:32 -0300
Subject: [PATCH 042/118] chore: bump electron vite version
---
package.json | 4 +-
yarn.lock | 359 ++++++++++++++++++++++++++++++---------------------
2 files changed, 214 insertions(+), 149 deletions(-)
diff --git a/package.json b/package.json
index b34425a0..08c1d80e 100644
--- a/package.json
+++ b/package.json
@@ -118,7 +118,7 @@
"@vitejs/plugin-react": "^4.2.1",
"electron": "^37.7.1",
"electron-builder": "^26.0.12",
- "electron-vite": "^3.1.0",
+ "electron-vite": "^4.0.1",
"eslint": "^8.56.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
@@ -130,7 +130,7 @@
"sass-embedded": "^1.80.6",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
- "vite": "5.4.20",
+ "vite": "5.4.21",
"vite-plugin-svgr": "^4.5.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
diff --git a/yarn.lock b/yarn.lock
index c362ada8..6340a43f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -601,7 +601,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04"
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
-"@babel/core@^7.21.3", "@babel/core@^7.26.10", "@babel/core@^7.28.0":
+"@babel/core@^7.21.3", "@babel/core@^7.28.0":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496"
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
@@ -622,6 +622,27 @@
json5 "^2.2.3"
semver "^6.3.1"
+"@babel/core@^7.27.7":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
+ integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.5"
+ "@babel/helper-compilation-targets" "^7.27.2"
+ "@babel/helper-module-transforms" "^7.28.3"
+ "@babel/helpers" "^7.28.4"
+ "@babel/parser" "^7.28.5"
+ "@babel/template" "^7.27.2"
+ "@babel/traverse" "^7.28.5"
+ "@babel/types" "^7.28.5"
+ "@jridgewell/remapping" "^2.3.5"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
"@babel/generator@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
@@ -633,6 +654,17 @@
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
+"@babel/generator@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298"
+ integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==
+ dependencies:
+ "@babel/parser" "^7.28.5"
+ "@babel/types" "^7.28.5"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
"@babel/helper-compilation-targets@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
@@ -681,6 +713,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
+"@babel/helper-validator-identifier@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
+ integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
+
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
@@ -701,7 +738,14 @@
dependencies:
"@babel/types" "^7.28.4"
-"@babel/plugin-transform-arrow-functions@^7.25.9":
+"@babel/parser@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08"
+ integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==
+ dependencies:
+ "@babel/types" "^7.28.5"
+
+"@babel/plugin-transform-arrow-functions@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a"
integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==
@@ -749,6 +793,19 @@
"@babel/types" "^7.28.4"
debug "^4.3.1"
+"@babel/traverse@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b"
+ integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.5"
+ "@babel/helper-globals" "^7.28.0"
+ "@babel/parser" "^7.28.5"
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.5"
+ debug "^4.3.1"
+
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a"
@@ -757,6 +814,14 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
+"@babel/types@^7.28.5":
+ version "7.28.5"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
+ integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.28.5"
+
"@borewit/text-codec@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@borewit/text-codec/-/text-codec-0.1.1.tgz#7e7f27092473d5eabcffef693a849f2cc48431da"
@@ -1121,245 +1186,245 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
-"@esbuild/aix-ppc64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97"
- integrity sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==
+"@esbuild/aix-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
+ integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
"@esbuild/android-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
-"@esbuild/android-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz#115fc76631e82dd06811bfaf2db0d4979c16e2cb"
- integrity sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==
+"@esbuild/android-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
+ integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
"@esbuild/android-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
-"@esbuild/android-arm@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz#8d5811912da77f615398611e5bbc1333fe321aa9"
- integrity sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==
+"@esbuild/android-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
+ integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
"@esbuild/android-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
-"@esbuild/android-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz#e3e96516b2d50d74105bb92594c473e30ddc16b1"
- integrity sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==
+"@esbuild/android-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
+ integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
"@esbuild/darwin-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
-"@esbuild/darwin-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz#6af6bb1d05887dac515de1b162b59dc71212ed76"
- integrity sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==
+"@esbuild/darwin-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84"
+ integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
"@esbuild/darwin-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
-"@esbuild/darwin-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz#99ae82347fbd336fc2d28ffd4f05694e6e5b723d"
- integrity sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==
+"@esbuild/darwin-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
+ integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
"@esbuild/freebsd-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
-"@esbuild/freebsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz#0c6d5558a6322b0bdb17f7025c19bd7d2359437d"
- integrity sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==
+"@esbuild/freebsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
+ integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
"@esbuild/freebsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
-"@esbuild/freebsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz#8c35873fab8c0857a75300a3dcce4324ca0b9844"
- integrity sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==
+"@esbuild/freebsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
+ integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
"@esbuild/linux-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
-"@esbuild/linux-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz#3edc2f87b889a15b4cedaf65f498c2bed7b16b90"
- integrity sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==
+"@esbuild/linux-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
+ integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
"@esbuild/linux-arm@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
-"@esbuild/linux-arm@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz#86501cfdfb3d110176d80c41b27ed4611471cde7"
- integrity sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==
+"@esbuild/linux-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
+ integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
"@esbuild/linux-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
-"@esbuild/linux-ia32@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz#e6589877876142537c6864680cd5d26a622b9d97"
- integrity sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==
+"@esbuild/linux-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
+ integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
"@esbuild/linux-loong64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
-"@esbuild/linux-loong64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz#11119e18781f136d8083ea10eb6be73db7532de8"
- integrity sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==
+"@esbuild/linux-loong64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
+ integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
"@esbuild/linux-mips64el@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
-"@esbuild/linux-mips64el@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz#3052f5436b0c0c67a25658d5fc87f045e7def9e6"
- integrity sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==
+"@esbuild/linux-mips64el@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
+ integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
"@esbuild/linux-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
-"@esbuild/linux-ppc64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz#2f098920ee5be2ce799f35e367b28709925a8744"
- integrity sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==
+"@esbuild/linux-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
+ integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
"@esbuild/linux-riscv64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
-"@esbuild/linux-riscv64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz#fa51d7fd0a22a62b51b4b94b405a3198cf7405dd"
- integrity sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==
+"@esbuild/linux-riscv64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
+ integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
"@esbuild/linux-s390x@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
-"@esbuild/linux-s390x@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz#a27642e36fc282748fdb38954bd3ef4f85791e8a"
- integrity sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==
+"@esbuild/linux-s390x@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
+ integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
"@esbuild/linux-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
-"@esbuild/linux-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz#9d9b09c0033d17529570ced6d813f98315dfe4e9"
- integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==
+"@esbuild/linux-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
+ integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
-"@esbuild/netbsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz#25c09a659c97e8af19e3f2afd1c9190435802151"
- integrity sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==
+"@esbuild/netbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
+ integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
"@esbuild/netbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
-"@esbuild/netbsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz#7fa5f6ffc19be3a0f6f5fd32c90df3dc2506937a"
- integrity sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==
+"@esbuild/netbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
+ integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
-"@esbuild/openbsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz#8faa6aa1afca0c6d024398321d6cb1c18e72a1c3"
- integrity sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==
+"@esbuild/openbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
+ integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
"@esbuild/openbsd-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
-"@esbuild/openbsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz#a42979b016f29559a8453d32440d3c8cd420af5e"
- integrity sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==
+"@esbuild/openbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
+ integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
-"@esbuild/openharmony-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz#fd87bfeadd7eeb3aa384bbba907459ffa3197cb1"
- integrity sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==
+"@esbuild/openharmony-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
+ integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
"@esbuild/sunos-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
-"@esbuild/sunos-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz#3a18f590e36cb78ae7397976b760b2b8c74407f4"
- integrity sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==
+"@esbuild/sunos-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
+ integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
"@esbuild/win32-arm64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
-"@esbuild/win32-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz#e71741a251e3fd971408827a529d2325551f530c"
- integrity sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==
+"@esbuild/win32-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
+ integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
"@esbuild/win32-ia32@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
-"@esbuild/win32-ia32@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz#c6f010b5d3b943d8901a0c87ea55f93b8b54bf94"
- integrity sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==
+"@esbuild/win32-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
+ integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
"@esbuild/win32-x64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
-"@esbuild/win32-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz#e4b3e255a1b4aea84f6e1d2ae0b73f826c3785bd"
- integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==
+"@esbuild/win32-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f"
+ integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.9.0"
@@ -4646,15 +4711,15 @@ electron-updater@^6.6.2:
semver "^7.6.3"
tiny-typed-emitter "^2.1.0"
-electron-vite@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/electron-vite/-/electron-vite-3.1.0.tgz#1784907a83d23c6c8093ec68b8e414a74d814385"
- integrity sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q==
+electron-vite@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/electron-vite/-/electron-vite-4.0.1.tgz#6cdf798f842c255779983ccadd06b3d475c02f57"
+ integrity sha512-QqacJbA8f1pmwUTqki1qLL5vIBaOQmeq13CZZefZ3r3vKVaIoC7cpoTgE+KPKxJDFTax+iFZV0VYvLVWPiQ8Aw==
dependencies:
- "@babel/core" "^7.26.10"
- "@babel/plugin-transform-arrow-functions" "^7.25.9"
+ "@babel/core" "^7.27.7"
+ "@babel/plugin-transform-arrow-functions" "^7.27.1"
cac "^6.7.14"
- esbuild "^0.25.1"
+ esbuild "^0.25.5"
magic-string "^0.30.17"
picocolors "^1.1.1"
@@ -4900,37 +4965,37 @@ esbuild@^0.21.3:
"@esbuild/win32-ia32" "0.21.5"
"@esbuild/win32-x64" "0.21.5"
-esbuild@^0.25.1:
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.10.tgz#37f5aa5cd14500f141be121c01b096ca83ac34a9"
- integrity sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==
+esbuild@^0.25.5:
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d"
+ integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==
optionalDependencies:
- "@esbuild/aix-ppc64" "0.25.10"
- "@esbuild/android-arm" "0.25.10"
- "@esbuild/android-arm64" "0.25.10"
- "@esbuild/android-x64" "0.25.10"
- "@esbuild/darwin-arm64" "0.25.10"
- "@esbuild/darwin-x64" "0.25.10"
- "@esbuild/freebsd-arm64" "0.25.10"
- "@esbuild/freebsd-x64" "0.25.10"
- "@esbuild/linux-arm" "0.25.10"
- "@esbuild/linux-arm64" "0.25.10"
- "@esbuild/linux-ia32" "0.25.10"
- "@esbuild/linux-loong64" "0.25.10"
- "@esbuild/linux-mips64el" "0.25.10"
- "@esbuild/linux-ppc64" "0.25.10"
- "@esbuild/linux-riscv64" "0.25.10"
- "@esbuild/linux-s390x" "0.25.10"
- "@esbuild/linux-x64" "0.25.10"
- "@esbuild/netbsd-arm64" "0.25.10"
- "@esbuild/netbsd-x64" "0.25.10"
- "@esbuild/openbsd-arm64" "0.25.10"
- "@esbuild/openbsd-x64" "0.25.10"
- "@esbuild/openharmony-arm64" "0.25.10"
- "@esbuild/sunos-x64" "0.25.10"
- "@esbuild/win32-arm64" "0.25.10"
- "@esbuild/win32-ia32" "0.25.10"
- "@esbuild/win32-x64" "0.25.10"
+ "@esbuild/aix-ppc64" "0.25.11"
+ "@esbuild/android-arm" "0.25.11"
+ "@esbuild/android-arm64" "0.25.11"
+ "@esbuild/android-x64" "0.25.11"
+ "@esbuild/darwin-arm64" "0.25.11"
+ "@esbuild/darwin-x64" "0.25.11"
+ "@esbuild/freebsd-arm64" "0.25.11"
+ "@esbuild/freebsd-x64" "0.25.11"
+ "@esbuild/linux-arm" "0.25.11"
+ "@esbuild/linux-arm64" "0.25.11"
+ "@esbuild/linux-ia32" "0.25.11"
+ "@esbuild/linux-loong64" "0.25.11"
+ "@esbuild/linux-mips64el" "0.25.11"
+ "@esbuild/linux-ppc64" "0.25.11"
+ "@esbuild/linux-riscv64" "0.25.11"
+ "@esbuild/linux-s390x" "0.25.11"
+ "@esbuild/linux-x64" "0.25.11"
+ "@esbuild/netbsd-arm64" "0.25.11"
+ "@esbuild/netbsd-x64" "0.25.11"
+ "@esbuild/openbsd-arm64" "0.25.11"
+ "@esbuild/openbsd-x64" "0.25.11"
+ "@esbuild/openharmony-arm64" "0.25.11"
+ "@esbuild/sunos-x64" "0.25.11"
+ "@esbuild/win32-arm64" "0.25.11"
+ "@esbuild/win32-ia32" "0.25.11"
+ "@esbuild/win32-x64" "0.25.11"
escalade@^3.1.1, escalade@^3.2.0:
version "3.2.0"
@@ -8858,10 +8923,10 @@ vite-plugin-svgr@^4.5.0:
"@svgr/core" "^8.1.0"
"@svgr/plugin-jsx" "^8.1.0"
-vite@5.4.20:
- version "5.4.20"
- resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.20.tgz#3267a5e03f21212f44edfd72758138e8fcecd76a"
- integrity sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==
+vite@5.4.21:
+ version "5.4.21"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
+ integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
From 54632bd06d2cec7df82e23c8d74380b5bfb023a4 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 09:16:55 +0000
Subject: [PATCH 043/118] ci: testing windows 2019
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c269359b..5062c7ad 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
- os: [windows-2019, ubuntu-latest]
+ os: [windows-2022, ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
From eb006c5e9097e259c7066a48ea1f205043148173 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 09:51:11 +0000
Subject: [PATCH 044/118] ci: testing windows 2019
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5062c7ad..d6bc0785 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
- os: [windows-2022, ubuntu-latest]
+ os: [self-hosted, ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
From 549e1270ee12121ad6fa3351a8c31cfb23a96e6c Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 09:56:45 +0000
Subject: [PATCH 045/118] ci: testing windows
From 1effa8031164d47d96783b5cdeeee2be1d09294d Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 10:35:20 +0000
Subject: [PATCH 046/118] ci: testing windows
---
.github/workflows/build.yml | 191 ++++++++++++++++++++----------------
1 file changed, 108 insertions(+), 83 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d6bc0785..ebceea1e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,93 +7,118 @@ concurrency:
on: pull_request
jobs:
- build:
- strategy:
- matrix:
- os: [self-hosted, ubuntu-latest]
- fail-fast: false
-
- runs-on: ${{ matrix.os }}
-
+ verify-openssl:
+ runs-on: windows-2022
steps:
- - name: Check out Git repository
+ - name: Check out repository
uses: actions/checkout@v4
- - name: Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20.18.3
-
- - name: Install dependencies
- run: yarn --frozen-lockfile
-
- - name: Install Python
- uses: actions/setup-python@v5
- with:
- python-version: 3.9
-
- - name: Install dependencies
- run: pip install -r requirements.txt
-
- - name: Build with cx_Freeze
- run: python python_rpc/setup.py build
-
- - name: Build Linux
- if: matrix.os == 'ubuntu-latest'
+ - name: Verify OpenSSL 1.1 DLLs exist
+ shell: pwsh
run: |
- yarn build:linux
- env:
- MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
- MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
- MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
- MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
- RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
- MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
- RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
+ $dll1 = "vendor/openssl-1.1/win64/libcrypto-1_1-x64.dll"
+ $dll2 = "vendor/openssl-1.1/win64/libssl-1_1-x64.dll"
- - name: Build Windows
- if: matrix.os == 'windows-2022'
- run: yarn build:win
- env:
- MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
- MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
- MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
- MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
- RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
- MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
- RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
+ $missing = @()
- - name: Upload build
- env:
- BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
- S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
- S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
- S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
- S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
- BUILDS_URL: ${{ secrets.BUILDS_URL }}
- BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
- GITHUB_ACTOR: ${{ github.actor }}
- run: node scripts/upload-build.cjs
+ if (-not (Test-Path $dll1)) { $missing += $dll1 }
+ if (-not (Test-Path $dll2)) { $missing += $dll2 }
- - name: Create artifact
- uses: actions/upload-artifact@v4
- with:
- name: Build-${{ matrix.os }}
- path: |
- dist/*-portable.exe
- dist/*.zip
- dist/*.dmg
- dist/*.deb
- dist/*.rpm
- dist/*.tar.gz
- dist/*.yml
- dist/*.blockmap
- dist/*.AppImage
+ if ($missing.Count -gt 0) {
+ Write-Error "Missing required OpenSSL 1.1 DLLs:`n$($missing -join "`n")"
+ exit 1
+ } else {
+ Write-Host "✅ Both OpenSSL 1.1 DLLs found"
+ }
+
+# jobs:
+# build:
+# strategy:
+# matrix:
+# os: [windows-2022, ubuntu-latest]
+# fail-fast: false
+
+# runs-on: ${{ matrix.os }}
+
+# steps:
+# - name: Check out Git repository
+# uses: actions/checkout@v4
+
+# - name: Install Node.js
+# uses: actions/setup-node@v4
+# with:
+# node-version: 20.18.3
+
+# - name: Install dependencies
+# run: yarn --frozen-lockfile
+
+# - name: Install Python
+# uses: actions/setup-python@v5
+# with:
+# python-version: 3.9
+
+# - name: Install dependencies
+# run: pip install -r requirements.txt
+
+# - name: Build with cx_Freeze
+# run: python python_rpc/setup.py build
+
+# - name: Build Linux
+# if: matrix.os == 'ubuntu-latest'
+# run: |
+# yarn build:linux
+# env:
+# MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
+# MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
+# MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
+# MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
+# RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+# MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+# RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
+# RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
+# RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
+
+# - name: Build Windows
+# if: matrix.os == 'windows-2022'
+# run: yarn build:win
+# env:
+# MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
+# MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
+# MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
+# MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
+# RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+# MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+# RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
+# RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
+# RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
+
+# - name: Upload build
+# env:
+# BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+# S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
+# S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
+# S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+# S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
+# BUILDS_URL: ${{ secrets.BUILDS_URL }}
+# BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
+# GITHUB_ACTOR: ${{ github.actor }}
+# run: node scripts/upload-build.cjs
+
+# - name: Create artifact
+# uses: actions/upload-artifact@v4
+# with:
+# name: Build-${{ matrix.os }}
+# path: |
+# dist/*-portable.exe
+# dist/*.zip
+# dist/*.dmg
+# dist/*.deb
+# dist/*.rpm
+# dist/*.tar.gz
+# dist/*.yml
+# dist/*.blockmap
+# dist/*.AppImage
From 3ab1e2957821ebf44bc80fca93c4689d730b6457 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 10:47:19 +0000
Subject: [PATCH 047/118] ci: testing windows
---
.github/workflows/build.yml | 206 +++++++++++++++++-------------------
1 file changed, 100 insertions(+), 106 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ebceea1e..598b157f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,124 +1,118 @@
name: Build
+on:
+ pull_request:
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
-on: pull_request
-
jobs:
- verify-openssl:
- runs-on: windows-2022
+ build:
+ strategy:
+ matrix:
+ os: [windows-2022, ubuntu-latest]
+ fail-fast: false
+
+ runs-on: ${{ matrix.os }}
+
steps:
- - name: Check out repository
+ - name: Check out Git repository
uses: actions/checkout@v4
- - name: Verify OpenSSL 1.1 DLLs exist
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.18.3
+
+ - name: Install dependencies
+ run: yarn --frozen-lockfile
+
+ - name: Install Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: 3.9
+
+ - name: Install dependencies
+ run: pip install -r requirements.txt
+
+ - name: Download OpenSSL 1.1.1w installer
+ if: matrix.os == 'windows-2022'
shell: pwsh
run: |
- $dll1 = "vendor/openssl-1.1/win64/libcrypto-1_1-x64.dll"
- $dll2 = "vendor/openssl-1.1/win64/libssl-1_1-x64.dll"
+ $url = "https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe"
+ $out = "$env:RUNNER_TEMP\Win64OpenSSL-1_1_1w.exe"
+ Invoke-WebRequest $url -OutFile $out
- $missing = @()
+ - name: Silent install OpenSSL 1.1.1w
+ if: matrix.os == 'windows-2022'
+ shell: pwsh
+ run: |
+ $installer = "$env:RUNNER_TEMP\Win64OpenSSL-1_1_1w.exe"
+ if (!(Test-Path $installer)) { Write-Error "Installer not found: $installer"; exit 1 }
+ $dest = Join-Path $env:ProgramFiles "OpenSSL-Win64"
+ $args = "/VERYSILENT /SUPPRESSMSGBOXES /SP- /NORESTART /DIR=""$dest"""
+ Start-Process -FilePath $installer -ArgumentList $args -Wait -NoNewWindow
- if (-not (Test-Path $dll1)) { $missing += $dll1 }
- if (-not (Test-Path $dll2)) { $missing += $dll2 }
+ - name: Build with cx_Freeze
+ run: python python_rpc/setup.py build
- if ($missing.Count -gt 0) {
- Write-Error "Missing required OpenSSL 1.1 DLLs:`n$($missing -join "`n")"
- exit 1
- } else {
- Write-Host "✅ Both OpenSSL 1.1 DLLs found"
- }
+ - name: Build Linux
+ if: matrix.os == 'ubuntu-latest'
+ run: |
+ yarn build:linux
+ env:
+ MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
+ MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
+ MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
+ MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
+ RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+ MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
+ RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
+ RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
-# jobs:
-# build:
-# strategy:
-# matrix:
-# os: [windows-2022, ubuntu-latest]
-# fail-fast: false
+ - name: Build Windows
+ if: matrix.os == 'windows-2022'
+ run: yarn build:win
+ env:
+ MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
+ MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
+ MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
+ MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
+ RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+ MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
+ RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
+ RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
-# runs-on: ${{ matrix.os }}
+ - name: Upload build
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+ S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
+ S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
+ S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+ S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
+ BUILDS_URL: ${{ secrets.BUILDS_URL }}
+ BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
+ GITHUB_ACTOR: ${{ github.actor }}
+ run: node scripts/upload-build.cjs
-# steps:
-# - name: Check out Git repository
-# uses: actions/checkout@v4
-
-# - name: Install Node.js
-# uses: actions/setup-node@v4
-# with:
-# node-version: 20.18.3
-
-# - name: Install dependencies
-# run: yarn --frozen-lockfile
-
-# - name: Install Python
-# uses: actions/setup-python@v5
-# with:
-# python-version: 3.9
-
-# - name: Install dependencies
-# run: pip install -r requirements.txt
-
-# - name: Build with cx_Freeze
-# run: python python_rpc/setup.py build
-
-# - name: Build Linux
-# if: matrix.os == 'ubuntu-latest'
-# run: |
-# yarn build:linux
-# env:
-# MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
-# MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
-# MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
-# MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
-# RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
-# MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
-# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
-# RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
-# RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
-# RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
-
-# - name: Build Windows
-# if: matrix.os == 'windows-2022'
-# run: yarn build:win
-# env:
-# MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
-# MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
-# MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
-# MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
-# RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
-# MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
-# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
-# RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
-# RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
-# RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
-
-# - name: Upload build
-# env:
-# BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
-# S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
-# S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
-# S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
-# S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
-# BUILDS_URL: ${{ secrets.BUILDS_URL }}
-# BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
-# GITHUB_ACTOR: ${{ github.actor }}
-# run: node scripts/upload-build.cjs
-
-# - name: Create artifact
-# uses: actions/upload-artifact@v4
-# with:
-# name: Build-${{ matrix.os }}
-# path: |
-# dist/*-portable.exe
-# dist/*.zip
-# dist/*.dmg
-# dist/*.deb
-# dist/*.rpm
-# dist/*.tar.gz
-# dist/*.yml
-# dist/*.blockmap
-# dist/*.AppImage
+ - name: Create artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: Build-${{ matrix.os }}
+ path: |
+ dist/*-portable.exe
+ dist/*.zip
+ dist/*.dmg
+ dist/*.deb
+ dist/*.rpm
+ dist/*.tar.gz
+ dist/*.yml
+ dist/*.blockmap
+ dist/*.AppImage
From fc6068d6035654d57d3671e739ee717659a8dec2 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 27 Oct 2025 12:33:29 +0000
Subject: [PATCH 048/118] fix: fixing dlls
---
.github/workflows/build.yml | 24 ++++++------------------
1 file changed, 6 insertions(+), 18 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 598b157f..949bc864 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -36,27 +36,15 @@ jobs:
- name: Install dependencies
run: pip install -r requirements.txt
- - name: Download OpenSSL 1.1.1w installer
- if: matrix.os == 'windows-2022'
- shell: pwsh
- run: |
- $url = "https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe"
- $out = "$env:RUNNER_TEMP\Win64OpenSSL-1_1_1w.exe"
- Invoke-WebRequest $url -OutFile $out
-
- - name: Silent install OpenSSL 1.1.1w
- if: matrix.os == 'windows-2022'
- shell: pwsh
- run: |
- $installer = "$env:RUNNER_TEMP\Win64OpenSSL-1_1_1w.exe"
- if (!(Test-Path $installer)) { Write-Error "Installer not found: $installer"; exit 1 }
- $dest = Join-Path $env:ProgramFiles "OpenSSL-Win64"
- $args = "/VERYSILENT /SUPPRESSMSGBOXES /SP- /NORESTART /DIR=""$dest"""
- Start-Process -FilePath $installer -ArgumentList $args -Wait -NoNewWindow
-
- name: Build with cx_Freeze
run: python python_rpc/setup.py build
+ - name: Copy OpenSSL DLLs
+ if: matrix.os == 'windows-2022'
+ run: |
+ cp hydra-python-rpc/lib/libcrypto-1_1.dll hydra-python-rpc/lib/libcrypto-1_1-x64.dll
+ cp hydra-python-rpc/lib/libssl-1_1.dll hydra-python-rpc/lib/libssl-1_1-x64.dll
+
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
From 1e1a1c61c97f9128de487fe0db7b2b430ee9aad7 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Mon, 27 Oct 2025 19:22:59 +0200
Subject: [PATCH 049/118] feat: showing playtime in review and changed
positions of elements in review
---
src/locales/en/translation.json | 1 +
.../src/pages/game-details/review-item.scss | 56 +++++++++----
.../src/pages/game-details/review-item.tsx | 78 +++++++++++--------
src/types/index.ts | 1 +
4 files changed, 88 insertions(+), 48 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 46bdb28c..e578b251 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -223,6 +223,7 @@
"show_more": "Show more",
"show_less": "Show less",
"reviews": "Reviews",
+ "played_for": "Played for",
"leave_a_review": "Leave a Review",
"write_review_placeholder": "Share your thoughts about this game...",
"sort_newest": "Newest",
diff --git a/src/renderer/src/pages/game-details/review-item.scss b/src/renderer/src/pages/game-details/review-item.scss
index d4f2d38c..b3577a75 100644
--- a/src/renderer/src/pages/game-details/review-item.scss
+++ b/src/renderer/src/pages/game-details/review-item.scss
@@ -22,7 +22,13 @@
&__review-user-info {
display: flex;
flex-direction: column;
- gap: calc(globals.$spacing-unit * 0.25);
+ gap: calc(globals.$spacing-unit * 0.45);
+ }
+
+ &__review-meta-row {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit * 0.75);
}
&__review-display-name {
@@ -157,28 +163,28 @@
&__review-score-stars {
display: flex;
align-items: center;
- gap: 2px;
+ gap: 4px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 2px 6px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ font-size: 11px;
+ font-weight: 500;
+ }
+
+ &__review-right {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
}
&__review-star {
- color: #666666;
+ color: rgba(255, 255, 255, 0.7);
transition: color 0.2s ease;
cursor: default;
&--filled {
- color: #ffffff;
-
- &.game-details__review-score--red {
- color: #fca5a5;
- }
-
- &.game-details__review-score--yellow {
- color: #fcd34d;
- }
-
- &.game-details__review-score--green {
- color: #86efac;
- }
+ color: rgba(255, 255, 255, 0.7);
}
&--empty {
@@ -198,6 +204,24 @@
font-size: globals.$small-font-size;
}
+ &__review-playtime {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 11px;
+ font-weight: 500;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 2px 6px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ margin-top: 0;
+
+ svg {
+ color: rgba(255, 255, 255, 0.6);
+ }
+ }
+
&__review-content {
color: globals.$body-color;
line-height: 1.5;
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index f5e3528a..d6b9fd9c 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -7,9 +7,10 @@ import { useState } from "react";
import type { GameReview } from "@types";
import { sanitizeHtml } from "@shared";
-import { useDate } from "@renderer/hooks";
+import { useDate, useFormat } from "@renderer/hooks";
import { formatNumber } from "@renderer/helpers";
import { Avatar } from "@renderer/components";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import "./review-item.scss";
@@ -29,12 +30,7 @@ interface ReviewItemProps {
) => void;
}
-const getScoreColorClass = (score: number): string => {
- if (score >= 1 && score <= 2) return "game-details__review-score--red";
- if (score >= 3 && score <= 3) return "game-details__review-score--yellow";
- if (score >= 4 && score <= 5) return "game-details__review-score--green";
- return "";
-};
+
const getRatingText = (score: number, t: (key: string) => string): string => {
switch (score) {
@@ -68,6 +64,7 @@ export function ReviewItem({
const navigate = useNavigate();
const { t, i18n } = useTranslation("game_details");
const { formatDistance } = useDate();
+ const { numberFormatter } = useFormat();
const [showOriginal, setShowOriginal] = useState(false);
@@ -100,6 +97,20 @@ export function ReviewItem({
}
};
+ // Format playtime similar to hero panel
+ const formatPlayTime = (playTimeInSeconds: number) => {
+ const minutes = playTimeInSeconds / 60;
+
+ if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
+ return t("amount_minutes", {
+ amount: minutes.toFixed(0),
+ });
+ }
+
+ const hours = minutes / 60;
+ return t("amount_hours", { amount: numberFormatter.format(hours) });
+ };
+
// Determine which content to show - always show original for own reviews
const displayContent = needsTranslation
? review.translations[i18n.language]
@@ -144,34 +155,37 @@ export function ReviewItem({
>
{review.user.displayName || "Anonymous"}
-
-
- {formatDistance(new Date(review.createdAt), new Date(), {
- addSuffix: true,
- })}
+
+
+
+
+ {review.score}/5
+
+
+ {review.playTimeInSeconds && review.playTimeInSeconds > 0 && (
+
+
+
+ {t("played_for")} {formatPlayTime(review.playTimeInSeconds)}
+
+
+ )}
-
- {[1, 2, 3, 4, 5].map((starValue) => (
-
- ))}
+
+
+
+ {formatDistance(new Date(review.createdAt), new Date(), {
+ addSuffix: true,
+ })}
+
diff --git a/src/types/index.ts b/src/types/index.ts
index 63b18645..38abdbb8 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -255,6 +255,7 @@ export interface GameReview {
isBlocked: boolean;
hasUpvoted: boolean;
hasDownvoted: boolean;
+ playTimeInSeconds?: number;
user: {
id: string;
displayName: string;
From 9e09a5decb7ed2556b9b881fc4d8f6c9a72c1a92 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Mon, 27 Oct 2025 19:28:29 +0200
Subject: [PATCH 050/118] fix: translation key fix and formatting
---
src/locales/en/translation.json | 2 +-
.../src/pages/game-details/review-item.tsx | 27 +++++++++----------
2 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index e578b251..cef82a4b 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -223,7 +223,7 @@
"show_more": "Show more",
"show_less": "Show less",
"reviews": "Reviews",
- "played_for": "Played for",
+ "review_played_for": "Played for",
"leave_a_review": "Leave a Review",
"write_review_placeholder": "Share your thoughts about this game...",
"sort_newest": "Newest",
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index d6b9fd9c..8ffe42c0 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -30,8 +30,6 @@ interface ReviewItemProps {
) => void;
}
-
-
const getRatingText = (score: number, t: (key: string) => string): string => {
switch (score) {
case 1:
@@ -157,22 +155,23 @@ export function ReviewItem({
-
-
- {review.score}/5
-
-
+ className="game-details__review-score-stars"
+ title={getRatingText(review.score, t)}
+ >
+
+
+ {review.score}/5
+
+
{review.playTimeInSeconds && review.playTimeInSeconds > 0 && (
- {t("played_for")} {formatPlayTime(review.playTimeInSeconds)}
+ {t("review_played_for")}{" "}
+ {formatPlayTime(review.playTimeInSeconds)}
)}
From b431ed479c84aa9406ae2fb581fe0cbfe880733e Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Mon, 27 Oct 2025 20:07:08 +0200
Subject: [PATCH 051/118] fix: converted conditional to boolean
---
src/renderer/src/pages/game-details/review-item.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index 8ffe42c0..15b12fbe 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -166,12 +166,14 @@ export function ReviewItem({
{review.score}/5
- {review.playTimeInSeconds && review.playTimeInSeconds > 0 && (
+ {Boolean(
+ review.playTimeInSeconds && review.playTimeInSeconds > 0
+ ) && (
{t("review_played_for")}{" "}
- {formatPlayTime(review.playTimeInSeconds)}
+ {formatPlayTime(review.playTimeInSeconds!)}
)}
From 5c770bc7e7eed580485b8e68d423f21e9df7d3bb Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Mon, 27 Oct 2025 20:12:24 +0200
Subject: [PATCH 052/118] fix: unnecessary assertion
---
src/renderer/src/pages/game-details/review-item.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index 15b12fbe..82d7128c 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -173,7 +173,7 @@ export function ReviewItem({
{t("review_played_for")}{" "}
- {formatPlayTime(review.playTimeInSeconds!)}
+ {formatPlayTime(review.playTimeInSeconds || 0)}
)}
From ddd6af0d4c945a3b0907ce94645616a324e2e6f8 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Mon, 27 Oct 2025 15:22:47 -0300
Subject: [PATCH 053/118] fix: add theme editor dev tools back
---
src/main/services/window-manager.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 7055fc09..178eb8de 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -462,6 +462,7 @@ export class WindowManager {
editorWindow.once("ready-to-show", () => {
editorWindow.show();
+ this.mainWindow?.webContents.openDevTools();
if (!app.isPackaged || isStaging) {
editorWindow.webContents.openDevTools();
}
@@ -474,6 +475,7 @@ export class WindowManager {
});
editorWindow.on("close", () => {
+ this.mainWindow?.webContents.closeDevTools();
this.editorWindows.delete(themeId);
});
}
From 61072aa02a1a36702e03687a34e3864ae53cee72 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Mon, 27 Oct 2025 15:25:18 -0300
Subject: [PATCH 054/118] fix: add theme editor dev tools back
---
src/main/services/window-manager.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 178eb8de..118ff98b 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -470,7 +470,7 @@ export class WindowManager {
editorWindow.webContents.on("before-input-event", (_event, input) => {
if (input.key === "F12") {
- editorWindow.webContents.toggleDevTools();
+ this.mainWindow?.webContents.toggleDevTools();
}
});
From 1123aaa65ea49bea512a3eef23210ce8389eddb2 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Tue, 28 Oct 2025 06:48:42 -0300
Subject: [PATCH 055/118] chore: remove zod dep
---
package.json | 3 +--
yarn.lock | 5 -----
2 files changed, 1 insertion(+), 7 deletions(-)
diff --git a/package.json b/package.json
index 08c1d80e..9ed25fa9 100644
--- a/package.json
+++ b/package.json
@@ -90,8 +90,7 @@
"winreg": "^1.2.5",
"ws": "^8.18.1",
"yaml": "^2.6.1",
- "yup": "^1.5.0",
- "zod": "^3.24.1"
+ "yup": "^1.5.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.705.0",
diff --git a/yarn.lock b/yarn.lock
index 6340a43f..6fb80492 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9190,8 +9190,3 @@ yup@^1.5.0:
tiny-case "^1.0.3"
toposort "^2.0.2"
type-fest "^2.19.0"
-
-zod@^3.24.1:
- version "3.25.76"
- resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
- integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
From 120aad6c1cacc2406aad9def4baa63b89d69eef8 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Tue, 28 Oct 2025 17:34:20 +0200
Subject: [PATCH 056/118] feat: Hide to tray on game startup and ability to
disable trailers auto-play
---
src/locales/en/translation.json | 4 +++-
src/main/services/process-watcher.ts | 15 +++++++++++-
.../gallery-slider/gallery-slider.tsx | 7 +++++-
.../src/pages/settings/settings-behavior.tsx | 24 +++++++++++++++++++
src/types/level.types.ts | 2 ++
5 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 46bdb28c..b17dda80 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -541,7 +541,9 @@
"hidden": "Hidden",
"test_notification": "Test notification",
"notification_preview": "Achievement Notification Preview",
- "enable_friend_start_game_notifications": "When a friend starts playing a game"
+ "enable_friend_start_game_notifications": "When a friend starts playing a game",
+ "autoplay_trailers_on_game_page": "Automatically start playing trailers on game page",
+ "hide_to_tray_on_game_start": "Hide Hydra to tray on game startup"
},
"notifications": {
"download_complete": "Download complete",
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index 06f5f7d8..9ca40b24 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -4,7 +4,8 @@ import type { Game, GameRunning } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
import { ProcessPayload } from "./download/types";
-import { gamesSublevel, levelKeys } from "@main/level";
+import { db, gamesSublevel, levelKeys } from "@main/level";
+import type { UserPreferences } from "@types";
import { CloudSync } from "./cloud-sync";
import { logger } from "./logger";
import path from "path";
@@ -209,6 +210,18 @@ function onOpenGame(game: Game) {
lastSyncTick: now,
});
+ // Hide Hydra to tray on game startup if enabled
+ db
+ .get(levelKeys.userPreferences, {
+ valueEncoding: "json",
+ })
+ .then((userPreferences) => {
+ if (userPreferences?.hideToTrayOnGameStart) {
+ WindowManager.mainWindow?.hide();
+ }
+ })
+ .catch(() => {});
+
if (game.remoteId) {
updateGamePlaytime(
game,
diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx
index 4bf8dc48..c9658636 100644
--- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx
+++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx
@@ -7,11 +7,16 @@ import {
} from "@primer/octicons-react";
import useEmblaCarousel from "embla-carousel-react";
import { gameDetailsContext } from "@renderer/context";
+import { useAppSelector } from "@renderer/hooks";
import "./gallery-slider.scss";
export function GallerySlider() {
const { shopDetails } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
+ const userPreferences = useAppSelector(
+ (state) => state.userPreferences.value
+ );
+ const autoplayEnabled = userPreferences?.autoplayGameTrailers !== false;
const hasScreenshots = shopDetails && shopDetails.screenshots?.length;
@@ -164,7 +169,7 @@ export function GallerySlider() {
poster={item.poster}
loop
muted
- autoPlay
+ autoPlay={autoplayEnabled}
tabIndex={-1}
>
diff --git a/src/renderer/src/pages/settings/settings-behavior.tsx b/src/renderer/src/pages/settings/settings-behavior.tsx
index 64df52d7..bc91fc9d 100644
--- a/src/renderer/src/pages/settings/settings-behavior.tsx
+++ b/src/renderer/src/pages/settings/settings-behavior.tsx
@@ -27,6 +27,8 @@ export function SettingsBehavior() {
showDownloadSpeedInMegabytes: false,
extractFilesByDefault: true,
enableSteamAchievements: false,
+ autoplayGameTrailers: true,
+ hideToTrayOnGameStart: false,
});
const { t } = useTranslation("settings");
@@ -49,6 +51,10 @@ export function SettingsBehavior() {
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
enableSteamAchievements:
userPreferences.enableSteamAchievements ?? false,
+ autoplayGameTrailers:
+ userPreferences.autoplayGameTrailers ?? true,
+ hideToTrayOnGameStart:
+ userPreferences.hideToTrayOnGameStart ?? false,
});
}
}, [userPreferences]);
@@ -76,6 +82,16 @@ export function SettingsBehavior() {
}
/>
+
+ handleChange({
+ hideToTrayOnGameStart: !form.hideToTrayOnGameStart,
+ })
+ }
+ />
+
{showRunAtStartup && (
)}
+
+ handleChange({ autoplayGameTrailers: !form.autoplayGameTrailers })
+ }
+ />
+
Date: Tue, 28 Oct 2025 17:36:11 +0200
Subject: [PATCH 057/118] ci: formatting
---
src/main/services/process-watcher.ts | 7 +++----
src/renderer/src/pages/settings/settings-behavior.tsx | 6 ++----
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index 9ca40b24..4130daba 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -211,10 +211,9 @@ function onOpenGame(game: Game) {
});
// Hide Hydra to tray on game startup if enabled
- db
- .get(levelKeys.userPreferences, {
- valueEncoding: "json",
- })
+ db.get(levelKeys.userPreferences, {
+ valueEncoding: "json",
+ })
.then((userPreferences) => {
if (userPreferences?.hideToTrayOnGameStart) {
WindowManager.mainWindow?.hide();
diff --git a/src/renderer/src/pages/settings/settings-behavior.tsx b/src/renderer/src/pages/settings/settings-behavior.tsx
index bc91fc9d..c5698ef7 100644
--- a/src/renderer/src/pages/settings/settings-behavior.tsx
+++ b/src/renderer/src/pages/settings/settings-behavior.tsx
@@ -51,10 +51,8 @@ export function SettingsBehavior() {
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
enableSteamAchievements:
userPreferences.enableSteamAchievements ?? false,
- autoplayGameTrailers:
- userPreferences.autoplayGameTrailers ?? true,
- hideToTrayOnGameStart:
- userPreferences.hideToTrayOnGameStart ?? false,
+ autoplayGameTrailers: userPreferences.autoplayGameTrailers ?? true,
+ hideToTrayOnGameStart: userPreferences.hideToTrayOnGameStart ?? false,
});
}
}, [userPreferences]);
From dbf5d7afc76bdfb7620c2597c744453a89cf40ad Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Tue, 28 Oct 2025 17:43:19 +0200
Subject: [PATCH 058/118] fix: multiple imports
---
src/main/services/process-watcher.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index 4130daba..6408c30d 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -1,11 +1,10 @@
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
-import type { Game, GameRunning } from "@types";
+import type { Game, GameRunning, UserPreferences } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
import { ProcessPayload } from "./download/types";
import { db, gamesSublevel, levelKeys } from "@main/level";
-import type { UserPreferences } from "@types";
import { CloudSync } from "./cloud-sync";
import { logger } from "./logger";
import path from "path";
From 6b96c99bb177f18dc8a54834acea21cfc4c0c495 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 21:37:28 +0000
Subject: [PATCH 059/118] ci: fixing release pipeline
---
.github/workflows/release.yml | 6 +++
src/locales/en/translation.json | 1 +
src/locales/pt-BR/translation.json | 3 ++
.../download-sources/add-download-source.ts | 9 +++-
.../remove-download-source.ts | 2 +-
src/main/main.ts | 7 +++-
src/main/services/hydra-api.ts | 5 ++-
src/main/services/index.ts | 1 +
src/main/services/user/index.ts | 3 ++
.../services/user/sync-download-sources.ts | 42 +++++++++++++++++++
.../settings/add-download-source-modal.tsx | 6 ++-
11 files changed, 80 insertions(+), 5 deletions(-)
create mode 100644 src/main/services/user/index.ts
create mode 100644 src/main/services/user/sync-download-sources.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3ceb42c7..9524c4b9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -39,6 +39,12 @@ jobs:
- name: Build with cx_Freeze
run: python python_rpc/setup.py build
+ - name: Copy OpenSSL DLLs
+ if: matrix.os == 'windows-2022'
+ run: |
+ cp hydra-python-rpc/lib/libcrypto-1_1.dll hydra-python-rpc/lib/libcrypto-1_1-x64.dll
+ cp hydra-python-rpc/lib/libssl-1_1.dll hydra-python-rpc/lib/libssl-1_1-x64.dll
+
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index bfc4a379..3977c27d 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -430,6 +430,7 @@
"add_download_source": "Add source",
"adding": "Adding…",
"failed_add_download_source": "Failed to add download source. Please try again.",
+ "download_source_already_exists": "This download source URL already exists.",
"download_count_zero": "No download options",
"download_count_one": "{{countFormatted}} download option",
"download_count_other": "{{countFormatted}} download options",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 968483a6..c9e908ac 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -416,6 +416,9 @@
"validate_download_source": "Validar",
"remove_download_source": "Remover",
"add_download_source": "Adicionar fonte",
+ "adding": "Adicionando…",
+ "failed_add_download_source": "Falha ao adicionar fonte de download. Tente novamente.",
+ "download_source_already_exists": "Esta URL de fonte de download já existe.",
"download_count_zero": "Sem downloads na lista",
"download_count_one": "{{countFormatted}} download na lista",
"download_count_other": "{{countFormatted}} downloads na lista",
diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts
index d4e65ef3..ee426a82 100644
--- a/src/main/events/download-sources/add-download-source.ts
+++ b/src/main/events/download-sources/add-download-source.ts
@@ -9,6 +9,13 @@ const addDownloadSource = async (
url: string
) => {
try {
+ const existingSources = await downloadSourcesSublevel.values().all();
+ const urlExists = existingSources.some((source) => source.url === url);
+
+ if (urlExists) {
+ throw new Error("Download source with this URL already exists");
+ }
+
const downloadSource = await HydraApi.post(
"/download-sources",
{
@@ -17,7 +24,7 @@ const addDownloadSource = async (
{ needsAuth: false }
);
- if (HydraApi.isLoggedIn()) {
+ if (HydraApi.isLoggedIn() && HydraApi.hasActiveSubscription()) {
try {
await HydraApi.post("/profile/download-sources", {
urls: [url],
diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts
index 8efe0072..9caeaba5 100644
--- a/src/main/events/download-sources/remove-download-source.ts
+++ b/src/main/events/download-sources/remove-download-source.ts
@@ -13,7 +13,7 @@ const removeDownloadSource = async (
if (downloadSourceId) params.set("downloadSourceId", downloadSourceId);
- if (HydraApi.isLoggedIn()) {
+ if (HydraApi.isLoggedIn() && HydraApi.hasActiveSubscription()) {
void HydraApi.delete(`/profile/download-sources?${params.toString()}`);
}
diff --git a/src/main/main.ts b/src/main/main.ts
index 6e477a18..f2440b9f 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -50,9 +50,14 @@ export const loadState = async () => {
DeckyPlugin.checkAndUpdateIfOutdated();
}
- await HydraApi.setupApi().then(() => {
+ await HydraApi.setupApi().then(async () => {
uploadGamesBatch();
void migrateDownloadSources();
+
+ const { syncDownloadSourcesFromApi } = await import(
+ "./services/user"
+ );
+ void syncDownloadSourcesFromApi();
// WSClient.connect();
});
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index ffc5756c..12090df3 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -46,7 +46,7 @@ export class HydraApi {
return this.userAuth.authToken !== "";
}
- private static hasActiveSubscription() {
+ public static hasActiveSubscription() {
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
return expiresAt > new Date();
}
@@ -105,6 +105,9 @@ export class HydraApi {
// WSClient.close();
// WSClient.connect();
+
+ const { syncDownloadSourcesFromApi } = await import("./user");
+ syncDownloadSourcesFromApi();
}
}
diff --git a/src/main/services/index.ts b/src/main/services/index.ts
index 88b39d1b..da4e6848 100644
--- a/src/main/services/index.ts
+++ b/src/main/services/index.ts
@@ -18,3 +18,4 @@ export * from "./library-sync";
export * from "./wine";
export * from "./lock";
export * from "./decky-plugin";
+export * from "./user";
diff --git a/src/main/services/user/index.ts b/src/main/services/user/index.ts
new file mode 100644
index 00000000..b5001f7a
--- /dev/null
+++ b/src/main/services/user/index.ts
@@ -0,0 +1,3 @@
+export * from "./get-user-data";
+export * from "./sync-download-sources";
+
diff --git a/src/main/services/user/sync-download-sources.ts b/src/main/services/user/sync-download-sources.ts
new file mode 100644
index 00000000..c5695d68
--- /dev/null
+++ b/src/main/services/user/sync-download-sources.ts
@@ -0,0 +1,42 @@
+import { HydraApi, logger } from "../";
+import { downloadSourcesSublevel } from "@main/level";
+import type { DownloadSource } from "@types";
+
+export const syncDownloadSourcesFromApi = async () => {
+ if (!HydraApi.isLoggedIn() || !HydraApi.hasActiveSubscription()) {
+ return;
+ }
+
+ try {
+ const profileSources = await HydraApi.get(
+ "/profile/download-sources"
+ );
+
+ const existingSources = await downloadSourcesSublevel.values().all();
+ const existingUrls = new Set(existingSources.map((source) => source.url));
+
+ for (const downloadSource of profileSources) {
+ if (!existingUrls.has(downloadSource.url)) {
+ try {
+ await downloadSourcesSublevel.put(downloadSource.id, {
+ ...downloadSource,
+ isRemote: true,
+ createdAt: new Date().toISOString(),
+ });
+
+ logger.log(
+ `Synced download source from profile: ${downloadSource.url}`
+ );
+ } catch (error) {
+ logger.error(
+ `Failed to sync download source ${downloadSource.url}:`,
+ error
+ );
+ }
+ }
+ }
+ } catch (error) {
+ logger.error("Failed to sync download sources from API:", error);
+ }
+};
+
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx
index d7071391..d96c67a5 100644
--- a/src/renderer/src/pages/settings/add-download-source-modal.tsx
+++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx
@@ -57,9 +57,13 @@ export function AddDownloadSourceModal({
onAddDownloadSource();
} catch (error) {
logger.error("Failed to add download source:", error);
+ const errorMessage = error instanceof Error && error.message.includes("already exists")
+ ? t("download_source_already_exists")
+ : t("failed_add_download_source");
+
setError("url", {
type: "server",
- message: t("failed_add_download_source"),
+ message: errorMessage,
});
} finally {
setIsLoading(false);
From a11b3e887796966b64e2ec73786f27d9c5a2e741 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 21:38:07 +0000
Subject: [PATCH 060/118] ci: fixing release pipeline
---
src/main/events/download-sources/add-download-source.ts | 2 +-
src/main/main.ts | 6 ++----
src/main/services/user/index.ts | 1 -
src/main/services/user/sync-download-sources.ts | 1 -
.../src/pages/settings/add-download-source-modal.tsx | 9 +++++----
5 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts
index ee426a82..bea009cb 100644
--- a/src/main/events/download-sources/add-download-source.ts
+++ b/src/main/events/download-sources/add-download-source.ts
@@ -11,7 +11,7 @@ const addDownloadSource = async (
try {
const existingSources = await downloadSourcesSublevel.values().all();
const urlExists = existingSources.some((source) => source.url === url);
-
+
if (urlExists) {
throw new Error("Download source with this URL already exists");
}
diff --git a/src/main/main.ts b/src/main/main.ts
index f2440b9f..ffb8f8a9 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -53,10 +53,8 @@ export const loadState = async () => {
await HydraApi.setupApi().then(async () => {
uploadGamesBatch();
void migrateDownloadSources();
-
- const { syncDownloadSourcesFromApi } = await import(
- "./services/user"
- );
+
+ const { syncDownloadSourcesFromApi } = await import("./services/user");
void syncDownloadSourcesFromApi();
// WSClient.connect();
});
diff --git a/src/main/services/user/index.ts b/src/main/services/user/index.ts
index b5001f7a..b1d8c9b7 100644
--- a/src/main/services/user/index.ts
+++ b/src/main/services/user/index.ts
@@ -1,3 +1,2 @@
export * from "./get-user-data";
export * from "./sync-download-sources";
-
diff --git a/src/main/services/user/sync-download-sources.ts b/src/main/services/user/sync-download-sources.ts
index c5695d68..ff9819ce 100644
--- a/src/main/services/user/sync-download-sources.ts
+++ b/src/main/services/user/sync-download-sources.ts
@@ -39,4 +39,3 @@ export const syncDownloadSourcesFromApi = async () => {
logger.error("Failed to sync download sources from API:", error);
}
};
-
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx
index d96c67a5..af6f8b4d 100644
--- a/src/renderer/src/pages/settings/add-download-source-modal.tsx
+++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx
@@ -57,10 +57,11 @@ export function AddDownloadSourceModal({
onAddDownloadSource();
} catch (error) {
logger.error("Failed to add download source:", error);
- const errorMessage = error instanceof Error && error.message.includes("already exists")
- ? t("download_source_already_exists")
- : t("failed_add_download_source");
-
+ const errorMessage =
+ error instanceof Error && error.message.includes("already exists")
+ ? t("download_source_already_exists")
+ : t("failed_add_download_source");
+
setError("url", {
type: "server",
message: errorMessage,
From ce0619bbe398b6e03e61641d3ecfae6d41d5bbca Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 22:40:06 +0000
Subject: [PATCH 061/118] ci: adding releases
---
.github/workflows/build-renderer.yml | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/build-renderer.yml b/.github/workflows/build-renderer.yml
index f7361883..0d219c1e 100644
--- a/.github/workflows/build-renderer.yml
+++ b/.github/workflows/build-renderer.yml
@@ -6,23 +6,38 @@ concurrency:
on:
push:
- branches: [main]
+ branches:
+ - main
+ - release/**
jobs:
build:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ env:
+ NODE_OPTIONS: --max-old-space-size=4096
+ BRANCH_NAME: ${{ github.ref_name }}
+
steps:
- name: Check out Git repository
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- - name: Install Node.js
+ - name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.21.0
+ cache: 'yarn'
+
+ - name: Enable Corepack (Yarn)
+ run: corepack enable
- name: Install dependencies
- run: yarn --frozen-lockfile --ignore-scripts
+ run: yarn install --frozen-lockfile --ignore-scripts
- name: Build Renderer
run: yarn build
@@ -36,5 +51,5 @@ jobs:
run: |
npx --yes wrangler@3 pages deploy out/renderer \
--project-name="hydra" \
- --commit-dirty=true \
- --branch="main"
+ --branch "$BRANCH_NAME" \
+ --commit-dirty
\ No newline at end of file
From dc8a19e8451e8e8466fe1453cb64d1fb9452f7a1 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 23:02:40 +0000
Subject: [PATCH 062/118] ci: adding ci vars
---
.env.example | 1 +
.github/workflows/build-renderer.yml | 4 ++--
.github/workflows/release.yml | 8 +++++---
src/main/services/window-manager.ts | 13 +++++++++----
4 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/.env.example b/.env.example
index 3f914eb3..051d8aa3 100644
--- a/.env.example
+++ b/.env.example
@@ -3,3 +3,4 @@ MAIN_VITE_AUTH_URL=
MAIN_VITE_WS_URL=
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
RENDERER_VITE_TORBOX_REFERRAL_CODE=
+MAIN_VITE_LAUNCHER_SUBDOMAIN=
diff --git a/.github/workflows/build-renderer.yml b/.github/workflows/build-renderer.yml
index 0d219c1e..2904bcb8 100644
--- a/.github/workflows/build-renderer.yml
+++ b/.github/workflows/build-renderer.yml
@@ -31,7 +31,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22.21.0
- cache: 'yarn'
+ cache: "yarn"
- name: Enable Corepack (Yarn)
run: corepack enable
@@ -52,4 +52,4 @@ jobs:
npx --yes wrangler@3 pages deploy out/renderer \
--project-name="hydra" \
--branch "$BRANCH_NAME" \
- --commit-dirty
\ No newline at end of file
+ --commit-dirty
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 75ff209a..7408665f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,7 +6,9 @@ concurrency:
on:
push:
- branches: [main]
+ branches:
+ - main
+ - release/**
jobs:
build:
@@ -61,7 +63,7 @@ jobs:
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
+ MAIN_VITE_LAUNCHER_SUBDOMAIN: ${{ vars.MAIN_VITE_LAUNCHER_SUBDOMAIN }}
- name: Build Windows
if: matrix.os == 'windows-2022'
@@ -78,7 +80,7 @@ jobs:
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
+ MAIN_VITE_LAUNCHER_SUBDOMAIN: ${{ vars.MAIN_VITE_LAUNCHER_SUBDOMAIN }}
- name: Create artifact
uses: actions/upload-artifact@v4
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 7055fc09..4c52b581 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -24,7 +24,8 @@ import type {
UserPreferences,
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
-import { isStaging } from "@main/constants";
+import { appVersion, isStaging } from "@main/constants";
+import { logger } from "./logger";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
@@ -54,6 +55,10 @@ export class WindowManager {
show: false,
};
+ private static formatVersionNumber(version: string) {
+ return version.replaceAll(".", "-");
+ }
+
private static async loadWindowURL(window: BrowserWindow, hash: string = "") {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
@@ -63,12 +68,12 @@ export class WindowManager {
// Try to load from remote URL in production
try {
await window.loadURL(
- `${import.meta.env.MAIN_VITE_RENDERER_URL}#/${hash}`
+ `https://release-${this.formatVersionNumber(appVersion)}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
);
} catch (error) {
// Fall back to local file if remote URL fails
- console.error(
- "Failed to load from MAIN_VITE_RENDERER_URL, falling back to local file:",
+ logger.error(
+ "Failed to load from MAIN_VITE_LAUNCHER_SUBDOMAIN, falling back to local file:",
error
);
window.loadFile(path.join(__dirname, "../renderer/index.html"), {
From 8a12c6e088195a6fb22e30c9ab2af0e1f1a7c3b7 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 23:26:28 +0000
Subject: [PATCH 063/118] chore: sync with main
---
src/main/services/window-manager.ts | 2 +-
src/main/vite-env.d.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 33b6785c..c7335f8a 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -68,7 +68,7 @@ export class WindowManager {
// Try to load from remote URL in production
try {
await window.loadURL(
- `https://release-${this.formatVersionNumber(appVersion)}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
+ `https://release-v${this.formatVersionNumber(appVersion)}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
);
} catch (error) {
// Fall back to local file if remote URL fails
diff --git a/src/main/vite-env.d.ts b/src/main/vite-env.d.ts
index c9b006d5..7b0ed536 100644
--- a/src/main/vite-env.d.ts
+++ b/src/main/vite-env.d.ts
@@ -7,7 +7,7 @@ interface ImportMetaEnv {
readonly MAIN_VITE_CHECKOUT_URL: string;
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
readonly MAIN_VITE_WS_URL: string;
- readonly MAIN_VITE_RENDERER_URL: string;
+ readonly MAIN_VITE_LAUNCHER_SUBDOMAIN: string;
readonly ELECTRON_RENDERER_URL: string;
}
From dc6d578462a392ba359b61a034df7598d70f4a70 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Tue, 28 Oct 2025 23:49:09 +0000
Subject: [PATCH 064/118] chore: sync with main
---
src/main/services/window-manager.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index c7335f8a..aeff3808 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -64,7 +64,7 @@ export class WindowManager {
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`);
- } else if (import.meta.env.MAIN_VITE_RENDERER_URL) {
+ } else if (import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN) {
// Try to load from remote URL in production
try {
await window.loadURL(
From b1069426e4b035703ba2f79aeb758030bc0344c0 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 29 Oct 2025 01:47:35 +0000
Subject: [PATCH 065/118] chore: sync with main
---
package.json | 2 +-
src/main/services/window-manager.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 9ed25fa9..5d84e763 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
- "version": "3.7.1",
+ "version": "3.7.2",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index aeff3808..673bf1a0 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -24,7 +24,7 @@ import type {
UserPreferences,
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
-import { appVersion, isStaging } from "@main/constants";
+import { isStaging } from "@main/constants";
import { logger } from "./logger";
export class WindowManager {
@@ -68,7 +68,7 @@ export class WindowManager {
// Try to load from remote URL in production
try {
await window.loadURL(
- `https://release-v${this.formatVersionNumber(appVersion)}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
+ `https://release-v${this.formatVersionNumber(app.getVersion())}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
);
} catch (error) {
// Fall back to local file if remote URL fails
From 274080069fd7997600c9ad296c52a148f821c934 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 29 Oct 2025 02:12:17 +0000
Subject: [PATCH 066/118] feat: forcing dev tools
---
src/main/services/window-manager.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 673bf1a0..3e241467 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -197,6 +197,8 @@ export class WindowManager {
this.mainWindow.removeMenu();
this.mainWindow.on("ready-to-show", () => {
+ WindowManager.mainWindow?.webContents.openDevTools();
+
if (!app.isPackaged || isStaging)
WindowManager.mainWindow?.webContents.openDevTools();
WindowManager.mainWindow?.show();
From f99f8d95548638678b21610b8511e44207e00730 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 29 Oct 2025 02:32:45 +0000
Subject: [PATCH 067/118] feat: forcing dev tools
---
src/main/services/window-manager.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 3e241467..a9b742ba 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -24,7 +24,7 @@ import type {
UserPreferences,
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
-import { isStaging } from "@main/constants";
+import { appVersion, isStaging } from "@main/constants";
import { logger } from "./logger";
export class WindowManager {
@@ -197,8 +197,6 @@ export class WindowManager {
this.mainWindow.removeMenu();
this.mainWindow.on("ready-to-show", () => {
- WindowManager.mainWindow?.webContents.openDevTools();
-
if (!app.isPackaged || isStaging)
WindowManager.mainWindow?.webContents.openDevTools();
WindowManager.mainWindow?.show();
From e143fadf381a5992e26b699ca6c7b4a38e0bfc06 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 29 Oct 2025 02:55:37 +0000
Subject: [PATCH 068/118] fix: fixing import
---
src/main/services/window-manager.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index a9b742ba..673bf1a0 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -24,7 +24,7 @@ import type {
UserPreferences,
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
-import { appVersion, isStaging } from "@main/constants";
+import { isStaging } from "@main/constants";
import { logger } from "./logger";
export class WindowManager {
From 58bdbdab71297a7cc559c0c5cfcd38b2131ceeb0 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 29 Oct 2025 16:16:11 +0200
Subject: [PATCH 069/118] fix: disabling unnecessary api calls if game is
custom
---
src/main/events/catalogue/get-game-stats.ts | 4 ++++
.../achievements/get-game-achievement-data.ts | 4 ++++
.../src/context/cloud-sync/cloud-sync.context.tsx | 6 ++++++
.../context/game-details/game-details.context.tsx | 12 +++++++-----
src/renderer/src/pages/game-details/game-reviews.tsx | 4 ++--
5 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts
index b836531d..b7b7125c 100644
--- a/src/main/events/catalogue/get-game-stats.ts
+++ b/src/main/events/catalogue/get-game-stats.ts
@@ -10,6 +10,10 @@ const getGameStats = async (
objectId: string,
shop: GameShop
) => {
+ if (shop === "custom") {
+ return null;
+ }
+
const cachedStats = await gamesStatsCacheSublevel.get(
levelKeys.game(shop, objectId)
);
diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts
index ffbfac1a..69437801 100644
--- a/src/main/services/achievements/get-game-achievement-data.ts
+++ b/src/main/services/achievements/get-game-achievement-data.ts
@@ -27,6 +27,10 @@ export const getGameAchievementData = async (
shop: GameShop,
useCachedData: boolean
) => {
+ if (shop === "custom") {
+ return [];
+ }
+
const gameKey = levelKeys.game(shop, objectId);
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
index b94c94d7..ce5f0a6e 100644
--- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
+++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
@@ -98,6 +98,12 @@ export function CloudSyncContextProvider({
);
const getGameArtifacts = useCallback(async () => {
+ // Don't make API requests for custom games
+ if (shop === "custom") {
+ setArtifacts([]);
+ return;
+ }
+
const params = new URLSearchParams({
objectId,
shop,
diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx
index 14e5d587..3706b02e 100644
--- a/src/renderer/src/context/game-details/game-details.context.tsx
+++ b/src/renderer/src/context/game-details/game-details.context.tsx
@@ -142,10 +142,12 @@ export function GameDetailsContextProvider({
}
});
- window.electron.getGameStats(objectId, shop).then((result) => {
- if (abortController.signal.aborted) return;
- setStats(result);
- });
+ if (shop !== "custom") {
+ window.electron.getGameStats(objectId, shop).then((result) => {
+ if (abortController.signal.aborted) return;
+ setStats(result);
+ });
+ }
const assetsPromise = window.electron.getGameAssets(objectId, shop);
@@ -167,7 +169,7 @@ export function GameDetailsContextProvider({
setIsLoading(false);
});
- if (userDetails) {
+ if (userDetails && shop !== "custom") {
window.electron
.getUnlockedAchievements(objectId, shop)
.then((achievements) => {
diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx
index f70c84b2..1a6fc675 100644
--- a/src/renderer/src/pages/game-details/game-reviews.tsx
+++ b/src/renderer/src/pages/game-details/game-reviews.tsx
@@ -117,7 +117,7 @@ export function GameReviews({
});
const checkUserReview = useCallback(async () => {
- if (!objectId || !userDetailsId) return;
+ if (!objectId || !userDetailsId || shop === "custom") return;
try {
const response = await window.electron.hydraApi.get<{
@@ -147,7 +147,7 @@ export function GameReviews({
const loadReviews = useCallback(
async (reset = false) => {
- if (!objectId) return;
+ if (!objectId || shop === "custom") return;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
From dff68a3e260e5105ca1b51e6d1c5200cc8b470bb Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 29 Oct 2025 16:22:12 +0200
Subject: [PATCH 070/118] fix: removed comments
---
src/renderer/src/context/cloud-sync/cloud-sync.context.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
index ce5f0a6e..abc359e9 100644
--- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
+++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx
@@ -98,7 +98,6 @@ export function CloudSyncContextProvider({
);
const getGameArtifacts = useCallback(async () => {
- // Don't make API requests for custom games
if (shop === "custom") {
setArtifacts([]);
return;
From 4b8d64c72b51ac7eb8f74fa73eb0e5c94d6d659f Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 29 Oct 2025 16:44:48 +0200
Subject: [PATCH 071/118] feat: disabled favorite/unfavorite get request for
custom games
---
src/main/events/library/add-game-to-favorites.ts | 4 +++-
src/main/events/library/remove-game-from-favorites.ts | 6 +++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/main/events/library/add-game-to-favorites.ts b/src/main/events/library/add-game-to-favorites.ts
index 68c81abb..53985a09 100644
--- a/src/main/events/library/add-game-to-favorites.ts
+++ b/src/main/events/library/add-game-to-favorites.ts
@@ -13,7 +13,9 @@ const addGameToFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
- HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
+ if (shop !== "custom") {
+ HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
+ }
try {
await gamesSublevel.put(gameKey, {
diff --git a/src/main/events/library/remove-game-from-favorites.ts b/src/main/events/library/remove-game-from-favorites.ts
index f06f55ce..7c79cbf4 100644
--- a/src/main/events/library/remove-game-from-favorites.ts
+++ b/src/main/events/library/remove-game-from-favorites.ts
@@ -13,7 +13,11 @@ const removeGameFromFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
- HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {});
+ if (shop !== "custom") {
+ HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(
+ () => {}
+ );
+ }
try {
await gamesSublevel.put(gameKey, {
From feedcb1dc7511fc336a3e7f15ed3ef6085d9f32c Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 29 Oct 2025 16:49:51 +0200
Subject: [PATCH 072/118] feat: disabled assets request for custom games
---
src/main/events/catalogue/get-game-assets.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts
index de1d2b1f..0e45f886 100644
--- a/src/main/events/catalogue/get-game-assets.ts
+++ b/src/main/events/catalogue/get-game-assets.ts
@@ -6,6 +6,10 @@ import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours
export const getGameAssets = async (objectId: string, shop: GameShop) => {
+ if (shop === "custom") {
+ return null;
+ }
+
const cachedAssets = await gamesShopAssetsSublevel.get(
levelKeys.game(shop, objectId)
);
From c24ad34bc785acc9c0ccdb15be4268792807921c Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 12:42:39 -0300
Subject: [PATCH 073/118] fix: hltb and achievements being called for custom
games
---
src/main/events/catalogue/get-game-shop-details.ts | 2 ++
src/main/events/library/remove-game-from-library.ts | 2 +-
src/renderer/src/pages/game-details/game-details-content.tsx | 4 ++--
src/types/game.types.ts | 2 +-
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts
index d6d27b9c..1a7fc455 100644
--- a/src/main/events/catalogue/get-game-shop-details.ts
+++ b/src/main/events/catalogue/get-game-shop-details.ts
@@ -26,6 +26,8 @@ const getGameShopDetails = async (
shop: GameShop,
language: string
): Promise => {
+ if (shop === "custom") return null;
+
if (shop === "steam") {
const [cachedData, cachedAssets] = await Promise.all([
gamesShopCacheSublevel.get(
diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts
index fbb60ab2..95133c70 100644
--- a/src/main/events/library/remove-game-from-library.ts
+++ b/src/main/events/library/remove-game-from-library.ts
@@ -84,7 +84,7 @@ const removeGameFromLibrary = async (
await resetShopAssets(gameKey);
}
- if (game?.remoteId) {
+ if (game.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}
diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx
index ab51a212..63c4c974 100644
--- a/src/renderer/src/pages/game-details/game-details-content.tsx
+++ b/src/renderer/src/pages/game-details/game-details-content.tsx
@@ -228,7 +228,7 @@ export function GameDetailsContent() {
)}
- {game?.shop !== "custom" && shop && objectId && (
+ {shop !== "custom" && shop && objectId && (
- {game?.shop !== "custom" && }
+ {shop !== "custom" && }
diff --git a/src/types/game.types.ts b/src/types/game.types.ts
index ed8fb852..35d537a8 100644
--- a/src/types/game.types.ts
+++ b/src/types/game.types.ts
@@ -1,4 +1,4 @@
-export type GameShop = "steam" | "epic" | "custom";
+export type GameShop = "steam" | "custom";
export type ShortcutLocation = "desktop" | "start_menu";
From ad588b5600a1172c4f7e67d1c2a8f78f6b0404ce Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Wed, 29 Oct 2025 19:51:09 +0200
Subject: [PATCH 074/118] fix: images with big height breaking layout
---
src/renderer/src/pages/game-details/hero.scss | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/renderer/src/pages/game-details/hero.scss b/src/renderer/src/pages/game-details/hero.scss
index 6bd63320..41264fe4 100644
--- a/src/renderer/src/pages/game-details/hero.scss
+++ b/src/renderer/src/pages/game-details/hero.scss
@@ -146,6 +146,8 @@ $hero-height: 350px;
&__game-logo {
width: 200px;
align-self: flex-end;
+ object-fit: contain;
+ object-position: left bottom;
@media (min-width: 768px) {
width: 250px;
@@ -153,6 +155,7 @@ $hero-height: 350px;
@media (min-width: 1024px) {
width: 300px;
+ max-height: 150px;
}
}
From 499a830e3ec1bd50124f97c515be01363ecf770e Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Wed, 29 Oct 2025 18:23:06 +0000
Subject: [PATCH 075/118] chore: sync with main
---
.github/workflows/release.yml | 1 -
.../src/components/text-field/text-field.tsx | 16 ++++++++++------
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7408665f..df01b358 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,7 +7,6 @@ concurrency:
on:
push:
branches:
- - main
- release/**
jobs:
diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx
index 7c0cbb58..76759126 100644
--- a/src/renderer/src/components/text-field/text-field.tsx
+++ b/src/renderer/src/components/text-field/text-field.tsx
@@ -4,10 +4,11 @@ import { useTranslation } from "react-i18next";
import cn from "classnames";
import "./text-field.scss";
-export interface TextFieldProps extends React.DetailedHTMLProps<
- React.InputHTMLAttributes,
- HTMLInputElement
-> {
+export interface TextFieldProps
+ extends React.DetailedHTMLProps<
+ React.InputHTMLAttributes,
+ HTMLInputElement
+ > {
theme?: "primary" | "dark";
label?: string | React.ReactNode;
hint?: string | React.ReactNode;
@@ -42,7 +43,10 @@ export const TextField = React.forwardRef(
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const { t } = useTranslation("forms");
const showPasswordToggleButton = props.type === "password";
- const inputType = props.type === "password" && isPasswordVisible ? "text" : props.type ?? "text";
+ const inputType =
+ props.type === "password" && isPasswordVisible
+ ? "text"
+ : (props.type ?? "text");
const hintContent = error ? (
{error}
) : hint ? (
@@ -106,4 +110,4 @@ export const TextField = React.forwardRef(
);
}
);
-TextField.displayName = "TextField";
\ No newline at end of file
+TextField.displayName = "TextField";
From 49df40650c8e8779b7cfff917441ead54df971f2 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 15:27:36 -0300
Subject: [PATCH 076/118] chore: prettier
---
.../src/components/text-field/text-field.tsx | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx
index 7c0cbb58..76759126 100644
--- a/src/renderer/src/components/text-field/text-field.tsx
+++ b/src/renderer/src/components/text-field/text-field.tsx
@@ -4,10 +4,11 @@ import { useTranslation } from "react-i18next";
import cn from "classnames";
import "./text-field.scss";
-export interface TextFieldProps extends React.DetailedHTMLProps<
- React.InputHTMLAttributes,
- HTMLInputElement
-> {
+export interface TextFieldProps
+ extends React.DetailedHTMLProps<
+ React.InputHTMLAttributes,
+ HTMLInputElement
+ > {
theme?: "primary" | "dark";
label?: string | React.ReactNode;
hint?: string | React.ReactNode;
@@ -42,7 +43,10 @@ export const TextField = React.forwardRef(
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const { t } = useTranslation("forms");
const showPasswordToggleButton = props.type === "password";
- const inputType = props.type === "password" && isPasswordVisible ? "text" : props.type ?? "text";
+ const inputType =
+ props.type === "password" && isPasswordVisible
+ ? "text"
+ : (props.type ?? "text");
const hintContent = error ? (
{error}
) : hint ? (
@@ -106,4 +110,4 @@ export const TextField = React.forwardRef(
);
}
);
-TextField.displayName = "TextField";
\ No newline at end of file
+TextField.displayName = "TextField";
From 2fb44a6c0e4fd899f85500ccad1c114d5481e5f2 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 15:49:43 -0300
Subject: [PATCH 077/118] chore: remove build renderer trigger on main
---
.github/workflows/build-renderer.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/build-renderer.yml b/.github/workflows/build-renderer.yml
index 2904bcb8..34f7d303 100644
--- a/.github/workflows/build-renderer.yml
+++ b/.github/workflows/build-renderer.yml
@@ -7,7 +7,6 @@ concurrency:
on:
push:
branches:
- - main
- release/**
jobs:
From 53c162f0e49f91250e61ef910fb76f9ae3f0524b Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 17:55:55 -0300
Subject: [PATCH 078/118] feat: add i18n
---
src/locales/es/translation.json | 4 +++-
src/locales/pt-BR/translation.json | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index dfa7f7a1..863b8332 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -541,7 +541,9 @@
"notification_preview": "Probar notificación de logro",
"debrid": "Debrid",
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
- "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego"
+ "enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
+ "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
+ "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
},
"notifications": {
"download_complete": "Descarga completada",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index c9e908ac..5bfc2af3 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -538,7 +538,9 @@
"hidden": "Oculta",
"test_notification": "Testar notificação",
"notification_preview": "Prévia da Notificação de Conquistas",
- "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
+ "enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
+ "autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo",
+ "hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo"
},
"notifications": {
"download_complete": "Download concluído",
From 0990951183325aac65245911898f750932ed1ca6 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 18:06:46 -0300
Subject: [PATCH 079/118] chore: fix aur package
---
.github/workflows/update-aur.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/update-aur.yml b/.github/workflows/update-aur.yml
index 2a3583bc..4ce84f3c 100644
--- a/.github/workflows/update-aur.yml
+++ b/.github/workflows/update-aur.yml
@@ -98,6 +98,7 @@ jobs:
# Update pkgver in PKGBUILD
cd hydra-launcher-bin
NEW_VERSION="${{ steps.get-version.outputs.version }}"
+ NEW_VERSION="${NEW_VERSION#v}"
echo "Updating PKGBUILD pkgver to $NEW_VERSION"
From 65e49550ad8f0f85c32f70f8c4ac2af79d62f22f Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Wed, 29 Oct 2025 18:10:27 -0300
Subject: [PATCH 080/118] chore: fix aur package
---
.github/workflows/update-aur.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/update-aur.yml b/.github/workflows/update-aur.yml
index 4ce84f3c..52fe907e 100644
--- a/.github/workflows/update-aur.yml
+++ b/.github/workflows/update-aur.yml
@@ -138,6 +138,9 @@ jobs:
COMMIT_MSG="v${{ steps.get-version.outputs.version }}"
git commit -m "$COMMIT_MSG"
+
+ export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -F ~/.ssh/config -o UserKnownHostsFile=$SSH_PATH/known_hosts"
+
git push origin master
echo "Successfully updated AUR package to version ${{ steps.get-version.outputs.version }}"
fi
From 0e7e53478a5e02ca9b09769e15b2a8f408d459d5 Mon Sep 17 00:00:00 2001
From: Wkeynhk <86107421+Wkeynhk@users.noreply.github.com>
Date: Thu, 30 Oct 2025 00:47:30 +0300
Subject: [PATCH 081/118] Update translation.json
---
src/locales/ru/translation.json | 41 ++++++++++++++++++++++-----------
1 file changed, 28 insertions(+), 13 deletions(-)
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 886c7d07..15a9c9cb 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -212,6 +212,7 @@
"stats": "Статистика",
"download_count": "Загрузки",
"player_count": "Активные игроки",
+ "rating_count": "Оценка",
"download_error": "Этот вариант загрузки недоступен",
"download": "Скачать",
"executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"",
@@ -252,17 +253,6 @@
"would_you_recommend_this_game": "Хотите оставить отзыв об этой игре?",
"yes": "Да",
"maybe_later": "Возможно позже",
- "rating_count": "Оценка",
- "delete_review": "Удалить отзыв",
- "remove_review": "Удалить отзыв",
- "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
- "delete_review_modal_description": "Это действие нельзя отменить.",
- "delete_review_modal_delete_button": "Удалить",
- "delete_review_modal_cancel_button": "Отмена",
- "show_original": "Показать оригинал",
- "show_translation": "Показать перевод",
- "show_original_translated_from": "Показать оригинал (переведено с {{language}})",
- "hide_original": "Скрыть оригинал",
"cloud_save": "Облачное сохранение",
"cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве",
"backups": "Резервные копии",
@@ -360,7 +350,18 @@
"caption": "Субтитры",
"audio": "Аудио",
"filter_by_source": "Фильтр по источнику",
- "no_repacks_found": "Источники для этой игры не найдены"
+ "no_repacks_found": "Источники для этой игры не найдены",
+ "delete_review": "Удалить отзыв",
+ "remove_review": "Удалить отзыв",
+ "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
+ "delete_review_modal_description": "Это действие нельзя отменить.",
+ "delete_review_modal_delete_button": "Удалить",
+ "delete_review_modal_cancel_button": "Отмена",
+ "vote_failed": "Не удалось зарегистрировать ваш голос. Пожалуйста, попробуйте снова.",
+ "show_original": "Показать оригинал",
+ "show_translation": "Показать перевод",
+ "show_original_translated_from": "Показать оригинал (переведено с {{language}})",
+ "hide_original": "Скрыть оригинал"
},
"activation": {
"title": "Активировать Hydra",
@@ -427,6 +428,9 @@
"validate_download_source": "Проверить",
"remove_download_source": "Удалить",
"add_download_source": "Добавить источник",
+ "adding": "Добавление…",
+ "failed_add_download_source": "Не удалось добавить источник. Пожалуйста, попробуйте снова.",
+ "download_source_already_exists": "Этот URL источника уже существует.",
"download_count_zero": "В списке нет загрузок",
"download_count_one": "{{countFormatted}} загрузка в списке",
"download_count_other": "{{countFormatted}} загрузок в списке",
@@ -434,9 +438,16 @@
"add_download_source_description": "Вставьте ссылку на .json-файл",
"download_source_up_to_date": "Обновлён",
"download_source_errored": "Ошибка",
+ "download_source_pending_matching": "Скоро обновится",
+ "download_source_matched": "Обновлен",
+ "download_source_matching": "Обновление",
+ "download_source_failed": "Ошибка",
+ "download_source_no_information": "Информация отсутствует",
"sync_download_sources": "Обновить источники",
"removed_download_source": "Источник удален",
"removed_download_sources": "Источники удалены",
+ "removed_all_download_sources": "Все источники удалены",
+ "download_sources_synced_successfully": "Все источники синхронизированы",
"cancel_button_confirmation_delete_all_sources": "Нет",
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
"title_confirmation_delete_all_sources": "Удалить все источники",
@@ -467,6 +478,7 @@
"seed_after_download_complete": "Раздавать после завершения загрузки",
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
"account": "Аккаунт",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "У вас нет заблокированных пользователей",
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
"manage_subscription": "Управлять подпиской",
@@ -540,7 +552,9 @@
"hidden": "Скрытый",
"test_notification": "Тестовое уведомление",
"notification_preview": "Предварительный просмотр уведомления о достижении",
- "enable_friend_start_game_notifications": "Когда друг начинает играть в игру"
+ "enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
+ "autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры",
+ "hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -590,6 +604,7 @@
"activity": "Недавняя активность",
"library": "Библиотека",
"pinned": "Закрепленные",
+ "sort_by": "Сортировать по:",
"achievements_earned": "Заработанные достижения",
"played_recently": "Недавно сыгранные",
"playtime": "Время игры",
From 4dd28bbbf149f138d9de384dc9eaba220d306b7f Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 01:12:29 +0100
Subject: [PATCH 082/118] Hungarian Translation 3.7.2
---
src/locales/hu/translation.json | 96 ++++++++++++++++++---------------
1 file changed, 52 insertions(+), 44 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index b72811d3..a91902dc 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -8,7 +8,7 @@
"no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott",
- "weekly": "📅 A hét felkapott játékai",
+ "weekly": "📅 A hét felkapottjai",
"achievements": "🏆 Achievement támogatott"
},
"sidebar": {
@@ -26,7 +26,7 @@
"sign_in": "Bejelentkezés",
"friends": "Barátok",
"need_help": "Elakadtál?",
- "favorites": "Kedvenc játékok",
+ "favorites": "Kedvenc Játékaim",
"playable_button_title": "Csak az azonnal játszható játékokat mutasd",
"add_custom_game_tooltip": "Saját játék hozzáadása",
"show_playable_only_tooltip": "Csak játszható játék mutatása",
@@ -76,8 +76,8 @@
"edit_game_modal_drop_hero_image_here": "Húzd ide a borítókép képét",
"edit_game_modal_drop_to_replace_icon": "Ikon kicserélése ráhúzással",
"edit_game_modal_drop_to_replace_logo": "Logó kicserélése ráhúzással",
- "edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással",
- "install_decky_plugin": "Decky Plugin Telepítése",
+ "edit_game_modal_drop_to_replace_hero": "Borítókép kicserélése ráhúzással",
+ "install_decky_plugin": "Decky Plugin Telepítése",
"update_decky_plugin": "Decky Plugin Frissítése",
"decky_plugin_installed_version": "Decky Plugin (v{{version}})",
"install_decky_plugin_title": "Telepítsd a Hydra Decky Plugint",
@@ -88,7 +88,7 @@
"decky_plugin_installation_failed": "Decky plugin telepítése sikertelen: {{error}}",
"decky_plugin_installation_error": "Decky plugin telepítése hibával járt el: {{error}}",
"confirm": "Megerősít",
- "cancel": "Mégse"
+ "cancel": "Mégse"
},
"header": {
"search": "Keresés",
@@ -120,7 +120,7 @@
"result_count": "{{resultCount}} találatok",
"filter_count": "{{filterCount}} elérhető",
"clear_filters": "{{filterCount}} kiválaszott szűrő törlése"
- },
+ },
"game_details": {
"open_download_options": "Letöltési opciók megnyitása",
"download_options_zero": "Nincs letöltési opció",
@@ -178,13 +178,13 @@
"open_folder": "Mappa megnyitása",
"open_download_location": "Letöltött fájlok megtekintése",
"create_shortcut": "Asztali parancsikon létrehozása",
- "create_shortcut_simple": "Parancsikon létrehozása",
+ "create_shortcut_simple": "Parancsikon létrehozása",
"clear": "Visszavon",
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
- "options": "Beállítások",
- "properties": "További beállítások",
+ "options": "Beállítások",ä
+ "properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
"downloads_section_title": "Letöltések",
@@ -204,27 +204,27 @@
"game_removed_from_library": "Játék eltávolítva a könyvtárból",
"failed_remove_from_library": "Játék eltávolítása a könyvtárból sikertelen",
"files_removed_success": "Fájlok eltávolítása sikeres",
- "failed_remove_files": "Fájlok eltávolítása sikertelen",
+ "failed_remove_files": "Fájlok eltávolítása sikertelen",
"nsfw_content_title": "Ez a játék tartalmaz nem megfelelő tartalmat",
- "nsfw_content_description": "A(z) {{title}} tartalma lehetséges hogy nem megfelelő minden korosztály számára. Biztosan folytatni szeretnéd?",
+ "nsfw_content_description": "A(z) {{title}} tartalma lehetséges hogy nem megfelelő minden korosztály számára. Biztosan folytatni szeretnéd?",
"allow_nsfw_content": "Folytatás",
"refuse_nsfw_content": "Vissza",
"stats": "Statisztikák",
"download_count": "Letöltések",
"player_count": "Aktív játékosok",
- "rating_count": "Értékelés",
- "download_error": "Ez a letöltési opció nem elérhető",
+ "rating_count": "Értékelés",
+ "download_error": "Ez a letöltési opció nem elérhető",
"download": "Letöltés",
"executable_path_in_use": "Ez a futtatható fájl már használatban van a(z) \"{{game}}\" által",
"warning": "Figyelmeztetés:",
"hydra_needs_to_remain_open": "ehhez a letöltéshez, a Hydrának muszáj nyitva maradnia hogy letöltődjön. Ha a Hydra bezáródik letöltés előtt, a letöltés elveszik.",
"achievements": "Achievementek",
"achievements_count": "Achievementek {{unlockedCount}}/{{achievementsCount}}",
- "show_more": "Mutass többet",
+ "show_more": "Mutass többet",
"show_less": "Mutass kevesebbet",
"reviews": "Vélemények",
"leave_a_review": "Hagyd itt a véleményed",
- "write_review_placeholder": "Oszd meg a gondolataid a játékról...",
+ "write_review_placeholder": "Oszd meg gondolatod a játékról...",
"sort_newest": "Legújabb",
"no_reviews_yet": "Még nem lett vélemény megosztva",
"be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
@@ -252,7 +252,7 @@
"you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
"would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
"yes": "Igen",
- "maybe_later": "Talán Később",
+ "maybe_later": "Talán később",
"cloud_save": "Mentés felhőben",
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
"backups": "Biztonsági másolatok",
@@ -350,19 +350,24 @@
"caption": "Felirat",
"audio": "Hang",
"filter_by_source": "Szűrés forrás szerint",
- "no_repacks_found": "Nem található forrás ehhez a játékhoz" ,
- "delete_review": "Vélemény törlése",
+ "no_repacks_found": "Nem található forrás ehhez a játékhoz",
+ "delete_review": "Vélemény törlése",
"remove_review": "Vélemény eltávolítása",
"delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
"delete_review_modal_delete_button": "Törlés",
- "delete_review_modal_cancel_button": "Mégse"
+ "delete_review_modal_cancel_button": "Mégse",
+ "vote_failed": "A szavazatod nem regisztrálódott. Kérlek próbáld újra.",
+ "show_original": "Eredeti megjelenítése",
+ "show_translation": "Fordítás megjelenítése",
+ "show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})",
+ "hide_original": "Eredeti elrejtése"
},
"activation": {
"title": "Hydra Aktiválása",
"installation_id": "Telepítési Azonosító:",
"enter_activation_code": "Írd be az aktiválási kódod",
- "message": "Ha nem tudod hol kérdezz efelől, akkor nem kéne ilyened legyen.",
+ "message": "Ha nem tudod merre kérdezz efelől, akkor nem kéne ilyened legyen.",
"activate": "Aktiválás",
"loading": "Töltés…"
},
@@ -394,7 +399,6 @@
"stop_seeding": "Seedelés leállítása",
"resume_seeding": "Seedelés folytatása",
"options": "Kezelés",
- "alldebrid_size_not_supported": "Letöltési információ az AllDebrid-hez még nem támogatott",
"extract": "Fájlok kibontása",
"extracting": "Fájlok kibontása…"
},
@@ -420,20 +424,30 @@
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve",
- "download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
+ "download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
+ "adding": "Hozzáadás…",
+ "failed_add_download_source": "Letöltési forrás hozzáadása sikertelen. Kérlek próbáld újra.",
+ "download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
"download_count_zero": "Nincs letöltési opció",
"download_count_one": "{{countFormatted}} letöltési opció",
"download_count_other": "{{countFormatted}} letöltési opció",
- "download_source_url": "URL forrás:",
+ "download_source_url": "URL Forrás:",
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
+ "download_source_pending_matching": "Frissítés hamarosan",
+ "download_source_matched": "Naprakész",
+ "download_source_matching": "Frissítés..",
+ "download_source_failed": "Hiba",
+ "download_source_no_information": "Nincs elérhető információ",
"sync_download_sources": "Források szinkronizálása",
"removed_download_source": "Letöltési forrás eltávolítva",
"removed_download_sources": "Letöltési források eltávolítva",
+ "removed_all_download_sources": "Összes letöltési forrás eltávolítva",
+ "download_sources_synced_successfully": "Az összes letöltési forrás szinkronizálva",
"cancel_button_confirmation_delete_all_sources": "Nem",
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
"title_confirmation_delete_all_sources": "Az összes letöltési forrás törlése",
@@ -446,6 +460,7 @@
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
+ "importing": "Importálás...",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
@@ -463,6 +478,7 @@
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
@@ -498,26 +514,15 @@
"delete_theme_description": "Ez törölni fogja a(z) {{theme}} témát",
"cancel": "Mégsem",
"appearance": "Megjelenés",
- "debrid": "Debrid",
- "debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
+ "debrid": "Debrid",
+ "debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, és csak az internet sebességed szab határt.",
"enable_torbox": "TorBox bekapcsolása",
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
"torbox_account_linked": "TorBox fiók összekapcsolva",
"create_real_debrid_account": "Kattints ide ha még nincs Real-Debrid fiókod",
"create_torbox_account": "Kattints ide ha még nincs TorBox fiókod",
"real_debrid_account_linked": "Real-Debrid fiók összekapcsolva",
- "enable_all_debrid": "All-Debrid bekapcsolása",
- "all_debrid_description": "Az All-Debrid egy korlátozásmentes letöltőprogram, ami lehetővé teszi a fájlok gyors letöltését különböző forrásokból.",
- "all_debrid_free_account_error": "Ez a fiók: \"{{username}}\" egy ingyenes fiók. Kérlek iratkozz fel az All-Debridre",
- "all_debrid_account_linked": "All-Debrid fiók összekapcsolva",
- "alldebrid_missing_key": "Kérlek adj meg egy API key-t",
- "alldebrid_invalid_key": "Érvénytelen API key",
- "alldebrid_blocked": "Az API key-ed Földrajzilag vagy IP-alapján van blokkolva",
- "alldebrid_banned": "Ez a fiók kitiltásra került",
- "alldebrid_unknown_error": "Egy ismeretlen hiba történt",
- "alldebrid_invalid_response": "Érvénytelen válasz az All-Debrid felől",
- "alldebrid_network_error": "Hálózati hiba. Ellenőrízd az internetkapcsolatod",
- "name_min_length": "A téma neve legalább 3 karakter hosszú legyen",
+ "name_min_length": "A téma neve legalább 3 karakter hosszú kell legyen",
"import_theme": "Téma importálása",
"import_theme_description": "Ezt a témát fogod importálni a Témaáruház-ból: {{theme}}",
"error_importing_theme": "Hiba lépett fel a téma importálása közben",
@@ -548,6 +553,8 @@
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
+ "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
+ "hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
},
"notifications": {
"download_complete": "Letöltés befejezve",
@@ -574,11 +581,11 @@
"game_card": {
"available_one": "Elérhető",
"available_other": "Elérhető",
- "no_downloads": "Nincs elérhető letöltés"
- "calculating": "Feldolgozás"
+ "no_downloads": "Nincs elérhető letöltés",
+ "calculating": "Számítás alatt.."
},
"binary_not_found_modal": {
- "title": "A programok nincsenek telepítve",
+ "title": "Hiányzó programok",
"description": "Wine vagy Lutris futtatható fájlok nem találhatók a rendszereden",
"instructions": "Ellenőrízd hogy melyiket kell helyesen telepíteni a Linux disztribúciódra, hogy a játék megfelelően fusson"
},
@@ -597,6 +604,7 @@
"activity": "Legutóbbi tevékenység",
"library": "Könyvtár",
"pinned": "Kitűzve",
+ "sort_by": "Rendezés:",
"achievements_earned": "Elért achievementek",
"played_recently": "Nemrég játszva",
"playtime": "Játszottidő",
@@ -666,7 +674,7 @@
"uploading_banner": "Borítókép feltöltése…",
"background_image_updated": "Borítókép frissítve",
"stats": "Statisztikák",
- "achievements": "achievementek",
+ "achievements": "achievement",
"games": "Játékok",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "A rangsor hetente frissül.",
@@ -678,10 +686,10 @@
"error_adding_friend": "Hiba, barátfelkérés sikertelen. Kérlek ellenőrízd a barát kódot",
"friend_code_length_error": "A barát kódnak 8 karakterből kell állnia",
"game_removed_from_pinned": "Játék eltávolítva a kitűzöttek közül",
- "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez"
+ "game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
"karma": "Karma",
"karma_count": "karma",
- "karma_description": "Pozitív értékelésekre kapott pontok alapján"
+ "karma_description": "Pozitív értékelésekkel szerzett pontok"
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
@@ -690,7 +698,7 @@
"unlocked_at": "Feloldva: {{date}}",
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
"new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
- "achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
+ "achievement_progress": "{{unlockedCount}}/{{totalCount}} achievement",
"achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
From 41092c2dd442c4efbf2ea15604df1cfc34a0c8be Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 01:35:54 +0100
Subject: [PATCH 083/118] Update to 3.7.2
---
src/locales/hu/translation.json | 36 ---------------------------------
1 file changed, 36 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index 88039aee..a91902dc 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -183,11 +183,7 @@
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
-<<<<<<< HEAD
"options": "Beállítások",ä
-=======
- "options": "Beállítások",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
@@ -228,11 +224,7 @@
"show_less": "Mutass kevesebbet",
"reviews": "Vélemények",
"leave_a_review": "Hagyd itt a véleményed",
-<<<<<<< HEAD
"write_review_placeholder": "Oszd meg gondolatod a játékról...",
-=======
- "write_review_placeholder": "Oszd meg a gondolataid a játékról...",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"sort_newest": "Legújabb",
"no_reviews_yet": "Még nem lett vélemény megosztva",
"be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
@@ -260,11 +252,7 @@
"you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
"would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
"yes": "Igen",
-<<<<<<< HEAD
"maybe_later": "Talán később",
-=======
- "maybe_later": "Talán Később",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"cloud_save": "Mentés felhőben",
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
"backups": "Biztonsági másolatok",
@@ -368,16 +356,12 @@
"delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
"delete_review_modal_delete_button": "Törlés",
-<<<<<<< HEAD
"delete_review_modal_cancel_button": "Mégse",
"vote_failed": "A szavazatod nem regisztrálódott. Kérlek próbáld újra.",
"show_original": "Eredeti megjelenítése",
"show_translation": "Fordítás megjelenítése",
"show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})",
"hide_original": "Eredeti elrejtése"
-=======
- "delete_review_modal_cancel_button": "Mégse"
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
},
"activation": {
"title": "Hydra Aktiválása",
@@ -531,22 +515,14 @@
"cancel": "Mégsem",
"appearance": "Megjelenés",
"debrid": "Debrid",
-<<<<<<< HEAD
"debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, és csak az internet sebességed szab határt.",
-=======
- "debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"enable_torbox": "TorBox bekapcsolása",
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
"torbox_account_linked": "TorBox fiók összekapcsolva",
"create_real_debrid_account": "Kattints ide ha még nincs Real-Debrid fiókod",
"create_torbox_account": "Kattints ide ha még nincs TorBox fiókod",
"real_debrid_account_linked": "Real-Debrid fiók összekapcsolva",
-<<<<<<< HEAD
"name_min_length": "A téma neve legalább 3 karakter hosszú kell legyen",
-=======
- "name_min_length": "A téma neve legalább 3 karakter hosszú legyen",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"import_theme": "Téma importálása",
"import_theme_description": "Ezt a témát fogod importálni a Témaáruház-ból: {{theme}}",
"error_importing_theme": "Hiba lépett fel a téma importálása közben",
@@ -606,11 +582,7 @@
"available_one": "Elérhető",
"available_other": "Elérhető",
"no_downloads": "Nincs elérhető letöltés",
-<<<<<<< HEAD
"calculating": "Számítás alatt.."
-=======
- "calculating": "Feldolgozás"
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
},
"binary_not_found_modal": {
"title": "Hiányzó programok",
@@ -717,11 +689,7 @@
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
"karma": "Karma",
"karma_count": "karma",
-<<<<<<< HEAD
"karma_description": "Pozitív értékelésekkel szerzett pontok"
-=======
- "karma_description": "Pozitív értékelésekre kapott pontok alapján"
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
@@ -730,11 +698,7 @@
"unlocked_at": "Feloldva: {{date}}",
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
"new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
-<<<<<<< HEAD
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievement",
-=======
- "achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
->>>>>>> 21074322fa5ef3a1d6168a2b841ec2505db8f0de
"achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",
From 90c5ccb7969e16554218007c35f1fccf606f9a5a Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 01:40:36 +0100
Subject: [PATCH 084/118] Update to 3.7.2
---
src/locales/hu/translation.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index a91902dc..3e0c408a 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -183,7 +183,7 @@
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
- "options": "Beállítások",ä
+ "options": "Beállítások",
"properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
@@ -428,7 +428,7 @@
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
- "adding": "Hozzáadás…",
+ "adding": "Hozzáadás…",
"failed_add_download_source": "Letöltési forrás hozzáadása sikertelen. Kérlek próbáld újra.",
"download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
"download_count_zero": "Nincs letöltési opció",
@@ -438,7 +438,7 @@
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
- "download_source_pending_matching": "Frissítés hamarosan",
+ "download_source_pending_matching": "Hamarosan frissítve",
"download_source_matched": "Naprakész",
"download_source_matching": "Frissítés..",
"download_source_failed": "Hiba",
@@ -460,7 +460,7 @@
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
- "importing": "Importálás...",
+ "importing": "Importálás...",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
@@ -478,7 +478,7 @@
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
- "hydra_cloud": "Hydra Cloud",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
@@ -553,7 +553,7 @@
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
- "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
+ "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
"hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
},
"notifications": {
From 3fce26f1f7a0384d14cbef10781e5a1472ecda3b Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 01:55:15 +0100
Subject: [PATCH 085/118] Update to 3.7.2
---
src/locales/hu/translation.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index 3e0c408a..e4bc7eb0 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -8,7 +8,7 @@
"no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott",
- "weekly": "📅 A hét felkapottjai",
+ "weekly": "📅 Heti kiemeltek",
"achievements": "🏆 Achievement támogatott"
},
"sidebar": {
From 6e76111e2346ca2c3b1c53f4d33b5152884ed072 Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 02:10:02 +0100
Subject: [PATCH 086/118] Missing Comma Fix
---
src/locales/hu/translation.json | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index e4bc7eb0..d771a8d4 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -8,7 +8,7 @@
"no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott",
- "weekly": "📅 Heti kiemeltek",
+ "weekly": "📅 A hét felkapottjai",
"achievements": "🏆 Achievement támogatott"
},
"sidebar": {
@@ -183,7 +183,7 @@
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
- "options": "Beállítások",
+ "options": "Beállítások",ä
"properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
@@ -391,7 +391,7 @@
"download_in_progress": "Folyamatban lévő",
"queued_downloads": "Várakozósoron lévő letöltések",
"downloads_completed": "Befejezett",
- "queued": "Várakozási sorban",
+ "queued": "Várakozásban",
"no_downloads_title": "Oly üres..",
"no_downloads_description": "Még nem töltöttél le semmit a Hydra segítségével, de soha nem késő elkezdeni.",
"checking_files": "Fájlok ellenőrzése…",
@@ -428,7 +428,7 @@
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
- "adding": "Hozzáadás…",
+ "adding": "Hozzáadás…",
"failed_add_download_source": "Letöltési forrás hozzáadása sikertelen. Kérlek próbáld újra.",
"download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
"download_count_zero": "Nincs letöltési opció",
@@ -438,7 +438,7 @@
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
- "download_source_pending_matching": "Hamarosan frissítve",
+ "download_source_pending_matching": "Frissítés hamarosan",
"download_source_matched": "Naprakész",
"download_source_matching": "Frissítés..",
"download_source_failed": "Hiba",
@@ -460,7 +460,7 @@
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
- "importing": "Importálás...",
+ "importing": "Importálás...",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
@@ -478,7 +478,7 @@
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
- "hydra_cloud": "Hydra Cloud",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
@@ -552,8 +552,8 @@
"hidden": "Rejtett",
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
- "enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
- "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
+ "enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot",
+ "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
"hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
},
"notifications": {
From dcc671f9993c57e8311aa7a0683613a7fb9caab3 Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 02:15:35 +0100
Subject: [PATCH 087/118] Mistake Correction
---
src/locales/hu/translation.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index d771a8d4..909f242b 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -183,7 +183,7 @@
"remove_files": "Fájlok eltávolítása",
"remove_from_library_title": "Biztos vagy ebben?",
"remove_from_library_description": "Ezzel eltávolítod a játékot {{game}} a könyvtáradból",
- "options": "Beállítások",ä
+ "options": "Beállítások",
"properties": "További beállítások",
"executable_section_title": "Futtatható fájl",
"executable_section_description": "A fájl amely futtatásra fog kerülni amikor a \"Játék\" lenyomásra kerül",
From 4ff8dc4fa7a563f282a63ba226cb1205da9df6c5 Mon Sep 17 00:00:00 2001
From: "Kiwo.2"
Date: Thu, 30 Oct 2025 02:32:18 +0100
Subject: [PATCH 088/118] Fix with Prettier
---
src/locales/hu/translation.json | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json
index 909f242b..8aea356b 100644
--- a/src/locales/hu/translation.json
+++ b/src/locales/hu/translation.json
@@ -357,7 +357,7 @@
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
"delete_review_modal_delete_button": "Törlés",
"delete_review_modal_cancel_button": "Mégse",
- "vote_failed": "A szavazatod nem regisztrálódott. Kérlek próbáld újra.",
+ "vote_failed": "A szavazatod nem regisztrálódott. Kérlek próbáld újra.",
"show_original": "Eredeti megjelenítése",
"show_translation": "Fordítás megjelenítése",
"show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})",
@@ -428,9 +428,9 @@
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
- "adding": "Hozzáadás…",
+ "adding": "Hozzáadás…",
"failed_add_download_source": "Letöltési forrás hozzáadása sikertelen. Kérlek próbáld újra.",
- "download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
+ "download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
"download_count_zero": "Nincs letöltési opció",
"download_count_one": "{{countFormatted}} letöltési opció",
"download_count_other": "{{countFormatted}} letöltési opció",
@@ -438,16 +438,16 @@
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
- "download_source_pending_matching": "Frissítés hamarosan",
+ "download_source_pending_matching": "Frissítés hamarosan",
"download_source_matched": "Naprakész",
"download_source_matching": "Frissítés..",
"download_source_failed": "Hiba",
- "download_source_no_information": "Nincs elérhető információ",
+ "download_source_no_information": "Nincs elérhető információ",
"sync_download_sources": "Források szinkronizálása",
"removed_download_source": "Letöltési forrás eltávolítva",
"removed_download_sources": "Letöltési források eltávolítva",
"removed_all_download_sources": "Összes letöltési forrás eltávolítva",
- "download_sources_synced_successfully": "Az összes letöltési forrás szinkronizálva",
+ "download_sources_synced_successfully": "Az összes letöltési forrás szinkronizálva",
"cancel_button_confirmation_delete_all_sources": "Nem",
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
"title_confirmation_delete_all_sources": "Az összes letöltési forrás törlése",
@@ -460,7 +460,7 @@
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
- "importing": "Importálás...",
+ "importing": "Importálás...",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
@@ -478,7 +478,7 @@
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
- "hydra_cloud": "Hydra Cloud",
+ "hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
@@ -553,7 +553,7 @@
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot",
- "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
+ "autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
"hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
},
"notifications": {
From a2ef0f304daa827d29aeaca4a13e92a4fe87b5fb Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 07:35:49 -0300
Subject: [PATCH 089/118] fix: playtime count and custom games request on
process watcher
---
.../achievement-watcher-manager.ts | 2 +
src/main/services/library-sync/create-game.ts | 4 ++
.../library-sync/update-game-playtime.ts | 4 ++
src/main/services/process-watcher.ts | 44 +++++++++++++------
4 files changed, 41 insertions(+), 13 deletions(-)
diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts
index b862abbe..dd65165a 100644
--- a/src/main/services/achievements/achievement-watcher-manager.ts
+++ b/src/main/services/achievements/achievement-watcher-manager.ts
@@ -167,6 +167,8 @@ export class AchievementWatcherManager {
shop: GameShop,
objectId: string
) {
+ if (shop === "custom") return;
+
const gameKey = levelKeys.game(shop, objectId);
if (this.alreadySyncedGames.get(gameKey)) return;
diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts
index a346d3b4..e9ec9612 100644
--- a/src/main/services/library-sync/create-game.ts
+++ b/src/main/services/library-sync/create-game.ts
@@ -3,6 +3,10 @@ import { HydraApi } from "../hydra-api";
import { gamesSublevel, levelKeys } from "@main/level";
export const createGame = async (game: Game) => {
+ if (game.shop === "custom") {
+ return;
+ }
+
return HydraApi.post(`/profile/games`, {
objectId: game.objectId,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds ?? 0),
diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts
index 3689b302..b53ebebc 100644
--- a/src/main/services/library-sync/update-game-playtime.ts
+++ b/src/main/services/library-sync/update-game-playtime.ts
@@ -6,6 +6,10 @@ export const updateGamePlaytime = async (
deltaInMillis: number,
lastTimePlayed: Date
) => {
+ if (game.shop === "custom") {
+ return;
+ }
+
return HydraApi.put(`/profile/games/${game.remoteId}`, {
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
lastTimePlayed,
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index 6408c30d..a1449255 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -198,11 +198,6 @@ export const watchProcesses = async () => {
function onOpenGame(game: Game) {
const now = performance.now();
- AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
- game.shop,
- game.objectId
- );
-
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
lastTick: now,
firstTick: now,
@@ -220,6 +215,13 @@ function onOpenGame(game: Game) {
})
.catch(() => {});
+ if (game.shop === "custom") return;
+
+ AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
+ game.shop,
+ game.objectId
+ );
+
if (game.remoteId) {
updateGamePlaytime(
game,
@@ -255,18 +257,20 @@ function onTickGame(game: Game) {
const delta = now - gamePlaytime.lastTick;
- gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
+ const updatedGame: Game = {
...game,
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
lastTimePlayed: new Date(),
- });
+ };
+
+ gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
...gamePlaytime,
lastTick: now,
});
- if (currentTick % TICKS_TO_UPDATE_API === 0) {
+ if (currentTick % TICKS_TO_UPDATE_API === 0 && game.shop !== "custom") {
const deltaToSync =
now -
gamePlaytime.lastSyncTick +
@@ -279,19 +283,20 @@ function onTickGame(game: Game) {
gamePromise
.then(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
- ...game,
+ ...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
- ...game,
+ ...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
})
.finally(() => {
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
...gamePlaytime,
+ lastTick: now,
lastSyncTick: now,
});
});
@@ -299,11 +304,24 @@ function onTickGame(game: Game) {
}
const onCloseGame = (game: Game) => {
+ const now = performance.now();
const gamePlaytime = gamesPlaytime.get(
levelKeys.game(game.shop, game.objectId)
)!;
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
+ const delta = now - gamePlaytime.lastTick;
+
+ const updatedGame: Game = {
+ ...game,
+ playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
+ lastTimePlayed: new Date(),
+ };
+
+ gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
+
+ if (game.shop === "custom") return;
+
if (game.remoteId) {
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
@@ -315,20 +333,20 @@ const onCloseGame = (game: Game) => {
}
const deltaToSync =
- performance.now() -
+ now -
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
.then(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
- ...game,
+ ...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
- ...game,
+ ...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
});
From 459bf731217ace778a9ff9e7c6dc18288d2a17bd Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 07:36:23 -0300
Subject: [PATCH 090/118] fix: request download-sources on custom game
---
src/renderer/src/context/game-details/game-details.context.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx
index c5b88607..bc1a6351 100644
--- a/src/renderer/src/context/game-details/game-details.context.tsx
+++ b/src/renderer/src/context/game-details/game-details.context.tsx
@@ -293,6 +293,8 @@ export function GameDetailsContextProvider({
}, [objectId, shop, userDetails]);
useEffect(() => {
+ if (shop === "custom") return;
+
const fetchDownloadSources = async () => {
try {
const sources = await window.electron.getDownloadSources();
From aadf648a2bfe7a52db7036d90ec7b418663f68c6 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 07:58:43 -0300
Subject: [PATCH 091/118] chore: unnecessary casting
---
.../src/pages/settings/settings-download-sources.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx
index 75f0cc73..675919e3 100644
--- a/src/renderer/src/pages/settings/settings-download-sources.tsx
+++ b/src/renderer/src/pages/settings/settings-download-sources.tsx
@@ -89,7 +89,7 @@ export function SettingsDownloadSources() {
try {
await window.electron.removeDownloadSource(false, downloadSource.id);
const sources = await window.electron.getDownloadSources();
- setDownloadSources(sources as DownloadSource[]);
+ setDownloadSources(sources);
showSuccessToast(t("removed_download_source"));
} catch (error) {
logger.error("Failed to remove download source:", error);
@@ -104,7 +104,7 @@ export function SettingsDownloadSources() {
try {
await window.electron.removeDownloadSource(true);
const sources = await window.electron.getDownloadSources();
- setDownloadSources(sources as DownloadSource[]);
+ setDownloadSources(sources);
showSuccessToast(t("removed_all_download_sources"));
} catch (error) {
logger.error("Failed to remove all download sources:", error);
@@ -117,7 +117,7 @@ export function SettingsDownloadSources() {
const handleAddDownloadSource = async () => {
try {
const sources = await window.electron.getDownloadSources();
- setDownloadSources(sources as DownloadSource[]);
+ setDownloadSources(sources);
} catch (error) {
logger.error("Failed to refresh download sources:", error);
}
@@ -128,7 +128,7 @@ export function SettingsDownloadSources() {
try {
await window.electron.syncDownloadSources();
const sources = await window.electron.getDownloadSources();
- setDownloadSources(sources as DownloadSource[]);
+ setDownloadSources(sources);
showSuccessToast(t("download_sources_synced_successfully"));
} finally {
From 4bfe6d7f86f864c8989339d7ee6a6bc6ea483f64 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 15:32:08 -0300
Subject: [PATCH 092/118] feat: limit game text search to 255 chars
---
src/renderer/src/components/header/header.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx
index e61f3954..8166658d 100644
--- a/src/renderer/src/components/header/header.tsx
+++ b/src/renderer/src/components/header/header.tsx
@@ -60,7 +60,7 @@ export function Header() {
};
const handleSearch = (value: string) => {
- dispatch(setFilters({ title: value }));
+ dispatch(setFilters({ title: value.trim().slice(0, 255) }));
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
From 2aa31c0db0556b1a674d6cd49d08e2ce1fecbf67 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 15:34:49 -0300
Subject: [PATCH 093/118] feat: limit game text search to 255 chars
---
src/renderer/src/pages/catalogue/catalogue.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx
index bbeda906..b9eb3c24 100644
--- a/src/renderer/src/pages/catalogue/catalogue.tsx
+++ b/src/renderer/src/pages/catalogue/catalogue.tsx
@@ -35,7 +35,7 @@ export default function Catalogue() {
const { steamDevelopers, steamPublishers, downloadSources } = useCatalogue();
- const { steamGenres, steamUserTags } = useAppSelector(
+ const { steamGenres, steamUserTags, filters, page } = useAppSelector(
(state) => state.catalogueSearch
);
@@ -47,8 +47,6 @@ export default function Catalogue() {
const { formatNumber } = useFormat();
- const { filters, page } = useAppSelector((state) => state.catalogueSearch);
-
const dispatch = useAppDispatch();
const { t, i18n } = useTranslation("catalogue");
From 80e0adcd4969ffda3eb25c8006009a22f1e879f7 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 30 Oct 2025 23:33:07 +0200
Subject: [PATCH 094/118] fix: removed ability to enter non-number symbols to
pagination
---
.../src/pages/catalogue/pagination.tsx | 37 +++++++++++++++----
1 file changed, 30 insertions(+), 7 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index 9febc8f8..8dd85cab 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -29,9 +29,11 @@ function JumpControl({
return isOpen ? (
) => {
- const val = e.target.value;
- if (val === "") {
+ const raw = e.target.value;
+ const digitsOnly = raw.replace(/\D+/g, "");
+ if (digitsOnly === "") {
setJumpValue("");
return;
}
- const num = Number(val);
+ const num = parseInt(digitsOnly, 10);
if (Number.isNaN(num)) {
+ setJumpValue("");
return;
}
if (num < 1) {
@@ -104,19 +108,38 @@ export function Pagination({
setJumpValue(String(totalPages));
return;
}
- setJumpValue(val);
+ setJumpValue(String(num));
};
const onJumpKeyDown = (e: KeyboardEvent) => {
+ // Allow common control keys
+ const controlKeys = [
+ "Backspace",
+ "Delete",
+ "Tab",
+ "ArrowLeft",
+ "ArrowRight",
+ "Home",
+ "End",
+ ];
+
+ if (controlKeys.includes(e.key) || e.ctrlKey || e.metaKey) {
+ return;
+ }
+
if (e.key === "Enter") {
- if (jumpValue.trim() === "") return;
- const parsed = Number(jumpValue);
+ const sanitized = jumpValue.replace(/\D+/g, "");
+ if (sanitized.trim() === "") return;
+ const parsed = parseInt(sanitized, 10);
if (Number.isNaN(parsed)) return;
const target = Math.max(1, Math.min(totalPages, parsed));
onPageChange(target);
setIsJumpOpen(false);
} else if (e.key === "Escape") {
setIsJumpOpen(false);
+ } else if (!/^\d$/.test(e.key)) {
+ // Block any non-digit input (e.g., '.', ',')
+ e.preventDefault();
}
};
From bbbf861594575fa8bfa685fecc3dd6e3ca967134 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 30 Oct 2025 23:36:41 +0200
Subject: [PATCH 095/118] fix: deleted comments
---
src/renderer/src/pages/catalogue/pagination.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index 8dd85cab..c41635d0 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -112,7 +112,6 @@ export function Pagination({
};
const onJumpKeyDown = (e: KeyboardEvent) => {
- // Allow common control keys
const controlKeys = [
"Backspace",
"Delete",
@@ -138,7 +137,6 @@ export function Pagination({
} else if (e.key === "Escape") {
setIsJumpOpen(false);
} else if (!/^\d$/.test(e.key)) {
- // Block any non-digit input (e.g., '.', ',')
e.preventDefault();
}
};
From bd059cc7fa3800276f053f9650bdd355a85677a9 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Thu, 30 Oct 2025 23:45:29 +0200
Subject: [PATCH 096/118] feat: update cursorrules
---
.cursorrules | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/.cursorrules b/.cursorrules
index 0b0c009c..5015ab7e 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -27,3 +27,11 @@
- Follow TypeScript strict mode conventions
- Use async/await instead of promises when possible
- Prefer named exports over default exports for utilities and services
+
+## Comments
+
+- Keep comments concise and purposeful; avoid verbose explanations.
+- Focus on the "why" or non-obvious context, not restating the code.
+- Prefer self-explanatory naming and structure over excessive comments.
+- Do not comment every line or obvious behavior; remove stale comments.
+- Use docblocks only where they add value (public APIs, complex logic).
From aadbda770b5a6479688c40ae6c6f36cc904e06ac Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Fri, 31 Oct 2025 00:19:49 +0200
Subject: [PATCH 097/118] fix: linting issue, marked props as read-only
---
src/renderer/src/pages/catalogue/pagination.tsx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx
index c41635d0..ecc2afe3 100644
--- a/src/renderer/src/pages/catalogue/pagination.tsx
+++ b/src/renderer/src/pages/catalogue/pagination.tsx
@@ -58,7 +58,7 @@ export function Pagination({
page,
totalPages,
onPageChange,
-}: PaginationProps) {
+}: Readonly) {
const { formatNumber } = useFormat();
const [isJumpOpen, setIsJumpOpen] = useState(false);
@@ -90,12 +90,12 @@ export function Pagination({
const onJumpChange = (e: ChangeEvent) => {
const raw = e.target.value;
- const digitsOnly = raw.replace(/\D+/g, "");
+ const digitsOnly = raw.replaceAll(/\D+/g, "");
if (digitsOnly === "") {
setJumpValue("");
return;
}
- const num = parseInt(digitsOnly, 10);
+ const num = Number.parseInt(digitsOnly, 10);
if (Number.isNaN(num)) {
setJumpValue("");
return;
@@ -127,9 +127,9 @@ export function Pagination({
}
if (e.key === "Enter") {
- const sanitized = jumpValue.replace(/\D+/g, "");
+ const sanitized = jumpValue.replaceAll(/\D+/g, "");
if (sanitized.trim() === "") return;
- const parsed = parseInt(sanitized, 10);
+ const parsed = Number.parseInt(sanitized, 10);
if (Number.isNaN(parsed)) return;
const target = Math.max(1, Math.min(totalPages, parsed));
onPageChange(target);
From aa148c0b70234013c2137b64afc7530faf1ea48f Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Thu, 30 Oct 2025 20:01:47 -0300
Subject: [PATCH 098/118] fix: trim
---
src/renderer/src/components/header/header.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx
index 8166658d..6f97729f 100644
--- a/src/renderer/src/components/header/header.tsx
+++ b/src/renderer/src/components/header/header.tsx
@@ -60,7 +60,7 @@ export function Header() {
};
const handleSearch = (value: string) => {
- dispatch(setFilters({ title: value.trim().slice(0, 255) }));
+ dispatch(setFilters({ title: value.slice(0, 255) }));
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
From b8af69b0fbc52533b9c144b23ad22c107de53cd7 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Fri, 31 Oct 2025 12:01:42 +0000
Subject: [PATCH 099/118] fix: fixing review partial
---
src/locales/en/translation.json | 4 +++-
src/locales/es/translation.json | 4 +++-
src/locales/pt-BR/translation.json | 4 +++-
src/locales/pt-PT/translation.json | 4 +++-
src/locales/ru/translation.json | 4 +++-
.../src/pages/game-details/game-reviews.tsx | 1 -
.../src/pages/game-details/review-item.tsx | 21 ++++++-------------
7 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 668f1547..b48c6ece 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -361,7 +361,9 @@
"show_original": "Show original",
"show_translation": "Show translation",
"show_original_translated_from": "Show original (translated from {{language}})",
- "hide_original": "Hide original"
+ "hide_original": "Hide original",
+ "show": "Show",
+ "hide": "Hide"
},
"activation": {
"title": "Activate Hydra",
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index 863b8332..75a9adce 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -361,7 +361,9 @@
"you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego",
"language": "Idioma",
"caption": "Subtítulo",
- "audio": "Audio"
+ "audio": "Audio",
+ "show": "Mostrar",
+ "hide": "Ocultar"
},
"activation": {
"title": "Activar Hydra",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 5bfc2af3..98946d51 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -349,7 +349,9 @@
"show_translation": "Mostrar tradução",
"show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original",
- "rating_count": "Avaliação"
+ "rating_count": "Avaliação",
+ "show": "Mostrar",
+ "hide": "Ocultar"
},
"activation": {
"title": "Ativação",
diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json
index 2894cf65..a99bb93d 100644
--- a/src/locales/pt-PT/translation.json
+++ b/src/locales/pt-PT/translation.json
@@ -180,7 +180,9 @@
"download_error_not_cached_on_torbox": "Este download não está disponível no TorBox e a verificação do status do download não está disponível.",
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos",
- "create_start_menu_shortcut": "Criar atalho no Menu Iniciar"
+ "create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
+ "show": "Mostrar",
+ "hide": "Ocultar"
},
"activation": {
"title": "Ativação",
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 886c7d07..388763bd 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -360,7 +360,9 @@
"caption": "Субтитры",
"audio": "Аудио",
"filter_by_source": "Фильтр по источнику",
- "no_repacks_found": "Источники для этой игры не найдены"
+ "no_repacks_found": "Источники для этой игры не найдены",
+ "show": "Показать",
+ "hide": "Скрыть"
},
"activation": {
"title": "Активировать Hydra",
diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx
index 1a6fc675..2dfd8864 100644
--- a/src/renderer/src/pages/game-details/game-reviews.tsx
+++ b/src/renderer/src/pages/game-details/game-reviews.tsx
@@ -163,7 +163,6 @@ export function GameReviews({
take: "20",
skip: skip.toString(),
sortBy: reviewsSortBy,
- language: i18n.language,
});
const response = await window.electron.hydraApi.get(
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index f5e3528a..7e407e20 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -71,24 +71,16 @@ export function ReviewItem({
const [showOriginal, setShowOriginal] = useState(false);
- // Check if this is the user's own review
const isOwnReview = userDetailsId === review.user.id;
- // Helper to get base language code (e.g., "pt" from "pt-BR")
- const getBaseLanguage = (lang: string) => lang.split("-")[0];
+ const getBaseLanguage = (lang: string | null) => lang?.split("-")[0] || "";
- // Check if the review is in a different language (comparing base language codes)
const isDifferentLanguage =
getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language);
- // Check if translation is available and needed (but not for own reviews)
const needsTranslation =
- !isOwnReview &&
- isDifferentLanguage &&
- review.translations &&
- review.translations[i18n.language];
+ !isOwnReview && isDifferentLanguage && review.translations[i18n.language];
- // Get the full language name using Intl.DisplayNames
const getLanguageName = (languageCode: string) => {
try {
const displayNames = new Intl.DisplayNames([i18n.language], {
@@ -100,7 +92,6 @@ export function ReviewItem({
}
};
- // Determine which content to show - always show original for own reviews
const displayContent = needsTranslation
? review.translations[i18n.language]
: review.reviewHtml;
@@ -109,12 +100,12 @@ export function ReviewItem({
return (
- Review from blocked user —{" "}
+ {t("review_from_blocked_user")}
onToggleVisibility(review.id)}
>
- Show
+ {t("show")}
@@ -191,7 +182,7 @@ export function ReviewItem({
{showOriginal
? t("hide_original")
: t("show_original_translated_from", {
- language: getLanguageName(review.detectedLanguage),
+ language: getLanguageName(review.detectedLanguage!),
})}
{showOriginal && (
@@ -323,7 +314,7 @@ export function ReviewItem({
className="game-details__blocked-review-hide-link"
onClick={() => onToggleVisibility(review.id)}
>
- Hide
+ {t("hide")}
)}
From ff8a61ff7a83394e08609a48770cfe1323c8d8ce Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Fri, 31 Oct 2025 12:05:24 +0000
Subject: [PATCH 100/118] fix: fixing review partial
---
src/locales/en/translation.json | 1 +
src/locales/es/translation.json | 1 +
src/locales/pt-BR/translation.json | 1 +
src/locales/pt-PT/translation.json | 1 +
src/locales/ru/translation.json | 3 ++-
5 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index b48c6ece..8b9ff73e 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -362,6 +362,7 @@
"show_translation": "Show translation",
"show_original_translated_from": "Show original (translated from {{language}})",
"hide_original": "Hide original",
+ "review_from_blocked_user": "Review from blocked user",
"show": "Show",
"hide": "Hide"
},
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index 75a9adce..adf25e33 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -362,6 +362,7 @@
"language": "Idioma",
"caption": "Subtítulo",
"audio": "Audio",
+ "review_from_blocked_user": "Reseña de usuario bloqueado",
"show": "Mostrar",
"hide": "Ocultar"
},
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 98946d51..42743a64 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -350,6 +350,7 @@
"show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original",
"rating_count": "Avaliação",
+ "review_from_blocked_user": "Avaliação de usuário bloqueado",
"show": "Mostrar",
"hide": "Ocultar"
},
diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json
index a99bb93d..6c1963cc 100644
--- a/src/locales/pt-PT/translation.json
+++ b/src/locales/pt-PT/translation.json
@@ -181,6 +181,7 @@
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
+ "review_from_blocked_user": "Avaliação de utilizador bloqueado",
"show": "Mostrar",
"hide": "Ocultar"
},
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 682b1322..6f4d4b92 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -363,7 +363,8 @@
"show_original": "Показать оригинал",
"show_translation": "Показать перевод",
"show_original_translated_from": "Показать оригинал (переведено с {{language}})",
- "hide_original": "Скрыть оригинал"
+ "hide_original": "Скрыть оригинал",
+ "review_from_blocked_user": "Отзыв от заблокированного пользователя"
},
"activation": {
"title": "Активировать Hydra",
From c71f5947ba8dfb7856625477289bb9b2f44346f2 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 31 Oct 2025 10:20:11 -0300
Subject: [PATCH 101/118] feat: use new ep to track game playtime
---
src/main/services/library-sync/update-game-playtime.ts | 4 ++--
src/main/services/process-watcher.ts | 8 ++++----
src/main/services/window-manager.ts | 8 +-------
3 files changed, 7 insertions(+), 13 deletions(-)
diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts
index b53ebebc..a669a363 100644
--- a/src/main/services/library-sync/update-game-playtime.ts
+++ b/src/main/services/library-sync/update-game-playtime.ts
@@ -1,7 +1,7 @@
import type { Game } from "@types";
import { HydraApi } from "../hydra-api";
-export const updateGamePlaytime = async (
+export const trackGamePlaytime = async (
game: Game,
deltaInMillis: number,
lastTimePlayed: Date
@@ -10,7 +10,7 @@ export const updateGamePlaytime = async (
return;
}
- return HydraApi.put(`/profile/games/${game.remoteId}`, {
+ return HydraApi.put(`/profile/games/${game.shop}/${game.objectId}`, {
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
lastTimePlayed,
});
diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts
index a1449255..db5bbee1 100644
--- a/src/main/services/process-watcher.ts
+++ b/src/main/services/process-watcher.ts
@@ -1,5 +1,5 @@
import { WindowManager } from "./window-manager";
-import { createGame, updateGamePlaytime } from "./library-sync";
+import { createGame, trackGamePlaytime } from "./library-sync";
import type { Game, GameRunning, UserPreferences } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
@@ -223,7 +223,7 @@ function onOpenGame(game: Game) {
);
if (game.remoteId) {
- updateGamePlaytime(
+ trackGamePlaytime(
game,
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
new Date()
@@ -277,7 +277,7 @@ function onTickGame(game: Game) {
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
const gamePromise = game.remoteId
- ? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
+ ? trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
: createGame(game);
gamePromise
@@ -337,7 +337,7 @@ const onCloseGame = (game: Game) => {
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
- return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
+ return trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
.then(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...updatedGame,
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 673bf1a0..2484e8e7 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -289,12 +289,6 @@ export class WindowManager {
}
}
- private static loadNotificationWindowURL() {
- if (this.notificationWindow) {
- this.loadWindowURL(this.notificationWindow, "achievement-notification");
- }
- }
-
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
@@ -387,7 +381,7 @@ export class WindowManager {
this.notificationWindow.setIgnoreMouseEvents(true);
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
- this.loadNotificationWindowURL();
+ this.loadWindowURL(this.notificationWindow, "achievement-notification");
if (!app.isPackaged || isStaging) {
this.notificationWindow.webContents.openDevTools();
From 138120460cf0957eaf584162172f8791c2c318e1 Mon Sep 17 00:00:00 2001
From: jarexe
Date: Fri, 31 Oct 2025 10:57:44 -0300
Subject: [PATCH 102/118] fix: correct achievement notification positioning on
multi-monitor setups
---
src/main/services/window-manager.ts | 38 +++++++++++++++++++----------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts
index 673bf1a0..834bf7ab 100644
--- a/src/main/services/window-manager.ts
+++ b/src/main/services/window-manager.ts
@@ -302,46 +302,58 @@ export class WindowManager {
position: AchievementCustomNotificationPosition | undefined
) {
const display = screen.getPrimaryDisplay();
- const { width, height } = display.workAreaSize;
+ const {
+ x: displayX,
+ y: displayY,
+ width: displayWidth,
+ height: displayHeight,
+ } = display.bounds;
if (position === "bottom-left") {
return {
- x: 0,
- y: height - this.NOTIFICATION_WINDOW_HEIGHT,
+ x: displayX,
+ y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-center") {
return {
- x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
- y: height - this.NOTIFICATION_WINDOW_HEIGHT,
+ x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
+ y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-right") {
return {
- x: width - this.NOTIFICATION_WINDOW_WIDTH,
- y: height - this.NOTIFICATION_WINDOW_HEIGHT,
+ x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
+ y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
+ };
+ }
+
+ if (position === "top-left") {
+ return {
+ x: displayX,
+ y: displayY,
};
}
if (position === "top-center") {
return {
- x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
- y: 0,
+ x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
+ y: displayY,
};
}
if (position === "top-right") {
return {
- x: width - this.NOTIFICATION_WINDOW_WIDTH,
- y: 0,
+ x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
+ y: displayY,
};
}
return {
- x: 0,
- y: 0,
+ x: displayX,
+ y: displayY,
};
}
From 51c4e4f5b38b411f0ac92d13a1b15d8b676ceecc Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 31 Oct 2025 13:07:06 -0300
Subject: [PATCH 103/118] chore: bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 5d84e763..ee039574 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
- "version": "3.7.2",
+ "version": "3.7.3",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
From d167628ed44e22c6090f352ee57cdb5710b32811 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 31 Oct 2025 13:57:15 -0300
Subject: [PATCH 104/118] fix: prevent crash when detectedLanguage is null
---
src/renderer/src/pages/game-details/review-item.tsx | 5 +++--
src/types/index.ts | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index 7e407e20..c411e0cb 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -81,7 +81,8 @@ export function ReviewItem({
const needsTranslation =
!isOwnReview && isDifferentLanguage && review.translations[i18n.language];
- const getLanguageName = (languageCode: string) => {
+ const getLanguageName = (languageCode: string | null) => {
+ if (!languageCode) return "";
try {
const displayNames = new Intl.DisplayNames([i18n.language], {
type: "language",
@@ -182,7 +183,7 @@ export function ReviewItem({
{showOriginal
? t("hide_original")
: t("show_original_translated_from", {
- language: getLanguageName(review.detectedLanguage!),
+ language: getLanguageName(review.detectedLanguage),
})}
{showOriginal && (
diff --git a/src/types/index.ts b/src/types/index.ts
index 4b13c496..c04b6232 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -252,7 +252,7 @@ export interface GameReview {
translations: {
[key: string]: string;
};
- detectedLanguage: string;
+ detectedLanguage: string | null;
}
export interface TrendingGame extends ShopAssets {
From 19bf99ff119f65394bcdaed84d0a1423472b1d88 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Fri, 31 Oct 2025 16:16:03 -0300
Subject: [PATCH 105/118] chore: add sleep to aur script
---
.github/workflows/update-aur.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/update-aur.yml b/.github/workflows/update-aur.yml
index 52fe907e..fa12b500 100644
--- a/.github/workflows/update-aur.yml
+++ b/.github/workflows/update-aur.yml
@@ -95,6 +95,8 @@ jobs:
- name: Update PKGBUILD and .SRCINFO
if: steps.check-update.outputs.update_needed == 'true'
run: |
+ # sleeps for 1 minute to be sure GH updated the release info
+ sleep 60
# Update pkgver in PKGBUILD
cd hydra-launcher-bin
NEW_VERSION="${{ steps.get-version.outputs.version }}"
From bf387aef3f240febd3e4b6d66c3d78be698c983f Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 17:30:45 +0000
Subject: [PATCH 106/118] feat: improving animations
---
.../user-profile/user-profile.context.tsx | 76 +-
.../profile-content/profile-content.scss | 27 +-
.../profile-content/profile-content.tsx | 681 +++++++++++-------
.../user-library-game-card.scss | 25 +-
.../user-library-game-card.tsx | 25 +-
5 files changed, 542 insertions(+), 292 deletions(-)
diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx
index 87e2a669..9f3a861d 100644
--- a/src/renderer/src/context/user-profile/user-profile.context.tsx
+++ b/src/renderer/src/context/user-profile/user-profile.context.tsx
@@ -14,12 +14,15 @@ export interface UserProfileContext {
isMe: boolean;
userStats: UserStats | null;
getUserProfile: () => Promise;
- getUserLibraryGames: (sortBy?: string) => Promise;
+ getUserLibraryGames: (sortBy?: string, reset?: boolean) => Promise;
+ loadMoreLibraryGames: (sortBy?: string) => Promise;
setSelectedBackgroundImage: React.Dispatch>;
backgroundImage: string;
badges: Badge[];
libraryGames: UserGame[];
pinnedGames: UserGame[];
+ hasMoreLibraryGames: boolean;
+ isLoadingLibraryGames: boolean;
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -30,12 +33,15 @@ export const userProfileContext = createContext({
isMe: false,
userStats: null,
getUserProfile: async () => {},
- getUserLibraryGames: async (_sortBy?: string) => {},
+ getUserLibraryGames: async (_sortBy?: string, _reset?: boolean) => {},
+ loadMoreLibraryGames: async (_sortBy?: string) => false,
setSelectedBackgroundImage: () => {},
backgroundImage: "",
badges: [],
libraryGames: [],
pinnedGames: [],
+ hasMoreLibraryGames: false,
+ isLoadingLibraryGames: false,
});
const { Provider } = userProfileContext;
@@ -62,6 +68,9 @@ export function UserProfileContextProvider({
DEFAULT_USER_PROFILE_BACKGROUND
);
const [selectedBackgroundImage, setSelectedBackgroundImage] = useState("");
+ const [libraryPage, setLibraryPage] = useState(0);
+ const [hasMoreLibraryGames, setHasMoreLibraryGames] = useState(true);
+ const [isLoadingLibraryGames, setIsLoadingLibraryGames] = useState(false);
const isMe = userDetails?.id === userProfile?.id;
@@ -93,7 +102,13 @@ export function UserProfileContextProvider({
}, [userId]);
const getUserLibraryGames = useCallback(
- async (sortBy?: string) => {
+ async (sortBy?: string, reset = true) => {
+ if (reset) {
+ setLibraryPage(0);
+ setHasMoreLibraryGames(true);
+ setIsLoadingLibraryGames(true);
+ }
+
try {
const params = new URLSearchParams();
params.append("take", "12");
@@ -115,18 +130,68 @@ export function UserProfileContextProvider({
if (response) {
setLibraryGames(response.library);
setPinnedGames(response.pinnedGames);
+ setHasMoreLibraryGames(response.library.length === 12);
} else {
setLibraryGames([]);
setPinnedGames([]);
+ setHasMoreLibraryGames(false);
}
} catch (error) {
setLibraryGames([]);
setPinnedGames([]);
+ setHasMoreLibraryGames(false);
+ } finally {
+ setIsLoadingLibraryGames(false);
}
},
[userId]
);
+ const loadMoreLibraryGames = useCallback(
+ async (sortBy?: string): Promise => {
+ if (isLoadingLibraryGames || !hasMoreLibraryGames) {
+ return false;
+ }
+
+ setIsLoadingLibraryGames(true);
+ try {
+ const nextPage = libraryPage + 1;
+ const params = new URLSearchParams();
+ params.append("take", "12");
+ params.append("skip", String(nextPage * 12));
+ if (sortBy) {
+ params.append("sortBy", sortBy);
+ }
+
+ const queryString = params.toString();
+ const url = queryString
+ ? `/users/${userId}/library?${queryString}`
+ : `/users/${userId}/library`;
+
+ const response = await window.electron.hydraApi.get<{
+ library: UserGame[];
+ pinnedGames: UserGame[];
+ }>(url);
+
+ if (response && response.library.length > 0) {
+ setLibraryGames((prev) => [...prev, ...response.library]);
+ setLibraryPage(nextPage);
+ setHasMoreLibraryGames(response.library.length === 12);
+ return true;
+ } else {
+ setHasMoreLibraryGames(false);
+ return false;
+ }
+ } catch (error) {
+ setHasMoreLibraryGames(false);
+ return false;
+ } finally {
+ setIsLoadingLibraryGames(false);
+ }
+ },
+ [userId, libraryPage, hasMoreLibraryGames, isLoadingLibraryGames]
+ );
+
const getUserProfile = useCallback(async () => {
getUserStats();
getUserLibraryGames();
@@ -164,6 +229,8 @@ export function UserProfileContextProvider({
setLibraryGames([]);
setPinnedGames([]);
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
+ setLibraryPage(0);
+ setHasMoreLibraryGames(true);
getUserProfile();
getBadges();
@@ -177,12 +244,15 @@ export function UserProfileContextProvider({
isMe,
getUserProfile,
getUserLibraryGames,
+ loadMoreLibraryGames,
setSelectedBackgroundImage,
backgroundImage: getBackgroundImageUrl(),
userStats,
badges,
libraryGames,
pinnedGames,
+ hasMoreLibraryGames,
+ isLoadingLibraryGames,
}}
>
{children}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index 15b9de6d..ffdf6a45 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -101,6 +101,11 @@
gap: calc(globals.$spacing-unit);
margin-bottom: calc(globals.$spacing-unit * 2);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ position: relative;
+ }
+
+ &__tab-wrapper {
+ position: relative;
}
&__tab {
@@ -111,19 +116,22 @@
cursor: pointer;
font-size: 14px;
font-weight: 500;
- border-bottom: 2px solid transparent;
- transition: all ease 0.2s;
-
- &:hover {
- color: rgba(255, 255, 255, 0.8);
- }
+ transition: color ease 0.2s;
&--active {
color: white;
- border-bottom-color: white;
}
}
+ &__tab-underline {
+ position: absolute;
+ bottom: -1px;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: white;
+ }
+
&__games-grid {
list-style: none;
margin: 0;
@@ -179,10 +187,6 @@
&__tab-panels {
display: block;
}
-
- &__tab-panel[hidden] {
- display: none;
- }
}
}
@@ -210,7 +214,6 @@
.user-reviews__review-item {
border-radius: 8px;
- padding: calc(globals.$spacing-unit * 2);
}
.user-reviews__review-header {
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index e284cb88..749c7588 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -26,6 +26,8 @@ import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
import type { GameShop } from "@types";
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
import "./profile-content.scss";
type SortOption = "playtime" | "achievementCount" | "playedRecently";
@@ -71,6 +73,9 @@ export function ProfileContent() {
libraryGames,
pinnedGames,
getUserLibraryGames,
+ loadMoreLibraryGames,
+ hasMoreLibraryGames,
+ isLoadingLibraryGames,
} = useContext(userProfileContext);
const { userDetails } = useUserDetails();
const { formatDistance } = useDate();
@@ -104,10 +109,69 @@ export function ProfileContent() {
useEffect(() => {
if (userProfile) {
- getUserLibraryGames(sortBy);
+ getUserLibraryGames(sortBy, true);
}
}, [sortBy, getUserLibraryGames, userProfile]);
+ const loadMoreRef = useRef(null);
+ const observerRef = useRef(null);
+
+ useEffect(() => {
+ if (activeTab !== "library" || !hasMoreLibraryGames) {
+ return;
+ }
+
+ // Clean up previous observer
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+
+ // Use setTimeout to ensure the DOM element is available after render
+ const timeoutId = setTimeout(() => {
+ const currentRef = loadMoreRef.current;
+ if (!currentRef) {
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const entry = entries[0];
+ if (
+ entry?.isIntersecting &&
+ hasMoreLibraryGames &&
+ !isLoadingLibraryGames
+ ) {
+ loadMoreLibraryGames(sortBy);
+ }
+ },
+ {
+ root: null,
+ rootMargin: "200px",
+ threshold: 0.1,
+ }
+ );
+
+ observerRef.current = observer;
+ observer.observe(currentRef);
+ }, 100);
+
+ return () => {
+ clearTimeout(timeoutId);
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+ };
+ }, [
+ activeTab,
+ hasMoreLibraryGames,
+ isLoadingLibraryGames,
+ loadMoreLibraryGames,
+ sortBy,
+ libraryGames.length,
+ ]);
+
// Clear reviews state and reset tab when switching users
useEffect(() => {
setReviews([]);
@@ -332,294 +396,373 @@ export function ProfileContent() {
-
setActiveTab("library")}
- >
- {t("library")}
-
-
setActiveTab("reviews")}
- >
- {t("user_reviews")}
-
+
+ setActiveTab("library")}
+ >
+ {t("library")}
+
+ {activeTab === "library" && (
+
+ )}
+
+
+ setActiveTab("reviews")}
+ >
+ {t("user_reviews")}
+
+ {activeTab === "reviews" && (
+
+ )}
+
-
- {hasAnyGames && (
-
- )}
+
+ {activeTab === "library" && (
+
+ {hasAnyGames && (
+
+ )}
- {!hasAnyGames && (
-
-
-
-
-
{t("no_recent_activity_title")}
- {isMe &&
{t("no_recent_activity_description")}
}
-
- )}
-
- {hasAnyGames && (
-
- {hasPinnedGames && (
-
-
-
- {/* removed collapse button */}
-
{t("pinned")}
-
- {pinnedGames.length}
-
-
+ {!hasAnyGames && (
+
+
+
-
- {/* render pinned games unconditionally */}
-
- {pinnedGames?.map((game) => (
- -
-
-
- ))}
-
+
{t("no_recent_activity_title")}
+ {isMe &&
{t("no_recent_activity_description")}
}
)}
- {hasGames && (
+ {hasAnyGames && (
-
-
-
{t("library")}
- {userStats && (
-
- {numberFormatter.format(userStats.libraryCount)}
-
- )}
-
-
-
-
- {libraryGames?.map((game) => (
- -
-
-
- ))}
-
-
- )}
-
- )}
-
-
-
-
-
-
- {/* removed collapse button */}
-
{t("user_reviews")}
- {reviewsTotalCount > 0 && (
-
- {reviewsTotalCount}
-
- )}
-
-
-
- {/* render reviews content unconditionally */}
- {isLoadingReviews && (
-
- {t("loading_reviews")}
-
- )}
- {!isLoadingReviews && reviews.length === 0 && (
-
-
{t("no_reviews", "No reviews yet")}
-
- )}
- {!isLoadingReviews && reviews.length > 0 && (
-
- {reviews.map((review) => {
- const isOwnReview = userDetails?.id === review.user.id;
-
- return (
-
-
-
- {formatDistance(
- new Date(review.createdAt),
- new Date(),
- { addSuffix: true }
- )}
-
-
-
- {Array.from({ length: 5 }, (_, index) => (
-
-
-
- ))}
+ {hasPinnedGames && (
+
+
+
+ {/* removed collapse button */}
+
{t("pinned")}
+
+ {pinnedGames.length}
+
-
+ {/* render pinned games unconditionally */}
+
+ {pinnedGames?.map((game) => (
+ -
+
+
+ ))}
+
+
+ )}
-
-
-
-
-

-
- navigate(
- buildGameDetailsPath(review.game)
- )
- }
+ {hasGames && (
+
+
+
+
{t("library")}
+ {userStats && (
+
+ {numberFormatter.format(
+ userStats.libraryCount
+ )}
+
+ )}
+
+
+
+
+ {libraryGames?.map((game) => (
+ -
+
+
+ ))}
+
+ {hasMoreLibraryGames && (
+
+ )}
+ {isLoadingLibraryGames && (
+
+
+ {Array.from({ length: 12 }).map((_, i) => (
+ -
- {review.game.title}
-
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )}
+
+ )}
+
+ {activeTab === "reviews" && (
+
+
+
+ {/* removed collapse button */}
+
{t("user_reviews")}
+ {reviewsTotalCount > 0 && (
+
+ {reviewsTotalCount}
+
+ )}
+
+
+
+ {/* render reviews content unconditionally */}
+ {isLoadingReviews && (
+
+ {t("loading_reviews")}
+
+ )}
+ {!isLoadingReviews && reviews.length === 0 && (
+
+
{t("no_reviews", "No reviews yet")}
+
+ )}
+ {!isLoadingReviews && reviews.length > 0 && (
+
+ {reviews.map((review) => {
+ const isOwnReview = userDetails?.id === review.user.id;
+
+ return (
+
+
+
+ {formatDistance(
+ new Date(review.createdAt),
+ new Date(),
+ { addSuffix: true }
+ )}
+
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+

+
+ navigate(
+ buildGameDetailsPath(review.game)
+ )
+ }
+ >
+ {review.game.title}
+
+
-
-
-
-
- handleVoteReview(review.id, true)
- }
- disabled={votingReviews.has(review.id)}
- style={{
- opacity: votingReviews.has(review.id)
- ? 0.5
- : 1,
- cursor: votingReviews.has(review.id)
- ? "not-allowed"
- : "pointer",
- }}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.upvotes}
-
-
-
+
+
+
+ handleVoteReview(review.id, true)
+ }
+ disabled={votingReviews.has(review.id)}
+ style={{
+ opacity: votingReviews.has(review.id)
+ ? 0.5
+ : 1,
+ cursor: votingReviews.has(review.id)
+ ? "not-allowed"
+ : "pointer",
+ }}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.upvotes}
+
+
+
-
- handleVoteReview(review.id, false)
- }
- disabled={votingReviews.has(review.id)}
- style={{
- opacity: votingReviews.has(review.id)
- ? 0.5
- : 1,
- cursor: votingReviews.has(review.id)
- ? "not-allowed"
- : "pointer",
- }}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.downvotes}
-
-
-
+
+ handleVoteReview(review.id, false)
+ }
+ disabled={votingReviews.has(review.id)}
+ style={{
+ opacity: votingReviews.has(review.id)
+ ? 0.5
+ : 1,
+ cursor: votingReviews.has(review.id)
+ ? "not-allowed"
+ : "pointer",
+ }}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.downvotes}
+
+
+
+
+
+ {isOwnReview && (
+
handleDeleteClick(review.id)}
+ title={t("delete_review")}
+ >
+
+ {t("delete_review")}
+
+ )}
-
- {isOwnReview && (
-
handleDeleteClick(review.id)}
- title={t("delete_review")}
- >
-
- {t("delete_review")}
-
- )}
-
-
- );
- })}
-
- )}
-
-
+
+ );
+ })}
+
+ )}
+
+ )}
+
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss
index 61640536..76bd25a9 100644
--- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.scss
@@ -36,6 +36,7 @@
box-shadow: 0 8px 10px -2px rgba(0, 0, 0, 0.5);
width: 100%;
position: relative;
+ overflow: hidden;
&:before {
content: "";
@@ -193,8 +194,28 @@
border-radius: 4px;
width: 100%;
height: 100%;
- min-width: 100%;
- min-height: 100%;
+ display: block;
+ }
+
+ &__cover-placeholder {
+ position: relative;
+ width: 100%;
+ padding-bottom: 150%;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0.08) 0%,
+ rgba(255, 255, 255, 0.04) 50%,
+ rgba(255, 255, 255, 0.08) 100%
+ );
+ border-radius: 4px;
+ color: rgba(255, 255, 255, 0.3);
+
+ svg {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
}
&__achievements-progress {
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
index 72b48a8c..81db6334 100644
--- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
@@ -2,7 +2,7 @@ import { UserGame } from "@types";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useFormat, useToast } from "@renderer/hooks";
import { useNavigate } from "react-router-dom";
-import { useCallback, useContext, useState } from "react";
+import { useCallback, useContext, useEffect, useState } from "react";
import {
buildGameAchievementPath,
buildGameDetailsPath,
@@ -15,6 +15,7 @@ import {
AlertFillIcon,
PinIcon,
PinSlashIcon,
+ ImageIcon,
} from "@primer/octicons-react";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { Tooltip } from "react-tooltip";
@@ -44,6 +45,11 @@ export function UserLibraryGameCard({
const navigate = useNavigate();
const [isTooltipHovered, setIsTooltipHovered] = useState(false);
const [isPinning, setIsPinning] = useState(false);
+ const [imageError, setImageError] = useState(false);
+
+ useEffect(() => {
+ setImageError(false);
+ }, [game.coverImageUrl]);
const getStatsItemCount = useCallback(() => {
let statsCount = 1;
@@ -233,11 +239,18 @@ export function UserLibraryGameCard({
)}
-

+ {imageError || !game.coverImageUrl ? (
+
+
+
+ ) : (
+

setImageError(true)}
+ />
+ )}
Date: Sun, 2 Nov 2025 20:23:12 +0000
Subject: [PATCH 107/118] feat: adding infinite scroll
---
package.json | 1 +
src/locales/en/translation.json | 2 +-
src/locales/es/translation.json | 7 +-
src/locales/pt-BR/translation.json | 7 +-
src/locales/pt-PT/translation.json | 9 +-
src/locales/ru/translation.json | 7 +-
src/renderer/src/app.tsx | 2 +-
.../user-profile/user-profile.context.tsx | 8 +-
.../src/pages/game-details/review-item.tsx | 1 -
.../profile-content/profile-content.scss | 67 +++-
.../profile-content/profile-content.tsx | 288 +++++++++---------
yarn.lock | 12 +
12 files changed, 258 insertions(+), 153 deletions(-)
diff --git a/package.json b/package.json
index ee039574..f06fef64 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0",
+ "react-infinite-scroll-component": "^6.1.0",
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index e34c113a..2888a2e2 100755
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -694,7 +694,7 @@
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Earned from positive likes on reviews",
- "user_reviews": "User's Reviews",
+ "user_reviews": "Reviews",
"delete_review": "Delete Review",
"loading_reviews": "Loading reviews..."
},
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index adf25e33..c7e9d13e 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -325,6 +325,7 @@
"maybe_later": "Tal vez después",
"no_repacks_found": "Sin fuentes encontradas para este juego",
"no_reviews_yet": "Sin reseñas aún",
+ "review_played_for": "Jugado por",
"properties": "Propiedades",
"rating": "Calificación",
"rating_count": "Calificación",
@@ -681,7 +682,11 @@
"karma_count": "karma",
"karma_description": "Conseguido por me gustas positivos en reseñas",
"sort_by": "Filtrar por:",
- "game_added_to_pinned": "Juego añadido a fijados"
+ "game_added_to_pinned": "Juego añadido a fijados",
+ "user_reviews": "Reseñas",
+ "loading_reviews": "Cargando reseñas...",
+ "no_reviews": "Sin reseñas aún",
+ "delete_review": "Eliminar reseña"
},
"achievement": {
"achievement_unlocked": "Logro desbloqueado",
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
index 42743a64..50049140 100755
--- a/src/locales/pt-BR/translation.json
+++ b/src/locales/pt-BR/translation.json
@@ -317,6 +317,7 @@
"sort_lowest_score": "Menor Nota",
"sort_most_voted": "Mais Votadas",
"no_reviews_yet": "Ainda não há avaliações",
+ "review_played_for": "Jogado por",
"be_first_to_review": "Seja o primeiro a compartilhar seus pensamentos sobre este jogo!",
"rating": "Avaliação",
"rating_stats": "Avaliação",
@@ -696,7 +697,11 @@
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Ganho a partir de curtidas positivas em avaliações",
- "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente"
+ "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
+ "user_reviews": "Avaliações",
+ "loading_reviews": "Carregando avaliações...",
+ "no_reviews": "Ainda não há avaliações",
+ "delete_review": "Excluir avaliação"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json
index 6c1963cc..c8e4586d 100644
--- a/src/locales/pt-PT/translation.json
+++ b/src/locales/pt-PT/translation.json
@@ -183,7 +183,8 @@
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"review_from_blocked_user": "Avaliação de utilizador bloqueado",
"show": "Mostrar",
- "hide": "Ocultar"
+ "hide": "Ocultar",
+ "review_played_for": "Jogado por"
},
"activation": {
"title": "Ativação",
@@ -469,7 +470,11 @@
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Mostre as suas conquistas no perfil",
- "show_points_on_profile": "Mostre os seus pontos ganhos no perfil"
+ "show_points_on_profile": "Mostre os seus pontos ganhos no perfil",
+ "user_reviews": "Avaliações",
+ "loading_reviews": "A carregar avaliações...",
+ "no_reviews": "Ainda não há avaliações",
+ "delete_review": "Eliminar avaliação"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index 6f4d4b92..2e7c1504 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -227,6 +227,7 @@
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
"sort_newest": "Сначала новые",
"no_reviews_yet": "Пока нет отзывов",
+ "review_played_for": "Играли",
"be_first_to_review": "Станьте первым, кто поделится своими мыслями об этой игре!",
"sort_oldest": "Сначала старые",
"sort_highest_score": "Высший балл",
@@ -692,7 +693,11 @@
"game_added_to_pinned": "Игра добавлена в закрепленные",
"karma": "Карма",
"karma_count": "карма",
- "karma_description": "Заработана положительными оценками отзывов"
+ "karma_description": "Заработана положительными оценками отзывов",
+ "user_reviews": "Отзывы",
+ "loading_reviews": "Загрузка отзывов...",
+ "no_reviews": "Пока нет отзывов",
+ "delete_review": "Удалить отзыв"
},
"achievement": {
"achievement_unlocked": "Достижение разблокировано",
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index 168a4435..305747c3 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -279,7 +279,7 @@ export function App() {
-
diff --git a/src/renderer/src/context/user-profile/user-profile.context.tsx b/src/renderer/src/context/user-profile/user-profile.context.tsx
index 9f3a861d..c2a6864c 100644
--- a/src/renderer/src/context/user-profile/user-profile.context.tsx
+++ b/src/renderer/src/context/user-profile/user-profile.context.tsx
@@ -174,7 +174,13 @@ export function UserProfileContextProvider({
}>(url);
if (response && response.library.length > 0) {
- setLibraryGames((prev) => [...prev, ...response.library]);
+ setLibraryGames((prev) => {
+ const existingIds = new Set(prev.map((game) => game.objectId));
+ const newGames = response.library.filter(
+ (game) => !existingIds.has(game.objectId)
+ );
+ return [...prev, ...newGames];
+ });
setLibraryPage(nextPage);
setHasMoreLibraryGames(response.library.length === 12);
return true;
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index 6b03e7ea..bfc02cfe 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -175,7 +175,6 @@ export function ReviewItem({
-
{formatDistance(new Date(review.createdAt), new Date(), {
addSuffix: true,
})}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index ffdf6a45..d41e3530 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -117,12 +117,30 @@
font-size: 14px;
font-weight: 500;
transition: color ease 0.2s;
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit * 0.5);
&--active {
color: white;
}
}
+ &__tab-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 6px;
+ background-color: rgba(255, 255, 255, 0.15);
+ border-radius: 9px;
+ font-size: 11px;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.9);
+ line-height: 1;
+ }
+
&__tab-underline {
position: absolute;
bottom: -1px;
@@ -209,7 +227,7 @@
.user-reviews__list {
display: flex;
flex-direction: column;
- gap: calc(globals.$spacing-unit * 2);
+ gap: calc(globals.$spacing-unit * 4);
}
.user-reviews__review-item {
@@ -219,10 +237,16 @@
.user-reviews__review-header {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: flex-start;
margin-bottom: calc(globals.$spacing-unit * 1.5);
}
+.user-reviews__review-meta-row {
+ display: flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit * 0.75);
+}
+
.user-reviews__review-footer {
display: flex;
justify-content: space-between;
@@ -267,18 +291,53 @@
}
.user-reviews__review-date {
+ display: flex;
+ align-items: center;
+ gap: 4px;
color: rgba(255, 255, 255, 0.6);
font-size: globals.$small-font-size;
}
.user-reviews__review-score-stars {
display: flex;
- gap: calc(globals.$spacing-unit * 0.5);
+ align-items: center;
+ gap: 4px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 2px 6px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ font-size: 11px;
+ font-weight: 500;
}
-.user-reviews__review-star-container {
+.user-reviews__review-star {
+ color: rgba(255, 255, 255, 0.7);
+ transition: color 0.2s ease;
+
+ &--filled {
+ color: rgba(255, 255, 255, 0.7);
+
+ svg {
+ fill: currentColor;
+ }
+ }
+}
+
+.user-reviews__review-score-text {
+ font-weight: 500;
+}
+
+.user-reviews__review-playtime {
display: flex;
align-items: center;
+ gap: 4px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 11px;
+ font-weight: 500;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 2px 6px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
}
.user-reviews__review-content {
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 749c7588..7ef486d4 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -1,5 +1,12 @@
import { userProfileContext } from "@renderer/context";
-import { useContext, useEffect, useMemo, useRef, useState } from "react";
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import { ProfileHero } from "../profile-hero/profile-hero";
import {
useAppDispatch,
@@ -22,12 +29,13 @@ import { SortOptions } from "./sort-options";
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { buildGameDetailsPath } from "@renderer/helpers";
+import { ClockIcon } from "@primer/octicons-react";
import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
import type { GameShop } from "@types";
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
-import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
-import "react-loading-skeleton/dist/skeleton.css";
+import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
+import InfiniteScroll from "react-infinite-scroll-component";
import "./profile-content.scss";
type SortOption = "playtime" | "achievementCount" | "playedRecently";
@@ -36,6 +44,7 @@ interface UserReview {
id: string;
reviewHtml: string;
score: number;
+ playTimeInSeconds?: number;
upvotes: number;
downvotes: number;
hasUpvoted: boolean;
@@ -58,11 +67,21 @@ interface UserReviewsResponse {
reviews: UserReview[];
}
-const getScoreColorClass = (score: number) => {
- if (score >= 1 && score <= 2) return "game-details__review-score--red";
- if (score === 3) return "game-details__review-score--yellow";
- if (score >= 4 && score <= 5) return "game-details__review-score--green";
- return "";
+const getRatingText = (score: number, t: (key: string) => string): string => {
+ switch (score) {
+ case 1:
+ return t("rating_very_negative");
+ case 2:
+ return t("rating_negative");
+ case 3:
+ return t("rating_neutral");
+ case 4:
+ return t("rating_positive");
+ case 5:
+ return t("rating_very_positive");
+ default:
+ return "";
+ }
};
export function ProfileContent() {
@@ -98,6 +117,21 @@ export function ProfileContent() {
const dispatch = useAppDispatch();
const { t } = useTranslation("user_profile");
+ const { t: tGameDetails } = useTranslation("game_details");
+ const { numberFormatter } = useFormat();
+
+ const formatPlayTime = (playTimeInSeconds: number) => {
+ const minutes = playTimeInSeconds / 60;
+
+ if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
+ return t("amount_minutes", {
+ amount: minutes.toFixed(0),
+ });
+ }
+
+ const hours = minutes / 60;
+ return t("amount_hours", { amount: numberFormatter.format(hours) });
+ };
useEffect(() => {
dispatch(setHeaderTitle(""));
@@ -109,67 +143,32 @@ export function ProfileContent() {
useEffect(() => {
if (userProfile) {
+ // When sortBy changes, clear animated games so all games animate in
+ if (currentSortByRef.current !== sortBy) {
+ animatedGameIdsRef.current.clear();
+ currentSortByRef.current = sortBy;
+ }
getUserLibraryGames(sortBy, true);
}
}, [sortBy, getUserLibraryGames, userProfile]);
- const loadMoreRef = useRef(null);
- const observerRef = useRef(null);
+ const animatedGameIdsRef = useRef>(new Set());
+ const currentSortByRef = useRef(sortBy);
- useEffect(() => {
- if (activeTab !== "library" || !hasMoreLibraryGames) {
- return;
+ const handleLoadMore = useCallback(() => {
+ if (
+ activeTab === "library" &&
+ hasMoreLibraryGames &&
+ !isLoadingLibraryGames
+ ) {
+ loadMoreLibraryGames(sortBy);
}
-
- // Clean up previous observer
- if (observerRef.current) {
- observerRef.current.disconnect();
- observerRef.current = null;
- }
-
- // Use setTimeout to ensure the DOM element is available after render
- const timeoutId = setTimeout(() => {
- const currentRef = loadMoreRef.current;
- if (!currentRef) {
- return;
- }
-
- const observer = new IntersectionObserver(
- (entries) => {
- const entry = entries[0];
- if (
- entry?.isIntersecting &&
- hasMoreLibraryGames &&
- !isLoadingLibraryGames
- ) {
- loadMoreLibraryGames(sortBy);
- }
- },
- {
- root: null,
- rootMargin: "200px",
- threshold: 0.1,
- }
- );
-
- observerRef.current = observer;
- observer.observe(currentRef);
- }, 100);
-
- return () => {
- clearTimeout(timeoutId);
- if (observerRef.current) {
- observerRef.current.disconnect();
- observerRef.current = null;
- }
- };
}, [
activeTab,
hasMoreLibraryGames,
isLoadingLibraryGames,
loadMoreLibraryGames,
sortBy,
- libraryGames.length,
]);
// Clear reviews state and reset tab when switching users
@@ -368,8 +367,6 @@ export function ProfileContent() {
};
}, [setStatsIndex, isAnimationRunning]);
- const { numberFormatter } = useFormat();
-
const usersAreFriends = useMemo(() => {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
@@ -423,6 +420,11 @@ export function ProfileContent() {
onClick={() => setActiveTab("reviews")}
>
{t("user_reviews")}
+ {reviewsTotalCount > 0 && (
+
+ {reviewsTotalCount}
+
+ )}
{activeTab === "reviews" && (
-
- {libraryGames?.map((game) => (
- -
-
-
- ))}
-
- {hasMoreLibraryGames && (
-
- )}
- {isLoadingLibraryGames && (
-
-
- {Array.from({ length: 12 }).map((_, i) => (
- -
+
+ {libraryGames?.map((game, index) => {
+ const hasAnimated =
+ animatedGameIdsRef.current.has(game.objectId);
+ const isNewGame =
+ !hasAnimated && !isLoadingLibraryGames;
+
+ return (
+ {
+ if (isNewGame) {
+ animatedGameIdsRef.current.add(
+ game.objectId
+ );
+ }
+ }}
>
-
-
- ))}
-
-
- )}
+
+ );
+ })}
+
+
)}
@@ -579,18 +591,6 @@ export function ProfileContent() {
transition={{ duration: 0.2 }}
aria-hidden={false}
>
-
-
- {/* removed collapse button */}
-
{t("user_reviews")}
- {reviewsTotalCount > 0 && (
-
- {reviewsTotalCount}
-
- )}
-
-
-
{/* render reviews content unconditionally */}
{isLoadingReviews && (
@@ -616,6 +616,37 @@ export function ProfileContent() {
transition={{ duration: 0.3 }}
>
+
+
+
+
+ {review.score}/5
+
+
+ {Boolean(
+ review.playTimeInSeconds &&
+ review.playTimeInSeconds > 0
+ ) && (
+
+
+
+ {tGameDetails("review_played_for")}{" "}
+ {formatPlayTime(
+ review.playTimeInSeconds || 0
+ )}
+
+
+ )}
+
{formatDistance(
new Date(review.createdAt),
@@ -623,29 +654,6 @@ export function ProfileContent() {
{ addSuffix: true }
)}
-
-
- {Array.from({ length: 5 }, (_, index) => (
-
-
-
- ))}
-
=2.2.7 <3":
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
From f0dc7478cfd08ed0b60c24ef7bcaf471146d7b63 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 20:29:16 +0000
Subject: [PATCH 108/118] feat: adding reviews to profile
---
src/renderer/src/app.tsx | 6 +-
.../profile/profile-content/library-tab.tsx | 176 +++++++
.../profile-content/profile-content.tsx | 429 ++----------------
.../profile-content/profile-review-item.tsx | 191 ++++++++
.../profile/profile-content/profile-tabs.tsx | 67 +++
.../profile/profile-content/reviews-tab.tsx | 92 ++++
6 files changed, 568 insertions(+), 393 deletions(-)
create mode 100644 src/renderer/src/pages/profile/profile-content/library-tab.tsx
create mode 100644 src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
create mode 100644 src/renderer/src/pages/profile/profile-content/profile-tabs.tsx
create mode 100644 src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx
index 305747c3..1ab76381 100644
--- a/src/renderer/src/app.tsx
+++ b/src/renderer/src/app.tsx
@@ -279,7 +279,11 @@ export function App() {
-
diff --git a/src/renderer/src/pages/profile/profile-content/library-tab.tsx b/src/renderer/src/pages/profile/profile-content/library-tab.tsx
new file mode 100644
index 00000000..bd461862
--- /dev/null
+++ b/src/renderer/src/pages/profile/profile-content/library-tab.tsx
@@ -0,0 +1,176 @@
+import { motion } from "framer-motion";
+import { useTranslation } from "react-i18next";
+import { TelescopeIcon } from "@primer/octicons-react";
+import InfiniteScroll from "react-infinite-scroll-component";
+import { useFormat } from "@renderer/hooks";
+import type { UserGame } from "@types";
+import { SortOptions } from "./sort-options";
+import { UserLibraryGameCard } from "./user-library-game-card";
+import "./profile-content.scss";
+
+type SortOption = "playtime" | "achievementCount" | "playedRecently";
+
+interface LibraryTabProps {
+ sortBy: SortOption;
+ onSortChange: (sortBy: SortOption) => void;
+ pinnedGames: UserGame[];
+ libraryGames: UserGame[];
+ hasMoreLibraryGames: boolean;
+ isLoadingLibraryGames: boolean;
+ statsIndex: number;
+ userStats: { libraryCount: number } | null;
+ animatedGameIdsRef: React.MutableRefObject>;
+ onLoadMore: () => void;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+ isMe: boolean;
+}
+
+export function LibraryTab({
+ sortBy,
+ onSortChange,
+ pinnedGames,
+ libraryGames,
+ hasMoreLibraryGames,
+ isLoadingLibraryGames,
+ statsIndex,
+ userStats,
+ animatedGameIdsRef,
+ onLoadMore,
+ onMouseEnter,
+ onMouseLeave,
+ isMe,
+}: Readonly) {
+ const { t } = useTranslation("user_profile");
+ const { numberFormatter } = useFormat();
+
+ const hasGames = libraryGames.length > 0;
+ const hasPinnedGames = pinnedGames.length > 0;
+ const hasAnyGames = hasGames || hasPinnedGames;
+
+ return (
+
+ {hasAnyGames && }
+
+ {!hasAnyGames && (
+
+
+
+
+
{t("no_recent_activity_title")}
+ {isMe &&
{t("no_recent_activity_description")}
}
+
+ )}
+
+ {hasAnyGames && (
+
+ {hasPinnedGames && (
+
+
+
+
{t("pinned")}
+
+ {pinnedGames.length}
+
+
+
+
+
+ {pinnedGames?.map((game) => (
+ -
+
+
+ ))}
+
+
+ )}
+
+ {hasGames && (
+
+
+
+
{t("library")}
+ {userStats && (
+
+ {numberFormatter.format(userStats.libraryCount)}
+
+ )}
+
+
+
+
+
+ {libraryGames?.map((game, index) => {
+ const hasAnimated =
+ animatedGameIdsRef.current.has(game.objectId);
+ const isNewGame = !hasAnimated && !isLoadingLibraryGames;
+
+ return (
+ {
+ if (isNewGame) {
+ animatedGameIdsRef.current.add(game.objectId);
+ }
+ }}
+ >
+
+
+ );
+ })}
+
+
+
+ )}
+
+ )}
+
+ );
+}
+
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index 7ef486d4..ab9fdf01 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -8,14 +8,8 @@ import {
useState,
} from "react";
import { ProfileHero } from "../profile-hero/profile-hero";
-import {
- useAppDispatch,
- useFormat,
- useDate,
- useUserDetails,
-} from "@renderer/hooks";
+import { useAppDispatch, useFormat, useUserDetails } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
-import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { LockedProfile } from "./locked-profile";
import { ReportProfile } from "../report-profile/report-profile";
@@ -23,19 +17,13 @@ import { FriendsBox } from "./friends-box";
import { RecentGamesBox } from "./recent-games-box";
import { UserStatsBox } from "./user-stats-box";
import { UserKarmaBox } from "./user-karma-box";
-import { UserLibraryGameCard } from "./user-library-game-card";
-import { SortOptions } from "./sort-options";
-
-import { motion, AnimatePresence } from "framer-motion";
-import { useNavigate } from "react-router-dom";
-import { buildGameDetailsPath } from "@renderer/helpers";
-import { ClockIcon } from "@primer/octicons-react";
-import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
-import type { GameShop } from "@types";
import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
-import InfiniteScroll from "react-infinite-scroll-component";
+import { ProfileTabs } from "./profile-tabs";
+import { LibraryTab } from "./library-tab";
+import { ReviewsTab } from "./reviews-tab";
+import { AnimatePresence } from "framer-motion";
import "./profile-content.scss";
type SortOption = "playtime" | "achievementCount" | "playedRecently";
@@ -58,7 +46,7 @@ interface UserReview {
title: string;
iconUrl: string;
objectId: string;
- shop: GameShop;
+ shop: string;
};
}
@@ -97,8 +85,6 @@ export function ProfileContent() {
isLoadingLibraryGames,
} = useContext(userProfileContext);
const { userDetails } = useUserDetails();
- const { formatDistance } = useDate();
- const navigate = useNavigate();
const [statsIndex, setStatsIndex] = useState(0);
const [isAnimationRunning, setIsAnimationRunning] = useState(true);
const [sortBy, setSortBy] = useState("playedRecently");
@@ -117,7 +103,6 @@ export function ProfileContent() {
const dispatch = useAppDispatch();
const { t } = useTranslation("user_profile");
- const { t: tGameDetails } = useTranslation("game_details");
const { numberFormatter } = useFormat();
const formatPlayTime = (playTimeInSeconds: number) => {
@@ -197,7 +182,7 @@ export function ProfileContent() {
setReviews(response.reviews);
setReviewsTotalCount(response.totalCount);
} catch (error) {
- console.error("Failed to fetch user reviews:", error);
+ // Error handling for fetching reviews
} finally {
setIsLoadingReviews(false);
}
@@ -392,383 +377,43 @@ export function ProfileContent() {
return (
-
-
- setActiveTab("library")}
- >
- {t("library")}
-
- {activeTab === "library" && (
-
- )}
-
-
- setActiveTab("reviews")}
- >
- {t("user_reviews")}
- {reviewsTotalCount > 0 && (
-
- {reviewsTotalCount}
-
- )}
-
- {activeTab === "reviews" && (
-
- )}
-
-
+
{activeTab === "library" && (
-
- {hasAnyGames && (
-
- )}
-
- {!hasAnyGames && (
-
-
-
-
-
{t("no_recent_activity_title")}
- {isMe &&
{t("no_recent_activity_description")}
}
-
- )}
-
- {hasAnyGames && (
-
- {hasPinnedGames && (
-
-
-
- {/* removed collapse button */}
-
{t("pinned")}
-
- {pinnedGames.length}
-
-
-
-
- {/* render pinned games unconditionally */}
-
- {pinnedGames?.map((game) => (
- -
-
-
- ))}
-
-
- )}
-
- {hasGames && (
-
-
-
-
{t("library")}
- {userStats && (
-
- {numberFormatter.format(
- userStats.libraryCount
- )}
-
- )}
-
-
-
-
-
- {libraryGames?.map((game, index) => {
- const hasAnimated =
- animatedGameIdsRef.current.has(game.objectId);
- const isNewGame =
- !hasAnimated && !isLoadingLibraryGames;
-
- return (
- {
- if (isNewGame) {
- animatedGameIdsRef.current.add(
- game.objectId
- );
- }
- }}
- >
-
-
- );
- })}
-
-
-
- )}
-
- )}
-
+
)}
{activeTab === "reviews" && (
-
- {/* render reviews content unconditionally */}
- {isLoadingReviews && (
-
- {t("loading_reviews")}
-
- )}
- {!isLoadingReviews && reviews.length === 0 && (
-
-
{t("no_reviews", "No reviews yet")}
-
- )}
- {!isLoadingReviews && reviews.length > 0 && (
-
- {reviews.map((review) => {
- const isOwnReview = userDetails?.id === review.user.id;
-
- return (
-
-
-
-
-
-
- {review.score}/5
-
-
- {Boolean(
- review.playTimeInSeconds &&
- review.playTimeInSeconds > 0
- ) && (
-
-
-
- {tGameDetails("review_played_for")}{" "}
- {formatPlayTime(
- review.playTimeInSeconds || 0
- )}
-
-
- )}
-
-
- {formatDistance(
- new Date(review.createdAt),
- new Date(),
- { addSuffix: true }
- )}
-
-
-
-
-
-
-
-
-
-

-
- navigate(
- buildGameDetailsPath(review.game)
- )
- }
- >
- {review.game.title}
-
-
-
-
-
-
-
-
-
- handleVoteReview(review.id, true)
- }
- disabled={votingReviews.has(review.id)}
- style={{
- opacity: votingReviews.has(review.id)
- ? 0.5
- : 1,
- cursor: votingReviews.has(review.id)
- ? "not-allowed"
- : "pointer",
- }}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.upvotes}
-
-
-
-
-
- handleVoteReview(review.id, false)
- }
- disabled={votingReviews.has(review.id)}
- style={{
- opacity: votingReviews.has(review.id)
- ? 0.5
- : 1,
- cursor: votingReviews.has(review.id)
- ? "not-allowed"
- : "pointer",
- }}
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- >
-
-
-
- {review.downvotes}
-
-
-
-
-
- {isOwnReview && (
-
handleDeleteClick(review.id)}
- title={t("delete_review")}
- >
-
- {t("delete_review")}
-
- )}
-
-
- );
- })}
-
- )}
-
+
)}
diff --git a/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx b/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
new file mode 100644
index 00000000..79e0440e
--- /dev/null
+++ b/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
@@ -0,0 +1,191 @@
+import { motion, AnimatePresence } from "framer-motion";
+import { useNavigate } from "react-router-dom";
+import { ClockIcon } from "@primer/octicons-react";
+import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { useDate } from "@renderer/hooks";
+import { buildGameDetailsPath } from "@renderer/helpers";
+import "./profile-content.scss";
+
+interface UserReview {
+ id: string;
+ reviewHtml: string;
+ score: number;
+ playTimeInSeconds?: number;
+ upvotes: number;
+ downvotes: number;
+ hasUpvoted: boolean;
+ hasDownvoted: boolean;
+ createdAt: string;
+ updatedAt: string;
+ user: {
+ id: string;
+ };
+ game: {
+ title: string;
+ iconUrl: string;
+ objectId: string;
+ shop: string;
+ };
+}
+
+interface ProfileReviewItemProps {
+ review: UserReview;
+ isOwnReview: boolean;
+ isVoting: boolean;
+ formatPlayTime: (playTimeInSeconds: number) => string;
+ getRatingText: (score: number, t: (key: string) => string) => string;
+ onVote: (reviewId: string, isUpvote: boolean) => void;
+ onDelete: (reviewId: string) => void;
+}
+
+export function ProfileReviewItem({
+ review,
+ isOwnReview,
+ isVoting,
+ formatPlayTime,
+ getRatingText,
+ onVote,
+ onDelete,
+}: Readonly
) {
+ const navigate = useNavigate();
+ const { formatDistance } = useDate();
+ const { t } = useTranslation("user_profile");
+ const { t: tGameDetails } = useTranslation("game_details");
+
+ return (
+
+
+
+
+
+
+ {review.score}/5
+
+
+ {Boolean(
+ review.playTimeInSeconds && review.playTimeInSeconds > 0
+ ) && (
+
+
+
+ {tGameDetails("review_played_for")}{" "}
+ {formatPlayTime(review.playTimeInSeconds || 0)}
+
+
+ )}
+
+
+ {formatDistance(new Date(review.createdAt), new Date(), {
+ addSuffix: true,
+ })}
+
+
+
+
+
+
+
+
+
+

+
navigate(buildGameDetailsPath(review.game))}
+ >
+ {review.game.title}
+
+
+
+
+
+
+
+
+
onVote(review.id, true)}
+ disabled={isVoting}
+ style={{
+ opacity: isVoting ? 0.5 : 1,
+ cursor: isVoting ? "not-allowed" : "pointer",
+ }}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.upvotes}
+
+
+
+
+
onVote(review.id, false)}
+ disabled={isVoting}
+ style={{
+ opacity: isVoting ? 0.5 : 1,
+ cursor: isVoting ? "not-allowed" : "pointer",
+ }}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+
+
+ {review.downvotes}
+
+
+
+
+
+ {isOwnReview && (
+
onDelete(review.id)}
+ title={t("delete_review")}
+ >
+
+ {t("delete_review")}
+
+ )}
+
+
+ );
+}
+
diff --git a/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx
new file mode 100644
index 00000000..bc76f40c
--- /dev/null
+++ b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx
@@ -0,0 +1,67 @@
+import { motion } from "framer-motion";
+import { useTranslation } from "react-i18next";
+import "./profile-content.scss";
+
+interface ProfileTabsProps {
+ activeTab: "library" | "reviews";
+ reviewsTotalCount: number;
+ onTabChange: (tab: "library" | "reviews") => void;
+}
+
+export function ProfileTabs({
+ activeTab,
+ reviewsTotalCount,
+ onTabChange,
+}: Readonly) {
+ const { t } = useTranslation("user_profile");
+
+ return (
+
+
+ onTabChange("library")}
+ >
+ {t("library")}
+
+ {activeTab === "library" && (
+
+ )}
+
+
+ onTabChange("reviews")}
+ >
+ {t("user_reviews")}
+ {reviewsTotalCount > 0 && (
+
+ {reviewsTotalCount}
+
+ )}
+
+ {activeTab === "reviews" && (
+
+ )}
+
+
+ );
+}
diff --git a/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
new file mode 100644
index 00000000..97924040
--- /dev/null
+++ b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
@@ -0,0 +1,92 @@
+import { motion } from "framer-motion";
+import { useTranslation } from "react-i18next";
+import { ProfileReviewItem } from "./profile-review-item";
+import "./profile-content.scss";
+
+interface UserReview {
+ id: string;
+ reviewHtml: string;
+ score: number;
+ playTimeInSeconds?: number;
+ upvotes: number;
+ downvotes: number;
+ hasUpvoted: boolean;
+ hasDownvoted: boolean;
+ createdAt: string;
+ updatedAt: string;
+ user: {
+ id: string;
+ };
+ game: {
+ title: string;
+ iconUrl: string;
+ objectId: string;
+ shop: string;
+ };
+}
+
+interface ReviewsTabProps {
+ reviews: UserReview[];
+ isLoadingReviews: boolean;
+ votingReviews: Set;
+ userDetailsId?: string;
+ formatPlayTime: (playTimeInSeconds: number) => string;
+ getRatingText: (score: number, t: (key: string) => string) => string;
+ onVote: (reviewId: string, isUpvote: boolean) => void;
+ onDelete: (reviewId: string) => void;
+}
+
+export function ReviewsTab({
+ reviews,
+ isLoadingReviews,
+ votingReviews,
+ userDetailsId,
+ formatPlayTime,
+ getRatingText,
+ onVote,
+ onDelete,
+}: Readonly) {
+ const { t } = useTranslation("user_profile");
+
+ return (
+
+ {isLoadingReviews && (
+ {t("loading_reviews")}
+ )}
+ {!isLoadingReviews && reviews.length === 0 && (
+
+
{t("no_reviews", "No reviews yet")}
+
+ )}
+ {!isLoadingReviews && reviews.length > 0 && (
+
+ {reviews.map((review) => {
+ const isOwnReview = userDetailsId === review.user.id;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
From fdc3fecd6f0ae4455067b79d512e42e255bc095b Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 20:42:42 +0000
Subject: [PATCH 109/118] feat: adding reviews to profile
---
.../src/pages/game-details/review-item.scss | 16 +-
.../src/pages/game-details/review-item.tsx | 86 ++++-----
.../profile/profile-content/library-tab.tsx | 10 +-
.../profile-content/profile-content.scss | 39 +++-
.../profile-content/profile-content.tsx | 4 +
.../profile-content/profile-review-item.tsx | 169 ++++++++++++------
.../profile/profile-content/reviews-tab.tsx | 5 +-
7 files changed, 225 insertions(+), 104 deletions(-)
diff --git a/src/renderer/src/pages/game-details/review-item.scss b/src/renderer/src/pages/game-details/review-item.scss
index b3577a75..64657bfd 100644
--- a/src/renderer/src/pages/game-details/review-item.scss
+++ b/src/renderer/src/pages/game-details/review-item.scss
@@ -8,11 +8,23 @@
&__review-header {
display: flex;
- justify-content: space-between;
- align-items: center;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 1);
margin-bottom: calc(globals.$spacing-unit * 1.5);
}
+ &__review-header-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ }
+
+ &__review-header-bottom {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ }
+
&__review-user {
display: flex;
align-items: center;
diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx
index bfc02cfe..09d91df8 100644
--- a/src/renderer/src/pages/game-details/review-item.tsx
+++ b/src/renderer/src/pages/game-details/review-item.tsx
@@ -126,60 +126,62 @@ export function ReviewItem({
return (
-
-
navigate(`/profile/${review.user.id}`)}
- title={review.user.displayName}
- >
-
-
-
+
+
- review.user.id && navigate(`/profile/${review.user.id}`)
- }
+ onClick={() => navigate(`/profile/${review.user.id}`)}
+ title={review.user.displayName}
>
- {review.user.displayName || "Anonymous"}
+
-
-
+
+ review.user.id && navigate(`/profile/${review.user.id}`)
+ }
>
-
-
- {review.score}/5
-
-
- {Boolean(
- review.playTimeInSeconds && review.playTimeInSeconds > 0
- ) && (
-
-
-
- {t("review_played_for")}{" "}
- {formatPlayTime(review.playTimeInSeconds || 0)}
-
-
- )}
+ {review.user.displayName || "Anonymous"}
+
-
-
{formatDistance(new Date(review.createdAt), new Date(), {
addSuffix: true,
})}
+
+
+
+
+
+ {review.score}/5
+
+
+ {Boolean(
+ review.playTimeInSeconds && review.playTimeInSeconds > 0
+ ) && (
+
+
+
+ {t("review_played_for")}{" "}
+ {formatPlayTime(review.playTimeInSeconds || 0)}
+
+
+ )}
+
+
- {hasAnyGames &&
}
+ {hasAnyGames && (
+
+ )}
{!hasAnyGames && (
@@ -123,8 +125,9 @@ export function LibraryTab({
>
{libraryGames?.map((game, index) => {
- const hasAnimated =
- animatedGameIdsRef.current.has(game.objectId);
+ const hasAnimated = animatedGameIdsRef.current.has(
+ game.objectId
+ );
const isNewGame = !hasAnimated && !isLoadingLibraryGames;
return (
@@ -173,4 +176,3 @@ export function LibraryTab({
);
}
-
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.scss b/src/renderer/src/pages/profile/profile-content/profile-content.scss
index d41e3530..958fe52d 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.scss
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.scss
@@ -235,10 +235,22 @@
}
.user-reviews__review-header {
+ display: flex;
+ flex-direction: column;
+ gap: calc(globals.$spacing-unit * 1);
+ margin-bottom: calc(globals.$spacing-unit * 1.5);
+}
+
+.user-reviews__review-header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
- margin-bottom: calc(globals.$spacing-unit * 1.5);
+}
+
+.user-reviews__review-header-bottom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.user-reviews__review-meta-row {
@@ -343,6 +355,31 @@
.user-reviews__review-content {
color: rgba(255, 255, 255, 0.85);
line-height: 1.5;
+ word-wrap: break-word;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+ max-width: 100%;
+}
+
+.user-reviews__review-translation-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: calc(globals.$spacing-unit * 1);
+ margin-top: calc(globals.$spacing-unit * 1.5);
+ padding: 0;
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.875rem;
+ cursor: pointer;
+ text-decoration: none;
+ transition: all 0.2s ease;
+
+ &:hover {
+ text-decoration: underline;
+ color: rgba(255, 255, 255, 0.9);
+ }
}
.user-reviews__review-actions {
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index ab9fdf01..bb6842d7 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -48,6 +48,10 @@ interface UserReview {
objectId: string;
shop: string;
};
+ translations: {
+ [key: string]: string;
+ };
+ detectedLanguage: string | null;
}
interface UserReviewsResponse {
diff --git a/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx b/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
index 79e0440e..bea569e7 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-review-item.tsx
@@ -1,8 +1,11 @@
import { motion, AnimatePresence } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { ClockIcon } from "@primer/octicons-react";
-import { Star, ThumbsUp, ThumbsDown, TrashIcon } from "lucide-react";
+import { Star, ThumbsUp, ThumbsDown, TrashIcon, Languages } from "lucide-react";
import { useTranslation } from "react-i18next";
+import { useState } from "react";
+import type { GameShop } from "@types";
+import { sanitizeHtml } from "@shared";
import { useDate } from "@renderer/hooks";
import { buildGameDetailsPath } from "@renderer/helpers";
import "./profile-content.scss";
@@ -25,8 +28,12 @@ interface UserReview {
title: string;
iconUrl: string;
objectId: string;
- shop: string;
+ shop: GameShop;
};
+ translations: {
+ [key: string]: string;
+ };
+ detectedLanguage: string | null;
}
interface ProfileReviewItemProps {
@@ -51,7 +58,32 @@ export function ProfileReviewItem({
const navigate = useNavigate();
const { formatDistance } = useDate();
const { t } = useTranslation("user_profile");
- const { t: tGameDetails } = useTranslation("game_details");
+ const { t: tGameDetails, i18n } = useTranslation("game_details");
+ const [showOriginal, setShowOriginal] = useState(false);
+
+ const getBaseLanguage = (lang: string | null) => lang?.split("-")[0] || "";
+
+ const isDifferentLanguage =
+ getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language);
+
+ const needsTranslation =
+ !isOwnReview && isDifferentLanguage && review.translations[i18n.language];
+
+ const getLanguageName = (languageCode: string | null) => {
+ if (!languageCode) return "";
+ try {
+ const displayNames = new Intl.DisplayNames([i18n.language], {
+ type: "language",
+ });
+ return displayNames.of(languageCode) || languageCode.toUpperCase();
+ } catch {
+ return languageCode.toUpperCase();
+ }
+ };
+
+ const displayContent = needsTranslation
+ ? review.translations[i18n.language]
+ : review.reviewHtml;
return (
-
-
-
-
- {review.score}/5
-
+
+
+
+
+

+
navigate(buildGameDetailsPath(review.game))}
+ >
+ {review.game.title}
+
+
+
- {Boolean(
- review.playTimeInSeconds && review.playTimeInSeconds > 0
- ) && (
-
-
-
- {tGameDetails("review_played_for")}{" "}
- {formatPlayTime(review.playTimeInSeconds || 0)}
+
+ {formatDistance(new Date(review.createdAt), new Date(), {
+ addSuffix: true,
+ })}
+
+
+
+
+
+
+
+ {review.score}/5
- )}
-
-
- {formatDistance(new Date(review.createdAt), new Date(), {
- addSuffix: true,
- })}
+ {Boolean(
+ review.playTimeInSeconds && review.playTimeInSeconds > 0
+ ) && (
+
+
+
+ {tGameDetails("review_played_for")}{" "}
+ {formatPlayTime(review.playTimeInSeconds || 0)}
+
+
+ )}
+
-
-
-
-
-
-
-

+
+ {needsTranslation && (
+ <>
+
setShowOriginal(!showOriginal)}
+ >
+
+ {showOriginal
+ ? tGameDetails("hide_original")
+ : tGameDetails("show_original_translated_from", {
+ language: getLanguageName(review.detectedLanguage),
+ })}
+
+ {showOriginal && (
+
-
navigate(buildGameDetailsPath(review.game))}
- >
- {review.game.title}
-
-
-
-
+ )}
+ >
+ )}
@@ -188,4 +250,3 @@ export function ProfileReviewItem({
);
}
-
diff --git a/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
index 97924040..9fcea37e 100644
--- a/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
+++ b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
@@ -23,6 +23,10 @@ interface UserReview {
objectId: string;
shop: string;
};
+ translations: {
+ [key: string]: string;
+ };
+ detectedLanguage: string | null;
}
interface ReviewsTabProps {
@@ -89,4 +93,3 @@ export function ReviewsTab({
);
}
-
From 48775e57fc17f4ca755f0825516c105d045f3582 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 20:43:59 +0000
Subject: [PATCH 110/118] feat: adding reviews to profile
---
.../src/pages/profile/profile-content/profile-content.tsx | 3 ++-
src/renderer/src/pages/profile/profile-content/reviews-tab.tsx | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
index bb6842d7..8176bace 100644
--- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx
+++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx
@@ -11,6 +11,7 @@ import { ProfileHero } from "../profile-hero/profile-hero";
import { useAppDispatch, useFormat, useUserDetails } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { useTranslation } from "react-i18next";
+import type { GameShop } from "@types";
import { LockedProfile } from "./locked-profile";
import { ReportProfile } from "../report-profile/report-profile";
import { FriendsBox } from "./friends-box";
@@ -46,7 +47,7 @@ interface UserReview {
title: string;
iconUrl: string;
objectId: string;
- shop: string;
+ shop: GameShop;
};
translations: {
[key: string]: string;
diff --git a/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
index 9fcea37e..afcc417b 100644
--- a/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
+++ b/src/renderer/src/pages/profile/profile-content/reviews-tab.tsx
@@ -1,5 +1,6 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
+import type { GameShop } from "@types";
import { ProfileReviewItem } from "./profile-review-item";
import "./profile-content.scss";
@@ -21,7 +22,7 @@ interface UserReview {
title: string;
iconUrl: string;
objectId: string;
- shop: string;
+ shop: GameShop;
};
translations: {
[key: string]: string;
From 37caeb8047d926761e0d834197c2453902ae2990 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 21:13:44 +0000
Subject: [PATCH 111/118] fix: fixing datanodes
---
src/main/services/hosters/datanodes.ts | 50 +++++++++++---------------
1 file changed, 21 insertions(+), 29 deletions(-)
diff --git a/src/main/services/hosters/datanodes.ts b/src/main/services/hosters/datanodes.ts
index 29708322..4cfb5242 100644
--- a/src/main/services/hosters/datanodes.ts
+++ b/src/main/services/hosters/datanodes.ts
@@ -1,6 +1,7 @@
import axios, { AxiosResponse } from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
+import { logger } from "@main/services";
export class DatanodesApi {
private static readonly jar = new CookieJar();
@@ -20,51 +21,42 @@ export class DatanodesApi {
await this.jar.setCookie("lang=english;", "https://datanodes.to");
- const payload = new URLSearchParams({
- op: "download2",
- id: fileCode,
- method_free: "Free Download >>",
- dl: "1",
- });
+ const formData = new FormData();
+ formData.append("op", "download2");
+ formData.append("id", fileCode);
+ formData.append("rand", "");
+ formData.append("referer", "https://datanodes.to/download");
+ formData.append("method_free", "Free Download >>");
+ formData.append("method_premium", "");
+ formData.append("__dl", "1");
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
- payload,
+ formData,
{
headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
+ accept: "*/*",
+ "accept-language": "en-US,en;q=0.9",
+ priority: "u=1, i",
+ "sec-ch-ua":
+ '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": '"Windows"',
+ "sec-fetch-dest": "empty",
+ "sec-fetch-mode": "cors",
+ "sec-fetch-site": "same-origin",
Referer: "https://datanodes.to/download",
- Origin: "https://datanodes.to",
- "Content-Type": "application/x-www-form-urlencoded",
},
- maxRedirects: 0,
- validateStatus: (status: number) => status === 302 || status < 400,
}
);
- if (response.status === 302) {
- return response.headers["location"];
- }
-
if (typeof response.data === "object" && response.data.url) {
return decodeURIComponent(response.data.url);
}
- const htmlContent = String(response.data);
- if (!htmlContent) {
- throw new Error("Empty response received");
- }
-
- const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
- const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
- if (downloadLinkMatch) {
- return downloadLinkMatch[1];
- }
-
throw new Error("Failed to get the download link");
} catch (error) {
- console.error("Error fetching download URL:", error);
+ logger.error("Error fetching download URL:", error);
throw error;
}
}
From f49fea3032846bb6f82e01e8834b7d1d5e4bdd4d Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sun, 2 Nov 2025 21:17:34 +0000
Subject: [PATCH 112/118] ci: bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index f06fef64..e2fec5ee 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
- "version": "3.7.3",
+ "version": "3.7.4",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
From 7fa50dc5a7423f9b2d1d8696281a3477ded7a2ad Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Mon, 3 Nov 2025 12:02:52 +0000
Subject: [PATCH 113/118] feat: adding ws client back
---
src/main/main.ts | 3 ++-
src/main/services/hydra-api.ts | 5 +++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/main/main.ts b/src/main/main.ts
index ffb8f8a9..6ae927a5 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -16,6 +16,7 @@ import {
Ludusavi,
Lock,
DeckyPlugin,
+ WSClient,
} from "@main/services";
import { migrateDownloadSources } from "./helpers/migrate-download-sources";
@@ -56,7 +57,7 @@ export const loadState = async () => {
const { syncDownloadSourcesFromApi } = await import("./services/user");
void syncDownloadSourcesFromApi();
- // WSClient.connect();
+ WSClient.connect();
});
const downloads = await downloadsSublevel
diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts
index 12090df3..f77a152f 100644
--- a/src/main/services/hydra-api.ts
+++ b/src/main/services/hydra-api.ts
@@ -11,6 +11,7 @@ import { getUserData } from "./user/get-user-data";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
+import { WSClient } from "./ws";
export interface HydraApiOptions {
needsAuth?: boolean;
@@ -103,8 +104,8 @@ export class HydraApi {
await clearGamesRemoteIds();
uploadGamesBatch();
- // WSClient.close();
- // WSClient.connect();
+ WSClient.close();
+ WSClient.connect();
const { syncDownloadSourcesFromApi } = await import("./user");
syncDownloadSourcesFromApi();
From cedf7e6e37c1b8a158b0740aed1054ad7419e46f Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Thu, 6 Nov 2025 19:03:23 +0530
Subject: [PATCH 114/118] style: improve color contrast in various components
and update prop types to readonly
---
.../src/pages/library/filter-options.scss | 7 +-
.../src/pages/library/filter-options.tsx | 2 +-
.../library/library-game-card-large.scss | 8 +-
.../pages/library/library-game-card-large.tsx | 70 ++++++-----
.../src/pages/library/library-game-card.tsx | 2 +-
src/renderer/src/pages/library/library.tsx | 112 +++++++++---------
.../src/pages/library/search-bar.scss | 6 +-
.../src/pages/library/view-options.scss | 4 +-
.../src/pages/library/view-options.tsx | 5 +-
9 files changed, 115 insertions(+), 101 deletions(-)
diff --git a/src/renderer/src/pages/library/filter-options.scss b/src/renderer/src/pages/library/filter-options.scss
index cc899d56..6f309662 100644
--- a/src/renderer/src/pages/library/filter-options.scss
+++ b/src/renderer/src/pages/library/filter-options.scss
@@ -15,8 +15,7 @@
padding: 8px 16px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
- border: none;
- color: rgba(255, 255, 255, 0.6);
+ color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 13px;
font-weight: 500;
@@ -51,8 +50,8 @@
}
&__count {
- background: rgba(255, 255, 255, 0.1);
- color: rgba(255, 255, 255, 0.8);
+ background: rgba(255, 255, 255, 0.16);
+ color: rgba(255, 255, 255, 0.95);
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
diff --git a/src/renderer/src/pages/library/filter-options.tsx b/src/renderer/src/pages/library/filter-options.tsx
index 07c935d9..572ebd35 100644
--- a/src/renderer/src/pages/library/filter-options.tsx
+++ b/src/renderer/src/pages/library/filter-options.tsx
@@ -19,7 +19,7 @@ export function FilterOptions({
favouritedCount,
newGamesCount,
top10Count,
-}: FilterOptionsProps) {
+}: Readonly) {
const { t } = useTranslation("library");
return (
diff --git a/src/renderer/src/pages/library/library-game-card-large.scss b/src/renderer/src/pages/library/library-game-card-large.scss
index 700c7d0b..3fceac03 100644
--- a/src/renderer/src/pages/library/library-game-card-large.scss
+++ b/src/renderer/src/pages/library/library-game-card-large.scss
@@ -104,7 +104,7 @@
justify-content: center;
cursor: pointer;
transition: all ease 0.2s;
- color: rgba(255, 255, 255, 0.8);
+ color: rgba(255, 255, 255, 0.95);
padding: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
opacity: 0;
@@ -162,7 +162,7 @@
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
- color: rgba(255, 255, 255, 0.8);
+ color: rgba(255, 255, 255, 0.95);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
display: flex;
@@ -242,7 +242,7 @@
&__achievement-percentage {
font-size: 12px;
- color: rgba(255, 255, 255, 0.7);
+ color: rgba(255, 255, 255, 0.85);
white-space: nowrap;
}
@@ -254,7 +254,7 @@
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
- color: rgba(255, 255, 255, 0.9);
+ color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
cursor: pointer;
diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx
index 3c9285e9..e56f0ae3 100644
--- a/src/renderer/src/pages/library/library-game-card-large.tsx
+++ b/src/renderer/src/pages/library/library-game-card-large.tsx
@@ -30,7 +30,9 @@ const getImageWithCustomPriority = (
return customUrl || originalUrl || fallbackUrl || "";
};
-export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
+export function LibraryGameCardLarge({
+ game,
+}: Readonly) {
const { t } = useTranslation("library");
const { numberFormatter } = useFormat();
const navigate = useNavigate();
@@ -82,7 +84,7 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
try {
await handleCloseGame();
} catch (e) {
- void e;
+ console.error(e);
}
return;
}
@@ -92,7 +94,7 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
try {
handleOpenDownloadOptions();
} catch (e) {
- void e;
+ console.error(e);
}
}
};
@@ -223,30 +225,44 @@ export function LibraryGameCardLarge({ game }: LibraryGameCardLargeProps) {
className="library-game-card-large__action-button"
onClick={handleActionClick}
>
- {isGameDownloading ? (
- <>
-
- {t("downloading")}
- >
- ) : isGameRunning ? (
- <>
-
- {t("close")}
- >
- ) : game.executablePath ? (
- <>
-
- {t("play")}
- >
- ) : (
- <>
-
- {t("download")}
- >
- )}
+ {(() => {
+ if (isGameDownloading) {
+ return (
+ <>
+
+ {t("downloading")}
+ >
+ );
+ }
+
+ if (isGameRunning) {
+ return (
+ <>
+
+ {t("close")}
+ >
+ );
+ }
+
+ if (game.executablePath) {
+ return (
+ <>
+
+ {t("play")}
+ >
+ );
+ }
+
+ return (
+ <>
+
+ {t("download")}
+ >
+ );
+ })()}
diff --git a/src/renderer/src/pages/library/library-game-card.tsx b/src/renderer/src/pages/library/library-game-card.tsx
index be0a5a73..a9f2aba2 100644
--- a/src/renderer/src/pages/library/library-game-card.tsx
+++ b/src/renderer/src/pages/library/library-game-card.tsx
@@ -25,7 +25,7 @@ export function LibraryGameCard({
game,
onMouseEnter,
onMouseLeave,
-}: LibraryGameCardProps) {
+}: Readonly
) {
const { t } = useTranslation("library");
const { numberFormatter } = useFormat();
const navigate = useNavigate();
diff --git a/src/renderer/src/pages/library/library.tsx b/src/renderer/src/pages/library/library.tsx
index 2d58bb08..1cafc870 100644
--- a/src/renderer/src/pages/library/library.tsx
+++ b/src/renderer/src/pages/library/library.tsx
@@ -13,6 +13,10 @@ import "./library.scss";
export default function Library() {
const { library, updateLibrary } = useLibrary();
+ type ElectronAPI = {
+ refreshLibraryAssets?: () => Promise;
+ onLibraryBatchComplete?: (cb: () => void) => () => void;
+ };
const [viewMode, setViewMode] = useState("compact");
const [filterBy, setFilterBy] = useState("all");
@@ -22,17 +26,22 @@ export default function Library() {
useEffect(() => {
dispatch(setHeaderTitle(t("library")));
-
- // Refresh library assets from cloud, then update library display
- window.electron
- .refreshLibraryAssets()
- .then(() => updateLibrary())
- .catch(() => updateLibrary()); // Fallback to local cache on error
-
- // Listen for library sync completion to refresh cover images
- const unsubscribe = window.electron.onLibraryBatchComplete(() => {
+ const electron = (globalThis as unknown as { electron?: ElectronAPI })
+ .electron;
+ let unsubscribe: () => void = () => undefined;
+ if (electron?.refreshLibraryAssets) {
+ electron
+ .refreshLibraryAssets()
+ .then(() => updateLibrary())
+ .catch(() => updateLibrary());
+ if (electron.onLibraryBatchComplete) {
+ unsubscribe = electron.onLibraryBatchComplete(() => {
+ updateLibrary();
+ });
+ }
+ } else {
updateLibrary();
- });
+ }
return () => {
unsubscribe();
@@ -47,31 +56,6 @@ export default function Library() {
// Optional: resume animations if needed
};
- // Simple fuzzy search function
- const fuzzySearch = (query: string, items: typeof library) => {
- if (!query.trim()) return items;
-
- const queryLower = query.toLowerCase();
- return items.filter((game) => {
- const titleLower = game.title.toLowerCase();
- let matches = 0;
- let queryIndex = 0;
-
- for (
- let i = 0;
- i < titleLower.length && queryIndex < queryLower.length;
- i++
- ) {
- if (titleLower[i] === queryLower[queryIndex]) {
- matches++;
- queryIndex++;
- }
- }
-
- return queryIndex === queryLower.length;
- });
- };
-
const filteredLibrary = useMemo(() => {
let filtered;
@@ -98,8 +82,25 @@ export default function Library() {
filtered = library;
}
- // Apply search filter
- return fuzzySearch(searchQuery, filtered);
+ if (!searchQuery.trim()) return filtered;
+
+ const queryLower = searchQuery.toLowerCase();
+ return filtered.filter((game) => {
+ const titleLower = game.title.toLowerCase();
+ let queryIndex = 0;
+
+ for (
+ let i = 0;
+ i < titleLower.length && queryIndex < queryLower.length;
+ i++
+ ) {
+ if (titleLower[i] === queryLower[queryIndex]) {
+ queryIndex++;
+ }
+ }
+
+ return queryIndex === queryLower.length;
+ });
}, [library, filterBy, searchQuery]);
// No sorting for now — rely on filteredLibrary
@@ -118,30 +119,25 @@ export default function Library() {
return (
{hasGames && (
- <>
-
-
-
-
-
+
)}
{!hasGames && (
diff --git a/src/renderer/src/pages/library/search-bar.scss b/src/renderer/src/pages/library/search-bar.scss
index 7c487ec5..6b09c683 100644
--- a/src/renderer/src/pages/library/search-bar.scss
+++ b/src/renderer/src/pages/library/search-bar.scss
@@ -22,7 +22,7 @@
}
&__icon {
- color: rgba(255, 255, 255, 0.5);
+ color: rgba(255, 255, 255, 0.75);
flex-shrink: 0;
transition: color 0.2s ease;
}
@@ -38,7 +38,7 @@
min-width: 0;
&::placeholder {
- color: rgba(255, 255, 255, 0.4);
+ color: rgba(255, 255, 255, 0.6);
}
&:focus ~ .search-bar__icon {
@@ -50,7 +50,7 @@
flex-shrink: 0;
background: transparent;
border: none;
- color: rgba(255, 255, 255, 0.5);
+ color: rgba(255, 255, 255, 0.65);
font-size: 18px;
font-weight: bold;
cursor: pointer;
diff --git a/src/renderer/src/pages/library/view-options.scss b/src/renderer/src/pages/library/view-options.scss
index b8cf1484..77bfc10e 100644
--- a/src/renderer/src/pages/library/view-options.scss
+++ b/src/renderer/src/pages/library/view-options.scss
@@ -10,7 +10,7 @@
&__label {
font-size: 14px;
font-weight: 600;
- color: rgba(255, 255, 255, 0.8);
+ color: rgba(255, 255, 255, 0.95);
white-space: nowrap;
}
@@ -30,7 +30,7 @@
border-radius: 6px;
background: rgba(255, 255, 255, 0.04);
border: none;
- color: rgba(255, 255, 255, 0.6);
+ color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 14px;
font-weight: 500;
diff --git a/src/renderer/src/pages/library/view-options.tsx b/src/renderer/src/pages/library/view-options.tsx
index 662bab65..905fac58 100644
--- a/src/renderer/src/pages/library/view-options.tsx
+++ b/src/renderer/src/pages/library/view-options.tsx
@@ -9,7 +9,10 @@ interface ViewOptionsProps {
onViewModeChange: (viewMode: ViewMode) => void;
}
-export function ViewOptions({ viewMode, onViewModeChange }: ViewOptionsProps) {
+export function ViewOptions({
+ viewMode,
+ onViewModeChange,
+}: Readonly
) {
const { t } = useTranslation("library");
return (
From 5e653be4c3fc51b557cb47c947ef5b03b1d1ea56 Mon Sep 17 00:00:00 2001
From: ctrlcat0x
Date: Thu, 6 Nov 2025 19:11:20 +0530
Subject: [PATCH 115/118] fix: add error logging in handleActionClick for
better debugging
---
src/renderer/src/pages/library/library-game-card-large.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/renderer/src/pages/library/library-game-card-large.tsx b/src/renderer/src/pages/library/library-game-card-large.tsx
index e56f0ae3..5628fe10 100644
--- a/src/renderer/src/pages/library/library-game-card-large.tsx
+++ b/src/renderer/src/pages/library/library-game-card-large.tsx
@@ -91,6 +91,7 @@ export function LibraryGameCardLarge({
try {
await handlePlayGame();
} catch (err) {
+ console.error(err);
try {
handleOpenDownloadOptions();
} catch (e) {
From 893802be559d90bd5a0ec24c13923e63cf18b019 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Fri, 7 Nov 2025 13:27:24 +0200
Subject: [PATCH 116/118] fix: next suggestion and title not being showed
---
src/main/services/steam-250.ts | 2 +-
.../src/pages/game-details/game-details.scss | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/main/services/steam-250.ts b/src/main/services/steam-250.ts
index 5652b0d3..826e528f 100644
--- a/src/main/services/steam-250.ts
+++ b/src/main/services/steam-250.ts
@@ -16,7 +16,7 @@ export const requestSteam250 = async (path: string) => {
if (!steamGameUrl) return null;
return {
- title: $title.textContent,
+ title: $title.getAttribute("data-title") || "",
objectId: steamGameUrl.split("/").pop(),
} as Steam250Game;
})
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss
index f5f77a86..9fdc9485 100644
--- a/src/renderer/src/pages/game-details/game-details.scss
+++ b/src/renderer/src/pages/game-details/game-details.scss
@@ -130,6 +130,22 @@
}
}
+ // Randomizer button styles
+ &__randomizer-button {
+ position: fixed;
+ bottom: calc(globals.$spacing-unit * 5);
+ right: calc(globals.$spacing-unit * 5);
+ z-index: 100;
+ padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
+ overflow: visible;
+ }
+
+ &__stars-icon-container {
+ width: 70px;
+ height: 70px;
+ position: relative;
+ }
+
// Skeleton-specific styles
&__skeleton {
.react-loading-skeleton {
From 6e6e0f7bb71f22404c0c7b2835c3eaff12232c43 Mon Sep 17 00:00:00 2001
From: Moyasee
Date: Fri, 7 Nov 2025 13:35:50 +0200
Subject: [PATCH 117/118] fix: duplicate next suggestion styling removal
---
.../src/pages/game-details/game-details.scss | 16 -------------
src/renderer/src/pages/game-details/hero.scss | 24 ++++++++++++-------
2 files changed, 16 insertions(+), 24 deletions(-)
diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss
index 9fdc9485..f5f77a86 100644
--- a/src/renderer/src/pages/game-details/game-details.scss
+++ b/src/renderer/src/pages/game-details/game-details.scss
@@ -130,22 +130,6 @@
}
}
- // Randomizer button styles
- &__randomizer-button {
- position: fixed;
- bottom: calc(globals.$spacing-unit * 5);
- right: calc(globals.$spacing-unit * 5);
- z-index: 100;
- padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
- overflow: visible;
- }
-
- &__stars-icon-container {
- width: 70px;
- height: 70px;
- position: relative;
- }
-
// Skeleton-specific styles
&__skeleton {
.react-loading-skeleton {
diff --git a/src/renderer/src/pages/game-details/hero.scss b/src/renderer/src/pages/game-details/hero.scss
index 41264fe4..82f64337 100644
--- a/src/renderer/src/pages/game-details/hero.scss
+++ b/src/renderer/src/pages/game-details/hero.scss
@@ -231,26 +231,36 @@ $hero-height: 350px;
}
&__randomizer-button {
- padding: calc(globals.$spacing-unit * 1.5);
+ position: fixed;
+ bottom: calc(globals.$spacing-unit * 5);
+ right: calc(globals.$spacing-unit * 5);
+ z-index: 100;
+ padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 8px;
transition: all ease 0.2s;
cursor: pointer;
min-height: 40px;
- min-width: 40px;
display: flex;
align-items: center;
justify-content: center;
+ gap: globals.$spacing-unit;
color: globals.$muted-color;
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
+ overflow: visible;
&:active {
opacity: 0.9;
}
+ &:disabled {
+ opacity: globals.$disabled-opacity;
+ cursor: not-allowed;
+ }
+
&:hover {
background-color: rgba(0, 0, 0, 0.5);
color: globals.$body-color;
@@ -258,17 +268,15 @@ $hero-height: 350px;
}
&__stars-icon-container {
- width: 20px;
+ width: 16px;
height: 16px;
- display: flex;
- align-items: center;
- justify-content: center;
position: relative;
}
&__stars-icon {
- width: 26px;
+ width: 70px;
position: absolute;
- top: -3px;
+ top: -28px;
+ left: -27px;
}
}
From 50b0a8220455e7005365e261ef8b3bb138ccf270 Mon Sep 17 00:00:00 2001
From: Chubby Granny Chaser
Date: Sat, 8 Nov 2025 08:17:19 +0000
Subject: [PATCH 118/118] feat: improving styles on randomizer button
---
src/renderer/src/pages/game-details/hero.scss | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/renderer/src/pages/game-details/hero.scss b/src/renderer/src/pages/game-details/hero.scss
index 82f64337..fd071eec 100644
--- a/src/renderer/src/pages/game-details/hero.scss
+++ b/src/renderer/src/pages/game-details/hero.scss
@@ -233,10 +233,10 @@ $hero-height: 350px;
&__randomizer-button {
position: fixed;
bottom: calc(globals.$spacing-unit * 5);
- right: calc(globals.$spacing-unit * 5);
+ right: calc(globals.$spacing-unit * 2);
z-index: 100;
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
- background-color: rgba(0, 0, 0, 0.6);
+ background-color: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
border-radius: 8px;
transition: all ease 0.2s;
@@ -248,21 +248,19 @@ $hero-height: 350px;
gap: globals.$spacing-unit;
color: globals.$muted-color;
border: solid 1px globals.$border-color;
- box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
+ box-shadow:
+ 0px 0px 10px 0px rgba(0, 0, 0, 0.8),
+ 0px 2px 8px 0px rgba(255, 255, 255, 0.1);
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
overflow: visible;
- &:active {
- opacity: 0.9;
- }
-
&:disabled {
opacity: globals.$disabled-opacity;
cursor: not-allowed;
}
&:hover {
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: rgba(255, 255, 255, 0.12);
color: globals.$body-color;
}
}