Compare commits

..

16 Commits

Author SHA1 Message Date
Chubby Granny Chaser
47bfc1648f Merge branch 'main' into fix/fixing-level-events 2025-12-10 17:34:01 +00:00
Chubby Granny Chaser
a69a6ec510 Merge pull request #1889 from Lianela/main
feat: new strings
2025-12-10 17:15:45 +00:00
Chubby Granny Chaser
fada6507c3 Merge branch 'main' into main 2025-12-10 17:15:21 +00:00
Chubby Granny Chaser
0479f1347b Merge pull request #1887 from hydralauncher/dependabot/npm_and_yarn/npm_and_yarn-a3f223628e
chore(deps): bump jws from 3.2.2 to 3.2.3 in the npm_and_yarn group across 1 directory
2025-12-10 17:14:44 +00:00
dependabot[bot]
f44d5c8b49 chore(deps): bump jws in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [jws](https://github.com/brianloveswords/node-jws).


Updates `jws` from 3.2.2 to 3.2.3
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 01:04:55 +00:00
Zamitto
c36109c092 chore: bump version
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
2025-12-07 22:03:02 -03:00
Zamitto
b59fb7dc36 feat: support workwonders 2025-12-07 20:38:53 -03:00
Kyatto
214a7af408 Fix JSON formatting in translation file 2025-12-07 13:14:50 -06:00
Kyatto
14679fc31e Add new translation strings in Spanish 2025-12-07 13:05:59 -06:00
Chubby Granny Chaser
b5445b3dfa chore: updating lock 2025-12-01 06:51:08 +00:00
Chubby Granny Chaser
1ccf70af12 chore: updating lock 2025-11-30 23:57:13 +00:00
Chubby Granny Chaser
bb45b95820 chore: updating lock 2025-11-30 23:39:47 +00:00
Chubby Granny Chaser
361c158a44 fix: fixing level events 2025-11-30 23:17:56 +00:00
Chubby Granny Chaser
1f5e84b32c fix: fixing level events 2025-11-30 23:17:09 +00:00
Chubby Granny Chaser
e49d885b30 chore: update package.json to use yarn commands for type checking and building
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-11-30 15:21:50 +00:00
Chubby Granny Chaser
cb01301a0d feat: add new translation keys for network statistics in multiple languages 2025-11-30 15:07:32 +00:00
39 changed files with 337 additions and 474 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.7.5", "version": "3.7.6",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -19,12 +19,12 @@
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web", "typecheck": "yarn run typecheck:node && yarn run typecheck:web",
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build", "build": "yarn run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs", "postinstall": "electron-builder install-app-deps && node ./scripts/postinstall.cjs",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "yarn run build && electron-builder --dir",
"build:win": "electron-vite build && electron-builder --win", "build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux", "build:linux": "electron-vite build && electron-builder --linux",

View File

@@ -414,7 +414,11 @@
"resume_seeding": "Resume seeding", "resume_seeding": "Resume seeding",
"options": "Manage", "options": "Manage",
"extract": "Extract files", "extract": "Extract files",
"extracting": "Extracting files…" "extracting": "Extracting files…",
"network": "Network",
"peak": "Peak",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Downloads path", "downloads_path": "Downloads path",

View File

@@ -414,7 +414,11 @@
"resume_seeding": "Continuar sembrando", "resume_seeding": "Continuar sembrando",
"options": "Administrar", "options": "Administrar",
"extract": "Extraer archivos", "extract": "Extraer archivos",
"extracting": "Extrayendo archivos…" "extracting": "Extrayendo archivos…",
"network": "Red",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Ruta de descarga", "downloads_path": "Ruta de descarga",
@@ -458,6 +462,7 @@
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas", "description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
"button_delete_all_sources": "Eliminar todo", "button_delete_all_sources": "Eliminar todo",
"added_download_source": "Añadir fuente de descarga", "added_download_source": "Añadir fuente de descarga",
"adding": "Añadiendo…",
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas", "download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
"insert_valid_json_url": "Introducí una URL de json válida", "insert_valid_json_url": "Introducí una URL de json válida",
"found_download_option_zero": "Sin opciones de descargas encontrada", "found_download_option_zero": "Sin opciones de descargas encontrada",
@@ -563,6 +568,19 @@
"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.", "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", "autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"change_achievement_sound": "Cambiar sonido de logro",
"download_source_already_exists": "Esta fuente de descarga URL ya existe.",
"download_source_failed": "Error",
"download_source_matched": "Actualizado",
"download_source_matching": "Actualizando",
"download_source_no_information": "Sin información disponible",
"download_source_pending_matching": "Actualizando pronto",
"download_sources_synced_successfully": "Todas las fuentes de descarga están sincronizadas",
"failed_add_download_source": "Error al añadir la fuente de descarga. Por favor intentá de nuevo.",
"hydra_cloud": "Hydra Cloud",
"preview_sound": "Vista previa de sonido",
"remove_achievement_sound": "Eliminar sonido de logros",
"removed_all_download_sources": "Todas las fuentes de descarga eliminadas",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego" "hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
}, },
"notifications": { "notifications": {

View File

@@ -402,7 +402,11 @@
"resume_seeding": "Semear", "resume_seeding": "Semear",
"options": "Gerenciar", "options": "Gerenciar",
"extract": "Extrair arquivos", "extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…" "extracting": "Extraindo arquivos…",
"network": "Rede",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Diretório dos downloads", "downloads_path": "Diretório dos downloads",

View File

@@ -229,7 +229,13 @@
"seeding": "A semear", "seeding": "A semear",
"stop_seeding": "Parar de semear", "stop_seeding": "Parar de semear",
"resume_seeding": "Semear", "resume_seeding": "Semear",
"options": "Opções" "options": "Opções",
"extract": "Extrair ficheiros",
"extracting": "A extrair ficheiros…",
"network": "Rede",
"peak": "Pico",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Local das transferências", "downloads_path": "Local das transferências",

View File

@@ -414,7 +414,11 @@
"resume_seeding": "Продолжить раздачу", "resume_seeding": "Продолжить раздачу",
"options": "Управлять", "options": "Управлять",
"extract": "Распаковать файлы", "extract": "Распаковать файлы",
"extracting": "Распаковка файлов…" "extracting": "Распаковка файлов…",
"network": "Сеть",
"peak": "Пик",
"seeds": "Seeds",
"peers": "Peers"
}, },
"settings": { "settings": {
"downloads_path": "Путь загрузок", "downloads_path": "Путь загрузок",

View File

@@ -1,20 +0,0 @@
import jwt from "jsonwebtoken";
import { registerEvent } from "../register-event";
import { db, levelKeys } from "@main/level";
import type { Auth } from "@types";
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
const auth = await db.get<string, Auth>(levelKeys.auth, {
valueEncoding: "json",
});
if (!auth) return null;
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
if (!payload) return null;
return payload.sessionId;
};
registerEvent("getSessionHash", getSessionHash);

View File

@@ -1,3 +1,2 @@
import "./get-session-hash";
import "./open-auth-window"; import "./open-auth-window";
import "./sign-out"; import "./sign-out";

View File

@@ -1,13 +0,0 @@
import { getDownloadSourcesCheckBaseline } from "@main/level";
import { registerEvent } from "../register-event";
const getDownloadSourcesCheckBaselineHandler = async (
_event: Electron.IpcMainInvokeEvent
) => {
return await getDownloadSourcesCheckBaseline();
};
registerEvent(
"getDownloadSourcesCheckBaseline",
getDownloadSourcesCheckBaselineHandler
);

View File

@@ -1,13 +0,0 @@
import { getDownloadSourcesSinceValue } from "@main/level";
import { registerEvent } from "../register-event";
const getDownloadSourcesSinceValueHandler = async (
_event: Electron.IpcMainInvokeEvent
) => {
return await getDownloadSourcesSinceValue();
};
registerEvent(
"getDownloadSourcesSinceValue",
getDownloadSourcesSinceValueHandler
);

View File

@@ -1,10 +0,0 @@
import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
import { orderBy } from "lodash-es";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
const allSources = await downloadSourcesSublevel.values().all();
return orderBy(allSources, "createdAt", "desc");
};
registerEvent("getDownloadSources", getDownloadSources);

