mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
feat: update achievements on sidebar
This commit is contained in:
parent
446b03eeff
commit
ffac677e3f
@ -145,7 +145,8 @@
|
||||
"no_backups": "You haven't created any backups for this game yet",
|
||||
"backup_uploaded": "Backup uploaded",
|
||||
"backup_deleted": "Backup deleted",
|
||||
"backup_restored": "Backup restored"
|
||||
"backup_restored": "Backup restored",
|
||||
"see_all_achievements": "See all achievements"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
|
@ -127,7 +127,7 @@
|
||||
"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 ({{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",
|
||||
@ -141,7 +141,8 @@
|
||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||
"backup_uploaded": "Backup criado",
|
||||
"backup_deleted": "Backup apagado",
|
||||
"backup_restored": "Backup restaurado"
|
||||
"backup_restored": "Backup restaurado",
|
||||
"see_all_achievements": "Ver todas as conquistas"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
|
@ -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 ({{unlockedCount}}/{{achievementsCount}})",
|
||||
"see_all_achievements": "Ver todas as conquistas"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
|
@ -1,4 +1,9 @@
|
||||
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types";
|
||||
import type {
|
||||
AchievementData,
|
||||
GameAchievement,
|
||||
GameShop,
|
||||
UnlockedAchievement,
|
||||
} from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
@ -18,7 +23,7 @@ const getAchievements = async (
|
||||
where: { objectId, shop },
|
||||
});
|
||||
|
||||
const achievementsData = cachedAchievements?.achievements
|
||||
const achievementsData: AchievementData[] = cachedAchievements?.achievements
|
||||
? JSON.parse(cachedAchievements.achievements)
|
||||
: await getGameAchievementData(objectId, shop);
|
||||
|
||||
@ -51,7 +56,7 @@ export const getGameAchievements = async (
|
||||
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
const unlockedAchiement = unlockedAchievements.find(
|
||||
const unlockedAchiementData = unlockedAchievements.find(
|
||||
(localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() ==
|
||||
@ -60,20 +65,33 @@ export const getGameAchievements = async (
|
||||
}
|
||||
);
|
||||
|
||||
if (unlockedAchiement) {
|
||||
const icongray = achievementData.icongray.endsWith("/")
|
||||
? achievementData.icon
|
||||
: achievementData.icongray;
|
||||
|
||||
if (unlockedAchiementData) {
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchiement.unlockTime,
|
||||
unlockTime: unlockedAchiementData.unlockTime,
|
||||
icongray,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...achievementData, unlocked: false, unlockTime: null };
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
icongray,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
.sort((a: GameAchievement, b: GameAchievement) => {
|
||||
if (a.unlocked && !b.unlocked) return -1;
|
||||
if (!a.unlocked && b.unlocked) return 1;
|
||||
return b.unlockTime - a.unlockTime;
|
||||
if (a.unlocked && b.unlocked) {
|
||||
return b.unlockTime! - a.unlockTime!;
|
||||
}
|
||||
return Number(a.hidden) - Number(b.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -39,6 +39,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
||||
if (location.pathname.startsWith("/achievements")) return headerTitle;
|
||||
if (location.pathname.startsWith("/profile")) return headerTitle;
|
||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { setHeaderTitle } from "@renderer/features";
|
||||
import { useAppDispatch, useDate } from "@renderer/hooks";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { GameAchievement, GameShop } from "@types";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -8,10 +9,13 @@ export function Achievement() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const objectId = searchParams.get("objectId");
|
||||
const shop = searchParams.get("shop");
|
||||
const title = searchParams.get("title");
|
||||
const userId = searchParams.get("userId");
|
||||
|
||||
const { format } = useDate();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -24,6 +28,12 @@ export function Achievement() {
|
||||
}
|
||||
}, [objectId, shop, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (title) {
|
||||
dispatch(setHeaderTitle(title + " Achievements"));
|
||||
}
|
||||
}, [dispatch, title]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Achievement</h1>
|
||||
@ -61,6 +71,7 @@ export function Achievement() {
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<p>{achievement.description}</p>
|
||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { globalStyle, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const contentSidebar = style({
|
||||
borderLeft: `solid 1px ${vars.color.border}`,
|
||||
@ -110,3 +111,46 @@ globalStyle(`${requirementsDetails} a`, {
|
||||
display: "flex",
|
||||
color: vars.color.body,
|
||||
});
|
||||
|
||||
export const list = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
transition: "all ease 0.1s",
|
||||
color: vars.color.muted,
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
borderRadius: "4px",
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
textDecoration: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export const listItemImage = recipe({
|
||||
base: {
|
||||
width: "54px",
|
||||
height: "54px",
|
||||
borderRadius: "4px",
|
||||
objectFit: "cover",
|
||||
},
|
||||
variants: {
|
||||
unlocked: {
|
||||
false: {
|
||||
filter: "grayscale(100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -7,10 +7,10 @@ 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 { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
||||
import { howLongToBeatEntriesTable } from "@renderer/dexie";
|
||||
import { SidebarSection } from "../sidebar-section/sidebar-section";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function Sidebar() {
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
@ -30,7 +30,12 @@ export function Sidebar() {
|
||||
const { numberFormatter } = useFormat();
|
||||
|
||||
const buildGameAchievementPath = () => {
|
||||
const urlParams = new URLSearchParams({ objectId: objectId!, shop });
|
||||
const urlParams = new URLSearchParams({
|
||||
objectId: objectId!,
|
||||
shop,
|
||||
title: gameTitle,
|
||||
});
|
||||
|
||||
return `/achievements?${urlParams.toString()}`;
|
||||
};
|
||||
|
||||
@ -80,49 +85,43 @@ export function Sidebar() {
|
||||
achievementsCount: achievements.length,
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
{achievements.slice(0, 6).map((achievement, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
title={achievement.description}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
height: "60px",
|
||||
width: "60px",
|
||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
||||
}}
|
||||
src={
|
||||
achievement.unlocked
|
||||
? achievement.icon
|
||||
: achievement.icongray
|
||||
}
|
||||
alt={achievement.displayName}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||
</div>
|
||||
</div>
|
||||
<ul className={styles.list}>
|
||||
{achievements.slice(0, 4).map((achievement, index) => (
|
||||
<li key={index}>
|
||||
<Link
|
||||
to={buildGameAchievementPath()}
|
||||
className={styles.listItem}
|
||||
title={achievement.description}
|
||||
>
|
||||
<img
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
src={
|
||||
achievement.unlocked
|
||||
? achievement.icon
|
||||
: achievement.icongray
|
||||
}
|
||||
alt={achievement.displayName}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div>
|
||||
<p>{achievement.displayName}</p>
|
||||
<small>
|
||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||
</small>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
style={{ textAlign: "center" }}
|
||||
to={buildGameAchievementPath()}
|
||||
>
|
||||
{t("see_all_achievements")}
|
||||
</Link>
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
|
||||
|
@ -28,6 +28,15 @@ export interface GameRepack {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AchievementData {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
icon: string;
|
||||
icongray: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface GameAchievement {
|
||||
name: string;
|
||||
displayName: string;
|
||||
@ -36,6 +45,7 @@ export interface GameAchievement {
|
||||
unlockTime: number | null;
|
||||
icon: string;
|
||||
icongray: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export type ShopDetails = SteamAppDetails & {
|
||||
|
Loading…
Reference in New Issue
Block a user