diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea9180bf..59527b31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,8 +67,6 @@ jobs: dist/*.exe dist/*.zip dist/*.dmg - dist/*.AppImage - dist/*.snap dist/*.deb dist/*.rpm dist/*.tar.gz diff --git a/src/locales/hu/translation.json b/src/locales/hu/translation.json index 901574a1..68b194b4 100644 --- a/src/locales/hu/translation.json +++ b/src/locales/hu/translation.json @@ -82,7 +82,6 @@ "repacks_modal_description": "Choose the repack you want to download", "downloads_path": "Letöltések helye", "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" }, "activation": { diff --git a/src/main/constants.ts b/src/main/constants.ts index 1697279f..39da625b 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -50,5 +50,7 @@ export const databasePath = path.join( "hydra.db" ); +export const imageCachePath = path.join(app.getPath("userData"), ".imagecache"); + export const INSTALLATION_ID_LENGTH = 6; export const ACTIVATION_KEY_MULTIPLIER = 7; diff --git a/src/main/data-source.ts b/src/main/data-source.ts index a2ca976c..9161bb15 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -2,7 +2,6 @@ import { DataSource } from "typeorm"; import { Game, GameShopCache, - ImageCache, Repack, RepackerFriendlyName, UserPreferences, @@ -19,7 +18,6 @@ export const createDataSource = (options: Partial) => database: databasePath, entities: [ Game, - ImageCache, Repack, RepackerFriendlyName, UserPreferences, diff --git a/src/main/entity/image-cache.entity.ts b/src/main/entity/image-cache.entity.ts deleted file mode 100644 index a795c0d8..00000000 --- a/src/main/entity/image-cache.entity.ts +++ /dev/null @@ -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; -} diff --git a/src/main/events/misc/get-or-cache-image.ts b/src/main/events/misc/get-or-cache-image.ts index 9734ff7e..3dd86358 100644 --- a/src/main/events/misc/get-or-cache-image.ts +++ b/src/main/events/misc/get-or-cache-image.ts @@ -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 { getImageBase64 } from "@main/helpers"; +import { getFileBuffer } from "@main/helpers"; import { logger } from "@main/services"; +import { imageCachePath } from "@main/constants"; const getOrCacheImage = async ( _event: Electron.IpcMainInvokeEvent, url: string ) => { - const cache = await imageCacheRepository.findOne({ - where: { - url, - }, - }); + if (!fs.existsSync(imageCachePath)) fs.mkdirSync(imageCachePath); - if (cache) return cache.data; + const extname = path.extname(url); - getImageBase64(url).then((data) => - imageCacheRepository - .save({ - url, - data, - }) - .catch(() => { - logger.error(`Failed to cache image "${url}"`, { + const checksum = crypto.createHash("sha256").update(url).digest("hex"); + const cachePath = path.join(imageCachePath, `${checksum}${extname}`); + + const cache = fs.existsSync(cachePath); + + if (cache) return `hydra://${cachePath}`; + + getFileBuffer(url).then((buffer) => + fs.writeFile(cachePath, buffer, (err) => { + if (err) { + logger.error(`Failed to cache image`, err, { method: "getOrCacheImage", }); - }) + } + }) ); return url; diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 93b79229..a6807378 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -74,12 +74,15 @@ export const getSteamAppAsset = ( 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) => - response.arrayBuffer().then((buffer) => { - return `data:image/jpeg;base64,${Buffer.from(buffer).toString("base64")}`; - }) + response.arrayBuffer().then((buffer) => Buffer.from(buffer)) ); +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 "./ps"; diff --git a/src/main/index.ts b/src/main/index.ts index df529366..4d953022 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from "electron"; +import { app, BrowserWindow, net, protocol } from "electron"; import { init } from "@sentry/electron/main"; import i18n from "i18next"; import path from "node:path"; @@ -52,6 +52,10 @@ if (process.defaultApp) { app.whenReady().then(() => { electronApp.setAppUserModelId("site.hydralauncher.hydra"); + protocol.handle("hydra", (request) => + net.fetch("file://" + request.url.slice("hydra://".length)) + ); + dataSource.initialize().then(async () => { await resolveDatabaseUpdates(); diff --git a/src/main/repository.ts b/src/main/repository.ts index f9edfa1c..697068a1 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -2,7 +2,6 @@ import { dataSource } from "./data-source"; import { Game, GameShopCache, - ImageCache, Repack, RepackerFriendlyName, UserPreferences, @@ -12,8 +11,6 @@ import { export const gameRepository = dataSource.getRepository(Game); -export const imageCacheRepository = dataSource.getRepository(ImageCache); - export const repackRepository = dataSource.getRepository(Repack); export const repackerFriendlyNameRepository = diff --git a/src/main/workers/torrent-parser.worker.ts b/src/main/workers/torrent-parser.worker.ts index 7502fd5f..555c0947 100644 --- a/src/main/workers/torrent-parser.worker.ts +++ b/src/main/workers/torrent-parser.worker.ts @@ -1,16 +1,12 @@ import { parentPort } from "worker_threads"; import parseTorrent from "parse-torrent"; +import { getFileBuffer } from "@main/helpers"; const port = parentPort; 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) => { - const buffer = await getTorrentBuffer(url); + const buffer = await getFileBuffer(url); const torrent = await parseTorrent(buffer); port.postMessage(torrent); diff --git a/src/renderer/index.html b/src/renderer/index.html index 3147179e..2b85e347 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@ Hydra diff --git a/src/renderer/src/components/sidebar/sidebar.css.ts b/src/renderer/src/components/sidebar/sidebar.css.ts index b8527645..7271b281 100644 --- a/src/renderer/src/components/sidebar/sidebar.css.ts +++ b/src/renderer/src/components/sidebar/sidebar.css.ts @@ -52,7 +52,7 @@ export const menu = style({ listStyle: "none", padding: "0", margin: "0", - gap: `${SPACING_UNIT * 2}px`, + gap: `${SPACING_UNIT / 2}px`, display: "flex", flexDirection: "column", overflow: "hidden", @@ -64,16 +64,16 @@ export const menuItem = recipe({ cursor: "pointer", textWrap: "nowrap", display: "flex", - opacity: "0.9", color: vars.color.muted, + borderRadius: "4px", ":hover": { - opacity: "1", + backgroundColor: "rgba(255, 255, 255, 0.15)", }, }, variants: { active: { true: { - opacity: "1", + backgroundColor: "rgba(255, 255, 255, 0.1)", fontWeight: "bold", }, }, @@ -96,6 +96,7 @@ export const menuItemButton = style({ cursor: "pointer", overflow: "hidden", width: "100%", + padding: `9px ${SPACING_UNIT}px`, selectors: { [`${menuItem({ active: true }).split(" ")[1]} &`]: { fontWeight: "bold", @@ -120,20 +121,12 @@ export const sectionTitle = style({ fontWeight: "bold", }); -export const section = recipe({ - base: { - padding: `${SPACING_UNIT * 2}px 0`, - gap: `${SPACING_UNIT * 2}px`, - display: "flex", - flexDirection: "column", - }, - variants: { - hasBorder: { - true: { - borderBottom: `solid 1px ${vars.color.border}`, - }, - }, - }, +export const section = style({ + padding: `${SPACING_UNIT * 2}px 0`, + gap: `${SPACING_UNIT * 2}px`, + display: "flex", + flexDirection: "column", + paddingBottom: `${SPACING_UNIT}px`, }); export const sidebarFooter = style({ diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 9906affb..fc0e52c1 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -6,7 +6,6 @@ import type { Game } from "@types"; import { AsyncImage, TextField } from "@renderer/components"; import { useDownload, useLibrary } from "@renderer/hooks"; -import { SPACING_UNIT } from "../../theme.css"; 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 * as styles from "./sidebar.css"; +import { vars } from "@renderer/theme.css"; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; @@ -95,7 +95,7 @@ export function Sidebar() { }, [library]); useEffect(() => { - window.onmousemove = (event) => { + window.onmousemove = (event: MouseEvent) => { if (isResizing) { const cursorXDelta = event.screenX - cursorPos.current.x; const newWidth = Math.max( @@ -165,11 +165,9 @@ export function Sidebar() { macos: window.electron.platform === "darwin", })} > - {window.electron.platform === "darwin" && ( -

Hydra

- )} + {window.electron.platform === "darwin" &&

Hydra

} -
+
    {routes.map(({ nameKey, path, render }) => (
-
+
{t("my_library")} -
+
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts index dd71ce59..72c5e4d3 100644 --- a/src/renderer/src/pages/game-details/game-details.css.ts +++ b/src/renderer/src/pages/game-details/game-details.css.ts @@ -174,7 +174,6 @@ export const descriptionHeader = style({ justifyContent: "space-between", alignItems: "center", backgroundColor: vars.color.background, - borderBottom: `solid 1px ${vars.color.border}`, 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", { marginTop: `${SPACING_UNIT * 2}px`, marginBottom: `${SPACING_UNIT * 2}px`, diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts index 08ffff8a..1fe8f23f 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts +++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts @@ -10,7 +10,6 @@ export const panel = style({ justifyContent: "space-between", transition: "all ease 0.2s", borderBottom: `solid 1px ${vars.color.border}`, - color: "#8e919b", boxShadow: "0px 0px 15px 0px #000000", }); diff --git a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx b/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx index 80370a96..007568fb 100644 --- a/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx +++ b/src/renderer/src/pages/game-details/installation-guides/dodi-installation-guide.tsx @@ -21,7 +21,7 @@ export function DODIInstallationGuide({ }: DODIInstallationGuideProps) { const { t } = useTranslation("game_details"); - const [dontShowAgain, setDontShowAgain] = useState(true); + const [dontShowAgain, setDontShowAgain] = useState(false); const handleClose = () => { if (dontShowAgain) { diff --git a/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx b/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx index ffa07c45..3460eaec 100644 --- a/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx +++ b/src/renderer/src/pages/game-details/installation-guides/online-fix-installation-guide.tsx @@ -22,7 +22,7 @@ export function OnlineFixInstallationGuide({ const [clipboardLocked, setClipboardLocked] = useState(false); const { t } = useTranslation("game_details"); - const [dontShowAgain, setDontShowAgain] = useState(true); + const [dontShowAgain, setDontShowAgain] = useState(false); const handleCopyToClipboard = () => { setClipboardLocked(true); diff --git a/src/renderer/src/pages/game-details/repacks-modal.tsx b/src/renderer/src/pages/game-details/repacks-modal.tsx index f6b7d7d4..caa98d5c 100644 --- a/src/renderer/src/pages/game-details/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/repacks-modal.tsx @@ -44,12 +44,17 @@ export function RepacksModal({ }; const handleFilter: React.ChangeEventHandler = (event) => { + const term = event.target.value.toLocaleLowerCase(); + setFilteredRepacks( - gameDetails.repacks.filter((repack) => - repack.title - .toLowerCase() - .includes(event.target.value.toLocaleLowerCase()) - ) + gameDetails.repacks.filter((repack) => { + const lowerCaseTitle = repack.title.toLowerCase(); + const lowerCaseRepacker = repack.repacker.toLowerCase(); + + return [lowerCaseTitle, lowerCaseRepacker].some((value) => + value.includes(term) + ); + }) ); }; diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx index 6ce68434..2def747d 100644 --- a/src/renderer/src/pages/home/search-results.tsx +++ b/src/renderer/src/pages/home/search-results.tsx @@ -4,7 +4,7 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import type { CatalogueEntry } from "@types"; import type { DebouncedFunc } from "lodash"; -import { debounce } from "lodash-es"; +import { debounce } from "lodash"; import { InboxIcon } from "@primer/octicons-react"; import { clearSearch } from "@renderer/features";