Merge branch 'fix/downloads-ui' of https://github.com/hydralauncher/hydra

This commit is contained in:
Chubby Granny Chaser
2025-11-30 05:06:59 +00:00
4 changed files with 187 additions and 137 deletions

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

@@ -28,7 +28,15 @@
}
&-count {
font-weight: 400;
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
min-width: 24px;
text-align: center;
flex-shrink: 0;
}
}
&--hero {
@@ -37,7 +45,7 @@
overflow: hidden;
margin: 0;
padding: 0;
padding-bottom: calc(globals.$spacing-unit * 3);
padding-bottom: globals.$spacing-unit;
}
&__hero-background {
@@ -80,34 +88,59 @@
gap: calc(globals.$spacing-unit * 2);
}
&__hero-header {
display: flex;
justify-content: flex-end;
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__hero-logo {
flex: 1;
min-width: 0;
img {
max-width: 600px;
max-height: 200px;
max-width: 180px;
max-height: 60px;
object-fit: contain;
@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: 64px;
font-size: 20px;
font-weight: 700;
color: #ffffff;
text-shadow: 2px 2px 12px rgba(0, 0, 0, 0.9);
margin: 0;
}
@container #{globals.$app-container} (min-width: 700px) {
font-size: 26px;
}
&__hero-actions {
display: flex;
gap: calc(globals.$spacing-unit);
align-items: center;
@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;
}
}
}
&__hero-action-row {
@@ -115,30 +148,60 @@
justify-content: space-between;
align-items: flex-end;
gap: calc(globals.$spacing-unit * 3);
margin-bottom: calc(globals.$spacing-unit * 3);
margin-top: calc(globals.$spacing-unit * 4);
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__hero-menu-btn {
background-color: rgba(0, 0, 0, 0.4);
padding: calc(globals.$spacing-unit * 1);
min-height: unset;
&__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-menu-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
&__hero-progress {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit);
margin-bottom: calc(globals.$spacing-unit * 3);
}
&__progress-header {
&__progress-info-row {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: calc(globals.$spacing-unit / 2);
}
&__progress-row {
display: flex;
align-items: flex-end;
gap: calc(globals.$spacing-unit * 2);
&--bar {
margin-top: calc(globals.$spacing-unit);
}
}
&__progress-status {
@@ -155,20 +218,14 @@
color: #ffffff;
}
&__progress-details {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.9);
margin-top: calc(globals.$spacing-unit / 2);
}
&__progress-size {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
&__progress-time {
font-size: 13px;
color: globals.$muted-color;
}
@@ -190,6 +247,7 @@
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 {
@@ -202,7 +260,7 @@
&__speed-chart-canvas {
width: 100%;
height: 100px;
height: 80px;
image-rendering: crisp-edges;
}
@@ -281,7 +339,7 @@
min-width: 0;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
gap: calc(globals.$spacing-unit / 1);
}
&__simple-title {
@@ -342,13 +400,20 @@
min-height: unset;
}
&__progress-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
}
&__progress-bar {
width: 100%;
height: 8px;
background-color: rgba(255, 255, 255, 0.08);
border-radius: 4px;
overflow: hidden;
position: relative;
margin-top: calc(globals.$spacing-unit / 2);
&--small {
height: 6px;
@@ -357,10 +422,8 @@
&__progress-fill {
height: 100%;
background-color: globals.$muted-color;
transition:
width 0.3s ease,
background 0.35s ease;
background-color: #fff;
transition: width 0.3s ease;
border-radius: 4px;
}
}

View File

@@ -29,23 +29,6 @@ import {
} from "@primer/octicons-react";
import { average } from "color.js";
const getProgressGradient = (
colorHex: string,
isPaused = false
): string | undefined => {
const hex = isPaused ? "#ffffff" : colorHex || "#08ea79";
if (!hex.startsWith("#")) return undefined;
try {
const r = Number.parseInt(hex.slice(1, 3), 16);
const g = Number.parseInt(hex.slice(3, 5), 16);
const b = Number.parseInt(hex.slice(5, 7), 16);
return `linear-gradient(90deg, rgba(${r},${g},${b},0.95) 0%, rgba(${r},${g},${b},0.65) 100%)`;
} catch {
return undefined;
}
};
interface SpeedChartProps {
speeds: number[];
peakSpeed: number;
@@ -158,12 +141,12 @@ interface HeroDownloadViewProps {
dominantColor: string;
lastPacket: ReturnType<typeof useDownload>["lastPacket"];
speedHistory: number[];
getGameActions: (game: LibraryGame) => DropdownMenuItem[];
getStatusText: (game: LibraryGame) => string;
formatSpeed: (speed: number) => string;
calculateETA: () => string;
pauseDownload: (shop: GameShop, objectId: string) => void;
resumeDownload: (shop: GameShop, objectId: string) => void;
cancelDownload: (shop: GameShop, objectId: string) => void;
t: (key: string) => string;
}
@@ -177,12 +160,12 @@ function HeroDownloadView({
dominantColor,
lastPacket,
speedHistory,
getGameActions,
getStatusText,
formatSpeed,
calculateETA,
pauseDownload,
resumeDownload,
cancelDownload,
t,
}: Readonly<HeroDownloadViewProps>) {
return (
@@ -196,16 +179,6 @@ function HeroDownloadView({
</div>
<div className="download-group__hero-content">
<div className="download-group__hero-header">
<div className="download-group__hero-actions">
<DropdownMenu align="end" items={getGameActions(game)}>
<Button className="download-group__hero-menu-btn" theme="outline">
<ThreeBarsIcon />
</Button>
</DropdownMenu>
</div>
</div>
<div className="download-group__hero-action-row">
<div className="download-group__hero-logo">
{game.logoImageUrl ? (
@@ -214,38 +187,12 @@ function HeroDownloadView({
<h1>{game.title}</h1>
)}
</div>
{isGameDownloading ? (
<Button
theme="primary"
onClick={() => pauseDownload(game.shop, game.objectId)}
className="download-group__hero-action-btn"
style={{
backgroundColor: dominantColor,
borderColor: dominantColor,
}}
>
<ColumnsIcon size={16} />
{t("pause")}
</Button>
) : (
<Button
theme="primary"
onClick={() => resumeDownload(game.shop, game.objectId)}
className="download-group__hero-action-btn"
style={{
backgroundColor: dominantColor,
borderColor: dominantColor,
}}
>
<PlayIcon size={16} />
{t("resume")}
</Button>
)}
</div>
<div className="download-group__hero-progress">
<div className="download-group__progress-header">
<div className="download-group__progress-row download-group__progress-row--bar">
<div className="download-group__progress-wrapper">
<div className="download-group__progress-info-row">
<span className="download-group__progress-status">
{getStatusText(game)}
</span>
@@ -253,19 +200,7 @@ function HeroDownloadView({
{formatDownloadProgress(currentProgress)}
</span>
</div>
<div className="download-group__progress-bar">
<div
className="download-group__progress-fill"
style={{
width: `${currentProgress * 100}%`,
background: getProgressGradient(
dominantColor,
game.download?.status === "paused"
),
}}
/>
</div>
<div className="download-group__progress-details">
<div className="download-group__progress-info-row">
<span className="download-group__progress-size">
{isGameDownloading && lastPacket
? `${formatBytes(lastPacket.download.bytesDownloaded)} / ${finalDownloadSize}`
@@ -279,6 +214,45 @@ function HeroDownloadView({
: ""}
</span>
</div>
<div className="download-group__progress-bar">
<div
className="download-group__progress-fill"
style={{
width: `${currentProgress * 100}%`,
}}
/>
</div>
</div>
<div className="download-group__hero-buttons">
{isGameDownloading ? (
<button
type="button"
onClick={() => pauseDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<ColumnsIcon size={14} />
{t("pause")}
</button>
) : (
<button
type="button"
onClick={() => resumeDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<PlayIcon size={14} />
{t("resume")}
</button>
)}
<button
type="button"
onClick={() => cancelDownload(game.shop, game.objectId)}
className="download-group__glass-btn"
>
<XCircleIcon size={14} />
{t("cancel")}
</button>
</div>
</div>
</div>
<div className="download-group__hero-stats">
@@ -717,12 +691,12 @@ export function DownloadGroup({
dominantColor={dominantColor}
lastPacket={lastPacket}
speedHistory={speedHistoryRef.current[game.id] || []}
getGameActions={getGameActions}
getStatusText={getStatusText}
formatSpeed={formatSpeed}
calculateETA={calculateETA}
pauseDownload={pauseDownload}
resumeDownload={resumeDownload}
cancelDownload={cancelDownload}
t={t}
/>
);

View File

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