From c4852b89f159a99e6961420cd9b54db2fb284660 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 15 Nov 2025 16:46:02 +0200 Subject: [PATCH 01/72] feat: checkbox to disable new game update badges --- src/locales/en/translation.json | 1 + src/main/main.ts | 4 +-- src/main/services/download-sources-checker.ts | 18 +++++++++++- .../components/sidebar/sidebar-game-item.tsx | 15 ++++++---- .../game-details/modals/repacks-modal.tsx | 28 +++++++++++++------ .../src/pages/settings/settings-behavior.tsx | 14 ++++++++++ src/types/level.types.ts | 1 + 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 5084a4a0..fc786be8 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -544,6 +544,7 @@ "show_download_speed_in_megabytes": "Show download speed in megabytes per second", "extract_files_by_default": "Extract files by default after download", "enable_steam_achievements": "Enable search for Steam achievements", + "enable_new_download_options_badges": "Show new download options badges", "achievement_custom_notification_position": "Achievement custom notification position", "top-left": "Top left", "top-center": "Top center", diff --git a/src/main/main.ts b/src/main/main.ts index 1cadcebd..81d4f53f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -59,8 +59,8 @@ export const loadState = async () => { const { syncDownloadSourcesFromApi } = await import("./services/user"); void syncDownloadSourcesFromApi(); - // Check for new download options on startup - DownloadSourcesChecker.checkForChanges(); + // Check for new download options on startup (if enabled) + void DownloadSourcesChecker.checkForChanges(); WSClient.connect(); }); diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index 928e3d52..169c199e 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -5,10 +5,12 @@ import { updateDownloadSourcesCheckBaseline, updateDownloadSourcesSinceValue, downloadSourcesSublevel, + db, + levelKeys, } from "@main/level"; import { logger } from "./logger"; import { WindowManager } from "./window-manager"; -import type { Game } from "@types"; +import type { Game, UserPreferences } from "@types"; interface DownloadSourcesChangeResponse { shop: string; @@ -101,6 +103,20 @@ export class DownloadSourcesChecker { logger.info("DownloadSourcesChecker.checkForChanges() called"); try { + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + if (userPreferences?.enableNewDownloadOptionsBadges === false) { + logger.info( + "New download options badges are disabled, skipping download sources check" + ); + return; + } + // Get all installed games (excluding custom games) const installedGames = await gamesSublevel.values().all(); const nonCustomGames = installedGames.filter( diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index 23223fc5..3414688d 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -5,6 +5,7 @@ import cn from "classnames"; import { useLocation } from "react-router-dom"; import { useState } from "react"; import { GameContextMenu } from ".."; +import { useAppSelector } from "@renderer/hooks"; interface SidebarGameItemProps { game: LibraryGame; @@ -18,6 +19,9 @@ export function SidebarGameItem({ getGameTitle, }: Readonly) { const location = useLocation(); + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); const [contextMenu, setContextMenu] = useState<{ visible: boolean; position: { x: number; y: number }; @@ -81,11 +85,12 @@ export function SidebarGameItem({ {getGameTitle(game)} - {(game.newDownloadOptionsCount ?? 0) > 0 && ( - - +{game.newDownloadOptionsCount} - - )} + {userPreferences?.enableNewDownloadOptionsBadges !== false && + (game.newDownloadOptionsCount ?? 0) > 0 && ( + + +{game.newDownloadOptionsCount} + + )} diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 91013da0..3754ef83 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -21,7 +21,12 @@ import { DownloadSettingsModal } from "./download-settings-modal"; import { gameDetailsContext } from "@renderer/context"; import { Downloader } from "@shared"; import { orderBy } from "lodash-es"; -import { useDate, useFeature, useAppDispatch } from "@renderer/hooks"; +import { + useDate, + useFeature, + useAppDispatch, + useAppSelector, +} from "@renderer/hooks"; import { clearNewDownloadOptions } from "@renderer/features"; import "./repacks-modal.scss"; @@ -68,6 +73,9 @@ export function RepacksModal({ const { formatDate } = useDate(); const navigate = useNavigate(); const dispatch = useAppDispatch(); + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); const getHashFromMagnet = (magnet: string) => { if (!magnet || typeof magnet !== "string") { @@ -115,10 +123,12 @@ export function RepacksModal({ setIsLoadingTimestamp(false); }; - if (visible) { + if (visible && userPreferences?.enableNewDownloadOptionsBadges !== false) { fetchLastCheckTimestamp(); + } else { + setIsLoadingTimestamp(false); } - }, [visible, repacks]); + }, [visible, repacks, userPreferences?.enableNewDownloadOptionsBadges]); useEffect(() => { if ( @@ -326,11 +336,13 @@ export function RepacksModal({ >

{repack.title} - {isNewRepack(repack) && ( - - {t("new_download_option")} - - )} + {userPreferences?.enableNewDownloadOptionsBadges !== + false && + isNewRepack(repack) && ( + + {t("new_download_option")} + + )}

{isLastDownloadedOption && ( diff --git a/src/renderer/src/pages/settings/settings-behavior.tsx b/src/renderer/src/pages/settings/settings-behavior.tsx index c5698ef7..0efbcb64 100644 --- a/src/renderer/src/pages/settings/settings-behavior.tsx +++ b/src/renderer/src/pages/settings/settings-behavior.tsx @@ -29,6 +29,7 @@ export function SettingsBehavior() { enableSteamAchievements: false, autoplayGameTrailers: true, hideToTrayOnGameStart: false, + enableNewDownloadOptionsBadges: true, }); const { t } = useTranslation("settings"); @@ -53,6 +54,8 @@ export function SettingsBehavior() { userPreferences.enableSteamAchievements ?? false, autoplayGameTrailers: userPreferences.autoplayGameTrailers ?? true, hideToTrayOnGameStart: userPreferences.hideToTrayOnGameStart ?? false, + enableNewDownloadOptionsBadges: + userPreferences.enableNewDownloadOptionsBadges ?? true, }); } }, [userPreferences]); @@ -209,6 +212,17 @@ export function SettingsBehavior() { + + + handleChange({ + enableNewDownloadOptionsBadges: + !form.enableNewDownloadOptionsBadges, + }) + } + /> ); } diff --git a/src/types/level.types.ts b/src/types/level.types.ts index fd930a12..98ae0eb2 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -124,6 +124,7 @@ export interface UserPreferences { enableSteamAchievements?: boolean; autoplayGameTrailers?: boolean; hideToTrayOnGameStart?: boolean; + enableNewDownloadOptionsBadges?: boolean; } export interface ScreenState { From 2adc132c335a834f55c10a433003418d465fada2 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sat, 15 Nov 2025 16:57:44 +0200 Subject: [PATCH 02/72] fix: removed void from main.ts --- src/main/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/main.ts b/src/main/main.ts index 81d4f53f..147ed7dd 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -60,7 +60,9 @@ export const loadState = async () => { void syncDownloadSourcesFromApi(); // Check for new download options on startup (if enabled) - void DownloadSourcesChecker.checkForChanges(); + (async () => { + await DownloadSourcesChecker.checkForChanges(); + })(); WSClient.connect(); }); From 1545f42d17d0bf7f46030691bf4cf9cc60a8aeef Mon Sep 17 00:00:00 2001 From: Nikolay Rovdo Date: Sun, 30 Nov 2025 14:51:24 +0100 Subject: [PATCH 03/72] Adding chocolatey publishing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c086cb2e..d360dde4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@
[](https://help.hydralauncher.gg) -

Hydra Launcher

@@ -10,6 +9,7 @@ [![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) +[![chocolatey](https://img.shields.io/chocolatey/v/hydralauncher.svg)](https://community.chocolatey.org/packages/hydralauncher) ![Hydra Launcher Home Page](./docs/screenshot.png) From 0268829946468ad04bd027d93d6209f219cf677f Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 12 Dec 2025 13:53:12 +0200 Subject: [PATCH 04/72] feat: add Wrapped 2025 view in profile --- src/locales/en/translation.json | 6 +- src/main/services/window-manager.ts | 8 +- .../profile-content/profile-content.tsx | 15 ++- .../profile/profile-content/profile-tabs.tsx | 17 ++- .../profile/profile-content/wrapped-tab.scss | 73 ++++++++++++ .../profile/profile-content/wrapped-tab.tsx | 104 ++++++++++++++++++ 6 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/renderer/src/pages/profile/profile-content/wrapped-tab.scss create mode 100644 src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index ed8c7d4e..bc6f45ee 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -715,7 +715,11 @@ "karma_description": "Earned from positive likes on reviews", "user_reviews": "Reviews", "delete_review": "Delete Review", - "loading_reviews": "Loading reviews..." + "loading_reviews": "Loading reviews...", + "wrapped_2025": "Wrapped 2025", + "view_wrapped_title": "View {{displayName}}'s Wrapped 2025?", + "view_wrapped_yes": "Yes", + "view_wrapped_no": "No" }, "library": { "library": "Library", diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 04c77619..26d13228 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -36,9 +36,9 @@ export class WindowManager { private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions = { width: 1200, - height: 720, + height: 860, minWidth: 1024, - minHeight: 540, + minHeight: 860, backgroundColor: "#1c1c1c", titleBarStyle: process.platform === "linux" ? "default" : "hidden", icon, @@ -106,7 +106,7 @@ export class WindowManager { valueEncoding: "json", } ); - return data ?? { isMaximized: false, height: 720, width: 1200 }; + return data ?? { isMaximized: false, height: 860, width: 1200 }; } private static updateInitialConfig( @@ -224,7 +224,7 @@ export class WindowManager { ? { x: undefined, y: undefined, - height: this.initialConfigInitializationMainWindow.height ?? 720, + height: this.initialConfigInitializationMainWindow.height ?? 860, width: this.initialConfigInitializationMainWindow.width ?? 1200, isMaximized: true, } diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 8176bace..a117c12a 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -21,9 +21,10 @@ import { UserKarmaBox } from "./user-karma-box"; import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal"; import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; -import { ProfileTabs } from "./profile-tabs"; +import { ProfileTabs, type ProfileTabType } from "./profile-tabs"; import { LibraryTab } from "./library-tab"; import { ReviewsTab } from "./reviews-tab"; +import { WrappedConfirmModal } from "./wrapped-tab"; import { AnimatePresence } from "framer-motion"; import "./profile-content.scss"; @@ -95,7 +96,7 @@ export function ProfileContent() { const [sortBy, setSortBy] = useState("playedRecently"); const statsAnimation = useRef(-1); - const [activeTab, setActiveTab] = useState<"library" | "reviews">("library"); + const [activeTab, setActiveTab] = useState("library"); // User reviews state const [reviews, setReviews] = useState([]); @@ -104,6 +105,7 @@ export function ProfileContent() { const [votingReviews, setVotingReviews] = useState>(new Set()); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [reviewToDelete, setReviewToDelete] = useState(null); + const [wrappedModalVisible, setWrappedModalVisible] = useState(false); const dispatch = useAppDispatch(); @@ -386,6 +388,7 @@ export function ProfileContent() { activeTab={activeTab} reviewsTotalCount={reviewsTotalCount} onTabChange={setActiveTab} + onWrappedClick={() => setWrappedModalVisible(true)} />

@@ -439,6 +442,13 @@ export function ProfileContent() { onClose={handleDeleteCancel} onConfirm={handleDeleteConfirm} /> + + setWrappedModalVisible(false)} + /> ); }, [ @@ -460,6 +470,7 @@ export function ProfileContent() { isLoadingReviews, votingReviews, deleteModalVisible, + wrappedModalVisible, ]); return ( diff --git a/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx index bc76f40c..9eac8843 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-tabs.tsx @@ -2,16 +2,20 @@ import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import "./profile-content.scss"; +export type ProfileTabType = "library" | "reviews"; + interface ProfileTabsProps { - activeTab: "library" | "reviews"; + activeTab: ProfileTabType; reviewsTotalCount: number; - onTabChange: (tab: "library" | "reviews") => void; + onTabChange: (tab: ProfileTabType) => void; + onWrappedClick: () => void; } export function ProfileTabs({ activeTab, reviewsTotalCount, onTabChange, + onWrappedClick, }: Readonly) { const { t } = useTranslation("user_profile"); @@ -62,6 +66,15 @@ export function ProfileTabs({ /> )}
+
+ +
); } diff --git a/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss b/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss new file mode 100644 index 00000000..6669586d --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/wrapped-tab.scss @@ -0,0 +1,73 @@ +@use "../../../scss/globals.scss"; + +.wrapped-fullscreen-modal { + position: fixed; + inset: 0; + z-index: 999; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + border: none; + background: transparent; + width: 100%; + height: 100%; + + &__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.9); + border: none; + z-index: 1; + } + + &__container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: calc(globals.$spacing-unit * 2); + pointer-events: none; + z-index: 2; + } + + &__close-button { + position: absolute; + top: calc(globals.$spacing-unit * 5); + right: calc(globals.$spacing-unit * 5); + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: white; + transition: background 0.2s ease; + z-index: 10; + pointer-events: auto; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + } + + &__content { + border-radius: 8px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5); + pointer-events: auto; + } + + &__iframe { + width: 100%; + height: 100%; + border: none; + } +} diff --git a/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx b/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx new file mode 100644 index 00000000..1716fcc0 --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { XIcon } from "@primer/octicons-react"; +import { ConfirmationModal } from "@renderer/components"; +import "./wrapped-tab.scss"; + +interface WrappedModalProps { + userId: string; + displayName: string; + isOpen: boolean; + onClose: () => void; +} + +interface ScaleConfig { + scale: number; + width: number; + height: number; +} + +const SCALE_CONFIGS: Record = { + 0.25: { scale: 0.25, width: 270, height: 480 }, + 0.3: { scale: 0.3, width: 324, height: 576 }, + 0.5: { scale: 0.5, width: 540, height: 960 }, +}; + +const getScaleConfigForHeight = (height: number): ScaleConfig => { + if (height >= 1000) return SCALE_CONFIGS[0.5]; + if (height >= 650) return SCALE_CONFIGS[0.3]; + return SCALE_CONFIGS[0.25]; +}; + +export function WrappedConfirmModal({ + userId, + displayName, + isOpen, + onClose, +}: Readonly) { + const { t } = useTranslation("user_profile"); + const [showFullscreen, setShowFullscreen] = useState(false); + const [config, setConfig] = useState(SCALE_CONFIGS[0.5]); + + useEffect(() => { + if (!showFullscreen) return; + + const updateConfig = () => { + setConfig(getScaleConfigForHeight(window.innerHeight)); + }; + + updateConfig(); + window.addEventListener("resize", updateConfig); + return () => window.removeEventListener("resize", updateConfig); + }, [showFullscreen]); + + const handleConfirm = () => { + onClose(); + setShowFullscreen(true); + }; + + return ( + <> + + + {showFullscreen && ( + + + +
+