mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 08:43:48 +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",
|
"no_backups": "You haven't created any backups for this game yet",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
|
@ -127,7 +127,7 @@
|
|||||||
"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 ({{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",
|
||||||
@ -141,7 +141,8 @@
|
|||||||
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
"no_backups": "Você ainda não fez nenhum backup deste jogo",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"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 ({{unlockedCount}}/{{achievementsCount}})",
|
||||||
|
"see_all_achievements": "Ver todas as conquistas"
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"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 { registerEvent } from "../register-event";
|
||||||
import {
|
import {
|
||||||
gameAchievementRepository,
|
gameAchievementRepository,
|
||||||
@ -18,7 +23,7 @@ const getAchievements = async (
|
|||||||
where: { objectId, shop },
|
where: { objectId, shop },
|
||||||
});
|
});
|
||||||
|
|
||||||
const achievementsData = cachedAchievements?.achievements
|
const achievementsData: AchievementData[] = cachedAchievements?.achievements
|
||||||
? JSON.parse(cachedAchievements.achievements)
|
? JSON.parse(cachedAchievements.achievements)
|
||||||
: await getGameAchievementData(objectId, shop);
|
: await getGameAchievementData(objectId, shop);
|
||||||
|
|
||||||
@ -51,7 +56,7 @@ export const getGameAchievements = async (
|
|||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
const unlockedAchiement = unlockedAchievements.find(
|
const unlockedAchiementData = unlockedAchievements.find(
|
||||||
(localAchievement) => {
|
(localAchievement) => {
|
||||||
return (
|
return (
|
||||||
localAchievement.name.toUpperCase() ==
|
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 {
|
return {
|
||||||
...achievementData,
|
...achievementData,
|
||||||
unlocked: true,
|
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;
|
||||||
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(() => {
|
const title = useMemo(() => {
|
||||||
if (location.pathname.startsWith("/game")) return headerTitle;
|
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("/profile")) return headerTitle;
|
||||||
if (location.pathname.startsWith("/search")) return t("search_results");
|
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 { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { GameAchievement, GameShop } from "@types";
|
import { GameAchievement, GameShop } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -8,10 +9,13 @@ export function Achievement() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const objectId = searchParams.get("objectId");
|
const objectId = searchParams.get("objectId");
|
||||||
const shop = searchParams.get("shop");
|
const shop = searchParams.get("shop");
|
||||||
|
const title = searchParams.get("title");
|
||||||
const userId = searchParams.get("userId");
|
const userId = searchParams.get("userId");
|
||||||
|
|
||||||
const { format } = useDate();
|
const { format } = useDate();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
const [achievements, setAchievements] = useState<GameAchievement[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -24,6 +28,12 @@ export function Achievement() {
|
|||||||
}
|
}
|
||||||
}, [objectId, shop, userId]);
|
}, [objectId, shop, userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (title) {
|
||||||
|
dispatch(setHeaderTitle(title + " Achievements"));
|
||||||
|
}
|
||||||
|
}, [dispatch, title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Achievement</h1>
|
<h1>Achievement</h1>
|
||||||
@ -61,6 +71,7 @@ export function Achievement() {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p>{achievement.displayName}</p>
|
<p>{achievement.displayName}</p>
|
||||||
|
<p>{achievement.description}</p>
|
||||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { globalStyle, style } from "@vanilla-extract/css";
|
import { globalStyle, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
|
||||||
export const contentSidebar = style({
|
export const contentSidebar = style({
|
||||||
borderLeft: `solid 1px ${vars.color.border}`,
|
borderLeft: `solid 1px ${vars.color.border}`,
|
||||||
@ -110,3 +111,46 @@ globalStyle(`${requirementsDetails} a`, {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
color: vars.color.body,
|
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 { gameDetailsContext } from "@renderer/context";
|
||||||
import { useDate, useFormat } from "@renderer/hooks";
|
import { useDate, useFormat } from "@renderer/hooks";
|
||||||
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
|
||||||
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 { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||||
@ -30,7 +30,12 @@ export function Sidebar() {
|
|||||||
const { numberFormatter } = useFormat();
|
const { numberFormatter } = useFormat();
|
||||||
|
|
||||||
const buildGameAchievementPath = () => {
|
const buildGameAchievementPath = () => {
|
||||||
const urlParams = new URLSearchParams({ objectId: objectId!, shop });
|
const urlParams = new URLSearchParams({
|
||||||
|
objectId: objectId!,
|
||||||
|
shop,
|
||||||
|
title: gameTitle,
|
||||||
|
});
|
||||||
|
|
||||||
return `/achievements?${urlParams.toString()}`;
|
return `/achievements?${urlParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,34 +85,18 @@ export function Sidebar() {
|
|||||||
achievementsCount: achievements.length,
|
achievementsCount: achievements.length,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>
|
<ul className={styles.list}>
|
||||||
<Link to={buildGameAchievementPath()}>Ver todas</Link>
|
{achievements.slice(0, 4).map((achievement, index) => (
|
||||||
</span>
|
<li key={index}>
|
||||||
<div
|
<Link
|
||||||
style={{
|
to={buildGameAchievementPath()}
|
||||||
display: "flex",
|
className={styles.listItem}
|
||||||
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}
|
title={achievement.description}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{
|
className={styles.listItemImage({
|
||||||
height: "60px",
|
unlocked: achievement.unlocked,
|
||||||
width: "60px",
|
})}
|
||||||
filter: achievement.unlocked ? "none" : "grayscale(100%)",
|
|
||||||
}}
|
|
||||||
src={
|
src={
|
||||||
achievement.unlocked
|
achievement.unlocked
|
||||||
? achievement.icon
|
? achievement.icon
|
||||||
@ -118,11 +107,21 @@ export function Sidebar() {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p>{achievement.displayName}</p>
|
<p>{achievement.displayName}</p>
|
||||||
|
<small>
|
||||||
{achievement.unlockTime && format(achievement.unlockTime)}
|
{achievement.unlockTime && format(achievement.unlockTime)}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
|
<Link
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
to={buildGameAchievementPath()}
|
||||||
|
>
|
||||||
|
{t("see_all_achievements")}
|
||||||
|
</Link>
|
||||||
|
</ul>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -28,6 +28,15 @@ export interface GameRepack {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AchievementData {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
icon: string;
|
||||||
|
icongray: string;
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameAchievement {
|
export interface GameAchievement {
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -36,6 +45,7 @@ export interface GameAchievement {
|
|||||||
unlockTime: number | null;
|
unlockTime: number | null;
|
||||||
icon: string;
|
icon: string;
|
||||||
icongray: string;
|
icongray: string;
|
||||||
|
hidden: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShopDetails = SteamAppDetails & {
|
export type ShopDetails = SteamAppDetails & {
|
||||||
|
Loading…
Reference in New Issue
Block a user