Merge pull request #1857 from hydralauncher/fix/use-local-achievement-cache

fix: achievements on library page
This commit is contained in:
Chubby Granny Chaser
2025-11-13 15:12:40 +00:00
committed by GitHub
8 changed files with 68 additions and 40 deletions

View File

@@ -153,8 +153,11 @@ def profile_image():
data = request.get_json() data = request.get_json()
image_path = data.get('image_path') image_path = data.get('image_path')
# use webp as default value for target_extension
target_extension = data.get('target_extension') or 'webp'
try: try:
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path) processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path, target_extension)
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200 return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400

View File

@@ -4,7 +4,7 @@ import os, uuid, tempfile
class ProfileImageProcessor: class ProfileImageProcessor:
@staticmethod @staticmethod
def get_parsed_image_data(image_path): def get_parsed_image_data(image_path, target_extension):
Image.MAX_IMAGE_PIXELS = 933120000 Image.MAX_IMAGE_PIXELS = 933120000
image = Image.open(image_path) image = Image.open(image_path)
@@ -16,7 +16,7 @@ class ProfileImageProcessor:
return image_path, mime_type return image_path, mime_type
else: else:
new_uuid = str(uuid.uuid4()) new_uuid = str(uuid.uuid4())
new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + ".webp" new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + "." + target_extension
image.save(new_image_path) image.save(new_image_path)
new_image = Image.open(new_image_path) new_image = Image.open(new_image_path)
@@ -26,5 +26,5 @@ class ProfileImageProcessor:
@staticmethod @staticmethod
def process_image(image_path): def process_image(image_path, target_extension):
return ProfileImageProcessor.get_parsed_image_data(image_path) return ProfileImageProcessor.get_parsed_image_data(image_path, target_extension)

View File