View File

@@ -1,6 +1,3 @@
import "./add-download-source"; import "./add-download-source";
import "./get-download-sources-check-baseline";
import "./get-download-sources-since-value";
import "./get-download-sources";
import "./remove-download-source"; import "./remove-download-source";
import "./sync-download-sources"; import "./sync-download-sources";

View File

@@ -1,27 +0,0 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { logger } from "@main/services";
import type { GameShop } from "@types";
const clearNewDownloadOptions = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
try {
await gamesSublevel.put(gameKey, {
...game,
newDownloadOptionsCount: undefined,
});
logger.info(`Cleared newDownloadOptionsCount for game ${gameKey}`);
} catch (error) {
logger.error(`Failed to clear newDownloadOptionsCount: ${error}`);
}
};
registerEvent("clearNewDownloadOptions", clearNewDownloadOptions);

View File

@@ -1,21 +0,0 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, downloadsSublevel, levelKeys } from "@main/level";
import type { GameShop } from "@types";
const getGameByObjectId = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const [game, download] = await Promise.all([
gamesSublevel.get(gameKey),
downloadsSublevel.get(gameKey),
]);
if (!game || game.isDeleted) return null;
return { id: gameKey, ...game, download };
};
registerEvent("getGameByObjectId", getGameByObjectId);

View File

@@ -1,49 +0,0 @@
import type { LibraryGame } from "@types";
import { registerEvent } from "../register-event";
import {
downloadsSublevel,
gameAchievementsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
} from "@main/level";
const getLibrary = async (): Promise<LibraryGame[]> => {
return gamesSublevel
.iterator()
.all()
.then((results) => {
return Promise.all(
results
.filter(([_key, game]) => game.isDeleted === false)
.map(async ([key, game]) => {
const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.get(key);
let unlockedAchievementCount = game.unlockedAchievementCount ?? 0;
if (!game.unlockedAchievementCount) {
const achievements = await gameAchievementsSublevel.get(key);
unlockedAchievementCount =
achievements?.unlockedAchievements.length ?? 0;
}
return {
id: key,
...game,
download: download ?? null,
unlockedAchievementCount,
achievementCount: game.achievementCount ?? 0,
// Spread gameAssets last to ensure all image URLs are properly set
...gameAssets,
// Preserve custom image URLs from game if they exist
customIconUrl: game.customIconUrl,
customLogoImageUrl: game.customLogoImageUrl,
customHeroImageUrl: game.customHeroImageUrl,
};
})
);
});
};
registerEvent("getLibrary", getLibrary);

View File

