mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-15 04:32:13 +00:00
feat: achievement section for user not logged in
This commit is contained in:
parent
a064958d4c
commit
a4475d2145
7 changed files with 131 additions and 19 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue