feat: adding background to item

This commit is contained in:
Hydra 2024-05-03 19:51:13 +01:00
parent f6072eeb5c
commit 1f2f269736
No known key found for this signature in database
20 changed files with 77 additions and 98 deletions

View File

@ -67,8 +67,6 @@ jobs:
dist/*.exe dist/*.exe
dist/*.zip dist/*.zip
dist/*.dmg dist/*.dmg
dist/*.AppImage
dist/*.snap
dist/*.deb dist/*.deb
dist/*.rpm dist/*.rpm
dist/*.tar.gz dist/*.tar.gz

View File

@ -82,7 +82,6 @@
"repacks_modal_description": "Choose the repack you want to download", "repacks_modal_description": "Choose the repack you want to download",
"downloads_path": "Letöltések helye", "downloads_path": "Letöltések helye",
"select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a", "select_folder_hint": "Ahhoz, hogy megváltoztasd a helyet, hozzákell férned a",
"hydra_settings": "Hydra beállítások",
"download_now": "Töltsd le most" "download_now": "Töltsd le most"
}, },
"activation": { "activation": {

View File

@ -50,5 +50,7 @@ export const databasePath = path.join(
"hydra.db" "hydra.db"
); );
export const imageCachePath = path.join(app.getPath("userData"), ".imagecache");
export const INSTALLATION_ID_LENGTH = 6; export const INSTALLATION_ID_LENGTH = 6;
export const ACTIVATION_KEY_MULTIPLIER = 7; export const ACTIVATION_KEY_MULTIPLIER = 7;

View File

@ -2,7 +2,6 @@ import { DataSource } from "typeorm";
import { import {
Game, Game,
GameShopCache, GameShopCache,
ImageCache,
Repack, Repack,
RepackerFriendlyName, RepackerFriendlyName,
UserPreferences, UserPreferences,
@ -19,7 +18,6 @@ export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
database: databasePath, database: databasePath,
entities: [ entities: [
Game, Game,
ImageCache,
Repack, Repack,
RepackerFriendlyName, RepackerFriendlyName,
UserPreferences, UserPreferences,

View File

@ -1,25 +0,0 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("image_cache")
export class ImageCache {
@PrimaryGeneratedColumn()
id: number;
@Column("text", { unique: true })
url: string;
@Column("text")
data: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,32 +1,35 @@
import { imageCacheRepository } from "@main/repository"; import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { getImageBase64 } from "@main/helpers"; import { getFileBuffer } from "@main/helpers";
import { logger } from "@main/services"; import { logger } from "@main/services";
import { imageCachePath } from "@main/constants";
const getOrCacheImage = async ( const getOrCacheImage = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
url: string url: string
) => { ) => {
const cache = await imageCacheRepository.findOne({ if (!fs.existsSync(imageCachePath)) fs.mkdirSync(imageCachePath);
where: {
url,
},
});
if (cache) return cache.data; const extname = path.extname(url);
getImageBase64(url).then((data) => const checksum = crypto.createHash("sha256").update(url).digest("hex");
imageCacheRepository const cachePath = path.join(imageCachePath, `${checksum}${extname}`);
.save({
url, const cache = fs.existsSync(cachePath);
data,
}) if (cache) return `hydra://${cachePath}`;
.catch(() => {
logger.error(`Failed to cache image "${url}"`, { getFileBuffer(url).then((buffer) =>
fs.writeFile(cachePath, buffer, (err) => {
if (err) {
logger.error(`Failed to cache image`, err, {
method: "getOrCacheImage", method: "getOrCacheImage",
}); });
}) }
})
); );
return url; return url;

View File

@ -74,12 +74,15 @@ export const getSteamAppAsset = (
return `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectID}/${clientIcon}.ico`; return `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/${objectID}/${clientIcon}.ico`;
}; };
export const getImageBase64 = async (url: string) => export const getFileBuffer = async (url: string) =>
fetch(url, { method: "GET" }).then((response) => fetch(url, { method: "GET" }).then((response) =>
response.arrayBuffer().then((buffer) => { response.arrayBuffer().then((buffer) => Buffer.from(buffer))
return `data:image/jpeg;base64,${Buffer.from(buffer).toString("base64")}`;
})
); );
export const getImageBase64 = async (url: string) =>
getFileBuffer(url).then((buffer) => {
return `data:image/jpeg;base64,${Buffer.from(buffer).toString("base64")}`;
});
export * from "./formatters"; export * from "./formatters";
export * from "./ps"; export * from "./ps";

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow } from "electron"; import { app, BrowserWindow, net, protocol } from "electron";
import { init } from "@sentry/electron/main"; import { init } from "@sentry/electron/main";
import i18n from "i18next"; import i18n from "i18next";
import path from "node:path"; import path from "node:path";
@ -52,6 +52,10 @@ if (process.defaultApp) {
app.whenReady().then(() => { app.whenReady().then(() => {
electronApp.setAppUserModelId("site.hydralauncher.hydra"); electronApp.setAppUserModelId("site.hydralauncher.hydra");
protocol.handle("hydra", (request) =>
net.fetch("file://" + request.url.slice("hydra://".length))
);
dataSource.initialize().then(async () => { dataSource.initialize().then(async () => {
await resolveDatabaseUpdates(); await resolveDatabaseUpdates();

View File

@ -2,7 +2,6 @@ import { dataSource } from "./data-source";
import { import {
Game, Game,
GameShopCache, GameShopCache,
ImageCache,
Repack, Repack,
RepackerFriendlyName, RepackerFriendlyName,
UserPreferences, UserPreferences,
@ -12,8 +11,6 @@ import {
export const gameRepository = dataSource.getRepository(Game); export const gameRepository = dataSource.getRepository(Game);
export const imageCacheRepository = dataSource.getRepository(ImageCache);
export const repackRepository = dataSource.getRepository(Repack); export const repackRepository = dataSource.getRepository(Repack);
export const repackerFriendlyNameRepository = export const repackerFriendlyNameRepository =

View File

@ -1,16 +1,12 @@
import { parentPort } from "worker_threads"; import { parentPort } from "worker_threads";
import parseTorrent from "parse-torrent"; import parseTorrent from "parse-torrent";
import { getFileBuffer } from "@main/helpers";
const port = parentPort; const port = parentPort;
if (!port) throw new Error("IllegalState"); if (!port) throw new Error("IllegalState");
const getTorrentBuffer = (url: string) =>
fetch(url, { method: "GET" }).then((response) =>
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
);
port.on("message", async (url: string) => { port.on("message", async (url: string) => {
const buffer = await getTorrentBuffer(url); const buffer = await getFileBuffer(url);
const torrent = await parseTorrent(buffer); const torrent = await parseTorrent(buffer);
port.postMessage(torrent); port.postMessage(torrent);

View File

@ -6,7 +6,7 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://cdn2.steamgriddb.com;" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: hydra: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
/> />
</head> </head>
<body style="background-color: #1c1c1c"> <body style="background-color: #1c1c1c">

View File

@ -52,7 +52,7 @@ export const menu = style({
listStyle: "none", listStyle: "none",
padding: "0", padding: "0",
margin: "0", margin: "0",
gap: `${SPACING_UNIT * 2}px`, gap: `${SPACING_UNIT / 2}px`,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
overflow: "hidden", overflow: "hidden",
@ -64,16 +64,16 @@ export const menuItem = recipe({
cursor: "pointer", cursor: "pointer",
textWrap: "nowrap", textWrap: "nowrap",
display: "flex", display: "flex",
opacity: "0.9",
color: vars.color.muted, color: vars.color.muted,
borderRadius: "4px",
":hover": { ":hover": {
opacity: "1", backgroundColor: "rgba(255, 255, 255, 0.15)",
}, },
}, },
variants: { variants: {
active: { active: {
true: { true: {
opacity: "1", backgroundColor: "rgba(255, 255, 255, 0.1)",
fontWeight: "bold", fontWeight: "bold",
}, },
}, },
@ -96,6 +96,7 @@ export const menuItemButton = style({
cursor: "pointer", cursor: "pointer",
overflow: "hidden", overflow: "hidden",
width: "100%", width: "100%",
padding: `9px ${SPACING_UNIT}px`,
selectors: { selectors: {
[`${menuItem({ active: true }).split(" ")[1]} &`]: { [`${menuItem({ active: true }).split(" ")[1]} &`]: {
fontWeight: "bold", fontWeight: "bold",
@ -120,20 +121,12 @@ export const sectionTitle = style({
fontWeight: "bold", fontWeight: "bold",
}); });
export const section = recipe({ export const section = style({
base: { padding: `${SPACING_UNIT * 2}px 0`,
padding: `${SPACING_UNIT * 2}px 0`, gap: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT * 2}px`, display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", paddingBottom: `${SPACING_UNIT}px`,
},
variants: {
hasBorder: {
true: {
borderBottom: `solid 1px ${vars.color.border}`,
},
},
},
}); });
export const sidebarFooter = style({ export const sidebarFooter = style({

View File

@ -6,7 +6,6 @@ import type { Game } from "@types";
import { AsyncImage, TextField } from "@renderer/components"; import { AsyncImage, TextField } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks"; import { useDownload, useLibrary } from "@renderer/hooks";
import { SPACING_UNIT } from "../../theme.css";
import { routes } from "./routes"; import { routes } from "./routes";
@ -15,6 +14,7 @@ import DiscordLogo from "@renderer/assets/discord-icon.svg?react";
import XLogo from "@renderer/assets/x-icon.svg?react"; import XLogo from "@renderer/assets/x-icon.svg?react";
import * as styles from "./sidebar.css"; import * as styles from "./sidebar.css";
import { vars } from "@renderer/theme.css";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
@ -95,7 +95,7 @@ export function Sidebar() {
}, [library]); }, [library]);
useEffect(() => { useEffect(() => {
window.onmousemove = (event) => { window.onmousemove = (event: MouseEvent) => {
if (isResizing) { if (isResizing) {
const cursorXDelta = event.screenX - cursorPos.current.x; const cursorXDelta = event.screenX - cursorPos.current.x;
const newWidth = Math.max( const newWidth = Math.max(
@ -165,11 +165,9 @@ export function Sidebar() {
macos: window.electron.platform === "darwin", macos: window.electron.platform === "darwin",
})} })}
> >
{window.electron.platform === "darwin" && ( {window.electron.platform === "darwin" && <h2>Hydra</h2>}
<h2 style={{ marginBottom: SPACING_UNIT }}>Hydra</h2>
)}
<section className={styles.section({ hasBorder: false })}> <section className={styles.section}>
<ul className={styles.menu}> <ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => ( {routes.map(({ nameKey, path, render }) => (
<li <li
@ -191,7 +189,7 @@ export function Sidebar() {
</ul> </ul>
</section> </section>
<section className={styles.section({ hasBorder: false })}> <section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small> <small className={styles.sectionTitle}>{t("my_library")}</small>
<TextField <TextField

View File

@ -13,7 +13,7 @@ export function GameDetailsSkeleton() {
<div className={styles.hero}> <div className={styles.hero}>
<Skeleton className={styles.heroImageSkeleton} /> <Skeleton className={styles.heroImageSkeleton} />
</div> </div>
<div className={styles.descriptionHeader}> <div className={styles.heroPanelSkeleton}>
<section className={styles.descriptionHeaderInfo}> <section className={styles.descriptionHeaderInfo}>
<Skeleton width={155} /> <Skeleton width={155} />
<Skeleton width={135} /> <Skeleton width={135} />

View File

@ -174,7 +174,6 @@ export const descriptionHeader = style({
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
borderBottom: `solid 1px ${vars.color.border}`,
height: "72px", height: "72px",
}); });
@ -233,6 +232,16 @@ export const randomizerButton = style({
}, },
}); });
export const heroPanelSkeleton = style({
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
display: "flex",
alignItems: "center",
backgroundColor: vars.color.background,
height: "72px",
borderBottom: `solid 1px ${vars.color.border}`,
});
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`,

View File

@ -10,7 +10,6 @@ export const panel = style({
justifyContent: "space-between", justifyContent: "space-between",
transition: "all ease 0.2s", transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.border}`, borderBottom: `solid 1px ${vars.color.border}`,
color: "#8e919b",
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px #000000",
}); });

View File

@ -21,7 +21,7 @@ export function DODIInstallationGuide({
}: DODIInstallationGuideProps) { }: DODIInstallationGuideProps) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(true); const [dontShowAgain, setDontShowAgain] = useState(false);
const handleClose = () => { const handleClose = () => {
if (dontShowAgain) { if (dontShowAgain) {

View File

@ -22,7 +22,7 @@ export function OnlineFixInstallationGuide({
const [clipboardLocked, setClipboardLocked] = useState(false); const [clipboardLocked, setClipboardLocked] = useState(false);
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const [dontShowAgain, setDontShowAgain] = useState(true); const [dontShowAgain, setDontShowAgain] = useState(false);
const handleCopyToClipboard = () => { const handleCopyToClipboard = () => {
setClipboardLocked(true); setClipboardLocked(true);

View File

@ -44,12 +44,17 @@ export function RepacksModal({
}; };
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => { const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const term = event.target.value.toLocaleLowerCase();
setFilteredRepacks( setFilteredRepacks(
gameDetails.repacks.filter((repack) => gameDetails.repacks.filter((repack) => {
repack.title const lowerCaseTitle = repack.title.toLowerCase();
.toLowerCase() const lowerCaseRepacker = repack.repacker.toLowerCase();
.includes(event.target.value.toLocaleLowerCase())
) return [lowerCaseTitle, lowerCaseRepacker].some((value) =>
value.includes(term)
);
})
); );
}; };

View File

@ -4,7 +4,7 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import type { CatalogueEntry } from "@types"; import type { CatalogueEntry } from "@types";
import type { DebouncedFunc } from "lodash"; import type { DebouncedFunc } from "lodash";
import { debounce } from "lodash-es"; import { debounce } from "lodash";
import { InboxIcon } from "@primer/octicons-react"; import { InboxIcon } from "@primer/octicons-react";
import { clearSearch } from "@renderer/features"; import { clearSearch } from "@renderer/features";