feat: achievement section for user not logged in

This commit is contained in:
Zamitto 2024-10-13 21:14:06 -03:00
parent a064958d4c
commit a4475d2145
7 changed files with 131 additions and 19 deletions

View file

@ -131,7 +131,8 @@
"executable_path_in_use": "Executable already in use by \"{{game}}\"", "executable_path_in_use": "Executable already in use by \"{{game}}\"",
"warning": "Warning:", "warning": "Warning:",
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.", "hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util its conclusion. In case Hydra closes before the conclusion, you will lose your progress.",
"achievements": "Achievements {{unlockedCount}}/{{achievementsCount}}", "achievements": "Achievements",
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Cloud save", "cloud_save": "Cloud save",
"cloud_save_description": "Save your progress in the cloud and continue playing on any device", "cloud_save_description": "Save your progress in the cloud and continue playing on any device",
"backups": "Backups", "backups": "Backups",
@ -146,7 +147,8 @@
"backup_uploaded": "Backup uploaded", "backup_uploaded": "Backup uploaded",
"backup_deleted": "Backup deleted", "backup_deleted": "Backup deleted",
"backup_restored": "Backup restored", "backup_restored": "Backup restored",
"see_all_achievements": "See all achievements" "see_all_achievements": "See all achievements",
"sign_in_to_see_achievements": "Sign in to see achievements"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View file

@ -127,7 +127,8 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"", "executable_path_in_use": "Executável em uso por \"{{game}}\"",
"warning": "Aviso:", "warning": "Aviso:",
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
"achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", "achievements": "Conquistas",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"cloud_save": "Salvamento em nuvem", "cloud_save": "Salvamento em nuvem",
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo", "cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
"backups": "Backups", "backups": "Backups",
@ -142,7 +143,8 @@
"backup_uploaded": "Backup criado", "backup_uploaded": "Backup criado",
"backup_deleted": "Backup apagado", "backup_deleted": "Backup apagado",
"backup_restored": "Backup restaurado", "backup_restored": "Backup restaurado",
"see_all_achievements": "Ver todas as conquistas" "see_all_achievements": "Ver todas as conquistas",
"sign_in_to_see_achievements": "Faça login para ver as conquistas"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View file