@@ -2,6 +2,7 @@ import type { LibraryGame } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { import {
downloadsSublevel, downloadsSublevel,
gameAchievementsSublevel,
gamesShopAssetsSublevel, gamesShopAssetsSublevel,
gamesSublevel, gamesSublevel,
} from "@main/level"; } from "@main/level";
@@ -18,11 +19,20 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
const download = await downloadsSublevel.get(key); const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.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 { return {
id: key, id: key,
...game, ...game,
download: download ?? null, download: download ?? null,
unlockedAchievementCount: game.unlockedAchievementCount ?? 0, unlockedAchievementCount,
achievementCount: game.achievementCount ?? 0, achievementCount: game.achievementCount ?? 0,
// Spread gameAssets last to ensure all image URLs are properly set // Spread gameAssets last to ensure all image URLs are properly set
...gameAssets, ...gameAssets,

View File

@@ -1,16 +1,20 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { PythonRPC } from "@main/services/python-rpc"; import { PythonRPC } from "@main/services/python-rpc";
const processProfileImage = async ( const processProfileImageEvent = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
path: string path: string
) => { ) => {
return processProfileImage(path, "webp");
};
export const processProfileImage = async (path: string, extension?: string) => {
return PythonRPC.rpc return PythonRPC.rpc
.post<{ .post<{
imagePath: string; imagePath: string;
mimeType: string; mimeType: string;
}>("/profile-image", { image_path: path }) }>("/profile-image", { image_path: path, target_extension: extension })
.then((response) => response.data); .then((response) => response.data);
}; };
registerEvent("processProfileImage", processProfileImage); registerEvent("processProfileImage", processProfileImageEvent);

View File

@@ -15,6 +15,13 @@ import { db, levelKeys, themesSublevel } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update"; import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path"; import { SystemPath } from "../system-path";
import { getThemeSoundPath } from "@main/helpers"; import { getThemeSoundPath } from "@main/helpers";
import { processProfileImage } from "@main/events/profile/process-profile-image";
const getStaticImage = async (path: string) => {
return processProfileImage(path, "jpg")
.then((response) => response.imagePath)
.catch(() => path);
};
async function downloadImage(url: string | null) { async function downloadImage(url: string | null) {
if (!url) return undefined; if (!url) return undefined;
@@ -31,8 +38,9 @@ async function downloadImage(url: string | null) {
response.data.pipe(writer); response.data.pipe(writer);
return new Promise<string | undefined>((resolve) => { return new Promise<string | undefined>((resolve) => {
writer.on("finish", () => { writer.on("finish", async () => {
resolve(outputPath); const staticImagePath = await getStaticImage(outputPath);
resolve(staticImagePath);
}); });
writer.on("error", () => { writer.on("error", () => {
logger.error("Failed to download image", { url }); logger.error("Failed to download image", { url });

View File

@@ -8,9 +8,11 @@ export const friendRequestEvent = async (payload: FriendRequest) => {
friendRequestCount: payload.friendRequestCount, friendRequestCount: payload.friendRequestCount,
}); });
const user = await HydraApi.get(`/users/${payload.senderId}`); if (payload.senderId) {
const user = await HydraApi.get(`/users/${payload.senderId}`);
if (user) { if (user) {
publishNewFriendRequestNotification(user); publishNewFriendRequestNotification(user);
}
} }
}; };

View File

@@ -1,7 +1,7 @@
import { LibraryGame } from "@types"; import { LibraryGame } from "@types";
import { useGameCard } from "@renderer/hooks"; import { useGameCard } from "@renderer/hooks";
import { ClockIcon, AlertFillIcon, TrophyIcon } from "@primer/octicons-react"; import { ClockIcon, AlertFillIcon, TrophyIcon } from "@primer/octicons-react";
import { memo, useMemo } from "react"; import { memo, useEffect, useMemo, useState } from "react";
import "./library-game-card-large.scss"; import "./library-game-card-large.scss";
interface LibraryGameCardLargeProps { interface LibraryGameCardLargeProps {
@@ -48,6 +48,20 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
] ]
); );
const [unlockedAchievementsCount, setUnlockedAchievementsCount] = useState(
game.unlockedAchievementCount ?? 0
);
useEffect(() => {
if (game.unlockedAchievementCount) return;
window.electron
.getUnlockedAchievements(game.objectId, game.shop)
.then((achievements) => {
setUnlockedAchievementsCount(achievements.length);
});
}, [game]);
const backgroundStyle = useMemo( const backgroundStyle = useMemo(
() => () =>
backgroundImage ? { backgroundImage: `url(${backgroundImage})` } : {}, backgroundImage ? { backgroundImage: `url(${backgroundImage})` } : {},
@@ -56,9 +70,9 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
const achievementBarStyle = useMemo( const achievementBarStyle = useMemo(
() => ({ () => ({
width: `${((game.unlockedAchievementCount ?? 0) / (game.achievementCount ?? 1)) * 100}%`, width: `${(unlockedAchievementsCount / (game.achievementCount ?? 1)) * 100}%`,
}), }),
[game.unlockedAchievementCount, game.achievementCount] [unlockedAchievementsCount, game.achievementCount]
); );
const logoImage = game.customLogoImageUrl ?? game.logoImageUrl; const logoImage = game.customLogoImageUrl ?? game.logoImageUrl;
@@ -116,14 +130,12 @@ export const LibraryGameCardLarge = memo(function LibraryGameCardLarge({
className="library-game-card-large__achievement-trophy" className="library-game-card-large__achievement-trophy"
/> />
<span className="library-game-card-large__achievement-count"> <span className="library-game-card-large__achievement-count">
{game.unlockedAchievementCount ?? 0} /{" "} {unlockedAchievementsCount} / {game.achievementCount ?? 0}
{game.achievementCount ?? 0}
</span> </span>
</div> </div>
<span className="library-game-card-large__achievement-percentage"> <span className="library-game-card-large__achievement-percentage">
{Math.round( {Math.round(
((game.unlockedAchievementCount ?? 0) / (unlockedAchievementsCount / (game.achievementCount ?? 1)) *
(game.achievementCount ?? 1)) *
100 100
)} )}
% %

View File

@@ -14,10 +14,6 @@ import "./library.scss";
export default function Library() { export default function Library() {
const { library, updateLibrary } = useLibrary(); const { library, updateLibrary } = useLibrary();
type ElectronAPI = {
refreshLibraryAssets?: () => Promise<unknown>;
onLibraryBatchComplete?: (cb: () => void) => () => void;
};
const [viewMode, setViewMode] = useState<ViewMode>(() => { const [viewMode, setViewMode] = useState<ViewMode>(() => {
const savedViewMode = localStorage.getItem("library-view-mode"); const savedViewMode = localStorage.getItem("library-view-mode");
@@ -41,22 +37,15 @@ export default function Library() {
useEffect(() => { useEffect(() => {
dispatch(setHeaderTitle(t("library"))); dispatch(setHeaderTitle(t("library")));
const electron = (globalThis as unknown as { electron?: ElectronAPI })
.electron; const unsubscribe = window.electron.onLibraryBatchComplete(() => {
let unsubscribe: () => void = () => undefined;
if (electron?.refreshLibraryAssets) {
electron
.refreshLibraryAssets()
.then(() => updateLibrary())
.catch(() => updateLibrary());
if (electron.onLibraryBatchComplete) {
unsubscribe = electron.onLibraryBatchComplete(() => {
updateLibrary();
});
}
} else {
updateLibrary(); updateLibrary();
} });
window.electron
.refreshLibraryAssets()
.then(() => updateLibrary())
.catch(() => updateLibrary());
return () => { return () => {
unsubscribe(); unsubscribe();