mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 18:13:55 +00:00
Compare commits
8 Commits
v3.8.1
...
fix/fixing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47bfc1648f | ||
|
|
b5445b3dfa | ||
|
|
1ccf70af12 | ||
|
|
bb45b95820 | ||
|
|
361c158a44 | ||
|
|
1f5e84b32c | ||
|
|
e49d885b30 | ||
|
|
cb01301a0d |
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Путь загрузок",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
import "./get-session-hash";
|
|
||||||
import "./open-auth-window";
|
import "./open-auth-window";
|
||||||
import "./sign-out";
|
import "./sign-out";
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
28
src/renderer/src/declaration.d.ts
vendored
28
src/renderer/src/declaration.d.ts
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
Reference in New Issue
Block a user