@@ -3,7 +3,6 @@ import "./add-game-to-favorites";
import "./add-game-to-library"; import "./add-game-to-library";
import "./change-game-playtime"; import "./change-game-playtime";
import "./cleanup-unused-assets"; import "./cleanup-unused-assets";
import "./clear-new-download-options";
import "./close-game"; import "./close-game";
import "./copy-custom-game-asset"; import "./copy-custom-game-asset";
import "./create-game-shortcut"; import "./create-game-shortcut";
@@ -11,8 +10,6 @@ import "./create-steam-shortcut";
import "./delete-game-folder"; import "./delete-game-folder";
import "./extract-game-download"; import "./extract-game-download";
import "./get-default-wine-prefix-selection-path"; import "./get-default-wine-prefix-selection-path";
import "./get-game-by-object-id";
import "./get-library";
import "./open-game-executable-path"; import "./open-game-executable-path";
import "./open-game-installer-path"; import "./open-game-installer-path";
import "./open-game-installer"; import "./open-game-installer";

View File

@@ -1,12 +0,0 @@
import { Theme } from "@types";
import { registerEvent } from "../register-event";
import { themesSublevel } from "@main/level";
const addCustomTheme = async (
_event: Electron.IpcMainInvokeEvent,
theme: Theme
) => {
await themesSublevel.put(theme.id, theme);
};
registerEvent("addCustomTheme", addCustomTheme);

View File

@@ -1,8 +0,0 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const deleteAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
await themesSublevel.clear();
};
registerEvent("deleteAllCustomThemes", deleteAllCustomThemes);

View File

@@ -1,11 +0,0 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const deleteCustomTheme = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
) => {
await themesSublevel.del(themeId);
};
registerEvent("deleteCustomTheme", deleteCustomTheme);

View File

@@ -1,9 +0,0 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const getActiveCustomTheme = async () => {
const allThemes = await themesSublevel.values().all();
return allThemes.find((theme) => theme.isActive);
};
registerEvent("getActiveCustomTheme", getActiveCustomTheme);

View File

@@ -1,8 +0,0 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const getAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
return themesSublevel.values().all();
};
registerEvent("getAllCustomThemes", getAllCustomThemes);

View File

@@ -1,11 +0,0 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const getCustomThemeById = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
) => {
return themesSublevel.get(themeId);
};
registerEvent("getCustomThemeById", getCustomThemeById);

View File

@@ -1,11 +1,5 @@
import "./add-custom-theme";
import "./close-editor-window"; import "./close-editor-window";
import "./copy-theme-achievement-sound"; import "./copy-theme-achievement-sound";
import "./delete-all-custom-themes";
import "./delete-custom-theme";
import "./get-active-custom-theme";
import "./get-all-custom-themes";
import "./get-custom-theme-by-id";
import "./get-theme-sound-data-url"; import "./get-theme-sound-data-url";
import "./get-theme-sound-path"; import "./get-theme-sound-path";
import "./import-theme-sound-from-store"; import "./import-theme-sound-from-store";

View File

@@ -1,10 +0,0 @@
import { registerEvent } from "../register-event";
import { db, levelKeys } from "@main/level";
import type { UserPreferences } from "@types";
const getUserPreferences = async () =>
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
});
registerEvent("getUserPreferences", getUserPreferences);

View File

@@ -1,5 +1,4 @@
import "./authenticate-real-debrid"; import "./authenticate-real-debrid";
import "./authenticate-torbox"; import "./authenticate-torbox";
import "./auto-launch"; import "./auto-launch";
import "./get-user-preferences";
import "./update-user-preferences"; import "./update-user-preferences";

View File

@@ -1,11 +0,0 @@
import { db, levelKeys } from "@main/level";
import type { Auth } from "@types";
import { registerEvent } from "../register-event";
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
db.get<string, Auth>(levelKeys.auth, {
valueEncoding: "json",
});
registerEvent("getAuth", getAuth);

View File

@@ -1,3 +1,2 @@
import "./get-auth";
import "./get-compared-unlocked-achievements"; import "./get-compared-unlocked-achievements";
import "./get-unlocked-achievements"; import "./get-unlocked-achievements";

View File

@@ -58,7 +58,13 @@ export class HydraApi {
const decodedBase64 = atob(payload as string); const decodedBase64 = atob(payload as string);
const jsonData = JSON.parse(decodedBase64); const jsonData = JSON.parse(decodedBase64);
const { accessToken, expiresIn, refreshToken } = jsonData; const {
accessToken,
expiresIn,
refreshToken,
featurebaseJwt,
workwondersJwt,
} = jsonData;
const now = new Date(); const now = new Date();
@@ -85,6 +91,8 @@ export class HydraApi {
accessToken, accessToken,
refreshToken, refreshToken,
tokenExpirationTimestamp, tokenExpirationTimestamp,
featurebaseJwt,
workwondersJwt,
}, },
{ valueEncoding: "json" } { valueEncoding: "json" }
); );

View File

