Merge branch 'rc/v2.0' of github.com:hydralauncher/hydra into rc/v2.0

This commit is contained in:
Chubby Granny Chaser 2024-06-21 01:07:34 +01:00
commit 0ac17e95ff
No known key found for this signature in database
24 changed files with 291 additions and 74 deletions

View file

@ -82,6 +82,7 @@
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@swc/core": "^1.4.16", "@swc/core": "^1.4.16",
"@types/auto-launch": "^5.0.5", "@types/auto-launch": "^5.0.5",
"@types/color": "^3.0.6",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",

View file

@ -19,7 +19,8 @@
"filter": "Filter library", "filter": "Filter library",
"home": "Home", "home": "Home",
"queued": "{{title}} (Queued)", "queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected" "game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in"
}, },
"header": { "header": {
"search": "Search games", "search": "Search games",
@ -232,7 +233,6 @@
"amount_hours": "{{amount}} hours", "amount_hours": "{{amount}} hours",
"amount_minutes": "{{amount}} minutes", "amount_minutes": "{{amount}} minutes",
"last_time_played": "Last played {{period}}", "last_time_played": "Last played {{period}}",
"sign_out": "Sign out",
"activity": "Recent activity", "activity": "Recent activity",
"library": "Library", "library": "Library",
"total_play_time": "Total playtime: {{amount}}", "total_play_time": "Total playtime: {{amount}}",
@ -244,9 +244,11 @@
"edit_profile": "Edit Profile", "edit_profile": "Edit Profile",
"saved_successfully": "Saved successfully", "saved_successfully": "Saved successfully",
"try_again": "Please, try again", "try_again": "Please, try again",
"signout_modal_title": "Are you sure?", "sign_out_modal_title": "Are you sure?",
"cancel": "Cancel", "cancel": "Cancel",
"signout": "Sign Out", "successfully_signed_out": "Successfully signed out",
"successfully_signed_out": "Successfully signed out" "sign_out": "Sign out",
"playing_for": "Playing for {{amount}}",
"sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?"
} }
} }

View file

@ -19,7 +19,8 @@
"filter": "Filtrar biblioteca", "filter": "Filtrar biblioteca",
"home": "Início", "home": "Início",
"queued": "{{title}} (Na fila)", "queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado" "game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login"
}, },
"header": { "header": {
"search": "Buscar jogos", "search": "Buscar jogos",
@ -232,7 +233,6 @@
"amount_hours": "{{amount}} horas", "amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos", "amount_minutes": "{{amount}} minutos",
"last_time_played": "Jogou {{period}}", "last_time_played": "Jogou {{period}}",
"sign_out": "Sair da conta",
"activity": "Atividade recente", "activity": "Atividade recente",
"library": "Biblioteca", "library": "Biblioteca",
"total_play_time": "Tempo total de jogo: {{amount}}", "total_play_time": "Tempo total de jogo: {{amount}}",
@ -245,8 +245,10 @@
"saved_successfully": "Salvo com sucesso", "saved_successfully": "Salvo com sucesso",
"try_again": "Por favor, tente novamente", "try_again": "Por favor, tente novamente",
"cancel": "Cancelar", "cancel": "Cancelar",
"signout": "Sair da conta", "successfully_signed_out": "Deslogado com sucesso",
"signout_modal_title": "Tem certeza?", "sign_out": "Sair da conta",
"successfully_signed_out": "Deslogado com sucesso" "sign_out_modal_title": "Tem certeza?",
"playing_for": "Jogando por {{amount}}",
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?"
} }
} }

View file

