mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
feat: adding accuracy as text instead of colours
This commit is contained in:
parent
05a31cf6a6
commit
c0274ee7e7
@ -2,5 +2,4 @@ module.exports = {
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
singleQuote: false,
|
||||
tabWidth: 2,
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
# 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>
|
||||
![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.
|
||||
The launcher is written in TypeScript (Electron) and Python, which handles the torrenting system by using [libtorrent](https://www.libtorrent.org/).
|
||||
|
@ -58,12 +58,9 @@
|
||||
"publisher": "Published by {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copy link",
|
||||
"copied_link_to_clipboard": "Link copied",
|
||||
"main_story": "Main story",
|
||||
"main_plus_sides": "Main story + sides",
|
||||
"completionist": "Completionist",
|
||||
"all_styles": "All styles",
|
||||
"hours": "hours",
|
||||
"minutes": "minutes"
|
||||
"minutes": "minutes",
|
||||
"accuracy": "{{accuracy}}% accuracy"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activate Hydra",
|
||||
|
@ -58,12 +58,9 @@
|
||||
"publisher": "Publicado por {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar enlace",
|
||||
"copied_link_to_clipboard": "Enlace copiado",
|
||||
"main_story": "Historia principal",
|
||||
"main_plus_sides": "Historia principal + extras",
|
||||
"completionist": "Completista",
|
||||
"all_styles": "Todo",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos"
|
||||
"minutes": "minutos",
|
||||
"accuracy": "{{accuracy}}% precisión"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Activar Hydra",
|
||||
|
@ -58,12 +58,9 @@
|
||||
"publisher": "Publicado por {{publisher}}",
|
||||
"copy_link_to_clipboard": "Copiar link",
|
||||
"copied_link_to_clipboard": "Link copiado",
|
||||
"main_story": "História principal",
|
||||
"main_plus_sides": "Historia principal + extras",
|
||||
"completionist": "Completista",
|
||||
"all_styles": "Tudo",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos"
|
||||
"minutes": "minutos",
|
||||
"accuracy": "{{accuracy}}% de precisão"
|
||||
},
|
||||
"activation": {
|
||||
"title": "Ativação",
|
||||
|
@ -2,6 +2,7 @@ import { formatName } from "@main/helpers";
|
||||
import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { requestWebPage } from "./repack-tracker/helpers";
|
||||
import { HowLongToBeatCategory } from "@types";
|
||||
|
||||
export interface HowLongToBeatResult {
|
||||
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",
|
||||
Referer: "https://howlongtobeat.com/",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return response.data as HowLongToBeatSearchResponse;
|
||||
};
|
||||
|
||||
export const classNameColor = {
|
||||
time_40: "#ff3a3a",
|
||||
time_50: "#cc3b51",
|
||||
time_60: "#824985",
|
||||
time_70: "#5650a1",
|
||||
time_80: "#485cab",
|
||||
time_90: "#3a6db5",
|
||||
time_100: "#287fc2",
|
||||
};
|
||||
|
||||
export const getHowLongToBeatGame = async (id: string) => {
|
||||
export const getHowLongToBeatGame = async (
|
||||
id: string,
|
||||
): Promise<HowLongToBeatCategory[]> => {
|
||||
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
@ -54,12 +47,14 @@ export const getHowLongToBeatGame = async (id: string) => {
|
||||
|
||||
return $lis.map(($li) => {
|
||||
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 {
|
||||
title,
|
||||
duration: $li.querySelector("h5").textContent,
|
||||
color: classNameColor[time as keyof typeof classNameColor],
|
||||
accuracy,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -139,19 +139,19 @@ export const descriptionHeaderInfo = style({
|
||||
});
|
||||
|
||||
export const howLongToBeatCategoriesList = style({
|
||||
margin: 0,
|
||||
padding: 16,
|
||||
margin: "0",
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
gap: "16px",
|
||||
});
|
||||
|
||||
export const howLongToBeatCategory = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
gap: "4px",
|
||||
backgroundColor: vars.color.background,
|
||||
borderRadius: 8,
|
||||
borderRadius: "8px",
|
||||
padding: `8px 16px`,
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
});
|
||||
@ -161,13 +161,19 @@ export const howLongToBeatCategoryLabel = style({
|
||||
color: "#DADBE1",
|
||||
});
|
||||
|
||||
export const howLongToBeatCategorySkeleton = style({
|
||||
border: `solid 1px ${vars.color.borderColor}`,
|
||||
borderRadius: "8px",
|
||||
height: "76px",
|
||||
});
|
||||
|
||||
globalStyle(".bb_tag", {
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
globalStyle(`${description} img`, {
|
||||
borderRadius: 5,
|
||||
borderRadius: "5px",
|
||||
marginTop: `${SPACING_UNIT}px`,
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
marginLeft: "auto",
|
||||
|
@ -31,9 +31,10 @@ export function GameDetails() {
|
||||
const [color, setColor] = useState("");
|
||||
const [clipboardLock, setClipboardLock] = useState(false);
|
||||
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<
|
||||
HowLongToBeatCategory[] | null
|
||||
>(null);
|
||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
||||
isLoading: boolean;
|
||||
data: HowLongToBeatCategory[] | null;
|
||||
}>({ isLoading: true, data: null });
|
||||
|
||||
const [game, setGame] = useState<Game | null>(null);
|
||||
const [activeRequirement, setActiveRequirement] =
|
||||
@ -81,7 +82,7 @@ export function GameDetails() {
|
||||
window.electron
|
||||
.getHowLongToBeat(objectID, "steam", result.name)
|
||||
.then((data) => {
|
||||
setHowLongToBeat(data);
|
||||
setHowLongToBeat({ isLoading: false, data });
|
||||
});
|
||||
|
||||
setGameDetails(result);
|
||||
@ -89,7 +90,7 @@ export function GameDetails() {
|
||||
});
|
||||
|
||||
getGame();
|
||||
setHowLongToBeat(null);
|
||||
setHowLongToBeat({ isLoading: true, data: null });
|
||||
setClipboardLock(false);
|
||||
}, [getGame, dispatch, navigate, objectID, i18n.language]);
|
||||
|
||||
@ -103,12 +104,12 @@ export function GameDetails() {
|
||||
shop,
|
||||
encodeURIComponent(gameDetails?.name),
|
||||
i18n.language,
|
||||
])
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(
|
||||
OPEN_HYDRA_URL + `/?${searchParams.toString()}`
|
||||
OPEN_HYDRA_URL + `/?${searchParams.toString()}`,
|
||||
);
|
||||
|
||||
const zero = performance.now();
|
||||
@ -134,7 +135,7 @@ export function GameDetails() {
|
||||
repackId,
|
||||
gameDetails.objectID,
|
||||
gameDetails.name,
|
||||
shop as GameShop
|
||||
shop as GameShop,
|
||||
).then(() => {
|
||||
getGame();
|
||||
setShowRepacksModal(false);
|
||||
@ -217,7 +218,10 @@ export function GameDetails() {
|
||||
</div>
|
||||
|
||||
<div className={styles.contentSidebar}>
|
||||
<HowLongToBeatSection howLongToBeatData={howLongToBeat} />
|
||||
<HowLongToBeatSection
|
||||
howLongToBeatData={howLongToBeat.data}
|
||||
isLoading={howLongToBeat.isLoading}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.contentSidebarTitle}
|
||||
|
@ -1,14 +1,8 @@
|
||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import * as styles from "./game-details.css";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const titleTranslation: Record<string, string> = {
|
||||
"Main Story": "main_story",
|
||||
"Main + Sides": "main_plus_sides",
|
||||
Completionist: "completionist",
|
||||
"All Styles": "all_styles",
|
||||
};
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import * as styles from "./game-details.css";
|
||||
|
||||
const durationTranslation: Record<string, string> = {
|
||||
Hours: "hours",
|
||||
@ -17,49 +11,59 @@ const durationTranslation: Record<string, string> = {
|
||||
|
||||
export interface HowLongToBeatSectionProps {
|
||||
howLongToBeatData: HowLongToBeatCategory[] | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function HowLongToBeatSection({
|
||||
howLongToBeatData,
|
||||
isLoading,
|
||||
}: HowLongToBeatSectionProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
|
||||
if (!howLongToBeatData) return null;
|
||||
|
||||
const getDuration = (duration: string) => {
|
||||
const [value, unit] = duration.split(" ");
|
||||
return `${value} ${t(durationTranslation[unit])}`;
|
||||
};
|
||||
|
||||
if (!howLongToBeatData && !isLoading) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||
<div className={styles.contentSidebarTitle}>
|
||||
<h3>HowLongToBeat</h3>
|
||||
</div>
|
||||
|
||||
<ul className={styles.howLongToBeatCategoriesList}>
|
||||
{howLongToBeatData.map((category) => (
|
||||
<li
|
||||
key={category.title}
|
||||
className={styles.howLongToBeatCategory}
|
||||
style={{ backgroundColor: category.color ?? vars.color.background }}
|
||||
>
|
||||
<p
|
||||
className={styles.howLongToBeatCategoryLabel}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{titleTranslation[category.title]
|
||||
? t(titleTranslation[category.title])
|
||||
: category.title}
|
||||
</p>
|
||||
<p className={styles.howLongToBeatCategoryLabel}>
|
||||
{getDuration(category.duration)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
{howLongToBeatData
|
||||
? howLongToBeatData.map((category) => (
|
||||
<li key={category.title} className={styles.howLongToBeatCategory}>
|
||||
<p
|
||||
className={styles.howLongToBeatCategoryLabel}
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{category.title}
|
||||
</p>
|
||||
|
||||
<p className={styles.howLongToBeatCategoryLabel}>
|
||||
{getDuration(category.duration)}
|
||||
</p>
|
||||
|
||||
{category.accuracy !== "00" && (
|
||||
<small>
|
||||
{t("accuracy", { accuracy: category.accuracy })}
|
||||
</small>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: Array.from({ length: 4 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className={styles.howLongToBeatCategorySkeleton}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
}
|
||||
|
@ -56,8 +56,8 @@ export function RepacksModal({
|
||||
gameDetails.repacks.filter((repack) =>
|
||||
repack.title
|
||||
.toLowerCase()
|
||||
.includes(event.target.value.toLocaleLowerCase())
|
||||
)
|
||||
.includes(event.target.value.toLocaleLowerCase()),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -106,5 +106,5 @@ export interface UserPreferences {
|
||||
export interface HowLongToBeatCategory {
|
||||
title: string;
|
||||
duration: string;
|
||||
color: string;
|
||||
accuracy: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user