@@ -138,7 +138,8 @@ export class WindowManager {
(details, callback) => { (details, callback) => {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot") details.url.includes("chatwoot") ||
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }
@@ -159,7 +160,8 @@ export class WindowManager {
if ( if (
details.webContentsId !== this.mainWindow?.webContents.id || details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("featurebase") || details.url.includes("featurebase") ||
details.url.includes("chatwoot") details.url.includes("chatwoot") ||
details.url.includes("workwonders")
) { ) {
return callback(details); return callback(details);
} }

View File

@@ -13,7 +13,6 @@ import type {
UpdateProfileRequest, UpdateProfileRequest,
SeedingStatus, SeedingStatus,
GameAchievement, GameAchievement,
Theme,
FriendRequestSync, FriendRequestSync,
ShortcutLocation, ShortcutLocation,
AchievementCustomNotificationPosition, AchievementCustomNotificationPosition,
@@ -86,7 +85,8 @@ contextBridge.exposeInMainWorld("electron", {
}, },
/* User preferences */ /* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), getUserPreferences: () =>
ipcRenderer.invoke("leveldbGet", "userPreferences", null, "json"),
updateUserPreferences: (preferences: UserPreferences) => updateUserPreferences: (preferences: UserPreferences) =>
ipcRenderer.invoke("updateUserPreferences", preferences), ipcRenderer.invoke("updateUserPreferences", preferences),
autoLaunch: (autoLaunchProps: { enabled: boolean; minimized: boolean }) => autoLaunch: (autoLaunchProps: { enabled: boolean; minimized: boolean }) =>
@@ -101,12 +101,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("addDownloadSource", url), ipcRenderer.invoke("addDownloadSource", url),
removeDownloadSource: (url: string, removeAll?: boolean) => removeDownloadSource: (url: string, removeAll?: boolean) =>
ipcRenderer.invoke("removeDownloadSource", url, removeAll), ipcRenderer.invoke("removeDownloadSource", url, removeAll),
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"), syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
getDownloadSourcesCheckBaseline: () =>
ipcRenderer.invoke("getDownloadSourcesCheckBaseline"),
getDownloadSourcesSinceValue: () =>
ipcRenderer.invoke("getDownloadSourcesSinceValue"),
/* Library */ /* Library */
toggleAutomaticCloudSync: ( toggleAutomaticCloudSync: (
@@ -183,8 +178,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("addGameToFavorites", shop, objectId), ipcRenderer.invoke("addGameToFavorites", shop, objectId),
removeGameFromFavorites: (shop: GameShop, objectId: string) => removeGameFromFavorites: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId), ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
clearNewDownloadOptions: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("clearNewDownloadOptions", shop, objectId),
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) => toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned), ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
updateLaunchOptions: ( updateLaunchOptions: (
@@ -201,7 +194,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath), ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath),
verifyExecutablePathInUse: (executablePath: string) => verifyExecutablePathInUse: (executablePath: string) =>
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath), ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
getLibrary: () => ipcRenderer.invoke("getLibrary"),
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"), refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
openGameInstaller: (shop: GameShop, objectId: string) => openGameInstaller: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("openGameInstaller", shop, objectId), ipcRenderer.invoke("openGameInstaller", shop, objectId),
@@ -230,8 +222,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("removeGame", shop, objectId), ipcRenderer.invoke("removeGame", shop, objectId),
deleteGameFolder: (shop: GameShop, objectId: string) => deleteGameFolder: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("deleteGameFolder", shop, objectId), ipcRenderer.invoke("deleteGameFolder", shop, objectId),
getGameByObjectId: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
resetGameAchievements: (shop: GameShop, objectId: string) => resetGameAchievements: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("resetGameAchievements", shop, objectId), ipcRenderer.invoke("resetGameAchievements", shop, objectId),
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) => changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
@@ -287,8 +277,6 @@ contextBridge.exposeInMainWorld("electron", {
gameArtifactId: string gameArtifactId: string
) => ) =>
ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId), ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId),
getGameArtifacts: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
getGameBackupPreview: (objectId: string, shop: GameShop) => getGameBackupPreview: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameBackupPreview", objectId, shop), ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
selectGameBackupPath: ( selectGameBackupPath: (
@@ -503,11 +491,10 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop), ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
/* Auth */ /* Auth */
getAuth: () => ipcRenderer.invoke("getAuth"), getAuth: () => ipcRenderer.invoke("leveldbGet", "auth", null, "json"),
signOut: () => ipcRenderer.invoke("signOut"), signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: (page: AuthPage) => openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page), ipcRenderer.invoke("openAuthWindow", page),
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
onSignIn: (cb: () => void) => { onSignIn: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signin", listener); ipcRenderer.on("on-signin", listener);
@@ -565,16 +552,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showAchievementTestNotification"), ipcRenderer.invoke("showAchievementTestNotification"),
/* Themes */ /* Themes */
addCustomTheme: (theme: Theme) => ipcRenderer.invoke("addCustomTheme", theme),
getAllCustomThemes: () => ipcRenderer.invoke("getAllCustomThemes"),
deleteAllCustomThemes: () => ipcRenderer.invoke("deleteAllCustomThemes"),
deleteCustomTheme: (themeId: string) =>
ipcRenderer.invoke("deleteCustomTheme", themeId),
updateCustomTheme: (themeId: string, code: string) => updateCustomTheme: (themeId: string, code: string) =>
ipcRenderer.invoke("updateCustomTheme", themeId, code), ipcRenderer.invoke("updateCustomTheme", themeId, code),
getCustomThemeById: (themeId: string) =>
ipcRenderer.invoke("getCustomThemeById", themeId),
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
toggleCustomTheme: (themeId: string, isActive: boolean) => toggleCustomTheme: (themeId: string, isActive: boolean) =>
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive), ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
copyThemeAchievementSound: (themeId: string, sourcePath: string) => copyThemeAchievementSound: (themeId: string, sourcePath: string) =>

View File

