mirror of
https://github.com/Xevion/dynamic-preauth.git
synced 2025-12-10 16:07:08 -06:00
DownloadButton progress
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Badge from "@/components/Badge";
|
||||
import DownloadButton from "@/components/DownloadButton";
|
||||
import Emboldened from "@/components/Emboldened";
|
||||
import useSocket from "@/components/useSocket";
|
||||
import { cn, plural, toHex, type ClassValue } from "@/util";
|
||||
@@ -9,9 +10,11 @@ type DemoProps = {
|
||||
};
|
||||
|
||||
const Demo = ({ class: className }: DemoProps) => {
|
||||
const { id, downloads } = useSocket();
|
||||
const { id, downloads, executables } = useSocket();
|
||||
// TODO: Toasts
|
||||
|
||||
console.log([executables == null]);
|
||||
|
||||
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
|
||||
const highlightedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -29,8 +32,8 @@ const Demo = ({ class: className }: DemoProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={cn(className, "px-5 leading-6")}>
|
||||
<p class="mt-3 mb-3">
|
||||
<div className={cn(className, "px-5 leading-6")}>
|
||||
<p className="mt-3 mb-3">
|
||||
This demo uses websockets to communicate between the server and the
|
||||
browser. Each download gets a unique identifier bound to the user
|
||||
session.
|
||||
@@ -45,7 +48,12 @@ const Demo = ({ class: className }: DemoProps) => {
|
||||
</Emboldened>{" "}
|
||||
known {plural("download", downloads?.length ?? 0)}.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-y-2.5">
|
||||
<div className="flex flex-wrap justify-center gap-y-2.5">
|
||||
<DownloadButton
|
||||
disabled={executables == null}
|
||||
buildLog={"https://railway.com"}
|
||||
executables={executables}
|
||||
/>
|
||||
{downloads?.map((download, i) => (
|
||||
<Badge
|
||||
className={cn(
|
||||
@@ -65,8 +73,8 @@ const Demo = ({ class: className }: DemoProps) => {
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-4 p-2 bg-zinc-900/90 rounded-md border border-zinc-700">
|
||||
<p class="my-0">
|
||||
<div className="mt-4 p-2 bg-zinc-900/90 rounded-md border border-zinc-700">
|
||||
<p className="my-0">
|
||||
The server running this is completely ephemeral, can restart at any
|
||||
time, and purges data on regular intervals - at which point the
|
||||
executables you've downloaded will no longer function.
|
||||
|
||||
177
frontend/src/components/DownloadButton.tsx
Normal file
177
frontend/src/components/DownloadButton.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import type { Executable } from "@/components/useSocket";
|
||||
import { cn, withBackend } from "@/util";
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
MenuSeparator,
|
||||
} from "@headlessui/react";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
BeakerIcon,
|
||||
ChevronDownIcon,
|
||||
} from "@heroicons/react/16/solid";
|
||||
import { useRef } from "react";
|
||||
|
||||
type DownloadButtonProps = {
|
||||
disabled?: boolean;
|
||||
executables: Executable[] | null;
|
||||
buildLog: string | null;
|
||||
};
|
||||
|
||||
type SystemType = "windows" | "macos" | "linux";
|
||||
|
||||
function getSystemType(): SystemType | null {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes("win")) {
|
||||
return "windows";
|
||||
} else if (userAgent.includes("mac")) {
|
||||
return "macos";
|
||||
} else if (userAgent.includes("linux")) {
|
||||
return "linux";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DownloadButton({
|
||||
disabled,
|
||||
executables,
|
||||
buildLog,
|
||||
}: DownloadButtonProps) {
|
||||
const menuRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
console.log({ disabled });
|
||||
|
||||
function getExecutable(id: string) {
|
||||
return executables?.find((e) => e.id.toLowerCase() === id.toLowerCase());
|
||||
}
|
||||
|
||||
async function handleDownload(id: string) {
|
||||
const executable = getExecutable(id);
|
||||
if (executable == null) {
|
||||
console.error(`Executable ${id} not found, cannot download`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(withBackend(`/download/${executable.id}`), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.headers.get("Content-Type") === "application/json") {
|
||||
const json = await response.json();
|
||||
console.error("Download failed", json);
|
||||
} else {
|
||||
console.error(
|
||||
"Download failed (unreadable response)",
|
||||
response.statusText
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create blob link to download
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a link element and click it to download the file
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", executable.filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode!.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Download failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadAutomatic() {
|
||||
const systemType = getSystemType();
|
||||
|
||||
// If the system type is unknown/unavailable, open the menu for manual selection
|
||||
if (systemType == null || getExecutable(systemType) == null) {
|
||||
menuRef.current?.click();
|
||||
}
|
||||
|
||||
// Otherwise, download the executable automatically
|
||||
else {
|
||||
handleDownload(systemType);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"[&>*]:py-1 overflow-clip transition-[background-color] text-sm/6 flex items-center shadow-inner align-middle text-white focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white",
|
||||
!disabled
|
||||
? "divide-white/[0.2] shadow-white/10 bg-emerald-800 data-[hover]:bg-emerald-700 data-[open]:bg-emerald-700"
|
||||
: "divide-white/[0.1] shadow-white/5 animate-pulse-dark data-[hover]:bg-[#064e3b] cursor-wait",
|
||||
"rounded-md divide-x h-full rounded-l-md"
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
onClick={handleDownloadAutomatic}
|
||||
disabled={disabled}
|
||||
className={cn("pl-3 font-semibold pr-2.5", {
|
||||
"hover:bg-white/5": !disabled,
|
||||
})}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
ref={menuRef}
|
||||
disabled={disabled ?? false}
|
||||
className={cn("pl-1.5 text-transparent min-h-8 pr-2", {
|
||||
"hover:bg-white/5": !disabled,
|
||||
})}
|
||||
>
|
||||
<ChevronDownIcon className="size-4 fill-white/60" />
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
transition
|
||||
anchor="bottom end"
|
||||
className="w-40 z-20 mt-1 origin-top-right rounded-xl border border-white/[0.08] bg-zinc-900 shadow-md p-1 text-sm/6 text-zinc-200 transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
{executables?.map((executable) => (
|
||||
<MenuItem key={executable.id}>
|
||||
<button
|
||||
className="group flex w-full items-center justify-between gap-2 rounded-lg py-1.5 pl-2 pr-2.5 data-[focus]:bg-white/10"
|
||||
onClick={() => handleDownload(executable.id)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ArrowDownTrayIcon className="size-4 fill-white/40" />
|
||||
{executable.id}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{(executable.size / 1024 / 1024).toFixed(1)} MiB
|
||||
</div>
|
||||
</button>
|
||||
</MenuItem>
|
||||
))}
|
||||
{buildLog != null ? (
|
||||
<>
|
||||
<MenuSeparator className="my-1 h-px bg-white/10" />
|
||||
<MenuItem>
|
||||
<button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-2 data-[focus]:bg-white/10">
|
||||
<BeakerIcon className="size-4 fill-white/40" />
|
||||
Build Logs
|
||||
</button>
|
||||
</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user