mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
Compare commits
21 Commits
chore/bump
...
fix/custom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad588b5600 | ||
|
|
c24ad34bc7 | ||
|
|
4f2c3105ce | ||
|
|
feedcb1dc7 | ||
|
|
4b8d64c72b | ||
|
|
dff68a3e26 | ||
|
|
58bdbdab71 | ||
|
|
574a012d8c | ||
|
|
8e3bf29a21 | ||
|
|
f6d21baff5 | ||
|
|
b2b7b36f70 | ||
|
|
dbf5d7afc7 | ||
|
|
bfc4bb1a83 | ||
|
|
120aad6c1c | ||
|
|
61072aa02a | ||
|
|
ddd6af0d4c | ||
|
|
6565ce5316 | ||
|
|
fee3a4522a | ||
|
|
cb3e52de34 | ||
|
|
7f2343413e | ||
|
|
ee35bc24b2 |
@@ -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",
|
||||
|
||||
@@ -6,6 +6,10 @@ import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
|
||||
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours
|
||||
|
||||
export const getGameAssets = async (objectId: string, shop: GameShop) => {
|
||||
if (shop === "custom") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedAssets = await gamesShopAssetsSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
@@ -26,6 +26,8 @@ const getGameShopDetails = async (
|
||||
shop: GameShop,
|
||||
language: string
|
||||
): Promise<ShopDetailsWithAssets | null> => {
|
||||
if (shop === "custom") return null;
|
||||
|
||||
if (shop === "steam") {
|
||||
const [cachedData, cachedAssets] = await Promise.all([
|
||||
gamesShopCacheSublevel.get(
|
||||
|
||||
@@ -10,6 +10,10 @@ const getGameStats = async (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
if (shop === "custom") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedStats = await gamesStatsCacheSublevel.get(
|
||||
levelKeys.game(shop, objectId)
|
||||
);
|
||||
|
||||
@@ -13,7 +13,9 @@ const addGameToFavorites = async (
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
|
||||
if (shop !== "custom") {
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
|
||||
}
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
|
||||
@@ -13,7 +13,11 @@ const removeGameFromFavorites = async (
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
if (!game) return;
|
||||
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {});
|
||||
if (shop !== "custom") {
|
||||
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await gamesSublevel.put(gameKey, {
|
||||
|
||||
@@ -84,7 +84,7 @@ const removeGameFromLibrary = async (
|
||||
await resetShopAssets(gameKey);
|
||||
}
|
||||
|
||||
if (game?.remoteId) {
|
||||
if (game.remoteId) {
|
||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ export const getGameAchievementData = async (
|
||||
shop: GameShop,
|
||||
useCachedData: boolean
|
||||
) => {
|
||||
if (shop === "custom") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const gameKey = levelKeys.game(shop, objectId);
|
||||
|
||||
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ export function CloudSyncContextProvider({
|
||||
);
|
||||
|
||||
const getGameArtifacts = useCallback(async () => {
|
||||
if (shop === "custom") {
|
||||
setArtifacts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
objectId,
|
||||
shop,
|
||||
|
||||
@@ -142,10 +142,12 @@ export function GameDetailsContextProvider({
|
||||
}
|
||||
});
|
||||
|
||||
window.electron.getGameStats(objectId, shop).then((result) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
setStats(result);
|
||||
});
|
||||
if (shop !== "custom") {
|
||||
window.electron.getGameStats(objectId, shop).then((result) => {
|
||||
if (abortController.signal.aborted) return;
|
||||
setStats(result);
|
||||
});
|
||||
}
|
||||
|
||||
const assetsPromise = window.electron.getGameAssets(objectId, shop);
|
||||
|
||||
@@ -167,7 +169,7 @@ export function GameDetailsContextProvider({
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
if (userDetails) {
|
||||
if (userDetails && shop !== "custom") {
|
||||
window.electron
|
||||
.getUnlockedAchievements(objectId, shop)
|
||||
.then((achievements) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -228,7 +228,7 @@ export function GameDetailsContent() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{game?.shop !== "custom" && shop && objectId && (
|
||||
{shop !== "custom" && shop && objectId && (
|
||||
<GameReviews
|
||||
shop={shop}
|
||||
objectId={objectId}
|
||||
@@ -241,7 +241,7 @@ export function GameDetailsContent() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{game?.shop !== "custom" && <Sidebar />}
|
||||
{shop !== "custom" && <Sidebar />}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export function GameReviews({
|
||||
});
|
||||
|
||||
const checkUserReview = useCallback(async () => {
|
||||
if (!objectId || !userDetailsId) return;
|
||||
if (!objectId || !userDetailsId || shop === "custom") return;
|
||||
|
||||
try {
|
||||
const response = await window.electron.hydraApi.get<{
|
||||
@@ -147,7 +147,7 @@ export function GameReviews({
|
||||
|
||||
const loadReviews = useCallback(
|
||||
async (reset = false) => {
|
||||
if (!objectId) return;
|
||||
if (!objectId || shop === "custom") return;
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
|
||||
@@ -146,6 +146,8 @@ $hero-height: 350px;
|
||||
&__game-logo {
|
||||
width: 200px;
|
||||
align-self: flex-end;
|
||||
object-fit: contain;
|
||||
object-position: left bottom;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: 250px;
|
||||
@@ -153,6 +155,7 @@ $hero-height: 350px;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
width: 300px;
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type GameShop = "steam" | "epic" | "custom";
|
||||
export type GameShop = "steam" | "custom";
|
||||
|
||||
export type ShortcutLocation = "desktop" | "start_menu";
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ export interface UserPreferences {
|
||||
showDownloadSpeedInMegabytes?: boolean;
|
||||
extractFilesByDefault?: boolean;
|
||||
enableSteamAchievements?: boolean;
|
||||
autoplayGameTrailers?: boolean;
|
||||
hideToTrayOnGameStart?: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenState {
|
||||
|
||||
Reference in New Issue
Block a user