@@ -7,11 +7,13 @@ import {
useToast, useToast,
useUserDetails, useUserDetails,
} from "@renderer/hooks"; } from "@renderer/hooks";
import { levelDBService } from "@renderer/services/leveldb.service";
import "./bottom-panel.scss"; import "./bottom-panel.scss";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { VERSION_CODENAME } from "@renderer/constants"; import { VERSION_CODENAME } from "@renderer/constants";
import type jwt from "jsonwebtoken";
export function BottomPanel() { export function BottomPanel() {
const { t } = useTranslation("bottom_panel"); const { t } = useTranslation("bottom_panel");
@@ -60,7 +62,28 @@ export function BottomPanel() {
}, [t, showSuccessToast]); }, [t, showSuccessToast]);
useEffect(() => { useEffect(() => {
window.electron.getSessionHash().then((result) => setSessionHash(result)); const getSessionHash = async () => {
const auth = (await levelDBService.get("auth", null, "json")) as {
accessToken?: string;
} | null;
if (!auth?.accessToken) {
setSessionHash(null);
return;
}
try {
const jwtModule = await import("jsonwebtoken");
const payload = jwtModule.decode(
auth.accessToken
) as jwt.JwtPayload | null;
setSessionHash(payload?.sessionId ?? null);
} catch {
setSessionHash(null);
}
};
getSessionHash();
}, [userDetails?.id]); }, [userDetails?.id]);
const status = useMemo(() => { const status = useMemo(() => {
@@ -122,10 +145,10 @@ export function BottomPanel() {
</button> </button>
<button <button
data-featurebase-changelog data-open-workwonders-changelog-mini
className="bottom-panel__version-button" className="bottom-panel__version-button"
> >
<small data-featurebase-changelog> <small>
{sessionHash ? `${sessionHash} -` : ""} v{version} &quot; {sessionHash ? `${sessionHash} -` : ""} v{version} &quot;
{VERSION_CODENAME}&quot; {VERSION_CODENAME}&quot;
</small> </small>

View File

@@ -12,6 +12,7 @@ import {
} from "@renderer/hooks"; } from "@renderer/hooks";
import type { import type {
Download,
DownloadSource, DownloadSource,
GameRepack, GameRepack,
GameShop, GameShop,
@@ -95,9 +96,19 @@ export function GameDetailsContextProvider({
); );
const updateGame = useCallback(async () => { const updateGame = useCallback(async () => {
return window.electron const gameKey = `${shop}:${objectId}`;
.getGameByObjectId(shop, objectId) const [game, download] = await Promise.all([
.then((result) => setGame(result)); levelDBService.get(gameKey, "games") as Promise<LibraryGame | null>,
levelDBService.get(gameKey, "downloads") as Promise<Download | null>,
]);
if (!game || game.isDeleted) {
setGame(null);
return;
}
const { id: _id, ...gameWithoutId } = game;
setGame({ id: gameKey, ...gameWithoutId, download: download ?? null });
}, [shop, objectId]); }, [shop, objectId]);
const isGameDownloading = const isGameDownloading =

View File

@@ -14,14 +14,11 @@ import type {
GameStats, GameStats,
UserDetails, UserDetails,
FriendRequestSync, FriendRequestSync,
GameArtifact,
LudusaviBackup, LudusaviBackup,
UserAchievement, UserAchievement,
ComparedAchievements, ComparedAchievements,
LibraryGame,
GameRunning, GameRunning,
TorBoxUser, TorBoxUser,
Theme,
Auth, Auth,
ShortcutLocation, ShortcutLocation,
ShopAssets, ShopAssets,
@@ -142,10 +139,6 @@ declare global {
shop: GameShop, shop: GameShop,
objectId: string objectId: string
) => Promise<void>; ) => Promise<void>;
clearNewDownloadOptions: (
shop: GameShop,
objectId: string
) => Promise<void>;
toggleGamePin: ( toggleGamePin: (
shop: GameShop, shop: GameShop,
objectId: string, objectId: string,
@@ -162,7 +155,6 @@ declare global {
winePrefixPath: string | null winePrefixPath: string | null
) => Promise<void>; ) => Promise<void>;
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>; verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
getLibrary: () => Promise<LibraryGame[]>;
refreshLibraryAssets: () => Promise<void>; refreshLibraryAssets: () => Promise<void>;
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>; openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>; openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
@@ -177,10 +169,6 @@ declare global {
removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>; removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>;
removeGame: (shop: GameShop, objectId: string) => Promise<void>; removeGame: (shop: GameShop, objectId: string) => Promise<void>;
deleteGameFolder: (shop: GameShop, objectId: string) => Promise<unknown>; deleteGameFolder: (shop: GameShop, objectId: string) => Promise<unknown>;
getGameByObjectId: (
shop: GameShop,
objectId: string
) => Promise<LibraryGame | null>;
onGamesRunning: ( onGamesRunning: (
cb: ( cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[] gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
@@ -194,9 +182,9 @@ declare global {
playtimeInSeconds: number playtimeInSeconds: number
) => Promise<void>; ) => Promise<void>;
/* User preferences */ /* User preferences */
getUserPreferences: () => Promise<UserPreferences | null>;
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>; authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>; authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
getUserPreferences: () => Promise<UserPreferences | null>;
updateUserPreferences: ( updateUserPreferences: (
preferences: Partial<UserPreferences> preferences: Partial<UserPreferences>
) => Promise<void>; ) => Promise<void>;
@@ -217,10 +205,7 @@ declare global {
removeAll = false, removeAll = false,
downloadSourceId?: string downloadSourceId?: string
) => Promise<void>; ) => Promise<void>;
getDownloadSources: () => Promise<DownloadSource[]>;
syncDownloadSources: () => Promise<void>; syncDownloadSources: () => Promise<void>;
getDownloadSourcesCheckBaseline: () => Promise<string | null>;
getDownloadSourcesSinceValue: () => Promise<string | null>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskUsage>; getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
@@ -237,10 +222,6 @@ declare global {
shop: GameShop, shop: GameShop,
gameArtifactId: string gameArtifactId: string
) => Promise<void>; ) => Promise<void>;
getGameArtifacts: (
objectId: string,
shop: GameShop
) => Promise<GameArtifact[]>;
getGameBackupPreview: ( getGameBackupPreview: (
objectId: string, objectId: string,
shop: GameShop shop: GameShop
@@ -355,7 +336,6 @@ declare global {
getAuth: () => Promise<Auth | null>; getAuth: () => Promise<Auth | null>;
signOut: () => Promise<void>; signOut: () => Promise<void>;
openAuthWindow: (page: AuthPage) => Promise<void>; openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>;
onSignIn: (cb: () => void) => () => Electron.IpcRenderer; onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer; onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
onSignOut: (cb: () => void) => () => Electron.IpcRenderer; onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
@@ -408,13 +388,7 @@ declare global {
showAchievementTestNotification: () => Promise<void>; showAchievementTestNotification: () => Promise<void>;
/* Themes */ /* Themes */
addCustomTheme: (theme: Theme) => Promise<void>;
getAllCustomThemes: () => Promise<Theme[]>;
deleteAllCustomThemes: () => Promise<void>;
deleteCustomTheme: (themeId: string) => Promise<void>;
updateCustomTheme: (themeId: string, code: string) => Promise<void>; updateCustomTheme: (themeId: string, code: string) => Promise<void>;
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
getActiveCustomTheme: () => Promise<Theme | null>;
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>; toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
copyThemeAchievementSound: ( copyThemeAchievementSound: (
themeId: string, themeId: string,

View File

@@ -1,15 +1,65 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useAppDispatch, useAppSelector } from "./redux"; import { useAppDispatch, useAppSelector } from "./redux";
import { setLibrary } from "@renderer/features"; import { setLibrary } from "@renderer/features";
import { levelDBService } from "@renderer/services/leveldb.service";
import type {
LibraryGame,
Game,
Download,
ShopAssets,
GameAchievement,
} from "@types";
export function useLibrary() { export function useLibrary() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const library = useAppSelector((state) => state.library.value); const library = useAppSelector((state) => state.library.value);
const updateLibrary = useCallback(async () => { const updateLibrary = useCallback(async () => {
return window.electron const results = await levelDBService.iterator("games");
.getLibrary()
.then((updatedLibrary) => dispatch(setLibrary(updatedLibrary))); const libraryGames = await Promise.all(
results
.filter(([_key, game]) => (game as Game).isDeleted === false)
.map(async ([key, game]) => {
const gameData = game as Game;
const download = (await levelDBService.get(
key,
"downloads"
)) as Download | null;
const gameAssets = (await levelDBService.get(
key,
"gameShopAssets"
)) as (ShopAssets & { updatedAt: number }) | null;
let unlockedAchievementCount = gameData.unlockedAchievementCount ?? 0;
if (!gameData.unlockedAchievementCount) {
const achievements = (await levelDBService.get(
key,
"gameAchievements"
)) as GameAchievement | null;
unlockedAchievementCount =
achievements?.unlockedAchievements.length ?? 0;
}
return {
id: key,
...gameData,
download: download ?? null,
unlockedAchievementCount,
achievementCount: gameData.achievementCount ?? 0,
// Spread gameAssets last to ensure all image URLs are properly set
...gameAssets,
// Preserve custom image URLs from game if they exist
customIconUrl: gameData.customIconUrl,
customLogoImageUrl: gameData.customLogoImageUrl,
customHeroImageUrl: gameData.customHeroImageUrl,
} as LibraryGame;
})
);
dispatch(setLibrary(libraryGames));
}, [dispatch]); }, [dispatch]);
return { library, updateLibrary }; return { library, updateLibrary };

View File

@@ -305,9 +305,11 @@ function HeroDownloadView({
)} )}
</span> </span>
)} )}
<span className="download-group__progress-percentage"> {(!lastPacket?.isCheckingFiles || currentProgress > 0) && (
<AnimatedPercentage value={currentProgress} /> <span className="download-group__progress-percentage">
</span> <AnimatedPercentage value={currentProgress} />
</span>
)}
</div> </div>
<div className="download-group__progress-bar"> <div className="download-group__progress-bar">
<div <div
@@ -358,7 +360,7 @@ function HeroDownloadView({
</span> </span>
<div className="download-group__stat-content"> <div className="download-group__stat-content">
<span className="download-group__stat-label"> <span className="download-group__stat-label">
{t("network")}: {t("network")}
</span> </span>
<span className="download-group__stat-value"> <span className="download-group__stat-value">
{isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"} {isGameDownloading ? formatSpeed(downloadSpeed) : "0 B/s"}
@@ -371,37 +373,38 @@ function HeroDownloadView({
<GraphIcon size={16} /> <GraphIcon size={16} />
</span> </span>
<div className="download-group__stat-content"> <div className="download-group__stat-content">
<span className="download-group__stat-label">{t("peak")}:</span> <span className="download-group__stat-label">{t("peak")}</span>
<span className="download-group__stat-value"> <span className="download-group__stat-value">
{peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"} {peakSpeed > 0 ? formatSpeed(peakSpeed) : "0 B/s"}
</span> </span>
</div> </div>
</div> </div>
{game.download?.downloader === Downloader.Torrent &&
isGameDownloading &&
lastPacket &&
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
<div className="download-group__stat-item">
<div className="download-group__stat-content">
<span className="download-group__stat-label">
Seeds:{" "}
<span className="download-group__stat-value">
{lastPacket.numSeeds}
</span>
, Peers:{" "}
<span className="download-group__stat-value">
{lastPacket.numPeers}
</span>
</span>
</div>
</div>
)}
{game.download?.downloader && ( {game.download?.downloader && (
<div className="download-group__stat-item"> <div className="download-group__stat-item">
<div className="download-group__stat-content"> <div
className="download-group__stat-content"
style={{
justifyContent: "space-between",
alignItems: "center",
}}
>
<Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge> <Badge>{DOWNLOADER_NAME[game.download.downloader]}</Badge>
{game.download?.downloader === Downloader.Torrent &&
isGameDownloading &&
lastPacket &&
(lastPacket.numSeeds > 0 || lastPacket.numPeers > 0) && (
<span className="download-group__stat-label">
{t("seeds")}{" "}
<span className="download-group__stat-value">
{lastPacket.numSeeds}
</span>
, {t("peers")}{" "}
<span className="download-group__stat-value">
{lastPacket.numPeers}
</span>
</span>
)}
</div> </div>
</div> </div>
)} )}
@@ -436,6 +439,7 @@ export function DownloadGroup({
seedingStatus, seedingStatus,
}: Readonly<DownloadGroupProps>) { }: Readonly<DownloadGroupProps>) {
const { t } = useTranslation("downloads"); const { t } = useTranslation("downloads");
const navigate = useNavigate();
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
@@ -867,12 +871,36 @@ export function DownloadGroup({
{downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => { {downloadInfo.map(({ game, size, progress, isSeeding: seeding }) => {
return ( return (
<li key={game.id} className="download-group__simple-card"> <li key={game.id} className="download-group__simple-card">
<div className="download-group__simple-thumbnail"> <button
type="button"
className="download-group__simple-thumbnail"
onClick={() => navigate(buildGameDetailsPath(game))}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
>
<img src={game.libraryImageUrl || ""} alt={game.title} /> <img src={game.libraryImageUrl || ""} alt={game.title} />
</div> </button>
<div className="download-group__simple-info"> <div className="download-group__simple-info">
<h3 className="download-group__simple-title">{game.title}</h3> <button
type="button"
className="download-group__simple-title"
onClick={() => navigate(buildGameDetailsPath(game))}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
textAlign: "left",
width: "100%",
}}
>
{game.title}
</button>
<div className="download-group__simple-meta"> <div className="download-group__simple-meta">
<div className="download-group__simple-meta-row"> <div className="download-group__simple-meta-row">
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge> <Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>

View File

@@ -87,12 +87,16 @@ export function LibraryTab({
<ul className="profile-content__games-grid"> <ul className="profile-content__games-grid">
{pinnedGames?.map((game) => ( {pinnedGames?.map((game) => (
<li key={game.objectId} style={{ listStyle: "none" }}> <li
key={game.objectId}
style={{ listStyle: "none" }}
className="user-library-game__wrapper"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<UserLibraryGameCard <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</li> </li>
@@ -134,6 +138,9 @@ export function LibraryTab({
<motion.li <motion.li
key={`${sortBy}-${game.objectId}`} key={`${sortBy}-${game.objectId}`}
style={{ listStyle: "none" }} style={{ listStyle: "none" }}
className="user-library-game__wrapper"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
initial={ initial={
isNewGame isNewGame
? { opacity: 0.5, y: 15, scale: 0.96 } ? { opacity: 0.5, y: 15, scale: 0.96 }
@@ -160,8 +167,6 @@ export function LibraryTab({
<UserLibraryGameCard <UserLibraryGameCard
game={game} game={game}
statIndex={statsIndex} statIndex={statsIndex}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sortBy={sortBy} sortBy={sortBy}
/> />
</motion.li> </motion.li>

View File

@@ -25,16 +25,12 @@ import "./user-library-game-card.scss";
interface UserLibraryGameCardProps { interface UserLibraryGameCardProps {
game: UserGame; game: UserGame;
statIndex: number; statIndex: number;
onMouseEnter: () => void;
onMouseLeave: () => void;
sortBy?: string; sortBy?: string;
} }
export function UserLibraryGameCard({ export function UserLibraryGameCard({
game, game,
statIndex, statIndex,
onMouseEnter,
onMouseLeave,
sortBy, sortBy,
}: UserLibraryGameCardProps) { }: UserLibraryGameCardProps) {
const { userProfile, isMe, getUserLibraryGames } = const { userProfile, isMe, getUserLibraryGames } =
@@ -130,129 +126,126 @@ export function UserLibraryGameCard({
return ( return (
<> <>
<li <div
onMouseEnter={onMouseEnter} className="user-library-game__cover"
onMouseLeave={onMouseLeave} onClick={() => navigate(buildUserGameDetailsPath(game))}
className="user-library-game__wrapper" onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
navigate(buildUserGameDetailsPath(game));
}
}}
role="button"
tabIndex={0}
title={isTooltipHovered ? undefined : game.title} title={isTooltipHovered ? undefined : game.title}
> >
<button <div className="user-library-game__overlay">
type="button" {isMe && (
className="user-library-game__cover" <div className="user-library-game__actions-container">
onClick={() => navigate(buildUserGameDetailsPath(game))} <button
> type="button"
<div className="user-library-game__overlay"> className="user-library-game__pin-button"
{isMe && ( onClick={(e) => {
<div className="user-library-game__actions-container"> e.stopPropagation();
<button toggleGamePinned();
type="button" }}
className="user-library-game__pin-button" disabled={isPinning}
onClick={(e) => { >
e.stopPropagation(); {game.isPinned ? (
toggleGamePinned(); <PinSlashIcon size={12} />
}} ) : (
disabled={isPinning} <PinIcon size={12} />
> )}
{game.isPinned ? ( </button>
<PinSlashIcon size={12} />
) : (
<PinIcon size={12} />
)}
</button>
</div>
)}
<div
className="user-library-game__playtime"
data-tooltip-place="top"
data-tooltip-content={
game.hasManuallyUpdatedPlaytime
? t("manual_playtime_tooltip")
: undefined
}
data-tooltip-id={game.objectId}
>
{game.hasManuallyUpdatedPlaytime ? (
<AlertFillIcon
size={11}
className="user-library-game__manual-playtime"
/>
) : (
<ClockIcon size={11} />
)}
<span className="user-library-game__playtime-long">
{formatPlayTime(game.playTimeInSeconds)}
</span>
<span className="user-library-game__playtime-short">
{formatPlayTime(game.playTimeInSeconds, true)}
</span>
</div> </div>
)}
<div
className="user-library-game__playtime"
data-tooltip-place="top"
data-tooltip-content={
game.hasManuallyUpdatedPlaytime
? t("manual_playtime_tooltip")
: undefined
}
data-tooltip-id={game.objectId}
>
{game.hasManuallyUpdatedPlaytime ? (
<AlertFillIcon
size={11}
className="user-library-game__manual-playtime"
/>
) : (
<ClockIcon size={11} />
)}
<span className="user-library-game__playtime-long">
{formatPlayTime(game.playTimeInSeconds)}
</span>
<span className="user-library-game__playtime-short">
{formatPlayTime(game.playTimeInSeconds, true)}
</span>
</div>
{userProfile?.hasActiveSubscription && {userProfile?.hasActiveSubscription && game.achievementCount > 0 && (
game.achievementCount > 0 && ( <div className="user-library-game__stats">
<div className="user-library-game__stats"> <div className="user-library-game__stats-header">
<div className="user-library-game__stats-header"> <div className="user-library-game__stats-content">
<div className="user-library-game__stats-content"> <div
<div className="user-library-game__stats-item"
className="user-library-game__stats-item" style={{
style={{ transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`, }}
}} >
> <TrophyIcon size={13} />
<TrophyIcon size={13} />
<span>
{game.unlockedAchievementCount} /{" "}
{game.achievementCount}
</span>
</div>
{game.achievementsPointsEarnedSum > 0 && (
<div
className="user-library-game__stats-item"
style={{
transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
}}
>
<HydraIcon width={16} height={16} />
{formatAchievementPoints(
game.achievementsPointsEarnedSum
)}
</div>
)}
</div>
<span> <span>
{formatDownloadProgress( {game.unlockedAchievementCount} / {game.achievementCount}
game.unlockedAchievementCount / game.achievementCount,
1
)}
</span> </span>
</div> </div>
<progress {game.achievementsPointsEarnedSum > 0 && (
max={1} <div
value={ className="user-library-game__stats-item"
game.unlockedAchievementCount / game.achievementCount style={{
} transform: `translateY(${-100 * (statIndex % getStatsItemCount())}%)`,
className="user-library-game__achievements-progress" }}
/> >
<HydraIcon width={16} height={16} />
{formatAchievementPoints(
game.achievementsPointsEarnedSum
)}
</div>
)}
</div> </div>
)}
</div>
{imageError || !game.coverImageUrl ? ( <span>
<div className="user-library-game__cover-placeholder"> {formatDownloadProgress(
<ImageIcon size={48} /> game.unlockedAchievementCount / game.achievementCount,
1
)}
</span>
</div>
<progress
max={1}
value={game.unlockedAchievementCount / game.achievementCount}
className="user-library-game__achievements-progress"
/>
</div> </div>
) : (
<img
src={game.coverImageUrl}
alt={game.title}
className="user-library-game__game-image"
onError={() => setImageError(true)}
/>
)} )}
</button> </div>
</li>
{imageError || !game.coverImageUrl ? (
<div className="user-library-game__cover-placeholder">
<ImageIcon size={48} />
</div>
) : (
<img
src={game.coverImageUrl}
alt={game.title}
className="user-library-game__game-image"
onError={() => setImageError(true)}
/>
)}
</div>
<Tooltip <Tooltip
id={game.objectId} id={game.objectId}
style={{ style={{

View File

@@ -20,6 +20,8 @@ export interface Auth {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
tokenExpirationTimestamp: number; tokenExpirationTimestamp: number;
featurebaseJwt: string;
workwondersJwt: string;
} }
export interface User { export interface User {

View File

@@ -6330,7 +6330,7 @@ jsonwebtoken@^9.0.2:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
jwa@^1.4.1: jwa@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
@@ -6340,11 +6340,11 @@ jwa@^1.4.1:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jws@^3.2.2: jws@^3.2.2:
version "3.2.2" version "3.2.3"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
dependencies: dependencies:
jwa "^1.4.1" jwa "^1.4.2"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
keyv@^4.0.0, keyv@^4.5.3: keyv@^4.0.0, keyv@^4.5.3: