mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
1 Commits
fix/fixing
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e578047929 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydralauncher",
|
||||
"version": "3.7.6",
|
||||
"version": "3.7.5",
|
||||
"description": "Hydra",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Los Broxas",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Library",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Settings",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Available",
|
||||
"my_library": "My library",
|
||||
"downloading_metadata": "{{title}} (Downloading metadata…)",
|
||||
"paused": "{{title}} (Paused)",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Librería",
|
||||
"downloads": "Descargas",
|
||||
"settings": "Ajustes",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Disponible",
|
||||
"my_library": "Mi Librería",
|
||||
"downloading_metadata": "{{title}} (Descargando metadatos…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
@@ -462,7 +463,6 @@
|
||||
"description_confirmation_delete_all_sources": "Vas a eliminar todas las fuentes de descargas",
|
||||
"button_delete_all_sources": "Eliminar todo",
|
||||
"added_download_source": "Añadir fuente de descarga",
|
||||
"adding": "Añadiendo…",
|
||||
"download_sources_synced": "Todas las fuentes de descarga están sincronizadas",
|
||||
"insert_valid_json_url": "Introducí una URL de json válida",
|
||||
"found_download_option_zero": "Sin opciones de descargas encontrada",
|
||||
@@ -568,19 +568,6 @@
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"notifications": {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Biblioteca",
|
||||
"downloads": "Downloads",
|
||||
"settings": "Ajustes",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
|
||||
"my_library": "Biblioteca",
|
||||
"downloading_metadata": "{{title}} (Baixando metadados…)",
|
||||
"paused": "{{title}} (Pausado)",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"catalogue": "Catálogo",
|
||||
"downloads": "Transferências",
|
||||
"settings": "Definições",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Já disponível",
|
||||
"my_library": "Biblioteca",
|
||||
"downloading_metadata": "{{title}} (A transferir metadados…)",
|
||||
"paused": "{{title}} (Em pausa)",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"library": "Библиотека",
|
||||
"downloads": "Загрузки",
|
||||
"settings": "Настройки",
|
||||
"hydra_2025_wrapped": "Hydra Wrapped 2025 Доступно",
|
||||
"my_library": "Библиотека",
|
||||
"downloading_metadata": "{{title}} (Загрузка метаданных…)",
|
||||
"paused": "{{title}} (Приостановлено)",
|
||||
|
||||
20
src/main/events/auth/get-session-hash.ts
Normal file
20
src/main/events/auth/get-session-hash.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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,2 +1,3 @@
|
||||
import "./get-session-hash";
|
||||
import "./open-auth-window";
|
||||
import "./sign-out";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getDownloadSourcesCheckBaseline } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSourcesCheckBaselineHandler = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
return await getDownloadSourcesCheckBaseline();
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"getDownloadSourcesCheckBaseline",
|
||||
getDownloadSourcesCheckBaselineHandler
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getDownloadSourcesSinceValue } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSourcesSinceValueHandler = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
) => {
|
||||
return await getDownloadSourcesSinceValue();
|
||||
};
|
||||
|
||||
registerEvent(
|
||||
"getDownloadSourcesSinceValue",
|
||||
getDownloadSourcesSinceValueHandler
|
||||
);
|
||||
10
src/main/events/download-sources/get-download-sources.ts
Normal file
10
src/main/events/download-sources/get-download-sources.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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,3 +1,6 @@
|
||||
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 "./sync-download-sources";
|
||||
|
||||
27
src/main/events/library/clear-new-download-options.ts
Normal file
27
src/main/events/library/clear-new-download-options.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
21
src/main/events/library/get-game-by-object-id.ts
Normal file
21
src/main/events/library/get-game-by-object-id.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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);
|
||||
49
src/main/events/library/get-library.ts
Normal file
49
src/main/events/library/get-library.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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,6 +3,7 @@ import "./add-game-to-favorites";
|
||||
import "./add-game-to-library";
|
||||
import "./change-game-playtime";
|
||||
import "./cleanup-unused-assets";
|
||||
import "./clear-new-download-options";
|
||||
import "./close-game";
|
||||
import "./copy-custom-game-asset";
|
||||
import "./create-game-shortcut";
|
||||
@@ -10,6 +11,8 @@ import "./create-steam-shortcut";
|
||||
import "./delete-game-folder";
|
||||
import "./extract-game-download";
|
||||
import "./get-default-wine-prefix-selection-path";
|
||||
import "./get-game-by-object-id";
|
||||
import "./get-library";
|
||||
import "./open-game-executable-path";
|
||||
import "./open-game-installer-path";
|
||||
import "./open-game-installer";
|
||||
|
||||
12
src/main/events/themes/add-custom-theme.ts
Normal file
12
src/main/events/themes/add-custom-theme.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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);
|
||||
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
8
src/main/events/themes/delete-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { themesSublevel } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const deleteAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
await themesSublevel.clear();
|
||||
};
|
||||
|
||||
registerEvent("deleteAllCustomThemes", deleteAllCustomThemes);
|
||||
11
src/main/events/themes/delete-custom-theme.ts
Normal file
11
src/main/events/themes/delete-custom-theme.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
9
src/main/events/themes/get-active-custom-theme.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
8
src/main/events/themes/get-all-custom-themes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { themesSublevel } from "@main/level";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getAllCustomThemes = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return themesSublevel.values().all();
|
||||
};
|
||||
|
||||
registerEvent("getAllCustomThemes", getAllCustomThemes);
|
||||
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
11
src/main/events/themes/get-custom-theme-by-id.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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,5 +1,11 @@
|
||||
import "./add-custom-theme";
|
||||
import "./close-editor-window";
|
||||
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-path";
|
||||
import "./import-theme-sound-from-store";
|
||||
|
||||
10
src/main/events/user-preferences/get-user-preferences.ts
Normal file
10
src/main/events/user-preferences/get-user-preferences.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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,4 +1,5 @@
|
||||
import "./authenticate-real-debrid";
|
||||
import "./authenticate-torbox";
|
||||
import "./auto-launch";
|
||||
import "./get-user-preferences";
|
||||
import "./update-user-preferences";
|
||||
|
||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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,2 +1,3 @@
|
||||
import "./get-auth";
|
||||
import "./get-compared-unlocked-achievements";
|
||||
import "./get-unlocked-achievements";
|
||||
|
||||
@@ -58,13 +58,7 @@ export class HydraApi {
|
||||
const decodedBase64 = atob(payload as string);
|
||||
const jsonData = JSON.parse(decodedBase64);
|
||||
|
||||
const {
|
||||
accessToken,
|
||||
expiresIn,
|
||||
refreshToken,
|
||||
featurebaseJwt,
|
||||
workwondersJwt,
|
||||
} = jsonData;
|
||||
const { accessToken, expiresIn, refreshToken } = jsonData;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
@@ -91,8 +85,6 @@ export class HydraApi {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpirationTimestamp,
|
||||
featurebaseJwt,
|
||||
workwondersJwt,
|
||||
},
|
||||
{ valueEncoding: "json" }
|
||||
);
|
||||
|
||||
@@ -138,8 +138,7 @@ export class WindowManager {
|
||||
(details, callback) => {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("chatwoot") ||
|
||||
details.url.includes("workwonders")
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
@@ -160,8 +159,7 @@ export class WindowManager {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("featurebase") ||
|
||||
details.url.includes("chatwoot") ||
|
||||
details.url.includes("workwonders")
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
@@ -356,6 +354,8 @@ export class WindowManager {
|
||||
public static async createNotificationWindow() {
|
||||
if (this.notificationWindow) return;
|
||||
|
||||
if (process.platform === "darwin") return;
|
||||
|
||||
const userPreferences = await db.get<string, UserPreferences | undefined>(
|
||||
levelKeys.userPreferences,
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
UpdateProfileRequest,
|
||||
SeedingStatus,
|
||||
GameAchievement,
|
||||
Theme,
|
||||
FriendRequestSync,
|
||||
ShortcutLocation,
|
||||
AchievementCustomNotificationPosition,
|
||||
@@ -85,8 +86,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
},
|
||||
|
||||
/* User preferences */
|
||||
getUserPreferences: () =>
|
||||
ipcRenderer.invoke("leveldbGet", "userPreferences", null, "json"),
|
||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||
updateUserPreferences: (preferences: UserPreferences) =>
|
||||
ipcRenderer.invoke("updateUserPreferences", preferences),
|
||||
autoLaunch: (autoLaunchProps: { enabled: boolean; minimized: boolean }) =>
|
||||
@@ -101,7 +101,12 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addDownloadSource", url),
|
||||
removeDownloadSource: (url: string, removeAll?: boolean) =>
|
||||
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
|
||||
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
|
||||
getDownloadSourcesCheckBaseline: () =>
|
||||
ipcRenderer.invoke("getDownloadSourcesCheckBaseline"),
|
||||
getDownloadSourcesSinceValue: () =>
|
||||
ipcRenderer.invoke("getDownloadSourcesSinceValue"),
|
||||
|
||||
/* Library */
|
||||
toggleAutomaticCloudSync: (
|
||||
@@ -178,6 +183,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("addGameToFavorites", shop, objectId),
|
||||
removeGameFromFavorites: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("removeGameFromFavorites", shop, objectId),
|
||||
clearNewDownloadOptions: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("clearNewDownloadOptions", shop, objectId),
|
||||
toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) =>
|
||||
ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned),
|
||||
updateLaunchOptions: (
|
||||
@@ -194,6 +201,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath),
|
||||
verifyExecutablePathInUse: (executablePath: string) =>
|
||||
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
|
||||
getLibrary: () => ipcRenderer.invoke("getLibrary"),
|
||||
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
|
||||
openGameInstaller: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("openGameInstaller", shop, objectId),
|
||||
@@ -222,6 +230,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("removeGame", shop, objectId),
|
||||
deleteGameFolder: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
||||
getGameByObjectId: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||
changeGamePlayTime: (shop: GameShop, objectId: string, playtime: number) =>
|
||||
@@ -277,6 +287,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
gameArtifactId: string
|
||||
) =>
|
||||
ipcRenderer.invoke("downloadGameArtifact", objectId, shop, gameArtifactId),
|
||||
getGameArtifacts: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameArtifacts", objectId, shop),
|
||||
getGameBackupPreview: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getGameBackupPreview", objectId, shop),
|
||||
selectGameBackupPath: (
|
||||
@@ -491,10 +503,11 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||
|
||||
/* Auth */
|
||||
getAuth: () => ipcRenderer.invoke("leveldbGet", "auth", null, "json"),
|
||||
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||
signOut: () => ipcRenderer.invoke("signOut"),
|
||||
openAuthWindow: (page: AuthPage) =>
|
||||
ipcRenderer.invoke("openAuthWindow", page),
|
||||
getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
|
||||
onSignIn: (cb: () => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => cb();
|
||||
ipcRenderer.on("on-signin", listener);
|
||||
@@ -552,8 +565,16 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("showAchievementTestNotification"),
|
||||
|
||||
/* 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) =>
|
||||
ipcRenderer.invoke("updateCustomTheme", themeId, code),
|
||||
getCustomThemeById: (themeId: string) =>
|
||||
ipcRenderer.invoke("getCustomThemeById", themeId),
|
||||
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
|
||||
toggleCustomTheme: (themeId: string, isActive: boolean) =>
|
||||
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
|
||||
copyThemeAchievementSound: (themeId: string, sourcePath: string) =>
|
||||
|
||||
@@ -7,13 +7,11 @@ import {
|
||||
useToast,
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
import { levelDBService } from "@renderer/services/leveldb.service";
|
||||
|
||||
import "./bottom-panel.scss";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { VERSION_CODENAME } from "@renderer/constants";
|
||||
import type jwt from "jsonwebtoken";
|
||||
|
||||
export function BottomPanel() {
|
||||
const { t } = useTranslation("bottom_panel");
|
||||
@@ -62,28 +60,7 @@ export function BottomPanel() {
|
||||
}, [t, showSuccessToast]);
|
||||
|
||||
useEffect(() => {
|
||||
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();
|
||||
window.electron.getSessionHash().then((result) => setSessionHash(result));
|
||||
}, [userDetails?.id]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
@@ -145,10 +122,10 @@ export function BottomPanel() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-open-workwonders-changelog-mini
|
||||
data-featurebase-changelog
|
||||
className="bottom-panel__version-button"
|
||||
>
|
||||
<small>
|
||||
<small data-featurebase-changelog>
|
||||
{sessionHash ? `${sessionHash} -` : ""} v{version} "
|
||||
{VERSION_CODENAME}"
|
||||
</small>
|
||||
|
||||
@@ -32,4 +32,15 @@ export const routes = [
|
||||
nameKey: "settings",
|
||||
render: () => <GearIcon />,
|
||||
},
|
||||
{
|
||||
path: "https://hydrawrapped.com",
|
||||
nameKey: "hydra_2025_wrapped",
|
||||
render: () => (
|
||||
<img
|
||||
src="https://cdn.losbroxas.org/thumbnail_hydra_badge2_fb01af31e3.png"
|
||||
alt="Hydra 2025 Wrapped"
|
||||
style={{ width: 16, height: 16 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -88,6 +88,34 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&--wrapped {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.25) 0%,
|
||||
rgba(123, 104, 238, 0.2) 25%,
|
||||
rgba(59, 130, 246, 0.25) 50%,
|
||||
rgba(96, 165, 250, 0.2) 75%,
|
||||
rgba(74, 144, 226, 0.25) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: wrapped-gradient-flow 8s ease infinite;
|
||||
color: globals.$muted-color;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 144, 226, 0.35) 0%,
|
||||
rgba(123, 104, 238, 0.3) 25%,
|
||||
rgba(59, 130, 246, 0.35) 50%,
|
||||
rgba(96, 165, 250, 0.3) 75%,
|
||||
rgba(74, 144, 226, 0.35) 100%
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-item-button {
|
||||
@@ -106,6 +134,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__menu-item-marquee {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__menu-item-marquee-content {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__menu-item-marquee-content span {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__game-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -228,3 +271,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wrapped-gradient-flow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-50% - 1em));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { logger } from "@renderer/logger";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
CommentDiscussionIcon,
|
||||
PlayIcon,
|
||||
@@ -238,8 +240,32 @@ export function Sidebar() {
|
||||
return game.title;
|
||||
};
|
||||
|
||||
const handleSidebarItemClick = (path: string) => {
|
||||
if (path !== location.pathname) {
|
||||
const handleSidebarItemClick = async (path: string) => {
|
||||
if (path.startsWith("http")) {
|
||||
if (path === "https://hydrawrapped.com") {
|
||||
try {
|
||||
const auth = await window.electron.getAuth();
|
||||
if (auth) {
|
||||
const payload = {
|
||||
accessToken: auth.accessToken,
|
||||
refreshToken: auth.refreshToken,
|
||||
expiresIn: 3600,
|
||||
};
|
||||
const base64Payload = btoa(JSON.stringify(payload));
|
||||
window.electron.openExternal(
|
||||
`${path}?payload=${encodeURIComponent(base64Payload)}`
|
||||
);
|
||||
} else {
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get auth for wrapped:", error);
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} else {
|
||||
window.electron.openExternal(path);
|
||||
}
|
||||
} else if (path !== location.pathname) {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
@@ -297,7 +323,10 @@ export function Sidebar() {
|
||||
<li
|
||||
key={nameKey}
|
||||
className={cn("sidebar__menu-item", {
|
||||
"sidebar__menu-item--active": location.pathname === path,
|
||||
"sidebar__menu-item--active":
|
||||
!path.startsWith("http") && location.pathname === path,
|
||||
"sidebar__menu-item--wrapped":
|
||||
nameKey === "hydra_2025_wrapped",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
@@ -306,7 +335,33 @@ export function Sidebar() {
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
{render()}
|
||||
<span>{t(nameKey)}</span>
|
||||
{nameKey === "hydra_2025_wrapped" ? (
|
||||
<div className="sidebar__menu-item-marquee">
|
||||
<motion.div
|
||||
className="sidebar__menu-item-marquee-content"
|
||||
animate={{
|
||||
x: ["0%", "-50%"],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 8,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{t(nameKey)}
|
||||
</span>
|
||||
<span>
|
||||
{t(nameKey)}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t(nameKey)}</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import type {
|
||||
Download,
|
||||
DownloadSource,
|
||||
GameRepack,
|
||||
GameShop,
|
||||
@@ -96,19 +95,9 @@ export function GameDetailsContextProvider({
|
||||
);
|
||||
|
||||
const updateGame = useCallback(async () => {
|
||||
const gameKey = `${shop}:${objectId}`;
|
||||
const [game, download] = await Promise.all([
|
||||
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 });
|
||||
return window.electron
|
||||
.getGameByObjectId(shop, objectId)
|
||||
.then((result) => setGame(result));
|
||||
}, [shop, objectId]);
|
||||
|
||||
const isGameDownloading =
|
||||
|
||||
28
src/renderer/src/declaration.d.ts
vendored
28
src/renderer/src/declaration.d.ts
vendored
@@ -14,11 +14,14 @@ import type {
|
||||
GameStats,
|
||||
UserDetails,
|
||||
FriendRequestSync,
|
||||
GameArtifact,
|
||||
LudusaviBackup,
|
||||
UserAchievement,
|
||||
ComparedAchievements,
|
||||
LibraryGame,
|
||||
GameRunning,
|
||||
TorBoxUser,
|
||||
Theme,
|
||||
Auth,
|
||||
ShortcutLocation,
|
||||
ShopAssets,
|
||||
@@ -139,6 +142,10 @@ declare global {
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
clearNewDownloadOptions: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<void>;
|
||||
toggleGamePin: (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
@@ -155,6 +162,7 @@ declare global {
|
||||
winePrefixPath: string | null
|
||||
) => Promise<void>;
|
||||
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
refreshLibraryAssets: () => Promise<void>;
|
||||
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
@@ -169,6 +177,10 @@ declare global {
|
||||
removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
removeGame: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
deleteGameFolder: (shop: GameShop, objectId: string) => Promise<unknown>;
|
||||
getGameByObjectId: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<LibraryGame | null>;
|
||||
onGamesRunning: (
|
||||
cb: (
|
||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||
@@ -182,9 +194,9 @@ declare global {
|
||||
playtimeInSeconds: number
|
||||
) => Promise<void>;
|
||||
/* User preferences */
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
|
||||
getUserPreferences: () => Promise<UserPreferences | null>;
|
||||
updateUserPreferences: (
|
||||
preferences: Partial<UserPreferences>
|
||||
) => Promise<void>;
|
||||
@@ -205,7 +217,10 @@ declare global {
|
||||
removeAll = false,
|
||||
downloadSourceId?: string
|
||||
) => Promise<void>;
|
||||
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||
syncDownloadSources: () => Promise<void>;
|
||||
getDownloadSourcesCheckBaseline: () => Promise<string | null>;
|
||||
getDownloadSourcesSinceValue: () => Promise<string | null>;
|
||||
|
||||
/* Hardware */
|
||||
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
|
||||
@@ -222,6 +237,10 @@ declare global {
|
||||
shop: GameShop,
|
||||
gameArtifactId: string
|
||||
) => Promise<void>;
|
||||
getGameArtifacts: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<GameArtifact[]>;
|
||||
getGameBackupPreview: (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
@@ -336,6 +355,7 @@ declare global {
|
||||
getAuth: () => Promise<Auth | null>;
|
||||
signOut: () => Promise<void>;
|
||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||
getSessionHash: () => Promise<string | null>;
|
||||
onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
|
||||
@@ -388,7 +408,13 @@ declare global {
|
||||
showAchievementTestNotification: () => Promise<void>;
|
||||
|
||||
/* Themes */
|
||||
addCustomTheme: (theme: Theme) => Promise<void>;
|
||||
getAllCustomThemes: () => Promise<Theme[]>;
|
||||
deleteAllCustomThemes: () => Promise<void>;
|
||||
deleteCustomTheme: (themeId: 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>;
|
||||
copyThemeAchievementSound: (
|
||||
themeId: string,
|
||||
|
||||
@@ -1,65 +1,15 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "./redux";
|
||||
import { setLibrary } from "@renderer/features";
|
||||
import { levelDBService } from "@renderer/services/leveldb.service";
|
||||
import type {
|
||||
LibraryGame,
|
||||
Game,
|
||||
Download,
|
||||
ShopAssets,
|
||||
GameAchievement,
|
||||
} from "@types";
|
||||
|
||||
export function useLibrary() {
|
||||
const dispatch = useAppDispatch();
|
||||
const library = useAppSelector((state) => state.library.value);
|
||||
|
||||
const updateLibrary = useCallback(async () => {
|
||||
const results = await levelDBService.iterator("games");
|
||||
|
||||
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));
|
||||
return window.electron
|
||||
.getLibrary()
|
||||
.then((updatedLibrary) => dispatch(setLibrary(updatedLibrary)));
|
||||
}, [dispatch]);
|
||||
|
||||
return { library, updateLibrary };
|
||||
|
||||
@@ -126,17 +126,10 @@ export function UserLibraryGameCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
className="user-library-game__cover"
|
||||
onClick={() => navigate(buildUserGameDetailsPath(game))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
navigate(buildUserGameDetailsPath(game));
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={isTooltipHovered ? undefined : game.title}
|
||||
>
|
||||
<div className="user-library-game__overlay">
|
||||
@@ -245,7 +238,7 @@ export function UserLibraryGameCard({
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<Tooltip
|
||||
id={game.objectId}
|
||||
style={{
|
||||
|
||||
@@ -20,8 +20,6 @@ export interface Auth {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
tokenExpirationTimestamp: number;
|
||||
featurebaseJwt: string;
|
||||
workwondersJwt: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -6330,7 +6330,7 @@ jsonwebtoken@^9.0.2:
|
||||
object.assign "^4.1.4"
|
||||
object.values "^1.1.6"
|
||||
|
||||
jwa@^1.4.2:
|
||||
jwa@^1.4.1:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
|
||||
integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==
|
||||
@@ -6340,11 +6340,11 @@ jwa@^1.4.2:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^3.2.2:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.3.tgz#5ac0690b460900a27265de24520526853c0b8ca1"
|
||||
integrity sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
|
||||
dependencies:
|
||||
jwa "^1.4.2"
|
||||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
keyv@^4.0.0, keyv@^4.5.3:
|
||||
|
||||
Reference in New Issue
Block a user