chore: resolving conflicts with yarn.lock

This commit is contained in:
Chubby Granny Chaser 2024-11-02 17:09:10 +00:00
commit c8165c10bf
No known key found for this signature in database
27 changed files with 481 additions and 649 deletions

View file

@ -362,7 +362,9 @@
"your_achievements": "Your Achievements",
"unlocked_at": "Unlocked at:",
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games"
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements",
"achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Hydra Cloud Subscription",

View file

@ -360,7 +360,9 @@
"user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueado em:",
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Assinatura Hydra Cloud",

View file

@ -19,6 +19,10 @@ export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds");
export const achievementSoundPath = app.isPackaged
? path.join(process.resourcesPath, "achievement.wav")
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
export const backupsPath = path.join(app.getPath("userData"), "Backups");
export const appVersion = app.getVersion();

View file

@ -98,7 +98,6 @@ app.whenReady().then(async () => {
WindowManager.createMainWindow();
}
WindowManager.createNotificationWindow();
WindowManager.createSystemTray(userPreferences?.language || "en");
});

View file

@ -13,7 +13,7 @@ import type { AchievementFile, UnlockedAchievement } from "@types";
import { achievementsLogger } from "../logger";
import { Cracker } from "@shared";
import { IsNull, Not } from "typeorm";
import { WindowManager } from "../window-manager";
import { publishCombinedNewAchievementNotification } from "../notifications";
const fileStats: Map<string, number> = new Map();
const fltFiles: Map<string, Set<string>> = new Map();
@ -249,11 +249,12 @@ export class AchievementWatcherManager {
0
);
WindowManager.notificationWindow?.webContents.send(
"on-combined-achievements-unlocked",
totalNewGamesWithAchievements,
totalNewAchievements
);
if (totalNewAchievements > 0) {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
this.hasFinishedMergingWithRemote = true;
};

View file

@ -8,11 +8,12 @@ import { HydraApi } from "../hydra-api";
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
import { Game } from "@main/entity";
import { achievementsLogger } from "../logger";
import { publishNewAchievementNotification } from "../notifications";
const saveAchievementsOnLocal = async (
objectId: string,
shop: GameShop,
achievements: any[],
achievements: UnlockedAchievement[],
sendUpdateEvent: boolean
) => {
return gameAchievementRepository
@ -82,6 +83,8 @@ export const mergeAchievements = async (
};
});
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
if (
newAchievements.length &&
publishNotification &&
@ -107,16 +110,15 @@ export const mergeAchievements = async (
};
});
WindowManager.notificationWindow?.webContents.send(
"on-achievement-unlocked",
game.objectID,
game.shop,
achievementsInfo
);
publishNewAchievementNotification({
achievements: achievementsInfo,
unlockedAchievementCount: mergedLocalAchievements.length,
totalAchievementCount: achievementsData.length,
gameTitle: game.title,
gameIcon: game.iconUrl,
});
}
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
if (game.remoteId) {
await HydraApi.put("/profile/games/achievements", {
id: game.remoteId,

View file

@ -1,67 +0,0 @@
import { Notification, nativeImage } from "electron";
import { t } from "i18next";
import { parseICO } from "icojs";
import trayIcon from "@resources/tray-icon.png?asset";
import { Game } from "@main/entity";
import { gameRepository, userPreferencesRepository } from "@main/repository";
const getGameIconNativeImage = async (gameId: number) => {
try {
const game = await gameRepository.findOne({
where: {
id: gameId,
},
});
if (!game?.iconUrl) return undefined;
const images = await parseICO(
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
);
const highResIcon = images.find((image) => image.width >= 128);
if (!highResIcon) return undefined;
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
} catch (err) {
return undefined;
}
};
export const publishDownloadCompleteNotification = async (game: Game) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
const icon = await getGameIconNativeImage(game.id);
if (userPreferences?.downloadNotificationsEnabled) {
new Notification({
title: t("download_complete", {
ns: "notifications",
}),
body: t("game_ready_to_install", {
ns: "notifications",
title: game.title,
}),
icon,
}).show();
}
};
export const publishNotificationUpdateReadyToInstall = async (
version: string
) => {
new Notification({
title: t("new_update_available", {
ns: "notifications",
version,
}),
body: t("restart_to_install_update", {
ns: "notifications",
}),
icon: trayIcon,
}).show();
};
export const publishNewFriendRequestNotification = async () => {};

View file

@ -0,0 +1,168 @@
import { Notification, app, nativeImage } from "electron";
import { t } from "i18next";
import { parseICO } from "icojs";
import trayIcon from "@resources/tray-icon.png?asset";
import { Game } from "@main/entity";
import { gameRepository, userPreferencesRepository } from "@main/repository";
import fs from "node:fs";
import axios from "axios";
import path from "node:path";
import sound from "sound-play";
import { achievementSoundPath } from "@main/constants";
import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
const getGameIconNativeImage = async (gameId: number) => {
try {
const game = await gameRepository.findOne({
where: {
id: gameId,
},
});
if (!game?.iconUrl) return undefined;
const images = await parseICO(
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
);
const highResIcon = images.find((image) => image.width >= 128);
if (!highResIcon) return undefined;
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
} catch (err) {
return undefined;
}
};
export const publishDownloadCompleteNotification = async (game: Game) => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
const icon = await getGameIconNativeImage(game.id);
if (userPreferences?.downloadNotificationsEnabled) {
new Notification({
title: t("download_complete", {
ns: "notifications",
}),
body: t("game_ready_to_install", {
ns: "notifications",
title: game.title,
}),
icon,
}).show();
}
};
export const publishNotificationUpdateReadyToInstall = async (
version: string
) => {
new Notification({
title: t("new_update_available", {
ns: "notifications",
version,
}),
body: t("restart_to_install_update", {
ns: "notifications",
}),
icon: trayIcon,
}).show();
};
export const publishNewFriendRequestNotification = async () => {};
async function downloadImage(url: string | null) {
if (!url) return null;
if (!url.startsWith("http")) return null;
const fileName = url.split("/").pop()!;
const outputPath = path.join(app.getPath("temp"), fileName);
const writer = fs.createWriteStream(outputPath);
const response = await axios.get(url, {
responseType: "stream",
});
response.data.pipe(writer);
return new Promise<string>((resolve, reject) => {
writer.on("finish", () => {
resolve(outputPath);
});
writer.on("error", reject);
});
}
export const publishCombinedNewAchievementNotification = async (
achievementCount,
gameCount
) => {
const options: NotificationOptions = {
title: t("achievement_unlocked", { ns: "achievement" }),
body: t("new_achievements_unlocked", {
ns: "achievement",
gameCount,
achievementCount,
}),
icon,
silent: true,
};
new Notification({
...options,
toastXml: toXmlString(options),
}).show();
if (process.platform !== "linux") {
sound.play(achievementSoundPath);
}
};
export const publishNewAchievementNotification = async (info: {
achievements: { displayName: string; iconUrl: string }[];
unlockedAchievementCount: number;
totalAchievementCount: number;
gameTitle: string;
gameIcon: string | null;
}) => {
const partialOptions =
info.achievements.length > 1
? {
title: t("achievements_unlocked_for_game", {
ns: "achievement",
gameTitle: info.gameTitle,
achievementCount: info.achievements.length,
}),
body: info.achievements.map((a) => a.displayName).join(", "),
icon: (await downloadImage(info.gameIcon)) ?? icon,
}
: {
title: t("achievement_unlocked", { ns: "achievement" }),
body: info.achievements[0].displayName,
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
};
const options: NotificationOptions = {
...partialOptions,
silent: true,
progress: {
value: info.unlockedAchievementCount / info.totalAchievementCount,
valueOverride: t("achievement_progress", {
ns: "achievement",
unlockedCount: info.unlockedAchievementCount,
totalCount: info.totalAchievementCount,
}),
},
};
new Notification({
...options,
toastXml: toXmlString(options),
}).show();
if (process.platform !== "linux") {
sound.play(achievementSoundPath);
}
};

View file

@ -0,0 +1,79 @@
export interface NotificationOptions {
title: string;
body?: string;
icon: string;
duration?: "short" | "long";
silent?: boolean;
progress?: {
status?: string;
value: number;
valueOverride: string;
};
}
function escape(string: string) {
return string.replace(/[<>&'"]/g, (match) => {
switch (match) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "'":
return "&apos;";
case '"':
return "&quot;";
default:
return "";
}
});
}
function addAttributeOrTrim(name: string, value: string) {
return value ? `${name}="${value}" ` : "";
}
export function toXmlString(options: NotificationOptions) {
let template =
"<toast " +
`displayTimestamp="${new Date().toISOString()}" ` +
`scenario="default" ` +
`duration="${options.duration ?? "short"}" ` +
`activationType="protocol" ` +
">";
//Visual
template += `<visual><binding template="ToastGeneric">`;
if (options.icon)
template += `<image placement="appLogoOverride" src="${options.icon}" hint-crop="none"/>`;
template +=
`<text><![CDATA[${options.title}]]></text>` +
`<text><![CDATA[${options.body}]]></text>`;
//Progress bar
if (options.progress) {
template +=
"<progress " +
`value="${options.progress.value}" ` +
`status="" ` +
addAttributeOrTrim(
"valueStringOverride",
escape(options.progress.valueOverride)
) +
"/>";
}
template += "</binding></visual>";
//Actions
template += "<actions>";
template += "</actions>";
//Audio
template += "<audio " + `silent="true" ` + `loop="false" ` + "/>";
//EOF
template += "</toast>";
return template;
}

