feat: updating play label on hero panel

This commit is contained in:
Hydra 2024-04-18 22:26:17 +01:00
parent f1bdec484e
commit 7d675f6acf
40 changed files with 2049 additions and 745 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -15,7 +15,8 @@ export interface HeaderProps {
}
const pathTitle: Record<string, string> = {
"/": "catalogue",
"/": "home",
"/catalogue": "catalogue",
"/downloads": "downloads",
"/settings": "settings",
};

View file

@ -30,7 +30,7 @@ export const heroMedia = style({
transition: "all ease 0.2s",
selectors: {
[`${hero}:hover &`]: {
transform: "scale(1.05)",
transform: "scale(1.02)",
},
},
});

View file

@ -1,14 +0,0 @@
import { style } from "@vanilla-extract/css";
export const downloadIconWrapper = style({
width: "16px",
height: "12px",
position: "relative",
});
export const downloadIcon = style({
width: "24px",
position: "absolute",
left: "-4px",
top: "-9px",
});

View file

@ -2,7 +2,6 @@ import { useRef } from "react";
import Lottie from "lottie-react";
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
import * as styles from "./download-icon.css";
export interface DownloadIconProps {
isDownloading: boolean;
@ -12,15 +11,12 @@ export function DownloadIcon({ isDownloading }: DownloadIconProps) {
const lottieRef = useRef(null);
return (
<div className={styles.downloadIconWrapper}>
<Lottie
lottieRef={lottieRef}
animationData={downloadingAnimation}
loop={isDownloading}
autoplay={isDownloading}
className={styles.downloadIcon}
onDOMLoaded={() => lottieRef.current?.setSpeed(1.7)}
/>
</div>
<Lottie
lottieRef={lottieRef}
animationData={downloadingAnimation}
loop={isDownloading}
autoplay={isDownloading}
style={{ width: 16 }}
/>
);
}

View file

@ -1,11 +1,16 @@
import { GearIcon, ListUnorderedIcon } from "@primer/octicons-react";
import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react";
import { DownloadIcon } from "./download-icon";
export const routes = [
{
path: "/",
nameKey: "home",
render: () => <HomeIcon />,
},
{
path: "/catalogue",
nameKey: "catalogue",
render: () => <ListUnorderedIcon />,
render: () => <AppsIcon />,
},
{
path: "/downloads",

View file

@ -45,6 +45,10 @@ declare global {
shop: GameShop,
title: string
) => Promise<HowLongToBeatCategory[] | null>;
getGames: (
take?: number,
prevCursor?: number
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
/* Library */
addGameToLibrary: (

View file

@ -3,13 +3,11 @@ import type { PayloadAction } from "@reduxjs/toolkit";
interface WindowState {
draggingDisabled: boolean;
scrollingDisabled: boolean;
headerTitle: string;
}
const initialState: WindowState = {
draggingDisabled: false,
scrollingDisabled: false,
headerTitle: "",
};
@ -20,14 +18,10 @@ export const windowSlice = createSlice({
toggleDragging: (state, action: PayloadAction<boolean>) => {
state.draggingDisabled = action.payload;
},
toggleScrolling: (state, action: PayloadAction<boolean>) => {
state.scrollingDisabled = action.payload;
},
setHeaderTitle: (state, action: PayloadAction<string>) => {
state.headerTitle = action.payload;
},
},
});
export const { toggleDragging, toggleScrolling, setHeaderTitle } =
windowSlice.actions;
export const { toggleDragging, setHeaderTitle } = windowSlice.actions;

View file

@ -19,11 +19,12 @@ import "react-loading-skeleton/dist/skeleton.css";
import { App } from "./app";
import {
Catalogue,
Home,
Downloads,
GameDetails,
SearchResults,
Settings,
Catalogue,
} from "@renderer/pages";
import { store } from "./store";
@ -41,6 +42,10 @@ const router = createHashRouter([
children: [
{
path: "/",
Component: Home,
},
{
path: "/catalogue",
Component: Catalogue,
},
{

View file

@ -1,141 +1,113 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button, GameCard } from "@renderer/components";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { useTranslation } from "react-i18next";
import { Button, GameCard, Hero } from "@renderer/components";
import type { CatalogueCategory, CatalogueEntry } from "@types";
import type { CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./catalogue.css";
import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks";
import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
import { useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "../home/home.css";
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
export function Catalogue() {
const dispatch = useAppDispatch();
const { t } = useTranslation("catalogue");
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
const [isLoading, setIsLoading] = useState(false);
const contentRef = useRef<HTMLElement>(null);
const cursorRef = useRef<number>(0);
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
const [searchParams] = useSearchParams();
const cursor = Number(searchParams.get("cursor") ?? 0);
const [catalogue, setCatalogue] = useState<
Record<CatalogueCategory, CatalogueEntry[]>
>({
trending: [],
recently_added: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setIsLoading(true);
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
});
}, []);
const currentCategory = searchParams.get("category") || categories[0];
const handleSelectCategory = (category: CatalogueCategory) => {
if (category !== currentCategory) {
getCatalogue(category);
navigate(`/?category=${category}`, { replace: true });
}
};
const getRandomGame = useCallback(() => {
setIsLoadingRandomGame(true);
window.electron
.getRandomGame()
.then((objectID) => {
randomGameObjectID.current = objectID;
})
.finally(() => {
setIsLoadingRandomGame(false);
});
}, []);
const handleRandomizerClick = () => {
const searchParams = new URLSearchParams({
fromRandomizer: "1",
});
navigate(
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
);
const handleGameClick = (game: CatalogueEntry) => {
dispatch(clearSearch());
navigate(`/game/${game.shop}/${game.objectID}`);
};
useEffect(() => {
if (contentRef.current) contentRef.current.scrollTop = 0;
setIsLoading(true);
getCatalogue(currentCategory as CatalogueCategory);
getRandomGame();
}, [getCatalogue, currentCategory, getRandomGame]);
setSearchResults([]);
window.electron
.getGames(24, cursor)
.then(({ results, cursor }) => {
return new Promise((resolve) => {
setTimeout(() => {
cursorRef.current = cursor;
setSearchResults(results);
resolve(null);
}, 500);
});
})
.finally(() => {
setIsLoading(false);
});
}, [dispatch, cursor, searchParams]);
const handleNextPage = () => {
const params = new URLSearchParams({
cursor: cursorRef.current.toString(),
});
navigate(`/catalogue?${params.toString()}`);
};
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<h2>{t("featured")}</h2>
<section
style={{
padding: `16px 32px`,
display: "flex",
width: "100%",
justifyContent: "space-between",
borderBottom: `1px solid ${vars.color.borderColor}`,
}}
>
<Button
onClick={() => navigate(-1)}
theme="outline"
disabled={cursor === 0 || isLoading}
>
<ArrowLeftIcon />
{t("previous_page")}
</Button>
<Hero />
<Button onClick={handleNextPage} theme="outline" disabled={isLoading}>
{t("next_page")}
<ArrowRightIcon />
</Button>
</section>
<section className={styles.catalogueHeader}>
<div className={styles.catalogueCategories}>
{categories.map((category) => (
<Button
key={category}
theme={currentCategory === category ? "primary" : "outline"}
onClick={() => handleSelectCategory(category)}
>
{t(category)}
</Button>
<section ref={contentRef} className={styles.content}>
<section className={styles.cards}>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))}
</div>
<Button
onClick={handleRandomizerClick}
theme="outline"
disabled={isLoadingRandomGame}
>
<div style={{ width: 16, height: 16, position: "relative" }}>
<Lottie
animationData={starsAnimation}
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
loop
/>
</div>
{t("surprise_me")}
</Button>
</section>
<h2>{t(currentCategory)}</h2>
<section className={styles.cards({})}>
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))
: catalogue[currentCategory as CatalogueCategory].map((result) => (
{!isLoading && searchResults.length > 0 && (
<>
{searchResults.map((game) => (
<GameCard
key={result.objectID}
game={result}
onClick={() =>
navigate(`/game/${result.shop}/${result.objectID}`)
}
key={game.objectID}
game={game}
onClick={() => handleGameClick(game)}
disabled={!game.repacks.length}
/>
))}
</>
)}
</section>
</section>
</SkeletonTheme>

View file

@ -217,16 +217,19 @@ export const howLongToBeatCategorySkeleton = style({
export const randomizerButton = style({
animationName: slideIn,
animationDuration: "0.4s",
animationDuration: "0.2s",
position: "fixed",
bottom: 26 + 16,
/* Bottom panel height + spacing */
bottom: `${26 + SPACING_UNIT * 2}px`,
/* Scroll bar + spacing */
right: `${9 + SPACING_UNIT * 2}px`,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 10px 3px",
border: `solid 1px ${vars.color.borderColor}`,
backgroundColor: vars.color.darkBackground,
border: `solid 2px ${vars.color.borderColor}`,
backgroundColor: vars.color.background,
":hover": {
backgroundColor: vars.color.background,
boxShadow: "rgba(255, 255, 255, 0.1) 0px 0px 15px 5px",
opacity: 1,
opacity: "1",
},
":active": {
transform: "scale(0.98)",

View file

@ -104,7 +104,9 @@ export function HeroPanel({
window.electron
.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Game executable (.exe)", extensions: ["exe"] }],
filters: [
{ name: "Game executable (.exe)", extensions: ["exe", "app"] },
],
})
.then(({ filePaths }) => {
if (filePaths && filePaths.length > 0) {
@ -209,11 +211,15 @@ export function HeroPanel({
})}
</p>
<p>
{t("last_time_played", {
period: lastTimePlayed,
})}
</p>
{isGamePlaying ? (
<p>{t("playing_now")}</p>
) : (
<p>
{t("last_time_played", {
period: lastTimePlayed,
})}
</p>
)}
</>
);
}

View file

@ -1,6 +1,6 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT } from "@renderer/theme.css";
export const catalogueCategories = style({
display: "flex",
@ -23,12 +23,4 @@ export const cards = recipe({
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
},
variants: {
searching: {
true: {
pointerEvents: "none",
opacity: vars.opacity.disabled,
},
},
},
});

View file

@ -1,13 +1,12 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
export const catalogueCategories = style({
export const homeCategories = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const catalogueHeader = style({
export const homeHeader = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
justifyContent: "space-between",
@ -24,30 +23,20 @@ export const content = style({
overflowY: "auto",
});
export const cards = recipe({
base: {
display: "grid",
gridTemplateColumns: "repeat(1, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
"@media": {
"(min-width: 768px)": {
gridTemplateColumns: "repeat(2, 1fr)",
},
"(min-width: 1250px)": {
gridTemplateColumns: "repeat(3, 1fr)",
},
"(min-width: 1600px)": {
gridTemplateColumns: "repeat(4, 1fr)",
},
export const cards = style({
display: "grid",
gridTemplateColumns: "repeat(1, 1fr)",
gap: `${SPACING_UNIT * 2}px`,
transition: "all ease 0.2s",
"@media": {
"(min-width: 768px)": {
gridTemplateColumns: "repeat(2, 1fr)",
},
},
variants: {
searching: {
true: {
pointerEvents: "none",
opacity: vars.opacity.disabled,
},
"(min-width: 1250px)": {
gridTemplateColumns: "repeat(3, 1fr)",
},
"(min-width: 1600px)": {
gridTemplateColumns: "repeat(4, 1fr)",
},
},
});

View file

@ -0,0 +1,143 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { Button, GameCard, Hero } from "@renderer/components";
import type { CatalogueCategory, CatalogueEntry } from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./home.css";
import { vars } from "@renderer/theme.css";
import Lottie from "lottie-react";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
export function Home() {
const { t } = useTranslation("home");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
const randomGameObjectID = useRef<string | null>(null);
const [searchParams] = useSearchParams();
const [catalogue, setCatalogue] = useState<
Record<CatalogueCategory, CatalogueEntry[]>
>({
trending: [],
recently_added: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setIsLoading(true);
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
});
}, []);
const currentCategory = searchParams.get("category") || categories[0];
const handleSelectCategory = (category: CatalogueCategory) => {
if (category !== currentCategory) {
getCatalogue(category);
navigate(`/?category=${category}`, { replace: true });
}
};
const getRandomGame = useCallback(() => {
setIsLoadingRandomGame(true);
window.electron
.getRandomGame()
.then((objectID) => {
randomGameObjectID.current = objectID;
})
.finally(() => {
setIsLoadingRandomGame(false);
});
}, []);
const handleRandomizerClick = () => {
const searchParams = new URLSearchParams({
fromRandomizer: "1",
});
navigate(
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
);
};
useEffect(() => {
setIsLoading(true);
getCatalogue(currentCategory as CatalogueCategory);
getRandomGame();
}, [getCatalogue, currentCategory, getRandomGame]);
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<h2>{t("featured")}</h2>
<Hero />
<section className={styles.homeHeader}>
<div className={styles.homeCategories}>
{categories.map((category) => (
<Button
key={category}
theme={currentCategory === category ? "primary" : "outline"}
onClick={() => handleSelectCategory(category)}
>
{t(category)}
</Button>
))}
</div>
<Button
onClick={handleRandomizerClick}
theme="outline"
disabled={isLoadingRandomGame}
>
<div style={{ width: 16, height: 16, position: "relative" }}>
<Lottie
animationData={starsAnimation}
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
loop
/>
</div>
{t("surprise_me")}
</Button>
</section>
<h2>{t(currentCategory)}</h2>
<section className={styles.cards}>
{isLoading
? Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />
))
: catalogue[currentCategory as CatalogueCategory].map((result) => (
<GameCard
key={result.objectID}
game={result}
onClick={() =>
navigate(`/game/${result.shop}/${result.objectID}`)
}
/>
))}
</section>
</section>
</SkeletonTheme>
);
}

View file

@ -13,12 +13,12 @@ import { vars } from "@renderer/theme.css";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "./catalogue.css";
import * as styles from "./home.css";
export function SearchResults() {
const dispatch = useAppDispatch();
const { t } = useTranslation("catalogue");
const { t } = useTranslation("home");
const [searchParams] = useSearchParams();
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
@ -54,7 +54,7 @@ export function SearchResults() {
return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}>
<section className={styles.cards({ searching: false })}>
<section className={styles.cards}>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton key={index} className={styles.cardSkeleton} />

View file

@ -1,5 +1,6 @@
export * from "./catalogue/catalogue";
export * from "./home/home";
export * from "./game-details/game-details";
export * from "./downloads/downloads";
export * from "./catalogue/search-results";
export * from "./home/search-results";
export * from "./settings/settings";
export * from "./catalogue/catalogue";