@ -1,11 +1,12 @@
import { userAuthRepository } from "@main/repository"; import { gameRepository, userAuthRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services/hydra-api"; import { HydraApi } from "@main/services/hydra-api";
const signOut = async (_event: Electron.IpcMainInvokeEvent): Promise<void> => { const signOut = async (_event: Electron.IpcMainInvokeEvent): Promise<void> => {
await Promise.all([ await Promise.all([
userAuthRepository.delete({ id: 1 }), userAuthRepository.delete({ id: 1 }),
HydraApi.post("/auth/logout"), gameRepository.delete({}),
HydraApi.post("/auth/logout").catch(),
]); ]);
}; };

View file

@ -40,7 +40,7 @@ import "./download-sources/validate-download-source";
import "./download-sources/add-download-source"; import "./download-sources/add-download-source";
import "./download-sources/remove-download-source"; import "./download-sources/remove-download-source";
import "./download-sources/sync-download-sources"; import "./download-sources/sync-download-sources";
import "./auth/signout"; import "./auth/sign-out";
import "./auth/open-auth-window"; import "./auth/open-auth-window";
import "./user/get-user"; import "./user/get-user";
import "./profile/get-me"; import "./profile/get-me";

View file

@ -5,6 +5,7 @@ import { gameRepository } from "@main/repository";
import { getProcesses } from "@main/helpers"; import { getProcesses } from "@main/helpers";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync"; import { createGame, updateGamePlaytime } from "./library-sync";
import { GameRunning } from "@types";
const gamesPlaytime = new Map< const gamesPlaytime = new Map<
number, number,
@ -46,10 +47,6 @@ export const watchProcesses = async () => {
const zero = gamePlaytime.lastTick; const zero = gamePlaytime.lastTick;
const delta = performance.now() - zero; const delta = performance.now() - zero;
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
}
await gameRepository.update(game.id, { await gameRepository.update(game.id, {
playTimeInMilliseconds: game.playTimeInMilliseconds + delta, playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(), lastTimePlayed: new Date(),
@ -92,10 +89,20 @@ export const watchProcesses = async () => {
gameRepository.update({ objectID: game.objectID }, { remoteId }); gameRepository.update({ objectID: game.objectID }, { remoteId });
}); });
} }
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
}
} }
} }
if (WindowManager.mainWindow) {
const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => {
return {
id: entry[0],
sessionDurationInMillis: performance.now() - entry[1].firstTick,
};
});
WindowManager.mainWindow.webContents.send(
"on-games-running",
gamesRunning as Pick<GameRunning, "id" | "sessionDurationInMillis">[]
);
}
}; };

View file

