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}}\"",
"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.",
"achievements": "Achievements {{unlockedCount}}/{{achievementsCount}}",
"achievements": "Achievements",
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Cloud save",
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
"backups": "Backups",
@ -146,7 +147,8 @@
"backup_uploaded": "Backup uploaded",
"backup_deleted": "Backup deleted",
"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": {
"title": "Activate Hydra",

View File

@ -127,7 +127,8 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
"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.",
"achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"achievements": "Conquistas",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"cloud_save": "Salvamento em nuvem",
"cloud_save_description": "Matenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
"backups": "Backups",
@ -142,7 +143,8 @@
"backup_uploaded": "Backup criado",
"backup_deleted": "Backup apagado",
"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": {
"title": "Ativação",

View File

@ -116,7 +116,8 @@
"executable_path_in_use": "Executável em uso por \"{{game}}\"",
"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.",
"achievements": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"achievements": "Conquistas",
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
"see_all_achievements": "Ver todas as conquistas"
},
"activation": {

View File

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

View File

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

View File

@ -1,16 +1,49 @@
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 { Button, Link } from "@renderer/components";
import * as styles from "./sidebar.css";
import { gameDetailsContext } from "@renderer/context";
import { useDate, useFormat } from "@renderer/hooks";
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
import { DownloadIcon, LockIcon, PeopleIcon } from "@primer/octicons-react";
import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { howLongToBeatEntriesTable } from "@renderer/dexie";
import { SidebarSection } from "../sidebar-section/sidebar-section";
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() {
const [howLongToBeat, setHowLongToBeat] = useState<{
@ -18,6 +51,8 @@ export function Sidebar() {
data: HowLongToBeatCategory[] | null;
}>({ isLoading: true, data: null });
const { userDetails } = useUserDetails();
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
@ -68,9 +103,53 @@ export function Sidebar() {
return (
<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
title={t("achievements", {
title={t("achievements_count", {
unlockedCount: achievements.filter((a) => a.unlocked).length,
achievementsCount: achievements.length,
})}
@ -93,7 +172,6 @@ export function Sidebar() {
})}
src={achievement.icon}
alt={achievement.displayName}
loading="lazy"
/>
<div>
<p>{achievement.displayName}</p>