mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 01:03:57 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e089ca8705 | ||
|
|
bf8fd0dacf | ||
|
|
c9b289cbde | ||
|
|
45b822ba10 | ||
|
|
cb758cceda | ||
|
|
8b804271bd | ||
|
|
57813784d2 | ||
|
|
7cbb8a00c4 | ||
|
|
4a4cb57348 | ||
|
|
7a82467933 | ||
|
|
6a65d191af | ||
|
|
d047d7a105 | ||
|
|
f05d0c2047 | ||
|
|
98e5b70f2e | ||
|
|
100ddd79aa | ||
|
|
0b2d4e2ba0 | ||
|
|
0c379d6c49 | ||
|
|
8e6f9fdb00 | ||
|
|
6b1713e54b | ||
|
|
44db5f9813 | ||
|
|
7d0fbbd960 | ||
|
|
4552256038 | ||
|
|
c5be5e94e8 | ||
|
|
3a6693c8b1 | ||
|
|
e9032ae6e4 | ||
|
|
7202f740d3 | ||
|
|
2a74526b0f |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.6.0",
|
"version": "3.6.2",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
@@ -101,8 +101,13 @@ def process_list():
|
|||||||
auth_error = validate_rpc_password()
|
auth_error = validate_rpc_password()
|
||||||
if auth_error:
|
if auth_error:
|
||||||
return auth_error
|
return auth_error
|
||||||
|
|
||||||
|
iter_list = ['exe', 'pid', 'name']
|
||||||
|
if sys.platform != 'win32':
|
||||||
|
iter_list.append('cwd')
|
||||||
|
iter_list.append('environ')
|
||||||
|
|
||||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'cwd', 'pid', 'name', 'environ'])]
|
process_list = [proc.info for proc in psutil.process_iter(iter_list)]
|
||||||
return jsonify(process_list), 200
|
return jsonify(process_list), 200
|
||||||
|
|
||||||
@app.route("/profile-image", methods=["POST"])
|
@app.route("/profile-image", methods=["POST"])
|
||||||
|
|||||||
@@ -515,7 +515,8 @@
|
|||||||
"earned_points": "Earned points",
|
"earned_points": "Earned points",
|
||||||
"show_achievements_on_profile": "Show your achievements on your profile",
|
"show_achievements_on_profile": "Show your achievements on your profile",
|
||||||
"show_points_on_profile": "Show your earned points on your profile",
|
"show_points_on_profile": "Show your earned points on your profile",
|
||||||
"error_adding_friend": "Could not send friend request. Please check friend code"
|
"error_adding_friend": "Could not send friend request. Please check friend code",
|
||||||
|
"friend_code_length_error": "Friend code must have 8 characters"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked",
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
"allow_nsfw_content": "Continuar",
|
"allow_nsfw_content": "Continuar",
|
||||||
"refuse_nsfw_content": "No, gracias",
|
"refuse_nsfw_content": "No, gracias",
|
||||||
"stats": "Estadísticas",
|
"stats": "Estadísticas",
|
||||||
"download_count": "Downloads",
|
"download_count": "Descargas",
|
||||||
"player_count": "Jugadores activos",
|
"player_count": "Jugadores activos",
|
||||||
"download_error": "Esta opción de descarga no está disponible.",
|
"download_error": "Esta opción de descarga no está disponible.",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
@@ -199,12 +199,12 @@
|
|||||||
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
|
||||||
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
|
||||||
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
"download_error_not_cached_on_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
|
||||||
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y el estado de descarga del sondeo aún no está disponible.",
|
"download_error_not_cached_on_torbox": "Esta descarga no está disponible en TorBox y aún no se puede verificar el estado de la descarga.",
|
||||||
"game_added_to_favorites": "Juego añadido a favoritos",
|
"game_added_to_favorites": "Juego añadido a favoritos",
|
||||||
"game_removed_from_favorites": "Juego removido de favoritos",
|
"game_removed_from_favorites": "Juego removido de favoritos",
|
||||||
"invalid_wine_prefix_path": "Ruta de prefixo Wine inválida",
|
"invalid_wine_prefix_path": "Ruta de prefijo de Wine inválida",
|
||||||
"invalid_wine_prefix_path_description": "La ruta al prefixo Wine es inválida. Por favor, verifica la ruta y vuelve a intentarlo.",
|
"invalid_wine_prefix_path_description": "La ruta del prefijo Wine es inválida. Por favor, checa la ruta y vuelve a intentarlo.",
|
||||||
"missing_wine_prefix": ""
|
"missing_wine_prefix": "Se requiere el prefijo Wine para crear una copia de seguridad en Linux"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
|
|||||||
@@ -508,7 +508,8 @@
|
|||||||
"earned_points": "Pontos ganhos",
|
"earned_points": "Pontos ganhos",
|
||||||
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
|
||||||
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
|
||||||
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido"
|
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
|
||||||
|
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
|
||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada",
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
|||||||
@@ -365,14 +365,14 @@
|
|||||||
"installing_common_redist": "Установка…",
|
"installing_common_redist": "Установка…",
|
||||||
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
|
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
|
||||||
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
|
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
|
||||||
"achievement_custom_notification_position": "Позиция настраиваемых уведомлений о достижениях",
|
"achievement_custom_notification_position": "Позиция уведомлений достижений",
|
||||||
"top-left": "Верхний левый угол",
|
"top-left": "Верхний левый угол",
|
||||||
"top-center": "Верхний центр",
|
"top-center": "Верхний центр",
|
||||||
"top-right": "Верхний правый угол",
|
"top-right": "Верхний правый угол",
|
||||||
"bottom-left": "Нижний левый угол",
|
"bottom-left": "Нижний левый угол",
|
||||||
"bottom-center": "Нижний центр",
|
"bottom-center": "Нижний центр",
|
||||||
"bottom-right": "Нижний правый угол",
|
"bottom-right": "Нижний правый угол",
|
||||||
"enable_achievement_custom_notifications": "Включить настраиваемые уведомления о достижениях",
|
"enable_achievement_custom_notifications": "Включить уведомления о достижениях",
|
||||||
"alignment": "Выравнивание",
|
"alignment": "Выравнивание",
|
||||||
"variation": "Вариация",
|
"variation": "Вариация",
|
||||||
"default": "По умолчанию",
|
"default": "По умолчанию",
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
import type { GameShop, GameStats } from "@types";
|
import type { GameShop, GameStats } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
import { gamesStatsCacheSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
|
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
|
||||||
|
|
||||||
const getGameStats = async (
|
const getGameStats = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
|
const cachedStats = await gamesStatsCacheSublevel.get(
|
||||||
|
levelKeys.game(shop, objectId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
cachedStats &&
|
||||||
|
cachedStats.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()
|
||||||
|
) {
|
||||||
|
return cachedStats;
|
||||||
|
}
|
||||||
|
|
||||||
return HydraApi.get<GameStats>(
|
return HydraApi.get<GameStats>(
|
||||||
`/games/stats`,
|
`/games/stats`,
|
||||||
{ objectId, shop },
|
{ objectId, shop },
|
||||||
{ needsAuth: false }
|
{ needsAuth: false }
|
||||||
);
|
).then(async (data) => {
|
||||||
|
await gamesStatsCacheSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
|
...data,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getGameStats", getGameStats);
|
registerEvent("getGameStats", getGameStats);
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ const saveGameShopAssets = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
assets: ShopAssets
|
assets: ShopAssets
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), assets);
|
const key = levelKeys.game(shop, objectId);
|
||||||
|
const existingAssets = await gamesShopAssetsSublevel.get(key);
|
||||||
|
return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
registerEvent("saveGameShopAssets", saveGameShopAssets);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import "./library/create-game-shortcut";
|
|||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
import "./library/delete-game-folder";
|
import "./library/delete-game-folder";
|
||||||
import "./library/get-game-by-object-id";
|
import "./library/get-game-by-object-id";
|
||||||
import "./library/sync-game-by-object-id";
|
|
||||||
import "./library/get-library";
|
import "./library/get-library";
|
||||||
import "./library/extract-game-download";
|
import "./library/extract-game-download";
|
||||||
import "./library/open-game";
|
import "./library/open-game";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
|
|
||||||
import {
|
import {
|
||||||
downloadsSublevel,
|
downloadsSublevel,
|
||||||
gamesShopAssetsSublevel,
|
gamesShopAssetsSublevel,
|
||||||
gamesSublevel,
|
gamesSublevel,
|
||||||
levelKeys,
|
levelKeys,
|
||||||
} from "@main/level";
|
} from "@main/level";
|
||||||
|
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -43,7 +43,10 @@ const addGameToLibrary = async (
|
|||||||
|
|
||||||
await createGame(game).catch(() => {});
|
await createGame(game).catch(() => {});
|
||||||
|
|
||||||
updateLocalUnlockedAchievements(game);
|
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||||
|
game.shop,
|
||||||
|
game.objectId
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const resetGameAchievements = async (
|
|||||||
objectId: string
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
const levelKey = levelKeys.game(shop, objectId);
|
||||||
|
const game = await gamesSublevel.get(levelKey);
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
@@ -29,8 +30,6 @@ const resetGameAchievements = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const levelKey = levelKeys.game(game.shop, game.objectId);
|
|
||||||
|
|
||||||
await gameAchievementsSublevel
|
await gameAchievementsSublevel
|
||||||
.get(levelKey)
|
.get(levelKey)
|
||||||
.then(async (gameAchievements) => {
|
.then(async (gameAchievements) => {
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
import type { GameShop, UserGameDetails } from "@types";
|
|
||||||
|
|
||||||
const syncGameByObjectId = async (
|
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
|
||||||
shop: GameShop,
|
|
||||||
objectId: string
|
|
||||||
) => {
|
|
||||||
return HydraApi.get<UserGameDetails>(
|
|
||||||
`/profile/games/${shop}/${objectId}`
|
|
||||||
).then(async (res) => {
|
|
||||||
const { id, playTimeInSeconds, isFavorite, ...rest } = res;
|
|
||||||
|
|
||||||
const gameKey = levelKeys.game(shop, objectId);
|
|
||||||
|
|
||||||
const currentData = await gamesSublevel.get(gameKey);
|
|
||||||
|
|
||||||
await gamesSublevel.put(gameKey, {
|
|
||||||
...currentData,
|
|
||||||
...rest,
|
|
||||||
remoteId: id,
|
|
||||||
playTimeInMilliseconds: playTimeInSeconds * 1000,
|
|
||||||
favorite: isFavorite ?? currentData?.favorite,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("syncGameByObjectId", syncGameByObjectId);
|
|
||||||
@@ -3,6 +3,7 @@ import { registerEvent } from "../register-event";
|
|||||||
|
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { db, levelKeys } from "@main/level";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||||
|
|
||||||
const getComparedUnlockedAchievements = async (
|
const getComparedUnlockedAchievements = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -10,6 +11,8 @@ const getComparedUnlockedAchievements = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
|
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||||
|
|
||||||
const userPreferences = await db.get<string, UserPreferences | null>(
|
const userPreferences = await db.get<string, UserPreferences | null>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
|
||||||
|
|
||||||
export const getUnlockedAchievements = async (
|
export const getUnlockedAchievements = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -62,7 +63,7 @@ export const getUnlockedAchievements = async (
|
|||||||
!achievementData.hidden || showHiddenAchievementsDescription
|
!achievementData.hidden || showHiddenAchievementsDescription
|
||||||
? achievementData.description
|
? achievementData.description
|
||||||
: undefined,
|
: undefined,
|
||||||
} as UserAchievement;
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.unlocked && !b.unlocked) return -1;
|
if (a.unlocked && !b.unlocked) return -1;
|
||||||
@@ -79,6 +80,7 @@ const getUnlockedAchievementsEvent = async (
|
|||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
): Promise<UserAchievement[]> => {
|
): Promise<UserAchievement[]> => {
|
||||||
|
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
|
||||||
return getUnlockedAchievements(objectId, shop, false);
|
return getUnlockedAchievements(objectId, shop, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
11
src/main/level/sublevels/game-stats-cache.ts
Normal file
11
src/main/level/sublevels/game-stats-cache.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { GameStats } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gamesStatsCacheSublevel = db.sublevel<
|
||||||
|
string,
|
||||||
|
GameStats & { updatedAt: number }
|
||||||
|
>(levelKeys.gameStatsCache, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ export * from "./downloads";
|
|||||||
export * from "./games";
|
export * from "./games";
|
||||||
export * from "./game-shop-assets";
|
export * from "./game-shop-assets";
|
||||||
export * from "./game-shop-cache";
|
export * from "./game-shop-cache";
|
||||||
|
export * from "./game-stats-cache";
|
||||||
export * from "./game-achievements";
|
export * from "./game-achievements";
|
||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
export * from "./themes";
|
export * from "./themes";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const levelKeys = {
|
|||||||
auth: "auth",
|
auth: "auth",
|
||||||
themes: "themes",
|
themes: "themes",
|
||||||
gameShopAssets: "gameShopAssets",
|
gameShopAssets: "gameShopAssets",
|
||||||
|
gameStatsCache: "gameStatsAssets",
|
||||||
gameShopCache: "gameShopCache",
|
gameShopCache: "gameShopCache",
|
||||||
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
`${shop}:${objectId}:${language}`,
|
`${shop}:${objectId}:${language}`,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AchievementFile,
|
AchievementFile,
|
||||||
Game,
|
Game,
|
||||||
|
GameShop,
|
||||||
UnlockedAchievement,
|
UnlockedAchievement,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
@@ -18,7 +19,7 @@ import { Cracker } from "@shared";
|
|||||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||||
import { db, gamesSublevel, levelKeys } from "@main/level";
|
import { db, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { sleep } from "@main/helpers";
|
import { setTimeout } from "node:timers/promises";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
@@ -37,7 +38,7 @@ const watchAchievementsWindows = async () => {
|
|||||||
const gameAchievementFiles: AchievementFile[] = [];
|
const gameAchievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
gameAchievementFiles.push(...(achievementFiles.get(objectId) ?? []));
|
||||||
|
|
||||||
gameAchievementFiles.push(
|
gameAchievementFiles.push(
|
||||||
...findAchievementFileInExecutableDirectory(game)
|
...findAchievementFileInExecutableDirectory(game)
|
||||||
@@ -127,6 +128,11 @@ const compareFile = (game: Game, file: AchievementFile) => {
|
|||||||
);
|
);
|
||||||
return processAchievementFileDiff(game, file);
|
return processAchievementFileDiff(game, file);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
achievementsLogger.error(
|
||||||
|
"Error reading file",
|
||||||
|
file.filePath,
|
||||||
|
err instanceof Error ? err.message : err
|
||||||
|
);
|
||||||
fileStats.set(file.filePath, -1);
|
fileStats.set(file.filePath, -1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -136,20 +142,69 @@ const processAchievementFileDiff = async (
|
|||||||
game: Game,
|
game: Game,
|
||||||
file: AchievementFile
|
file: AchievementFile
|
||||||
) => {
|
) => {
|
||||||
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
|
const parsedAchievements = parseAchievementFile(file.filePath, file.type);
|
||||||
|
|
||||||
if (unlockedAchievements.length) {
|
if (parsedAchievements.length) {
|
||||||
return mergeAchievements(game, unlockedAchievements, true);
|
return mergeAchievements(game, parsedAchievements, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AchievementWatcherManager {
|
export class AchievementWatcherManager {
|
||||||
private static hasFinishedMergingWithRemote = false;
|
private static _hasFinishedPreSearch = false;
|
||||||
|
|
||||||
|
public static get hasFinishedPreSearch() {
|
||||||
|
return this._hasFinishedPreSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly alreadySyncedGames: Map<string, boolean> = new Map();
|
||||||
|
|
||||||
|
public static async firstSyncWithRemoteIfNeeded(
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
|
) {
|
||||||
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
if (this.alreadySyncedGames.get(gameKey)) return;
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey).catch(() => null);
|
||||||
|
if (!game || game.isDeleted) return;
|
||||||
|
|
||||||
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
||||||
|
const achievementFileInsideDirectory =
|
||||||
|
findAchievementFileInExecutableDirectory(game);
|
||||||
|
|
||||||
|
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
||||||
|
|
||||||
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
|
|
||||||
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
|
const localAchievementFile = parseAchievementFile(
|
||||||
|
achievementFile.filePath,
|
||||||
|
achievementFile.type
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localAchievementFile.length) {
|
||||||
|
unlockedAchievements.push(...localAchievementFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.alreadySyncedGames.set(gameKey, true);
|
||||||
|
|
||||||
|
const newAchievements = await mergeAchievements(
|
||||||
|
game,
|
||||||
|
unlockedAchievements,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newAchievements > 0) {
|
||||||
|
this.notifyCombinedAchievementsUnlocked(1, newAchievements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static watchAchievements() {
|
public static watchAchievements() {
|
||||||
if (!this.hasFinishedMergingWithRemote) return;
|
if (!this.hasFinishedPreSearch) return;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return watchAchievementsWindows();
|
return watchAchievementsWindows();
|
||||||
@@ -188,7 +243,11 @@ export class AchievementWatcherManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeAchievements(game, unlockedAchievements, false);
|
if (unlockedAchievements.length) {
|
||||||
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getGameAchievementFilesWindows() {
|
private static async getGameAchievementFilesWindows() {
|
||||||
@@ -237,25 +296,44 @@ export class AchievementWatcherManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async preSearchAchievements() {
|
private static async notifyCombinedAchievementsUnlocked(
|
||||||
await sleep(2000);
|
totalNewGamesWithAchievements: number,
|
||||||
|
totalNewAchievements: number
|
||||||
|
) {
|
||||||
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||||
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
|
"on-combined-achievements-unlocked",
|
||||||
|
totalNewGamesWithAchievements,
|
||||||
|
totalNewAchievements,
|
||||||
|
userPreferences.achievementCustomNotificationPosition ?? "top-left"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
publishCombinedNewAchievementNotification(
|
||||||
|
totalNewAchievements,
|
||||||
|
totalNewGamesWithAchievements
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async preSearchAchievements() {
|
||||||
try {
|
try {
|
||||||
const gameAchievementFiles =
|
const gameAchievementFiles =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
? await this.getGameAchievementFilesWindows()
|
? await this.getGameAchievementFilesWindows()
|
||||||
: await this.getGameAchievementFilesLinux();
|
: await this.getGameAchievementFilesLinux();
|
||||||
|
|
||||||
const newAchievementsCount: number[] = [];
|
const newAchievementsCount = await Promise.all(
|
||||||
|
gameAchievementFiles.map(({ game, achievementFiles }) => {
|
||||||
for (const { game, achievementFiles } of gameAchievementFiles) {
|
return this.preProcessGameAchievementFiles(game, achievementFiles);
|
||||||
const result = await this.preProcessGameAchievementFiles(
|
})
|
||||||
game,
|
);
|
||||||
achievementFiles
|
|
||||||
);
|
|
||||||
|
|
||||||
newAchievementsCount.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
const totalNewGamesWithAchievements = newAchievementsCount.filter(
|
||||||
(achievements) => achievements
|
(achievements) => achievements
|
||||||
@@ -267,34 +345,16 @@ export class AchievementWatcherManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (totalNewAchievements > 0) {
|
if (totalNewAchievements > 0) {
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
await setTimeout(4000);
|
||||||
levelKeys.userPreferences,
|
this.notifyCombinedAchievementsUnlocked(
|
||||||
{
|
totalNewGamesWithAchievements,
|
||||||
valueEncoding: "json",
|
totalNewAchievements
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userPreferences.achievementNotificationsEnabled !== false) {
|
|
||||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
|
||||||
WindowManager.notificationWindow?.webContents.send(
|
|
||||||
"on-combined-achievements-unlocked",
|
|
||||||
totalNewGamesWithAchievements,
|
|
||||||
totalNewAchievements,
|
|
||||||
userPreferences.achievementCustomNotificationPosition ??
|
|
||||||
"top-left"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
publishCombinedNewAchievementNotification(
|
|
||||||
totalNewAchievements,
|
|
||||||
totalNewGamesWithAchievements
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
achievementsLogger.error("Error on preSearchAchievements", err);
|
achievementsLogger.error("Error on preSearchAchievements", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this._hasFinishedPreSearch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ export const findAchievementFileInExecutableDirectory = (
|
|||||||
"achievements.ini"
|
"achievements.ini"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
].filter((file) => fs.existsSync(file.filePath)) as AchievementFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapFileLocationWithObjectId = (
|
const mapFileLocationWithObjectId = (
|
||||||
|
|||||||
@@ -1,24 +1,39 @@
|
|||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import type { GameShop, SteamAchievement } from "@types";
|
import type { GameAchievement, GameShop, SteamAchievement } from "@types";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60; // 1 hour
|
||||||
|
|
||||||
|
const getModifiedSinceHeader = (
|
||||||
|
cachedAchievements: GameAchievement | undefined
|
||||||
|
): Date | undefined => {
|
||||||
|
if (!cachedAchievements) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedAchievements.updatedAt
|
||||||
|
? new Date(cachedAchievements.updatedAt)
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const getGameAchievementData = async (
|
export const getGameAchievementData = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
useCachedData: boolean
|
useCachedData: boolean
|
||||||
) => {
|
) => {
|
||||||
const cachedAchievements = await gameAchievementsSublevel.get(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
levelKeys.game(shop, objectId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cachedAchievements && useCachedData)
|
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
|
||||||
|
|
||||||
|
if (cachedAchievements?.achievements && useCachedData)
|
||||||
return cachedAchievements.achievements;
|
return cachedAchievements.achievements;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cachedAchievements &&
|
cachedAchievements?.achievements &&
|
||||||
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
|
Date.now() < (cachedAchievements.updatedAt ?? 0) + LOCAL_CACHE_EXPIRATION
|
||||||
) {
|
) {
|
||||||
return cachedAchievements.achievements;
|
return cachedAchievements.achievements;
|
||||||
}
|
}
|
||||||
@@ -29,18 +44,22 @@ export const getGameAchievementData = async (
|
|||||||
})
|
})
|
||||||
.then((language) => language || "en");
|
.then((language) => language || "en");
|
||||||
|
|
||||||
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
return HydraApi.get<SteamAchievement[]>(
|
||||||
shop,
|
"/games/achievements",
|
||||||
objectId,
|
{
|
||||||
language,
|
shop,
|
||||||
})
|
objectId,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ifModifiedSince: getModifiedSinceHeader(cachedAchievements),
|
||||||
|
}
|
||||||
|
)
|
||||||
.then(async (achievements) => {
|
.then(async (achievements) => {
|
||||||
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
await gameAchievementsSublevel.put(gameKey, {
|
||||||
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||||
achievements,
|
achievements,
|
||||||
cacheExpiresTimestamp: achievements.length
|
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
|
||||||
? Date.now() + 1000 * 60 * 30 // 30 minutes
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return achievements;
|
return achievements;
|
||||||
@@ -50,8 +69,14 @@ export const getGameAchievementData = async (
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNotModified = (err as AxiosError)?.response?.status === 304;
|
||||||
|
|
||||||
|
if (isNotModified) {
|
||||||
|
return cachedAchievements?.achievements ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
logger.error("Failed to get game achievements for", objectId, err);
|
logger.error("Failed to get game achievements for", objectId, err);
|
||||||
|
|
||||||
return [];
|
return cachedAchievements?.achievements ?? [];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { SubscriptionRequiredError } from "@shared";
|
|||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
import { getGameAchievementData } from "./get-game-achievement-data";
|
import { getGameAchievementData } from "./get-game-achievement-data";
|
||||||
|
import { AchievementWatcherManager } from "./achievement-watcher-manager";
|
||||||
|
|
||||||
const isRareAchievement = (points: number) => {
|
const isRareAchievement = (points: number) => {
|
||||||
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
const rawPercentage = (50 - Math.sqrt(points)) * 2;
|
||||||
@@ -35,7 +36,7 @@ const saveAchievementsOnLocal = async (
|
|||||||
await gameAchievementsSublevel.put(levelKey, {
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
achievements: gameAchievement?.achievements ?? [],
|
achievements: gameAchievement?.achievements ?? [],
|
||||||
unlockedAchievements: unlockedAchievements,
|
unlockedAchievements: unlockedAchievements,
|
||||||
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
|
updatedAt: gameAchievement?.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sendUpdateEvent) return;
|
if (!sendUpdateEvent) return;
|
||||||
@@ -56,9 +57,9 @@ export const mergeAchievements = async (
|
|||||||
achievements: UnlockedAchievement[],
|
achievements: UnlockedAchievement[],
|
||||||
publishNotification: boolean
|
publishNotification: boolean
|
||||||
) => {
|
) => {
|
||||||
let localGameAchievement = await gameAchievementsSublevel.get(
|
const gameKey = levelKeys.game(game.shop, game.objectId);
|
||||||
levelKeys.game(game.shop, game.objectId)
|
|
||||||
);
|
let localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||||
const userPreferences = await db.get<string, UserPreferences>(
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
levelKeys.userPreferences,
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
@@ -67,10 +68,8 @@ export const mergeAchievements = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!localGameAchievement) {
|
if (!localGameAchievement) {
|
||||||
await getGameAchievementData(game.objectId, game.shop, true);
|
await getGameAchievementData(game.objectId, game.shop, false);
|
||||||
localGameAchievement = await gameAchievementsSublevel.get(
|
localGameAchievement = await gameAchievementsSublevel.get(gameKey);
|
||||||
levelKeys.game(game.shop, game.objectId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const achievementsData = localGameAchievement?.achievements ?? [];
|
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||||
@@ -136,6 +135,12 @@ export const mergeAchievements = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
achievementsLogger.log(
|
||||||
|
"Publishing achievement notification",
|
||||||
|
game.objectId,
|
||||||
|
game.title
|
||||||
|
);
|
||||||
|
|
||||||
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
|
||||||
WindowManager.notificationWindow?.webContents.send(
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
"on-achievement-unlocked",
|
"on-achievement-unlocked",
|
||||||
@@ -153,7 +158,11 @@ export const mergeAchievements = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.remoteId) {
|
const shouldSyncWithRemote =
|
||||||
|
game.remoteId &&
|
||||||
|
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
|
||||||
|
|
||||||
|
if (shouldSyncWithRemote) {
|
||||||
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
|
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
|
||||||
"/profile/games/achievements",
|
"/profile/games/achievements",
|
||||||
{
|
{
|
||||||
@@ -194,8 +203,11 @@ export const mergeAchievements = async (
|
|||||||
mergedLocalAchievements,
|
mergedLocalAchievements,
|
||||||
publishNotification
|
publishNotification
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
AchievementWatcherManager.alreadySyncedGames.set(gameKey, true);
|
||||||
});
|
});
|
||||||
} else {
|
} else if (newAchievements.length) {
|
||||||
await saveAchievementsOnLocal(
|
await saveAchievementsOnLocal(
|
||||||
game.objectId,
|
game.objectId,
|
||||||
game.shop,
|
game.shop,
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import {
|
|
||||||
findAchievementFiles,
|
|
||||||
findAchievementFileInExecutableDirectory,
|
|
||||||
} from "./find-achivement-files";
|
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
|
||||||
import type { Game, UnlockedAchievement } from "@types";
|
|
||||||
|
|
||||||
export const updateLocalUnlockedAchievements = async (game: Game) => {
|
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
|
||||||
|
|
||||||
const achievementFileInsideDirectory =
|
|
||||||
findAchievementFileInExecutableDirectory(game);
|
|
||||||
|
|
||||||
gameAchievementFiles.push(...achievementFileInsideDirectory);
|
|
||||||
|
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
|
||||||
|
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
|
||||||
const localAchievementFile = parseAchievementFile(
|
|
||||||
achievementFile.filePath,
|
|
||||||
achievementFile.type
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localAchievementFile.length) {
|
|
||||||
unlockedAchievements.push(...localAchievementFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeAchievements(game, unlockedAchievements, false);
|
|
||||||
};
|
|
||||||
@@ -31,8 +31,8 @@ export interface ProcessPayload {
|
|||||||
exe: string | null;
|
exe: string | null;
|
||||||
pid: number;
|
pid: number;
|
||||||
name: string;
|
name: string;
|
||||||
environ: Record<string, string> | null;
|
environ?: Record<string, string> | null;
|
||||||
cwd: string | null;
|
cwd?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PauseSeedingPayload {
|
export interface PauseSeedingPayload {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { WSClient } from "./ws/ws-client";
|
|||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
needsSubscription?: boolean;
|
needsSubscription?: boolean;
|
||||||
|
ifModifiedSince?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HydraApiUserAuth {
|
interface HydraApiUserAuth {
|
||||||
@@ -337,8 +338,13 @@ export class HydraApi {
|
|||||||
) {
|
) {
|
||||||
await this.validateOptions(options);
|
await this.validateOptions(options);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...this.getAxiosConfig().headers,
|
||||||
|
"Hydra-If-Modified-Since": options?.ifModifiedSince?.toUTCString(),
|
||||||
|
};
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
.get<T>(url, { params, ...this.getAxiosConfig() })
|
.get<T>(url, { params, ...this.getAxiosConfig(), headers })
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
return HydraApi.get<ProfileGame[]>("/profile/games")
|
return HydraApi.get<ProfileGame[]>("/profile/games")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
for (const game of response) {
|
for (const game of response) {
|
||||||
const localGame = await gamesSublevel.get(
|
const gameKey = levelKeys.game(game.shop, game.objectId);
|
||||||
levelKeys.game(game.shop, game.objectId)
|
const localGame = await gamesSublevel.get(gameKey);
|
||||||
);
|
|
||||||
|
|
||||||
if (localGame) {
|
if (localGame) {
|
||||||
const updatedLastTimePlayed =
|
const updatedLastTimePlayed =
|
||||||
@@ -30,7 +29,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
? game.playTimeInMilliseconds
|
? game.playTimeInMilliseconds
|
||||||
: localGame.playTimeInMilliseconds;
|
: localGame.playTimeInMilliseconds;
|
||||||
|
|
||||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(gameKey, {
|
||||||
...localGame,
|
...localGame,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
lastTimePlayed: updatedLastTimePlayed,
|
lastTimePlayed: updatedLastTimePlayed,
|
||||||
@@ -38,7 +37,7 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
favorite: game.isFavorite ?? localGame.favorite,
|
favorite: game.isFavorite ?? localGame.favorite,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
await gamesSublevel.put(gameKey, {
|
||||||
objectId: game.objectId,
|
objectId: game.objectId,
|
||||||
title: game.title,
|
title: game.title,
|
||||||
remoteId: game.id,
|
remoteId: game.id,
|
||||||
@@ -51,20 +50,17 @@ export const mergeWithRemoteGames = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await gamesShopAssetsSublevel.put(
|
await gamesShopAssetsSublevel.put(gameKey, {
|
||||||
levelKeys.game(game.shop, game.objectId),
|
shop: game.shop,
|
||||||
{
|
objectId: game.objectId,
|
||||||
shop: game.shop,
|
title: game.title,
|
||||||
objectId: game.objectId,
|
coverImageUrl: game.coverImageUrl,
|
||||||
title: game.title,
|
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
||||||
coverImageUrl: game.coverImageUrl,
|
libraryImageUrl: game.libraryImageUrl,
|
||||||
libraryHeroImageUrl: game.libraryHeroImageUrl,
|
logoImageUrl: game.logoImageUrl,
|
||||||
libraryImageUrl: game.libraryImageUrl,
|
iconUrl: game.iconUrl,
|
||||||
logoImageUrl: game.logoImageUrl,
|
logoPosition: game.logoPosition,
|
||||||
iconUrl: game.iconUrl,
|
});
|
||||||
logoPosition: game.logoPosition,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ export const uploadGamesBatch = async () => {
|
|||||||
|
|
||||||
await mergeWithRemoteGames();
|
await mergeWithRemoteGames();
|
||||||
|
|
||||||
if (HydraApi.isLoggedIn()) {
|
AchievementWatcherManager.preSearchAchievements();
|
||||||
AchievementWatcherManager.preSearchAchievements();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WindowManager.mainWindow)
|
if (WindowManager.mainWindow)
|
||||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { gamesSublevel, levelKeys } from "@main/level";
|
|||||||
import { CloudSync } from "./cloud-sync";
|
import { CloudSync } from "./cloud-sync";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -24,7 +25,7 @@ interface GameExecutables {
|
|||||||
[key: string]: ExecutableInfo[];
|
[key: string]: ExecutableInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TICKS_TO_UPDATE_API = 80;
|
const TICKS_TO_UPDATE_API = 120;
|
||||||
let currentTick = 1;
|
let currentTick = 1;
|
||||||
|
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
@@ -190,6 +191,11 @@ export const watchProcesses = async () => {
|
|||||||
function onOpenGame(game: Game) {
|
function onOpenGame(game: Game) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
|
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
|
||||||
|
game.shop,
|
||||||
|
game.objectId
|
||||||
|
);
|
||||||
|
|
||||||
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
|
||||||
lastTick: now,
|
lastTick: now,
|
||||||
firstTick: now,
|
firstTick: now,
|
||||||
|
|||||||
@@ -370,14 +370,11 @@ export class WindowManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.notificationWindow.setIgnoreMouseEvents(true);
|
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||||
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
|
||||||
// visibleOnFullScreen: true,
|
|
||||||
// });
|
|
||||||
|
|
||||||
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
this.loadNotificationWindowURL();
|
this.loadNotificationWindowURL();
|
||||||
|
|
||||||
if (isStaging) {
|
if (!app.isPackaged || isStaging) {
|
||||||
this.notificationWindow.webContents.openDevTools();
|
this.notificationWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,7 +461,7 @@ export class WindowManager {
|
|||||||
editorWindow.once("ready-to-show", () => {
|
editorWindow.once("ready-to-show", () => {
|
||||||
editorWindow.show();
|
editorWindow.show();
|
||||||
this.mainWindow?.webContents.openDevTools();
|
this.mainWindow?.webContents.openDevTools();
|
||||||
if (isStaging) {
|
if (!app.isPackaged || isStaging) {
|
||||||
editorWindow.webContents.openDevTools();
|
editorWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -187,8 +187,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
ipcRenderer.invoke("deleteGameFolder", shop, objectId),
|
||||||
getGameByObjectId: (shop: GameShop, objectId: string) =>
|
getGameByObjectId: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
|
||||||
syncGameByObjectId: (shop: GameShop, objectId: string) =>
|
|
||||||
ipcRenderer.invoke("syncGameByObjectId", shop, objectId),
|
|
||||||
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
resetGameAchievements: (shop: GameShop, objectId: string) =>
|
||||||
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
|
||||||
extractGameDownload: (shop: GameShop, objectId: string) =>
|
extractGameDownload: (shop: GameShop, objectId: string) =>
|
||||||
|
|||||||
@@ -74,6 +74,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__error-label {
|
&__error-label {
|
||||||
color: globals.$danger-color;
|
color: globals.$error-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,11 +182,6 @@ export function GameDetailsContextProvider({
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.electron.syncGameByObjectId(shop, objectId).then(() => {
|
|
||||||
if (abortController.signal.aborted) return;
|
|
||||||
updateGame();
|
|
||||||
});
|
|
||||||
}, [
|
}, [
|
||||||
updateGame,
|
updateGame,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
|||||||
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@@ -155,7 +155,6 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) => Promise<LibraryGame | null>;
|
) => Promise<LibraryGame | null>;
|
||||||
syncGameByObjectId: (shop: GameShop, objectId: string) => Promise<void>;
|
|
||||||
onGamesRunning: (
|
onGamesRunning: (
|
||||||
cb: (
|
cb: (
|
||||||
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ $logo-max-width: 200px;
|
|||||||
&-points {
|
&-points {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
margin-right: 4px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
&--locked {
|
&--locked {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export function AchievementNotification() {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
isRare: false,
|
isRare: false,
|
||||||
isPlatinum: false,
|
isPlatinum: false,
|
||||||
points: 0,
|
|
||||||
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
|
iconUrl: "https://cdn.losbroxas.org/favicon.svg",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ export const UserFriendModalAddFriend = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateFriendCode = (callback: () => void) => {
|
||||||
|
if (friendCode.length === 8) {
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
showErrorToast(t("friend_code_length_error"));
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancelFriendRequest = (userId: string) => {
|
const handleCancelFriendRequest = (userId: string) => {
|
||||||
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
||||||
showErrorToast(t("try_again"));
|
showErrorToast(t("try_again"));
|
||||||
@@ -91,13 +99,13 @@ export const UserFriendModalAddFriend = ({
|
|||||||
disabled={isAddingFriend}
|
disabled={isAddingFriend}
|
||||||
className="user-friend-modal-add-friend__button"
|
className="user-friend-modal-add-friend__button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClickAddFriend}
|
onClick={() => validateFriendCode(handleClickAddFriend)}
|
||||||
>
|
>
|
||||||
{isAddingFriend ? t("sending") : t("add")}
|
{isAddingFriend ? t("sending") : t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleClickSeeProfile}
|
onClick={() => validateFriendCode(handleClickSeeProfile)}
|
||||||
disabled={isAddingFriend}
|
disabled={isAddingFriend}
|
||||||
className="user-friend-modal-add-friend__button"
|
className="user-friend-modal-add-friend__button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ $body-color: #8e919b;
|
|||||||
$border-color: rgba(255, 255, 255, 0.15);
|
$border-color: rgba(255, 255, 255, 0.15);
|
||||||
$success-color: #1c9749;
|
$success-color: #1c9749;
|
||||||
$danger-color: #801d1e;
|
$danger-color: #801d1e;
|
||||||
|
$error-color: #e11d48;
|
||||||
$warning-color: #ffc107;
|
$warning-color: #ffc107;
|
||||||
|
|
||||||
$brand-teal: #16b195;
|
$brand-teal: #16b195;
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ export interface UserFriend {
|
|||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
currentGame:
|
currentGame:
|
||||||
| (ShopAssets & {
|
| (ShopAssets & {
|
||||||
sessionDurationInSeconds: number;
|
sessionDurationInSeconds: number;
|
||||||
@@ -146,8 +144,6 @@ export interface UserRelation {
|
|||||||
AId: string;
|
AId: string;
|
||||||
BId: string;
|
BId: string;
|
||||||
status: "ACCEPTED" | "PENDING";
|
status: "ACCEPTED" | "PENDING";
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserProfileCurrentGame = GameRunning &
|
export type UserProfileCurrentGame = GameRunning &
|
||||||
@@ -326,17 +322,11 @@ export interface CatalogueSearchPayload {
|
|||||||
|
|
||||||
export type CatalogueSearchResult = {
|
export type CatalogueSearchResult = {
|
||||||
id: string;
|
id: string;
|
||||||
tags: string[];
|
|
||||||
genres: string[];
|
|
||||||
objectId: string;
|
objectId: string;
|
||||||
shop: GameShop;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
title: string;
|
title: string;
|
||||||
installCount: number;
|
shop: GameShop;
|
||||||
achievementCount: number;
|
genres: string[];
|
||||||
shopData: string;
|
} & Pick<ShopAssets, "libraryImageUrl">;
|
||||||
} & ShopAssets;
|
|
||||||
|
|
||||||
export type LibraryGame = Game &
|
export type LibraryGame = Game &
|
||||||
Partial<ShopAssets> & {
|
Partial<ShopAssets> & {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export interface Download {
|
|||||||
export interface GameAchievement {
|
export interface GameAchievement {
|
||||||
achievements: SteamAchievement[];
|
achievements: SteamAchievement[];
|
||||||
unlockedAchievements: UnlockedAchievement[];
|
unlockedAchievements: UnlockedAchievement[];
|
||||||
cacheExpiresTimestamp: number | undefined;
|
updatedAt: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AchievementCustomNotificationPosition =
|
export type AchievementCustomNotificationPosition =
|
||||||
|
|||||||
Reference in New Issue
Block a user