mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 12:51:03 +00:00
Merge pull request #1857 from hydralauncher/fix/use-local-achievement-cache
fix: achievements on library page
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
%
|
%
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user