mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Merge branch 'fix/downloads-ui' of https://github.com/hydralauncher/hydra
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user