feat: adding accuracy as text instead of colours

This commit is contained in:
Hydra 2024-04-15 00:38:25 +01:00
parent 05a31cf6a6
commit c0274ee7e7
No known key found for this signature in database
11 changed files with 83 additions and 82 deletions

View File

@ -2,5 +2,4 @@ module.exports = {
semi: true, semi: true,
trailingComma: "all", trailingComma: "all",
singleQuote: false, singleQuote: false,
tabWidth: 2,
}; };

View File

@ -1,6 +1,8 @@
# Hydra # Hydra
<a href="https://discord.gg/hydralauncher" target="_blank">![Discord](https://img.shields.io/discord/1220692017311645737?style=flat&logo=discord&label=Hydra&labelColor=%231c1c1c)</a> <a href="https://discord.gg/hydralauncher" target="_blank">![Discord](https://img.shields.io/discord/1220692017311645737?style=flat&logo=discord&label=Hydra&labelColor=%231c1c1c)</a>
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)
![GitHub package.json version](https://img.shields.io/github/package-json/v/hydralauncher/hydra)
Hydra is a game launcher with its own embedded bittorrent client and a self-managed repack scraper. Hydra is a game launcher with its own embedded bittorrent client and a self-managed repack scraper.
The launcher is written in TypeScript (Electron) and Python, which handles the torrenting system by using [libtorrent](https://www.libtorrent.org/). The launcher is written in TypeScript (Electron) and Python, which handles the torrenting system by using [libtorrent](https://www.libtorrent.org/).

View File

@ -58,12 +58,9 @@
"publisher": "Published by {{publisher}}", "publisher": "Published by {{publisher}}",
"copy_link_to_clipboard": "Copy link", "copy_link_to_clipboard": "Copy link",
"copied_link_to_clipboard": "Link copied", "copied_link_to_clipboard": "Link copied",
"main_story": "Main story",
"main_plus_sides": "Main story + sides",
"completionist": "Completionist",
"all_styles": "All styles",
"hours": "hours", "hours": "hours",
"minutes": "minutes" "minutes": "minutes",
"accuracy": "{{accuracy}}% accuracy"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -58,12 +58,9 @@
"publisher": "Publicado por {{publisher}}", "publisher": "Publicado por {{publisher}}",
"copy_link_to_clipboard": "Copiar enlace", "copy_link_to_clipboard": "Copiar enlace",
"copied_link_to_clipboard": "Enlace copiado", "copied_link_to_clipboard": "Enlace copiado",
"main_story": "Historia principal",
"main_plus_sides": "Historia principal + extras",
"completionist": "Completista",
"all_styles": "Todo",
"hours": "horas", "hours": "horas",
"minutes": "minutos" "minutes": "minutos",
"accuracy": "{{accuracy}}% precisión"
}, },
"activation": { "activation": {
"title": "Activar Hydra", "title": "Activar Hydra",

View File

@ -58,12 +58,9 @@
"publisher": "Publicado por {{publisher}}", "publisher": "Publicado por {{publisher}}",
"copy_link_to_clipboard": "Copiar link", "copy_link_to_clipboard": "Copiar link",
"copied_link_to_clipboard": "Link copiado", "copied_link_to_clipboard": "Link copiado",
"main_story": "História principal",
"main_plus_sides": "Historia principal + extras",
"completionist": "Completista",
"all_styles": "Tudo",
"hours": "horas", "hours": "horas",
"minutes": "minutos" "minutes": "minutos",
"accuracy": "{{accuracy}}% de precisão"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -2,6 +2,7 @@ import { formatName } from "@main/helpers";
import axios from "axios"; import axios from "axios";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { requestWebPage } from "./repack-tracker/helpers"; import { requestWebPage } from "./repack-tracker/helpers";
import { HowLongToBeatCategory } from "@types";
export interface HowLongToBeatResult { export interface HowLongToBeatResult {
game_id: number; game_id: number;
@ -27,23 +28,15 @@ export const searchHowLongToBeat = async (gameName: string) => {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
Referer: "https://howlongtobeat.com/", Referer: "https://howlongtobeat.com/",
}, },
} },
); );
return response.data as HowLongToBeatSearchResponse; return response.data as HowLongToBeatSearchResponse;
}; };
export const classNameColor = { export const getHowLongToBeatGame = async (
time_40: "#ff3a3a", id: string,
time_50: "#cc3b51", ): Promise<HowLongToBeatCategory[]> => {
time_60: "#824985",
time_70: "#5650a1",
time_80: "#485cab",
time_90: "#3a6db5",
time_100: "#287fc2",
};
export const getHowLongToBeatGame = async (id: string) => {
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`); const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const { window } = new JSDOM(response); const { window } = new JSDOM(response);
@ -54,12 +47,14 @@ export const getHowLongToBeatGame = async (id: string) => {
return $lis.map(($li) => { return $lis.map(($li) => {
const title = $li.querySelector("h4").textContent; const title = $li.querySelector("h4").textContent;
const [, time] = Array.from(($li as HTMLElement).classList); const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
const accuracy = accuracyClassName.split("time_").at(1);
return { return {
title, title,
duration: $li.querySelector("h5").textContent, duration: $li.querySelector("h5").textContent,
color: classNameColor[time as keyof typeof classNameColor], accuracy,
}; };
}); });
}; };

View File

@ -139,19 +139,19 @@ export const descriptionHeaderInfo = style({
}); });
export const howLongToBeatCategoriesList = style({ export const howLongToBeatCategoriesList = style({
margin: 0, margin: "0",
padding: 16, padding: "16px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 16, gap: "16px",
}); });
export const howLongToBeatCategory = style({ export const howLongToBeatCategory = style({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 4, gap: "4px",
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderRadius: 8, borderRadius: "8px",
padding: `8px 16px`, padding: `8px 16px`,
border: `solid 1px ${vars.color.borderColor}`, border: `solid 1px ${vars.color.borderColor}`,
}); });
@ -161,13 +161,19 @@ export const howLongToBeatCategoryLabel = style({
color: "#DADBE1", color: "#DADBE1",
}); });
export const howLongToBeatCategorySkeleton = style({
border: `solid 1px ${vars.color.borderColor}`,
borderRadius: "8px",
height: "76px",
});
globalStyle(".bb_tag", { globalStyle(".bb_tag", {
marginTop: `${SPACING_UNIT * 2}px`, marginTop: `${SPACING_UNIT * 2}px`,
marginBottom: `${SPACING_UNIT * 2}px`, marginBottom: `${SPACING_UNIT * 2}px`,
}); });
globalStyle(`${description} img`, { globalStyle(`${description} img`, {
borderRadius: 5, borderRadius: "5px",
marginTop: `${SPACING_UNIT}px`, marginTop: `${SPACING_UNIT}px`,
marginBottom: `${SPACING_UNIT}px`, marginBottom: `${SPACING_UNIT}px`,
marginLeft: "auto", marginLeft: "auto",

View File

@ -31,9 +31,10 @@ export function GameDetails() {
const [color, setColor] = useState(""); const [color, setColor] = useState("");
const [clipboardLock, setClipboardLock] = useState(false); const [clipboardLock, setClipboardLock] = useState(false);
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null); const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
const [howLongToBeat, setHowLongToBeat] = useState< const [howLongToBeat, setHowLongToBeat] = useState<{
HowLongToBeatCategory[] | null isLoading: boolean;
>(null); data: HowLongToBeatCategory[] | null;
}>({ isLoading: true, data: null });
const [game, setGame] = useState<Game | null>(null); const [game, setGame] = useState<Game | null>(null);
const [activeRequirement, setActiveRequirement] = const [activeRequirement, setActiveRequirement] =
@ -81,7 +82,7 @@ export function GameDetails() {
window.electron window.electron
.getHowLongToBeat(objectID, "steam", result.name) .getHowLongToBeat(objectID, "steam", result.name)
.then((data) => { .then((data) => {
setHowLongToBeat(data); setHowLongToBeat({ isLoading: false, data });
}); });
setGameDetails(result); setGameDetails(result);
@ -89,7 +90,7 @@ export function GameDetails() {
}); });
getGame(); getGame();
setHowLongToBeat(null); setHowLongToBeat({ isLoading: true, data: null });
setClipboardLock(false); setClipboardLock(false);
}, [getGame, dispatch, navigate, objectID, i18n.language]); }, [getGame, dispatch, navigate, objectID, i18n.language]);
@ -103,12 +104,12 @@ export function GameDetails() {
shop, shop,
encodeURIComponent(gameDetails?.name), encodeURIComponent(gameDetails?.name),
i18n.language, i18n.language,
]) ]),
), ),
}); });
navigator.clipboard.writeText( navigator.clipboard.writeText(
OPEN_HYDRA_URL + `/?${searchParams.toString()}` OPEN_HYDRA_URL + `/?${searchParams.toString()}`,
); );
const zero = performance.now(); const zero = performance.now();
@ -134,7 +135,7 @@ export function GameDetails() {
repackId, repackId,
gameDetails.objectID, gameDetails.objectID,
gameDetails.name, gameDetails.name,
shop as GameShop shop as GameShop,
).then(() => { ).then(() => {
getGame(); getGame();
setShowRepacksModal(false); setShowRepacksModal(false);
@ -217,7 +218,10 @@ export function GameDetails() {
</div> </div>
<div className={styles.contentSidebar}> <div className={styles.contentSidebar}>
<HowLongToBeatSection howLongToBeatData={howLongToBeat} /> <HowLongToBeatSection
howLongToBeatData={howLongToBeat.data}
isLoading={howLongToBeat.isLoading}
/>
<div <div
className={styles.contentSidebarTitle} className={styles.contentSidebarTitle}

View File

@ -1,14 +1,8 @@
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { HowLongToBeatCategory } from "@types"; import type { HowLongToBeatCategory } from "@types";
import * as styles from "./game-details.css";
import { vars } from "@renderer/theme.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { vars } from "@renderer/theme.css";
const titleTranslation: Record<string, string> = { import * as styles from "./game-details.css";
"Main Story": "main_story",
"Main + Sides": "main_plus_sides",
Completionist: "completionist",
"All Styles": "all_styles",
};
const durationTranslation: Record<string, string> = { const durationTranslation: Record<string, string> = {
Hours: "hours", Hours: "hours",
@ -17,49 +11,59 @@ const durationTranslation: Record<string, string> = {
export interface HowLongToBeatSectionProps { export interface HowLongToBeatSectionProps {
howLongToBeatData: HowLongToBeatCategory[] | null; howLongToBeatData: HowLongToBeatCategory[] | null;
isLoading: boolean;
} }
export function HowLongToBeatSection({ export function HowLongToBeatSection({
howLongToBeatData, howLongToBeatData,
isLoading,
}: HowLongToBeatSectionProps) { }: HowLongToBeatSectionProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
if (!howLongToBeatData) return null;
const getDuration = (duration: string) => { const getDuration = (duration: string) => {
const [value, unit] = duration.split(" "); const [value, unit] = duration.split(" ");
return `${value} ${t(durationTranslation[unit])}`; return `${value} ${t(durationTranslation[unit])}`;
}; };
if (!howLongToBeatData && !isLoading) return null;
return ( return (
<> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<div className={styles.contentSidebarTitle}> <div className={styles.contentSidebarTitle}>
<h3>HowLongToBeat</h3> <h3>HowLongToBeat</h3>
</div> </div>
<ul className={styles.howLongToBeatCategoriesList}> <ul className={styles.howLongToBeatCategoriesList}>
{howLongToBeatData.map((category) => ( {howLongToBeatData
<li ? howLongToBeatData.map((category) => (
key={category.title} <li key={category.title} className={styles.howLongToBeatCategory}>
className={styles.howLongToBeatCategory}
style={{ backgroundColor: category.color ?? vars.color.background }}
>
<p <p
className={styles.howLongToBeatCategoryLabel} className={styles.howLongToBeatCategoryLabel}
style={{ style={{
fontWeight: "bold", fontWeight: "bold",
}} }}
> >
{titleTranslation[category.title] {category.title}
? t(titleTranslation[category.title])
: category.title}
</p> </p>
<p className={styles.howLongToBeatCategoryLabel}> <p className={styles.howLongToBeatCategoryLabel}>
{getDuration(category.duration)} {getDuration(category.duration)}
</p> </p>
{category.accuracy !== "00" && (
<small>
{t("accuracy", { accuracy: category.accuracy })}
</small>
)}
</li> </li>
))
: Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={index}
className={styles.howLongToBeatCategorySkeleton}
/>
))} ))}
</ul> </ul>
</> </SkeletonTheme>
); );
} }

View File

@ -56,8 +56,8 @@ export function RepacksModal({
gameDetails.repacks.filter((repack) => gameDetails.repacks.filter((repack) =>
repack.title repack.title
.toLowerCase() .toLowerCase()
.includes(event.target.value.toLocaleLowerCase()) .includes(event.target.value.toLocaleLowerCase()),
) ),
); );
}; };

View File

@ -106,5 +106,5 @@ export interface UserPreferences {
export interface HowLongToBeatCategory { export interface HowLongToBeatCategory {
title: string; title: string;
duration: string; duration: string;
color: string; accuracy: string;
} }