View file

@ -20,7 +20,6 @@ import UserAgent from "user-agents";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static notificationWindow: Electron.BrowserWindow | null = null;
private static loadMainWindowURL(hash = "") {
// HMR for renderer base on electron-vite cli.
@ -39,21 +38,6 @@ export class WindowManager {
}
}
private static loadNotificationWindowURL() {
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.notificationWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
);
} else {
this.notificationWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "achievement-notification",
}
);
}
}
public static createMainWindow() {
if (this.mainWindow) return;
@ -151,32 +135,6 @@ export class WindowManager {
});
}
public static createNotificationWindow() {
this.notificationWindow = new BrowserWindow({
transparent: true,
maximizable: false,
autoHideMenuBar: true,
minimizable: false,
focusable: false,
skipTaskbar: true,
frame: false,
width: 350,
height: 104,
x: 0,
y: 0,
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.notificationWindow.setIgnoreMouseEvents(true);
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
// visibleOnFullScreen: true,
// });
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
this.loadNotificationWindowURL();
}
public static openAuthWindow() {
if (this.mainWindow) {
const authWindow = new BrowserWindow({

View file

@ -51,35 +51,6 @@ contextBridge.exposeInMainWorld("electron", {
getGameStats: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameStats", objectId, shop),
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
onAchievementUnlocked: (
cb: (
objectId: string,
shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[]
) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
objectId: string,
shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[]
) => cb(objectId, shop, achievements);
ipcRenderer.on("on-achievement-unlocked", listener);
return () =>
ipcRenderer.removeListener("on-achievement-unlocked", listener);
},
onCombinedAchievementsUnlocked: (
cb: (gameCount: number, achievementsCount: number) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
gameCount: number,
achievementCount: number
) => cb(gameCount, achievementCount);
ipcRenderer.on("on-combined-achievements-unlocked", listener);
return () =>
ipcRenderer.removeListener("on-combined-achievements-unlocked", listener);
},
onUpdateAchievements: (
objectId: string,
shop: GameShop,

View file

@ -66,16 +66,6 @@ declare global {
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
getTrendingGames: () => Promise<TrendingGame[]>;
onAchievementUnlocked: (
cb: (
objectId: string,
shop: GameShop,
achievements?: { displayName: string; iconUrl: string }[]
) => void
) => () => Electron.IpcRenderer;
onCombinedAchievementsUnlocked: (
cb: (gameCount: number, achievementCount: number) => void
) => () => Electron.IpcRenderer;
onUpdateAchievements: (
objectId: string,
shop: GameShop,

View file

@ -17,7 +17,6 @@ import { App } from "./app";
import { store } from "./store";
import resources from "@locales";
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
import { RepacksContextProvider } from "./context";
import { SuspenseWrapper } from "./components";
@ -92,10 +91,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
element={<SuspenseWrapper Component={Achievements} />}
/>
</Route>
<Route
path="/achievement-notification"
Component={AchievementNotification}
/>
</Routes>
</HashRouter>
</RepacksContextProvider>

View file

@ -273,7 +273,6 @@ export function AchievementsContent({
src={steamUrlBuilder.libraryHero(objectId)}
style={{ display: "none" }}
alt={gameTitle}
className={styles.heroImage}
onLoad={handleHeroLoad}
/>

View file

@ -23,31 +23,6 @@ export const hero = style({
flexDirection: "column",
position: "relative",
transition: "all ease 0.2s",
"@media": {
"(min-width: 1250px)": {
height: "350px",
minHeight: "350px",
},
},
});
export const heroImage = style({
width: "100%",
height: `${HERO_HEIGHT}px`,
minHeight: `${HERO_HEIGHT}px`,
objectFit: "cover",
objectPosition: "top",
transition: "all ease 0.2s",
position: "absolute",
zIndex: "0",
filter: "blur(5px)",
"@media": {
"(min-width: 1250px)": {
objectPosition: "center",
height: "350px",
minHeight: "350px",
},
},
});
export const heroContent = style({

View file

@ -1,44 +0,0 @@
import { recipe } from "@vanilla-extract/recipes";
import { vars } from "../../../theme.css";
import { keyframes, style } from "@vanilla-extract/css";
const animationIn = keyframes({
"0%": { transform: `translateY(-240px)` },
"100%": { transform: "translateY(0)" },
});
const animationOut = keyframes({
"0%": { transform: `translateY(0)` },
"100%": { transform: "translateY(-240px)" },
});
export const container = recipe({
base: {
marginTop: "24px",
marginLeft: "24px",
animationDuration: "1.0s",
height: "60px",
display: "flex",
},
variants: {
closing: {
true: {
animationName: animationOut,
transform: "translateY(-240px)",
},
false: {
animationName: animationIn,
transform: "translateY(0)",
},
},
},
});
export const content = style({
display: "flex",
flexDirection: "row",
gap: "8px",
alignItems: "center",
background: vars.color.background,
paddingRight: "8px",
});

View file

@ -1,141 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next";
import * as styles from "./achievement-notification.css";
interface AchievementInfo {
displayName: string;
iconUrl: string;
}
const NOTIFICATION_TIMEOUT = 4000;
export function AchievementNotification() {
const { t } = useTranslation("achievement");
const [isClosing, setIsClosing] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
const [currentAchievement, setCurrentAchievement] =
useState<AchievementInfo | null>(null);
const achievementAnimation = useRef(-1);
const closingAnimation = useRef(-1);
const visibleAnimation = useRef(-1);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.2;
audio.play();
}, []);
useEffect(() => {
const unsubscribe = window.electron.onCombinedAchievementsUnlocked(
(gameCount, achievementCount) => {
if (gameCount === 0 || achievementCount === 0) return;
setAchievements([
{
displayName: t("new_achievements_unlocked", {
gameCount,
achievementCount,
}),
iconUrl:
"https://avatars.githubusercontent.com/u/164102380?s=400&u=01a13a7b4f0c642f7e547b8e1d70440ea06fa750&v=4",
},
]);
playAudio();
}
);
return () => {
unsubscribe();
};
}, [playAudio]);
useEffect(() => {
const unsubscribe = window.electron.onAchievementUnlocked(
(_object, _shop, achievements) => {
if (!achievements || !achievements.length) return;
setAchievements((ach) => ach.concat(achievements));
playAudio();
}
);
return () => {
unsubscribe();
};
}, [playAudio]);
const hasAchievementsPending = achievements.length > 0;
const startAnimateClosing = useCallback(() => {
cancelAnimationFrame(closingAnimation.current);
cancelAnimationFrame(visibleAnimation.current);
cancelAnimationFrame(achievementAnimation.current);
setIsClosing(true);
const zero = performance.now();
closingAnimation.current = requestAnimationFrame(
function animateClosing(time) {
if (time - zero <= 1000) {
closingAnimation.current = requestAnimationFrame(animateClosing);
} else {
setIsVisible(false);
}
}
);
}, []);
useEffect(() => {
if (hasAchievementsPending) {
setIsClosing(false);
setIsVisible(true);
let zero = performance.now();
cancelAnimationFrame(closingAnimation.current);
cancelAnimationFrame(visibleAnimation.current);
cancelAnimationFrame(achievementAnimation.current);
achievementAnimation.current = requestAnimationFrame(
function animateLock(time) {
if (time - zero > NOTIFICATION_TIMEOUT) {
zero = performance.now();
setAchievements((ach) => ach.slice(1));
}
achievementAnimation.current = requestAnimationFrame(animateLock);
}
);
} else {
startAnimateClosing();
}
}, [hasAchievementsPending]);
useEffect(() => {
if (achievements.length) {
setCurrentAchievement(achievements[0]);
}
}, [achievements]);
if (!isVisible || !currentAchievement) return null;
return (
<div className={styles.container({ closing: isClosing })}>
<div className={styles.content}>
<img
src={currentAchievement.iconUrl}
alt={currentAchievement.displayName}
style={{ flex: 1, width: "60px" }}
/>
<div>
<p>{t("achievement_unlocked")}</p>
<p>{currentAchievement.displayName}</p>
</div>
</div>
</div>
);
}

View file

@ -254,6 +254,7 @@ export function ProfileHero() {
if (gameRunning)
return {
...gameRunning,
objectId: gameRunning.objectID,
sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
};
@ -330,7 +331,7 @@ export function ProfileHero() {
<Link
to={buildGameDetailsPath({
...currentGame,
objectId: currentGame.objectID,
objectId: currentGame.objectId,
})}
>
{currentGame.title}