Compare commits

...

30 Commits

Author SHA1 Message Date
Chubby Granny Chaser
e872b2ea8a chore: bump version to 3.7.5
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
2025-11-30 06:26:43 +00:00
Chubby Granny Chaser
dd7c84b433 Merge pull request #1881 from hydralauncher/fix/downloads-ui
fix: auto-resuming download isnt working after restart
2025-11-30 06:26:08 +00:00
Chubby Granny Chaser
1546da29cf Merge branch 'fix/downloads-ui' of https://github.com/hydralauncher/hydra into fix/downloads-ui 2025-11-30 06:25:39 +00:00
Chubby Granny Chaser
a89b0bb2a8 style: refactor download group component to optimize download state management and improve UI responsiveness 2025-11-30 06:25:17 +00:00
Moyasee
9bdb216e0f fix: deleted comment 2025-11-30 08:23:49 +02:00
Moyasee
9779aed8c1 fix: auto-resuming download isnt working after restart 2025-11-30 08:05:45 +02:00
Chubby Granny Chaser
058a148c7f style: add button styling and refactor logo click handling in download group for improved accessibility and user experience 2025-11-30 05:44:18 +00:00
Chubby Granny Chaser
16e3d52508 style: enhance download group styling for improved layout, responsiveness, and user interaction 2025-11-30 05:39:01 +00:00
Chubby Granny Chaser
7e0002cf95 style: format imports in download-group.tsx for improved readability 2025-11-30 05:14:48 +00:00
Chubby Granny Chaser
bf8b3ca836 style: update download group layout and styling for improved responsiveness 2025-11-30 05:14:26 +00:00
Moyasee
77e376e742 fix: peak spead not working 2025-11-30 07:13:12 +02:00
Chubby Granny Chaser
bd28b202c4 Merge branch 'fix/downloads-ui' of https://github.com/hydralauncher/hydra 2025-11-30 05:06:59 +00:00
Moyasee
153b954e78 fix: progress bar, context menu, repacks modal, responsiveness and styling fix 2025-11-30 07:05:19 +02:00
Chubby Granny Chaser
a9e63730be Merge pull request #1880 from hydralauncher/fix/fixing-hls-videos
Some checks failed
Build / build (ubuntu-latest) (push) Has been cancelled
Build / build (windows-2022) (push) Has been cancelled
Fix/fixing hls videos
2025-11-30 03:45:10 +00:00
Chubby Granny Chaser
316480930d Merge branch 'main' into fix/fixing-hls-videos 2025-11-30 03:45:00 +00:00
Chubby Granny Chaser
0b5c9acaaa Merge pull request #1861 from iam-sahil/Downloads-UI
feat: enhance download page UI with improved layout and styling for the download cards.
2025-11-30 03:44:33 +00:00
Chubby Granny Chaser
814a2da05c Merge branch 'main' into Downloads-UI 2025-11-30 03:44:10 +00:00
Chubby Granny Chaser
0ad1ebd6a2 fix: fixing hls videos 2025-11-30 03:43:22 +00:00
Chubby Granny Chaser
e9de8264e2 fix: fixing hls videos 2025-11-30 03:41:41 +00:00
Chubby Granny Chaser
b135087ffe fix: fixing hls videos 2025-11-30 03:38:23 +00:00
Chubby Granny Chaser
b4a1af78a6 Merge pull request #1877 from egionCode/main
adding sorting for recently played based on last time the game was op…
2025-11-30 03:21:08 +00:00
Chubby Granny Chaser
ede5bb0c23 Merge branch 'main' into main 2025-11-30 03:20:03 +00:00
Chubby Granny Chaser
9a27875cd8 Merge pull request #1866 from hydralauncher/feat/search-autosuggest
Feat: search history and auto-suggest
2025-11-30 03:19:57 +00:00
Chubby Granny Chaser
cf20a942ae Merge branch 'main' into main 2025-11-30 03:17:07 +00:00
Victor
e176e624be adding sorting for recently played based on last time the game was opened 2025-11-27 11:23:50 -03:00
ctrlcat0x
5bffaf17fa fix: adjust padding for completed downloads and improve conditional rendering in download actions 2025-11-15 13:47:50 +05:30
ctrlcat0x
cc38be4383 Fixed linter and sonarcloud errors, refactored some functions and fixed UI padding issues with certain themes. 2025-11-15 11:31:39 +05:30
ctrlcat0x
0b70a28c08 feat: enhance download group UI with speed chart improvements and gradient progress bar 2025-11-15 01:16:23 +05:30
ctrlcat0x
3ff50a9932 feat: update download group UI with hero section and speed chart integration 2025-11-15 00:44:54 +05:30
ctrlcat0x
83fbf20383 feat: enhance download page UI with improved layout and styling for cards 2025-11-14 20:02:10 +05:30
18 changed files with 1462 additions and 325 deletions

View File

@@ -28,6 +28,19 @@
- Use async/await instead of promises when possible
- Prefer named exports over default exports for utilities and services
## ESLint Issues
- **Always try to fix ESLint errors properly before disabling rules**
- When encountering ESLint errors, explore these solutions in order:
1. **Fix the code to comply with the rule** (e.g., add missing required elements, fix accessibility issues)
2. **Use minimal markup to satisfy the rule** (e.g., add empty `<track>` elements for videos without captions, add `role` attributes)
3. **Only disable the rule as a last resort** when no reasonable solution exists
- When disabling a rule, always include a comment explaining why it's necessary
- Examples of proper fixes:
- For `jsx-a11y/media-has-caption`: Add `<track kind="captions" />` even if no captions are available
- For `jsx-a11y/alt-text`: Add meaningful alt text or `alt=""` for decorative images
- For accessibility rules: Add appropriate ARIA attributes rather than disabling
## TypeScript Array Syntax
- **Always use `T[]` syntax instead of `Array<T>`** for array types

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.7.4",
"version": "3.7.5",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -63,6 +63,7 @@
"embla-carousel-react": "^8.6.0",
"file-type": "^20.5.0",
"framer-motion": "^12.15.0",
"hls.js": "^1.5.12",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jsdom": "^24.0.0",

View File

@@ -13,7 +13,11 @@ const resumeGameDownload = async (
const download = await downloadsSublevel.get(gameKey);
if (download?.status === "paused") {
if (
download &&
(download.status === "paused" || download.status === "active") &&
download.progress !== 1
) {
await DownloadManager.pauseDownload();
for await (const [key, value] of downloadsSublevel.iterator()) {

View File

@@ -1,5 +1,5 @@
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { orderBy } from "lodash-es";
import { Downloader } from "@shared";
import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types";
@@ -68,7 +68,7 @@ export const loadState = async () => {
.values()
.all()
.then((games) => {
return sortBy(games, "timestamp", "DESC");
return orderBy(games, "timestamp", "desc");
});
downloads.forEach((download) => {

View File

@@ -20,7 +20,7 @@ import { RealDebridClient } from "./real-debrid";
import path from "path";
import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es";
import { orderBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager";
import { HydraDebridClient } from "./hydra-debrid";
@@ -194,10 +194,10 @@ export class DownloadManager {
.values()
.all()
.then((games) => {
return sortBy(
return orderBy(
games.filter((game) => game.status === "paused" && game.queued),
"timestamp",
"DESC"
"desc"
);
});

View File

@@ -6,7 +6,7 @@
<title>Hydra Launcher</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline' * data: local:;"
content="default-src 'self' 'unsafe-inline' * data: local:; media-src 'self' 'unsafe-inline' * data: local: blob:;"
/>
</head>
<body>

View File

@@ -18,6 +18,7 @@ interface DropdownMenuProps {
side?: "top" | "bottom" | "left" | "right";
align?: "start" | "center" | "end";
alignOffset?: number;
collisionPadding?: number;
}
export function DropdownMenu({
@@ -29,6 +30,7 @@ export function DropdownMenu({
loop = true,
align = "center",
alignOffset = 0,
collisionPadding = 16,
}: Readonly<DropdownMenuProps>) {
return (
<DropdownMenuPrimitive.Root>
@@ -43,6 +45,7 @@ export function DropdownMenu({
loop={loop}
align={align}
alignOffset={alignOffset}
collisionPadding={collisionPadding}
className="dropdown-menu__content"
>
{title && (

View File

@@ -10,3 +10,4 @@ export * from "./use-download-options-listener";
export * from "./use-game-card";
export * from "./use-search-history";
export * from "./use-search-suggestions";
export * from "./use-hls-video";

View File

@@ -0,0 +1,102 @@
import { useEffect, useRef } from "react";
import Hls from "hls.js";
import { logger } from "@renderer/logger";
interface UseHlsVideoOptions {
videoSrc: string | undefined;
videoType: string | undefined;
autoplay?: boolean;
muted?: boolean;
loop?: boolean;
}
export function useHlsVideo(
videoRef: React.RefObject<HTMLVideoElement>,
{ videoSrc, videoType, autoplay, muted, loop }: UseHlsVideoOptions
) {
const hlsRef = useRef<Hls | null>(null);
useEffect(() => {
const video = videoRef.current;
if (!video || !videoSrc) return;
const isHls = videoType === "application/x-mpegURL";
if (!isHls) {
return undefined;
}
if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
});
hlsRef.current = hls;
hls.loadSource(videoSrc);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (autoplay) {
video.play().catch((err) => {
logger.warn("Failed to autoplay HLS video:", err);
});
}
});
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
logger.error("HLS network error, trying to recover");
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
logger.error("HLS media error, trying to recover");
hls.recoverMediaError();
break;
default:
logger.error("HLS fatal error, destroying instance");
hls.destroy();
break;
}
}
});
return () => {
hls.destroy();
hlsRef.current = null;
};
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = videoSrc;
video.load();
if (autoplay) {
video.play().catch((err) => {
logger.warn("Failed to autoplay HLS video:", err);
});
}
return () => {
video.src = "";
};
} else {
logger.warn("HLS playback is not supported in this browser");
return undefined;
}
}, [videoRef, videoSrc, videoType, autoplay, muted, loop]);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (muted !== undefined) {
video.muted = muted;
}
if (loop !== undefined) {
video.loop = loop;
}
}, [videoRef, muted, loop]);
return hlsRef.current;
}

View File

@@ -4,158 +4,512 @@
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
margin-inline: calc(globals.$spacing-unit * 3);
padding-block: calc(globals.$spacing-unit * 3);
&__details-with-article {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
align-self: flex-start;
cursor: pointer;
&--queued {
padding-bottom: 0;
}
&--completed {
padding-top: calc(globals.$spacing-unit * 3);
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: calc(globals.$spacing-unit * 2);
gap: calc(globals.$spacing-unit);
&-divider {
&-title-group {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
flex: 1;
background-color: globals.$border-color;
height: 1px;
h2 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: rgba(255, 255, 255, 0.95);
}
}
&-count {
font-weight: 400;
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
min-width: 24px;
text-align: center;
flex-shrink: 0;
}
}
&__title-wrapper {
display: flex;
align-items: center;
margin-bottom: globals.$spacing-unit;
gap: globals.$spacing-unit;
}
&__title {
font-weight: bold;
cursor: pointer;
color: globals.$body-color;
text-align: left;
font-size: 16px;
display: block;
&:hover {
text-decoration: underline;
}
}
&__downloads {
&--hero {
width: 100%;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
margin: 0;
padding: 0;
margin-top: globals.$spacing-unit;
padding-bottom: globals.$spacing-unit;
}
&__item {
&__hero-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: globals.$background-color;
display: flex;
border-radius: 8px;
border: solid 1px globals.$border-color;
overflow: hidden;
box-shadow: 0px 0px 5px 0px #000000;
transition: all ease 0.2s;
height: 140px;
min-height: 140px;
max-height: 140px;
position: relative;
height: 120%;
z-index: 0;
&--hydra {
box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 20%;
}
}
&__cover {
width: 280px;
min-width: 280px;
height: auto;
border-right: solid 1px globals.$border-color;
// PLEASE FIX THE COLORS
&__hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
rgb(5, 5, 5) 70%,
rgb(26, 26, 26) 100%
);
}
&__hero-content {
position: relative;
z-index: 1;
&-content {
width: 100%;
height: 100%;
padding: globals.$spacing-unit;
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
&-backdrop {
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.8) 5%,
transparent 100%
);
display: flex;
overflow: hidden;
z-index: 1;
}
&-image {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
}
}
&__right-content {
display: flex;
padding: calc(globals.$spacing-unit * 2);
flex: 1;
gap: globals.$spacing-unit;
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
}
&__details {
padding: calc(globals.$spacing-unit * 4);
padding-bottom: 0;
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
gap: calc(globals.$spacing-unit / 2);
font-size: 14px;
gap: calc(globals.$spacing-unit * 2);
}
&__actions {
&__hero-logo {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: globals.$spacing-unit;
&-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
transition: opacity 0.2s ease;
outline: none;
&:hover {
opacity: 0.8;
}
&:focus,
&:focus-visible {
outline: none;
}
}
img {
max-width: 180px;
max-height: 60px;
object-fit: contain;
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
&:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 4px;
border-radius: 4px;
}
@container #{globals.$app-container} (min-width: 700px) {
max-width: 220px;
max-height: 75px;
}
@container #{globals.$app-container} (min-width: 900px) {
max-width: 280px;
max-height: 95px;
}
@container #{globals.$app-container} (min-width: 1200px) {
max-width: 340px;
max-height: 115px;
}
@container #{globals.$app-container} (min-width: 1500px) {
max-width: 400px;
max-height: 130px;
}
}
h1 {
font-size: 20px;
font-weight: 700;
color: #ffffff;
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
margin: 0;
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.8;
}
&:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 4px;
border-radius: 4px;
}
@container #{globals.$app-container} (min-width: 700px) {
font-size: 26px;
}
@container #{globals.$app-container} (min-width: 900px) {
font-size: 32px;
}
@container #{globals.$app-container} (min-width: 1200px) {
font-size: 38px;
}
@container #{globals.$app-container} (min-width: 1500px) {
font-size: 44px;
}
}
}
&__menu-button {
position: absolute;
top: 12px;
right: 12px;
border-radius: 50%;
border: none;
padding: 8px;
&__hero-action-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: calc(globals.$spacing-unit * 3);
margin-top: calc(globals.$spacing-unit * 4);
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__hero-buttons {
display: flex;
gap: calc(globals.$spacing-unit);
align-items: center;
flex-shrink: 0;
}
&__glass-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -4px rgba(0, 0, 0, 0.1);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
&__hero-progress {
display: flex;
flex-direction: column;
}
&__progress-info-row {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
&__progress-row {
display: flex;
align-items: flex-end;
gap: calc(globals.$spacing-unit * 2);
&--bar {
margin-top: calc(globals.$spacing-unit);
}
}
&__progress-status {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__progress-percentage {
font-size: 14px;
font-weight: 700;
color: #ffffff;
align-self: flex-end;
display: inline-block;
overflow: hidden;
line-height: 1.2;
span {
display: inline-block;
}
}
&__progress-size {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
&__progress-status {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
&__progress-time {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
font-size: 13px;
color: globals.$muted-color;
}
&__hero-stats {
display: flex;
gap: calc(globals.$spacing-unit * 4);
padding: calc(globals.$spacing-unit * 2);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(26, 26, 26, 0.1);
backdrop-filter: blur(8px);
margin-top: calc(globals.$spacing-unit * 2);
}
&__stats-column {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
min-width: 200px;
padding-right: calc(globals.$spacing-unit * 2);
border-right: 1px solid rgba(255, 255, 255, 0.1);
align-self: flex-start;
}
&__speed-chart {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
&__speed-chart-canvas {
width: 100%;
height: 80px;
image-rendering: crisp-edges;
}
&__stat-item {
display: flex;
align-items: flex-end;
gap: calc(globals.$spacing-unit);
svg {
opacity: 0.8;
flex-shrink: 0;
}
}
&__stat-content {
display: flex;
justify-content: space-between;
gap: calc(globals.$spacing-unit / 2);
width: 100%;
}
&__stat-label {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
}
&__stat-value {
color: #ffffff;
font-weight: 700;
font-size: 11px;
line-height: 1.2;
}
&__simple-list {
width: 100%;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
margin: 0;
padding: 0;
list-style: none;
}
&__simple-card {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
border-radius: 8px;
}
&__simple-thumbnail {
width: 200px;
height: 100px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid globals.$border-color;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
&__simple-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 1);
}
&__simple-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__simple-meta {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 1.5);
}
&__simple-meta-row {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
font-size: 13px;
color: globals.$muted-color;
}
&__simple-size {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
font-weight: 500;
}
&__simple-extracting {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit / 2);
font-weight: 500;
color: globals.$muted-color;
}
&__simple-seeding {
color: #4ade80;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
&__simple-progress {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
width: 200px;
flex-shrink: 0;
}
&__simple-progress-text {
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
text-align: right;
}
&__simple-actions {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
gap: calc(globals.$spacing-unit);
}
&__simple-menu-btn {
padding: calc(globals.$spacing-unit);
min-height: unset;
}
&__hydra-gradient {
background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
&__progress-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
}
&__progress-bar {
width: 100%;
position: absolute;
bottom: 0;
height: 2px;
z-index: 1;
height: 8px;
background-color: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
margin-top: calc(globals.$spacing-unit / 2);
&--small {
height: 6px;
}
}
&__progress-fill {
height: 100%;
background-color: #fff;
transition: width 0.3s ease;
border-radius: 4px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
.downloads {
&__container {
display: flex;
padding: calc(globals.$spacing-unit * 3);
flex-direction: column;
width: 100%;
}

View File

@@ -8,6 +8,7 @@ import {
import useEmblaCarousel from "embla-carousel-react";
import { gameDetailsContext } from "@renderer/context";
import { useAppSelector } from "@renderer/hooks";
import { VideoPlayer } from "./video-player";
import "./gallery-slider.scss";
export function GallerySlider() {
@@ -106,8 +107,6 @@ export function GallerySlider() {
if (shopDetails?.movies) {
shopDetails.movies.forEach((video, index) => {
// Prefer new formats: HLS (best browser support), then DASH H264, then DASH AV1
// Fallback to old format: mp4/webm if new formats are not available
let videoSrc: string | undefined;
let videoType: string | undefined;
@@ -121,11 +120,9 @@ export function GallerySlider() {
videoSrc = video.dash_av1;
videoType = "application/dash+xml";
} else if (video.mp4?.max) {
// Fallback to old format
videoSrc = video.mp4.max;
videoType = "video/mp4";
} else if (video.webm?.max) {
// Fallback to webm if mp4 is not available
videoSrc = video.webm.max;
videoType = "video/webm";
}
@@ -191,19 +188,17 @@ export function GallerySlider() {
{mediaItems.map((item) => (
<div key={item.id} className="gallery-slider__slide">
{item.type === "video" ? (
<video
controls
className="gallery-slider__media"
<VideoPlayer
videoSrc={item.videoSrc}
videoType={item.videoType}
poster={item.poster}
autoplay={autoplayEnabled}
loop
muted
autoPlay={autoplayEnabled}
controls
className="gallery-slider__media"
tabIndex={-1}
>
{item.videoSrc && (
<source src={item.videoSrc} type={item.videoType} />
)}
</video>
/>
) : (
<img
className="gallery-slider__media"

View File

@@ -0,0 +1,70 @@
import { useRef } from "react";
import { useHlsVideo } from "@renderer/hooks";
interface VideoPlayerProps {
videoSrc?: string;
videoType?: string;
poster?: string;
autoplay?: boolean;
muted?: boolean;
loop?: boolean;
controls?: boolean;
tabIndex?: number;
className?: string;
}
export function VideoPlayer({
videoSrc,
videoType,
poster,
autoplay = false,
muted = true,
loop = false,
controls = true,
tabIndex = -1,
className,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const isHls = videoType === "application/x-mpegURL";
useHlsVideo(videoRef, {
videoSrc,
videoType,
autoplay,
muted,
loop,
});
if (isHls) {
return (
<video
ref={videoRef}
controls={controls}
className={className}
poster={poster}
loop={loop}
muted={muted}
autoPlay={autoplay}
tabIndex={tabIndex}
>
<track kind="captions" />
</video>
);
}
return (
<video
ref={videoRef}
controls={controls}
className={className}
poster={poster}
loop={loop}
muted={muted}
autoPlay={autoplay}
tabIndex={tabIndex}
>
{videoSrc && <source src={videoSrc} type={videoType} />}
<track kind="captions" />
</video>
);
}

View File

@@ -231,9 +231,19 @@ export function RepacksModal({
return false;
}
const lastCheckUtc = new Date(lastCheckTimestamp).toISOString();
try {
const lastCheckDate = new Date(lastCheckTimestamp);
return repack.createdAt > lastCheckUtc;
if (isNaN(lastCheckDate.getTime())) {
return false;
}
const lastCheckUtc = lastCheckDate.toISOString();
return repack.createdAt > lastCheckUtc;
} catch {
return false;
}
};
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);

View File

@@ -76,7 +76,13 @@ export default function Library() {
switch (filterBy) {
case "recently_played":
filtered = library.filter((game) => game.lastTimePlayed !== null);
filtered = library
.filter((game) => game.lastTimePlayed !== null)
.sort(
(a: any, b: any) =>
new Date(b.lastTimePlayed).getTime() -
new Date(a.lastTimePlayed).getTime()
);
break;
case "favorites":
filtered = library.filter((game) => game.favorite);

View File

@@ -14,7 +14,7 @@ export interface SteamVideoSource {
"480": string;
}
export interface SteamMovies {
export interface SteamMovie {
id: number;
dash_av1?: string;
dash_h264?: string;
@@ -34,7 +34,7 @@ export interface SteamAppDetails {
short_description: string;
publishers: string[];
genres: SteamGenre[];
movies?: SteamMovies[];
movies?: SteamMovie[];
supported_languages: string;
screenshots?: SteamScreenshot[];
pc_requirements: {

View File

@@ -5690,6 +5690,11 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hls.js@^1.5.12:
version "1.6.15"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.15.tgz#9ce13080d143a9bc9b903fb43f081e335b8321e5"
integrity sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"