@ -116,7 +116,8 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"", "executable_path_in_use": "Executável em uso por \"{{game}}\"",
"warning": "Aviso:", "warning": "Aviso:",
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.", "hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
"achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})", "achievements": "Conquistas",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"see_all_achievements": "Ver todas as conquistas" "see_all_achievements": "Ver todas as conquistas"
}, },
"activation": { "activation": {

View file

@ -4,6 +4,7 @@ import {
} from "@main/repository"; } from "@main/repository";
import { HydraApi } from "../hydra-api"; import { HydraApi } from "../hydra-api";
import { AchievementData } from "@types"; import { AchievementData } from "@types";
import { UserNotLoggedInError } from "@shared";
export const getGameAchievementData = async ( export const getGameAchievementData = async (
objectId: string, objectId: string,
@ -30,7 +31,11 @@ export const getGameAchievementData = async (
return achievements; return achievements;
}) })
.catch(() => { .catch((err) => {
if (err instanceof UserNotLoggedInError) {
throw err;
}
return gameAchievementRepository return gameAchievementRepository
.findOne({ .findOne({
where: { objectId, shop }, where: { objectId, shop },

View file

@ -3,12 +3,18 @@ import {
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useRef,
useState, useState,
} from "react"; } from "react";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage } from "@renderer/helpers"; import { getSteamLanguage } from "@renderer/helpers";
import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks"; import {
useAppDispatch,
useAppSelector,
useDownload,
useUserDetails,
} from "@renderer/hooks";
import type { import type {
Game, Game,
@ -67,6 +73,7 @@ export function GameDetailsContextProvider({
const [achievements, setAchievements] = useState<UserAchievement[]>([]); const [achievements, setAchievements] = useState<UserAchievement[]>([]);
const [game, setGame] = useState<Game | null>(null); const [game, setGame] = useState<Game | null>(null);
const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const [stats, setStats] = useState<GameStats | null>(null); const [stats, setStats] = useState<GameStats | null>(null);
@ -93,6 +100,7 @@ export function GameDetailsContextProvider({
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { lastPacket } = useDownload(); const { lastPacket } = useDownload();
const { userDetails } = useUserDetails();
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
(state) => state.userPreferences.value (state) => state.userPreferences.value
@ -111,6 +119,10 @@ export function GameDetailsContextProvider({
}, [updateGame, isGameDownloading, lastPacket?.game.status]); }, [updateGame, isGameDownloading, lastPacket?.game.status]);
useEffect(() => { useEffect(() => {
if (abortControllerRef.current) abortControllerRef.current.abort();
const abortController = new AbortController();
abortControllerRef.current = abortController;
window.electron window.electron
.getGameShopDetails( .getGameShopDetails(
objectId!, objectId!,
@ -118,6 +130,8 @@ export function GameDetailsContextProvider({
getSteamLanguage(i18n.language) getSteamLanguage(i18n.language)
) )
.then((result) => { .then((result) => {
if (abortController.signal.aborted) return;
setShopDetails(result); setShopDetails(result);
if ( if (
@ -133,21 +147,29 @@ export function GameDetailsContextProvider({
}); });
window.electron.getGameStats(objectId, shop as GameShop).then((result) => { window.electron.getGameStats(objectId, shop as GameShop).then((result) => {
if (abortController.signal.aborted) return;
setStats(result); setStats(result);
}); });
window.electron window.electron
.getGameAchievements(objectId, shop as GameShop) .getGameAchievements(objectId, shop as GameShop)
.then((achievements) => { .then((achievements) => {
// TODO: race condition if (abortController.signal.aborted) return;
if (!userDetails) return;
setAchievements(achievements); setAchievements(achievements);
}) })
.catch(() => { .catch(() => {});
// TODO: handle user not logged in error
});
updateGame(); updateGame();
}, [updateGame, dispatch, gameTitle, objectId, shop, i18n.language]); }, [
updateGame,
dispatch,
gameTitle,
objectId,
shop,
i18n.language,
userDetails,
]);
useEffect(() => { useEffect(() => {
setShopDetails(null); setShopDetails(null);
@ -180,6 +202,7 @@ export function GameDetailsContextProvider({
objectId, objectId,
shop, shop,
(achievements) => { (achievements) => {
if (!userDetails) return;
setAchievements(achievements); setAchievements(achievements);
} }
); );
@ -187,7 +210,7 @@ export function GameDetailsContextProvider({
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, [objectId, shop]); }, [objectId, shop, userDetails]);
const getDownloadsPath = async () => { const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath; if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;

View file

@ -29,6 +29,7 @@ export function SidebarSection({ title, children }: SidebarSectionProps) {
maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0", maxHeight: isOpen ? `${content.current?.scrollHeight}px` : "0",
overflow: "hidden", overflow: "hidden",
transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)", transition: "max-height 0.4s cubic-bezier(0, 1, 0, 1)",
position: "relative",
}} }}
> >
{children} {children}

View file

@ -1,16 +1,49 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import type { HowLongToBeatCategory, SteamAppDetails } from "@types"; import type {
HowLongToBeatCategory,
SteamAppDetails,
UserAchievement,
} from "@types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Link } from "@renderer/components"; import { Button, Link } from "@renderer/components";
import * as styles from "./sidebar.css"; import * as styles from "./sidebar.css";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import { useDate, useFormat } from "@renderer/hooks"; import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; import { DownloadIcon, LockIcon, PeopleIcon } from "@primer/octicons-react";
import { HowLongToBeatSection } from "./how-long-to-beat-section"; import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { howLongToBeatEntriesTable } from "@renderer/dexie"; import { howLongToBeatEntriesTable } from "@renderer/dexie";
import { SidebarSection } from "../sidebar-section/sidebar-section"; import { SidebarSection } from "../sidebar-section/sidebar-section";
import { buildGameAchievementPath } from "@renderer/helpers"; import { buildGameAchievementPath } from "@renderer/helpers";
import { SPACING_UNIT } from "@renderer/theme.css";
const fakeAchievements: UserAchievement[] = [
{
displayName: "Timber!!",
name: "",
hidden: false,
description: "Chop down your first tree.",
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
unlocked: true,
unlockTime: Date.now(),
},
{
displayName: "Supreme Helper Minion!",
name: "",
hidden: false,
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
unlocked: false,
unlockTime: null,
},
{
displayName: "Feast of Midas",
name: "",
hidden: false,
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
unlocked: false,
unlockTime: null,
},
];
export function Sidebar() { export function Sidebar() {
const [howLongToBeat, setHowLongToBeat] = useState<{ const [howLongToBeat, setHowLongToBeat] = useState<{
@ -18,6 +51,8 @@ export function Sidebar() {
data: HowLongToBeatCategory[] | null; data: HowLongToBeatCategory[] | null;
}>({ isLoading: true, data: null }); }>({ isLoading: true, data: null });
const { userDetails } = useUserDetails();
const [activeRequirement, setActiveRequirement] = const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum"); useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
@ -68,9 +103,53 @@ export function Sidebar() {
return ( return (
<aside className={styles.contentSidebar}> <aside className={styles.contentSidebar}>
{achievements.length > 0 && ( {userDetails === null && (
<SidebarSection title={t("achievements")}>
<div
style={{
position: "absolute",
zIndex: 2,
inset: 0,
width: "100%",
height: "100%",
background: "rgba(0, 0, 0, 0.7)",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
}}
>
<LockIcon size={36} />
<h3>{t("sign_in_to_see_achievements")}</h3>
</div>
<ul className={styles.list} style={{ filter: "blur(4px)" }}>
{fakeAchievements.map((achievement, index) => (
<li key={index}>
<div className={styles.listItem}>
<img
style={{ filter: "blur(8px)" }}
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
src={achievement.icon}
alt={achievement.displayName}
/>
<div>
<p>{achievement.displayName}</p>
<small>
{achievement.unlockTime && format(achievement.unlockTime)}
</small>
</div>
</div>
</li>
))}
</ul>
</SidebarSection>
)}
{userDetails && achievements.length > 0 && (
<SidebarSection <SidebarSection
title={t("achievements", { title={t("achievements_count", {
unlockedCount: achievements.filter((a) => a.unlocked).length, unlockedCount: achievements.filter((a) => a.unlocked).length,
achievementsCount: achievements.length, achievementsCount: achievements.length,
})} })}
@ -93,7 +172,6 @@ export function Sidebar() {
})} })}
src={achievement.icon} src={achievement.icon}
alt={achievement.displayName} alt={achievement.displayName}
loading="lazy"
/> />
<div> <div>
<p>{achievement.displayName}</p> <p>{achievement.displayName}</p>