@ -8,6 +8,7 @@ import type {
UserPreferences, UserPreferences,
AppUpdaterEvent, AppUpdaterEvent,
StartGameDownloadPayload, StartGameDownloadPayload,
GameRunning,
} from "@types"; } from "@types";
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {
@ -84,17 +85,15 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("deleteGameFolder", gameId), ipcRenderer.invoke("deleteGameFolder", gameId),
getGameByObjectID: (objectID: string) => getGameByObjectID: (objectID: string) =>
ipcRenderer.invoke("getGameByObjectID", objectID), ipcRenderer.invoke("getGameByObjectID", objectID),
onPlaytime: (cb: (gameId: number) => void) => { onGamesRunning: (
const listener = (_event: Electron.IpcRendererEvent, gameId: number) => cb: (
cb(gameId); gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
ipcRenderer.on("on-playtime", listener); ) => void
return () => ipcRenderer.removeListener("on-playtime", listener); ) => {
}, const listener = (_event: Electron.IpcRendererEvent, gamesRunning) =>
onGameClose: (cb: (gameId: number) => void) => { cb(gamesRunning);
const listener = (_event: Electron.IpcRendererEvent, gameId: number) => ipcRenderer.on("on-games-running", listener);
cb(gameId); return () => ipcRenderer.removeListener("on-games-running", listener);
ipcRenderer.on("on-game-close", listener);
return () => ipcRenderer.removeListener("on-game-close", listener);
}, },
onLibraryBatchComplete: (cb: () => void) => { onLibraryBatchComplete: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();

View file

@ -22,6 +22,7 @@ import {
closeToast, closeToast,
setUserDetails, setUserDetails,
setProfileBackground, setProfileBackground,
setGameRunning,
} from "@renderer/features"; } from "@renderer/features";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -31,7 +32,7 @@ export interface AppProps {
export function App() { export function App() {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary } = useLibrary(); const { updateLibrary, library } = useLibrary();
const { t } = useTranslation("app"); const { t } = useTranslation("app");
@ -110,6 +111,32 @@ export function App() {
}); });
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]); }, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
useEffect(() => {
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
if (gamesRunning.length) {
const lastGame = gamesRunning[gamesRunning.length - 1];
const libraryGame = library.find(
(library) => library.id === lastGame.id
);
if (libraryGame) {
dispatch(
setGameRunning({
...libraryGame,
sessionDurationInMillis: lastGame.sessionDurationInMillis,
})
);
return;
}
}
dispatch(setGameRunning(null));
});
return () => {
unsubscribe();
};
}, [dispatch, library]);
useEffect(() => { useEffect(() => {
const listeners = [ const listeners = [
window.electron.onSignIn(onSignIn), window.electron.onSignIn(onSignIn),

View file

@ -9,7 +9,7 @@ export const profileButton = style({
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
color: vars.color.muted, color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`, borderBottom: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
":hover": { ":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)", backgroundColor: "rgba(255, 255, 255, 0.15)",
}, },
@ -20,6 +20,7 @@ export const profileButtonContent = style({
alignItems: "center", alignItems: "center",
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`, gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
height: "40px", height: "40px",
width: "100%",
}); });
export const profileAvatar = style({ export const profileAvatar = style({
@ -39,6 +40,8 @@ export const profileButtonInformation = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
flex: "1",
minWidth: 0,
}); });
export const statusBadge = style({ export const statusBadge = style({
@ -55,4 +58,9 @@ export const statusBadge = style({
export const profileButtonTitle = style({ export const profileButtonTitle = style({
fontWeight: "bold", fontWeight: "bold",
fontSize: vars.size.body, fontSize: vars.size.body,
width: "100%",
textAlign: "left",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}); });

View file

@ -2,14 +2,19 @@ import { useNavigate } from "react-router-dom";
import { PersonIcon } from "@primer/octicons-react"; import { PersonIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css"; import * as styles from "./sidebar-profile.css";
import { useUserDetails } from "@renderer/hooks"; import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export function SidebarProfile() { export function SidebarProfile() {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation("sidebar");
const { userDetails, profileBackground } = useUserDetails(); const { userDetails, profileBackground } = useUserDetails();
const { gameRunning } = useAppSelector((state) => state.gameRunning);
const handleButtonClick = () => { const handleButtonClick = () => {
if (userDetails === null) { if (userDetails === null) {
window.electron.openAuthWindow(); window.electron.openAuthWindow();
@ -46,9 +51,24 @@ export function SidebarProfile() {
<div className={styles.profileButtonInformation}> <div className={styles.profileButtonInformation}>
<p className={styles.profileButtonTitle}> <p className={styles.profileButtonTitle}>
{userDetails ? userDetails.displayName : "Sign in"} {userDetails ? userDetails.displayName : t("sign_in")}
</p> </p>
{userDetails && gameRunning && (
<div>
<small>{gameRunning.title}</small>
</div>
)}
</div> </div>
{userDetails && gameRunning && (
<img
alt={gameRunning.title}
width={24}
style={{ borderRadius: 4 }}
src={gameRunning.iconUrl}
/>
)}
</div> </div>
</button> </button>
); );

View file

@ -109,20 +109,19 @@ export function GameDetailsContextProvider({
}, [objectID, gameTitle, dispatch]); }, [objectID, gameTitle, dispatch]);
useEffect(() => { useEffect(() => {
const listeners = [ const unsubscribe = window.electron.onGamesRunning((gamesIds) => {
window.electron.onGameClose(() => { const updatedIsGameRunning =
if (isGameRunning) setisGameRunning(false); !!game?.id &&
}), !!gamesIds.find((gameRunning) => gameRunning.id == game.id);
window.electron.onPlaytime((gameId) => {
if (gameId === game?.id) {
if (!isGameRunning) setisGameRunning(true);
updateGame();
}
}),
];
if (isGameRunning != updatedIsGameRunning) {
updateGame();
}
setisGameRunning(updatedIsGameRunning);
});
return () => { return () => {
listeners.forEach((unsubscribe) => unsubscribe()); unsubscribe();
}; };
}, [game?.id, isGameRunning, updateGame]); }, [game?.id, isGameRunning, updateGame]);

View file

@ -71,8 +71,11 @@ declare global {
removeGame: (gameId: number) => Promise<void>; removeGame: (gameId: number) => Promise<void>;
deleteGameFolder: (gameId: number) => Promise<unknown>; deleteGameFolder: (gameId: number) => Promise<unknown>;
getGameByObjectID: (objectID: string) => Promise<Game | null>; getGameByObjectID: (objectID: string) => Promise<Game | null>;
onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; onGamesRunning: (
onGameClose: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
) => void
) => () => Electron.IpcRenderer;
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
/* User preferences */ /* User preferences */

View file

@ -5,3 +5,4 @@ export * from "./download-slice";
export * from "./window-slice"; export * from "./window-slice";
export * from "./toast-slice"; export * from "./toast-slice";
export * from "./user-details-slice"; export * from "./user-details-slice";
export * from "./running-game-slice";

View file

@ -0,0 +1,22 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { GameRunning } from "@types";
export interface GameRunningState {
gameRunning: GameRunning | null;
}
const initialState: GameRunningState = {
gameRunning: null,
};
export const gameRunningSlice = createSlice({
name: "running-game",
initialState,
reducers: {
setGameRunning: (state, action: PayloadAction<GameRunning | null>) => {
state.gameRunning = action.payload;
},
},
});
export const { setGameRunning } = gameRunningSlice.actions;

View file

@ -43,5 +43,5 @@ export const buildGameDetailsPath = (
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`; return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
}; };
export const darkenColor = (color: string, amount: number) => export const darkenColor = (color: string, amount: number, alpha: number = 1) =>
new Color(color).darken(amount).toString(); new Color(color).darken(amount).alpha(alpha).toString();

View file

@ -1,4 +1,4 @@
import { formatDistance } from "date-fns"; import { formatDistance, subMilliseconds } from "date-fns";
import type { FormatDistanceOptions } from "date-fns"; import type { FormatDistanceOptions } from "date-fns";
import { import {
ptBR, ptBR,
@ -52,5 +52,20 @@ export function useDate() {
return ""; return "";
} }
}, },
formatDiffInMillis: (
millis: number,
baseDate: string | number | Date,
options?: FormatDistanceOptions
) => {
try {
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
...options,
locale: getDateLocale(),
});
} catch (err) {
return "";
}
},
}; };
} }

View file

@ -36,8 +36,7 @@ export function useUserDetails() {
format: "hex", format: "hex",
}); });
const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8)})`; const profileBackground = `linear-gradient(135deg, ${darkenColor(output as string, 0.6)}, ${darkenColor(output as string, 0.8, 0.7)})`;
dispatch(setProfileBackground(profileBackground)); dispatch(setProfileBackground(profileBackground));
window.localStorage.setItem( window.localStorage.setItem(
@ -45,9 +44,13 @@ export function useUserDetails() {
JSON.stringify({ ...userDetails, profileBackground }) JSON.stringify({ ...userDetails, profileBackground })
); );
} else { } else {
dispatch(setProfileBackground(null)); const profileBackground = `#151515B3`;
dispatch(setProfileBackground(profileBackground));
window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); window.localStorage.setItem(
"userDetails",
JSON.stringify({ ...userDetails, profileBackground })
);
} }
}, },
[dispatch] [dispatch]

View file

@ -138,8 +138,9 @@ export const randomizerButton = style({
bottom: `${26 + SPACING_UNIT * 2}px`, bottom: `${26 + SPACING_UNIT * 2}px`,
/* Scroll bar + spacing */ /* Scroll bar + spacing */
right: `${9 + SPACING_UNIT * 2}px`, right: `${9 + SPACING_UNIT * 2}px`,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px", boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 1px",
border: `solid 2px ${vars.color.border}`, border: `solid 2px ${vars.color.border}`,
zIndex: "1",
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
":hover": { ":hover": {
backgroundColor: vars.color.background, backgroundColor: vars.color.background,

View file

@ -6,9 +6,14 @@ import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { useDate, useToast, useUserDetails } from "@renderer/hooks"; import {
useAppSelector,
useDate,
useToast,
useUserDetails,
} from "@renderer/hooks";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
import { PersonIcon, TelescopeIcon } from "@primer/octicons-react"; import { PersonIcon, TelescopeIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import { UserEditProfileModal } from "./user-edit-modal"; import { UserEditProfileModal } from "./user-edit-modal";
@ -33,6 +38,8 @@ export function UserContent({
const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [showSignOutModal, setShowSignOutModal] = useState(false); const [showSignOutModal, setShowSignOutModal] = useState(false);
const { gameRunning } = useAppSelector((state) => state.gameRunning);
const navigate = useNavigate(); const navigate = useNavigate();
const numberFormatter = useMemo(() => { const numberFormatter = useMemo(() => {
@ -41,7 +48,7 @@ export function UserContent({
}); });
}, [i18n.language]); }, [i18n.language]);
const { formatDistance } = useDate(); const { formatDistance, formatDiffInMillis } = useDate();
const formatPlayTime = () => { const formatPlayTime = () => {
const seconds = userProfile.libraryGames.reduce( const seconds = userProfile.libraryGames.reduce(
@ -102,10 +109,32 @@ export function UserContent({
<section <section
className={styles.profileContentBox} className={styles.profileContentBox}
style={{ style={{
background: profileContentBoxBackground,
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`, padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
position: "relative",
}} }}
> >
{gameRunning && isMe && (
<div
style={{
backgroundImage: `url(${steamUrlBuilder.libraryHero(gameRunning.objectID)})`,
backgroundPosition: "top",
position: "absolute",
inset: 0,
backgroundSize: "cover",
borderRadius: "4px",
}}
></div>
)}
<div
style={{
background: profileContentBoxBackground,
position: "absolute",
inset: 0,
borderRadius: "4px",
}}
></div>
<div className={styles.profileAvatarContainer}> <div className={styles.profileAvatarContainer}>
{userProfile.profileImageUrl ? ( {userProfile.profileImageUrl ? (
<img <img
@ -120,10 +149,45 @@ export function UserContent({
<div className={styles.profileInformation}> <div className={styles.profileInformation}>
<h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2> <h2 style={{ fontWeight: "bold" }}>{userProfile.displayName}</h2>
{isMe && gameRunning && (
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT / 2}px`,
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<p>{gameRunning.title}</p>
</div>
<small>
{t("playing_for", {
amount: formatDiffInMillis(
gameRunning.sessionDurationInMillis,
new Date()
),
})}
</small>
</div>
)}
</div> </div>
{isMe && ( {isMe && (
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}> <div
style={{
flex: 1,
display: "flex",
justifyContent: "end",
zIndex: 1,
}}
>
<div <div
style={{ style={{
display: "flex", display: "flex",
@ -133,7 +197,7 @@ export function UserContent({
> >
<> <>
<Button theme="outline" onClick={handleEditProfile}> <Button theme="outline" onClick={handleEditProfile}>
Editar perfil {t("edit_profile")}
</Button> </Button>
<Button <Button

View file

@ -19,17 +19,20 @@ export const UserSignOutModal = ({
<> <>
<Modal <Modal
visible={visible} visible={visible}
title={t("signout_modal_title")} title={t("sign_out_modal_title")}
onClose={onClose} onClose={onClose}
> >
<div className={styles.signOutModalButtonsContainer}> <div className={styles.signOutModalContent}>
<Button onClick={onConfirm} theme="outline"> <p style={{ fontFamily: "Fira Sans" }}>{t("sign_out_modal_text")}</p>
{t("signout")} <div className={styles.signOutModalButtonsContainer}>
</Button> <Button onClick={onConfirm} theme="danger">
{t("sign_out")}
</Button>
<Button onClick={onClose} theme="primary"> <Button onClick={onClose} theme="primary">
{t("cancel")} {t("cancel")}
</Button> </Button>
</div>
</div> </div>
</Modal> </Modal>
</> </>

View file

@ -4,7 +4,6 @@ import { style } from "@vanilla-extract/css";
export const wrapper = style({ export const wrapper = style({
padding: "24px", padding: "24px",
width: "100%", width: "100%",
height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT * 3}px`, gap: `${SPACING_UNIT * 3}px`,
@ -33,6 +32,7 @@ export const profileAvatarContainer = style({
overflow: "hidden", overflow: "hidden",
border: `solid 1px ${vars.color.border}`, border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)", boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
zIndex: 1,
}); });
export const profileAvatarEditContainer = style({ export const profileAvatarEditContainer = style({
@ -56,7 +56,6 @@ export const profileAvatar = style({
borderRadius: "50%", borderRadius: "50%",
overflow: "hidden", overflow: "hidden",
objectFit: "cover", objectFit: "cover",
animationPlayState: "paused",
}); });
export const profileAvatarEditOverlay = style({ export const profileAvatarEditOverlay = style({
@ -72,8 +71,10 @@ export const profileAvatarEditOverlay = style({
export const profileInformation = style({ export const profileInformation = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: `${SPACING_UNIT}px`,
alignItems: "flex-start", alignItems: "flex-start",
color: "#c0c1c7", color: "#c0c1c7",
zIndex: 1,
}); });
export const profileContent = style({ export const profileContent = style({
@ -189,10 +190,18 @@ export const noDownloads = style({
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
}); });
export const signOutModalContent = style({
display: "flex",
width: "100%",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
});
export const signOutModalButtonsContainer = style({ export const signOutModalButtonsContainer = style({
display: "flex", display: "flex",
width: "100%", width: "100%",
justifyContent: "end", justifyContent: "end",
alignItems: "center", alignItems: "center",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
paddingTop: `${SPACING_UNIT}px`,
}); });

View file

@ -7,6 +7,7 @@ import {
userPreferencesSlice, userPreferencesSlice,
toastSlice, toastSlice,
userDetailsSlice, userDetailsSlice,
gameRunningSlice,
} from "@renderer/features"; } from "@renderer/features";
export const store = configureStore({ export const store = configureStore({
@ -18,6 +19,7 @@ export const store = configureStore({
download: downloadSlice.reducer, download: downloadSlice.reducer,
toast: toastSlice.reducer, toast: toastSlice.reducer,
userDetails: userDetailsSlice.reducer, userDetails: userDetailsSlice.reducer,
gameRunning: gameRunningSlice.reducer,
}, },
}); });

View file

@ -127,6 +127,15 @@ export interface Game {
export type LibraryGame = Omit<Game, "repacks">; export type LibraryGame = Omit<Game, "repacks">;
export interface GameRunning {
id: number;
title: string;
iconUrl: string;
objectID: string;
shop: GameShop;
sessionDurationInMillis: number;
}
export interface DownloadProgress { export interface DownloadProgress {
downloadSpeed: number; downloadSpeed: number;
timeRemaining: number; timeRemaining: number;

View file

@ -1251,6 +1251,25 @@
"@types/node" "*" "@types/node" "*"
"@types/responselike" "^1.0.0" "@types/responselike" "^1.0.0"
"@types/color-convert@*":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.3.tgz#e93f5c991eda87a945058b47044f5f0008b0dce9"
integrity sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==
dependencies:
"@types/color-name" "*"
"@types/color-name@*":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.4.tgz#e002611ff627347818d440a05e81650e9a4053b8"
integrity sha512-hulKeREDdLFesGQjl96+4aoJSHY5b2GRjagzzcqCfIrWhe5vkCqIvrLbqzBaI1q94Vg8DNJZZqTR5ocdWmWclg==
"@types/color@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.6.tgz#29c27a99d4de2975e1676712679a0bd7f646a3fb"
integrity sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==
dependencies:
"@types/color-convert" "*"
"@types/conventional-commits-parser@^5.0.0": "@types/conventional-commits-parser@^5.0.0":
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8" resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8"