Compare commits

...

13 Commits

Author SHA1 Message Date
Chubby Granny Chaser
8e3bf29a21 Merge branch 'main' into feat/settings-trailer-and-launch 2025-10-28 23:11:19 +00:00
Chubby Granny Chaser
f6d21baff5 Merge pull request #1831 from hydralauncher/feat/catalogue-manual-pagination
Feat: Manual selection of the page in catalogue.
2025-10-28 23:09:53 +00:00
Chubby Granny Chaser
b2b7b36f70 Merge branch 'main' into feat/catalogue-manual-pagination 2025-10-28 23:08:46 +00:00
Moyasee
dbf5d7afc7 fix: multiple imports 2025-10-28 17:43:19 +02:00
Moyasee
bfc4bb1a83 ci: formatting 2025-10-28 17:36:11 +02:00
Moyasee
120aad6c1c feat: Hide to tray on game startup and ability to disable trailers auto-play 2025-10-28 17:34:20 +02:00
Zamitto
61072aa02a fix: add theme editor dev tools back 2025-10-27 15:25:18 -03:00
Zamitto
ddd6af0d4c fix: add theme editor dev tools back 2025-10-27 15:22:47 -03:00
Moyasee
6565ce5316 fix: moved component out of parent component 2025-10-26 19:59:43 +02:00
Moyasee
fee3a4522a fix: duplications 2025-10-26 19:49:15 +02:00
Moyasee
cb3e52de34 fix: go to page button did not appear correctly for the last pages 2025-10-26 19:37:57 +02:00
Moyasee
7f2343413e feat: added manual page selection and changed functionality of pagination 2025-10-26 17:26:25 +02:00
Zamitto
ee35bc24b2 chore: undo remove hydra api logs 2025-10-24 21:11:56 -03:00
9 changed files with 222 additions and 19 deletions

View File

@@ -541,7 +541,9 @@
"hidden": "Hidden",
"test_notification": "Test notification",
"notification_preview": "Achievement Notification Preview",
"enable_friend_start_game_notifications": "When a friend starts playing a game"
"enable_friend_start_game_notifications": "When a friend starts playing a game",
"autoplay_trailers_on_game_page": "Automatically start playing trailers on game page",
"hide_to_tray_on_game_start": "Hide Hydra to tray on game startup"
},
"notifications": {
"download_complete": "Download complete",

View File

@@ -29,7 +29,7 @@ export class HydraApi {
private static instance: AxiosInstance;
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
private static readonly ADD_LOG_INTERCEPTOR = false;
private static readonly ADD_LOG_INTERCEPTOR = true;
private static secondsToMilliseconds(seconds: number) {
return seconds * 1000;

View File

@@ -1,10 +1,10 @@
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import type { Game, GameRunning } from "@types";
import type { Game, GameRunning, UserPreferences } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { CloudSync } from "./cloud-sync";
import { logger } from "./logger";
import path from "path";
@@ -209,6 +209,17 @@ function onOpenGame(game: Game) {
lastSyncTick: now,
});
// Hide Hydra to tray on game startup if enabled
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
})
.then((userPreferences) => {
if (userPreferences?.hideToTrayOnGameStart) {
WindowManager.mainWindow?.hide();
}
})
.catch(() => {});
if (game.remoteId) {
updateGamePlaytime(
game,

View File

@@ -462,6 +462,7 @@ export class WindowManager {
editorWindow.once("ready-to-show", () => {
editorWindow.show();
this.mainWindow?.webContents.openDevTools();
if (!app.isPackaged || isStaging) {
editorWindow.webContents.openDevTools();
}
@@ -469,11 +470,12 @@ export class WindowManager {
editorWindow.webContents.on("before-input-event", (_event, input) => {
if (input.key === "F12") {
editorWindow.webContents.toggleDevTools();
this.mainWindow?.webContents.toggleDevTools();
}
});
editorWindow.on("close", () => {
this.mainWindow?.webContents.closeDevTools();
this.editorWindows.delete(themeId);
});
}

View File

@@ -1,3 +1,5 @@
@use "../../scss/globals.scss";
.pagination {
display: flex;
gap: 4px;
@@ -18,4 +20,31 @@
font-size: 16px;
}
}
&__page-input {
box-sizing: border-box;
width: 40px;
min-width: 40px;
max-width: 40px;
min-height: 40px;
border-radius: 8px;
border: solid 1px globals.$border-color;
background-color: transparent;
color: globals.$muted-color;
text-align: center;
font-size: 12px;
padding: 0 6px;
outline: none;
}
&__double-chevron {
display: flex;
align-items: center;
justify-content: center;
font-size: 0; // remove whitespace node width between SVGs
}
&__double-chevron > svg + svg {
margin-left: -8px; // pull the second chevron closer
}
}

View File

@@ -1,8 +1,51 @@
import { Button } from "@renderer/components/button/button";
import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react";
import { useFormat } from "@renderer/hooks/use-format";
import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, KeyboardEvent, RefObject } from "react";
import "./pagination.scss";
interface JumpControlProps {
isOpen: boolean;
value: string;
totalPages: number;
inputRef: RefObject<HTMLInputElement>;
onOpen: () => void;
onClose: () => void;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
}
function JumpControl({
isOpen,
value,
totalPages,
inputRef,
onOpen,
onClose,
onChange,
onKeyDown,
}: JumpControlProps) {
return isOpen ? (
<input
ref={inputRef}
type="number"
min={1}
max={totalPages}
className="pagination__page-input"
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
onBlur={onClose}
aria-label="Go to page"
/>
) : (
<Button theme="outline" className="pagination__button" onClick={onOpen}>
...
</Button>
);
}
interface PaginationProps {
page: number;
totalPages: number;
@@ -16,20 +59,82 @@ export function Pagination({
}: PaginationProps) {
const { formatNumber } = useFormat();
const [isJumpOpen, setIsJumpOpen] = useState(false);
const [jumpValue, setJumpValue] = useState<string>("");
const jumpInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isJumpOpen) {
setJumpValue("");
setTimeout(() => jumpInputRef.current?.focus(), 0);
}
}, [isJumpOpen, page]);
if (totalPages <= 1) return null;
const visiblePages = 3;
const isLastThree = totalPages > 3 && page >= totalPages - 2;
let startPage = Math.max(1, page - 1);
let endPage = startPage + visiblePages - 1;
if (endPage > totalPages) {
if (isLastThree) {
startPage = Math.max(1, totalPages - 2);
endPage = totalPages;
} else if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - visiblePages + 1);
}
const onJumpChange = (e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val === "") {
setJumpValue("");
return;
}
const num = Number(val);
if (Number.isNaN(num)) {
return;
}
if (num < 1) {
setJumpValue("1");
return;
}
if (num > totalPages) {
setJumpValue(String(totalPages));
return;
}
setJumpValue(val);
};
const onJumpKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
if (jumpValue.trim() === "") return;
const parsed = Number(jumpValue);
if (Number.isNaN(parsed)) return;
const target = Math.max(1, Math.min(totalPages, parsed));
onPageChange(target);
setIsJumpOpen(false);
} else if (e.key === "Escape") {
setIsJumpOpen(false);
}
};
return (
<div className="pagination">
{startPage > 1 && (
<Button
theme="outline"
onClick={() => onPageChange(1)}
className="pagination__button"
>
<span className="pagination__double-chevron">
<ChevronLeftIcon />
<ChevronLeftIcon />
</span>
</Button>
)}
<Button
theme="outline"
onClick={() => onPageChange(page - 1)}
@@ -39,20 +144,25 @@ export function Pagination({
<ChevronLeftIcon />
</Button>
{page > 2 && (
{isLastThree && startPage > 1 && (
<>
<Button
theme="outline"
onClick={() => onPageChange(1)}
className="pagination__button"
disabled={page === 1}
onClick={() => onPageChange(1)}
>
{1}
{formatNumber(1)}
</Button>
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
<JumpControl
isOpen={isJumpOpen}
value={jumpValue}
totalPages={totalPages}
inputRef={jumpInputRef}
onOpen={() => setIsJumpOpen(true)}
onClose={() => setIsJumpOpen(false)}
onChange={onJumpChange}
onKeyDown={onJumpKeyDown}
/>
</>
)}
@@ -70,11 +180,18 @@ export function Pagination({
</Button>
))}
{page < totalPages - 1 && (
{!isLastThree && page < totalPages - 1 && (
<>
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
<JumpControl
isOpen={isJumpOpen}
value={jumpValue}
totalPages={totalPages}
inputRef={jumpInputRef}
onOpen={() => setIsJumpOpen(true)}
onClose={() => setIsJumpOpen(false)}
onChange={onJumpChange}
onKeyDown={onJumpKeyDown}
/>
<Button
theme="outline"
@@ -95,6 +212,19 @@ export function Pagination({
>
<ChevronRightIcon />
</Button>
{endPage < totalPages && (
<Button
theme="outline"
onClick={() => onPageChange(totalPages)}
className="pagination__button"
>
<span className="pagination__double-chevron">
<ChevronRightIcon />
<ChevronRightIcon />
</span>
</Button>
)}
</div>
);
}

View File

@@ -7,11 +7,16 @@ import {
} from "@primer/octicons-react";
import useEmblaCarousel from "embla-carousel-react";
import { gameDetailsContext } from "@renderer/context";
import { useAppSelector } from "@renderer/hooks";
import "./gallery-slider.scss";
export function GallerySlider() {
const { shopDetails } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
);
const autoplayEnabled = userPreferences?.autoplayGameTrailers !== false;
const hasScreenshots = shopDetails && shopDetails.screenshots?.length;
@@ -164,7 +169,7 @@ export function GallerySlider() {
poster={item.poster}
loop
muted
autoPlay
autoPlay={autoplayEnabled}
tabIndex={-1}
>
<source src={item.videoSrc} />

View File

@@ -27,6 +27,8 @@ export function SettingsBehavior() {
showDownloadSpeedInMegabytes: false,
extractFilesByDefault: true,
enableSteamAchievements: false,
autoplayGameTrailers: true,
hideToTrayOnGameStart: false,
});
const { t } = useTranslation("settings");
@@ -49,6 +51,8 @@ export function SettingsBehavior() {
extractFilesByDefault: userPreferences.extractFilesByDefault ?? true,
enableSteamAchievements:
userPreferences.enableSteamAchievements ?? false,
autoplayGameTrailers: userPreferences.autoplayGameTrailers ?? true,
hideToTrayOnGameStart: userPreferences.hideToTrayOnGameStart ?? false,
});
}
}, [userPreferences]);
@@ -76,6 +80,16 @@ export function SettingsBehavior() {
}
/>
<CheckboxField
label={t("hide_to_tray_on_game_start")}
checked={form.hideToTrayOnGameStart}
onChange={() =>
handleChange({
hideToTrayOnGameStart: !form.hideToTrayOnGameStart,
})
}
/>
{showRunAtStartup && (
<CheckboxField
label={t("launch_with_system")}
@@ -120,6 +134,14 @@ export function SettingsBehavior() {
/>
)}
<CheckboxField
label={t("autoplay_trailers_on_game_page")}
checked={form.autoplayGameTrailers}
onChange={() =>
handleChange({ autoplayGameTrailers: !form.autoplayGameTrailers })
}
/>
<CheckboxField
label={t("disable_nsfw_alert")}
checked={form.disableNsfwAlert}

View File

@@ -118,6 +118,8 @@ export interface UserPreferences {
showDownloadSpeedInMegabytes?: boolean;
extractFilesByDefault?: boolean;
enableSteamAchievements?: boolean;
autoplayGameTrailers?: boolean;
hideToTrayOnGameStart?: boolean;
}
export interface ScreenState {