IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

usb-storage-dialog.tsx

Open raw
"use client";

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { Loader2, RefreshCw, X, HardDrive, Plug, EjectIcon } from "@/components/icons/platform-icons";
import { useUsbDrives } from "@/modules/files/hooks/useUsbDrives";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/shared/query-keys";

type UsbStorageDialogProps = {
  isOpen: boolean;
  onClose: () => void;
  onNavigateToUsb: (path: string[]) => void;
};

export function UsbStorageDialog({ isOpen, onClose, onNavigateToUsb }: UsbStorageDialogProps) {
  const queryClient = useQueryClient();
  const { drives, mount, unmount, eject, isMounting, isUnmounting, isEjecting } = useUsbDrives();

  const isBusy = isMounting || isUnmounting || isEjecting;

  function handleRefresh() {
    void queryClient.invalidateQueries({ queryKey: queryKeys.usbDrives });
  }

  return (
    <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
      <DialogContent
        showCloseButton={false}
        className="max-w-[min(96vw,60rem)] gap-0 overflow-hidden rounded-[calc(var(--radius)+0.75rem)] border-glass-border bg-popover/96 p-0 shadow-2xl shadow-black/45 backdrop-blur-2xl"
      >
        <DialogHeader className="sr-only">
          <DialogTitle>USB & Removable Storage</DialogTitle>
          <DialogDescription>Manage removable drives connected to your server.</DialogDescription>
        </DialogHeader>

        {/* Window chrome title bar */}
        <div className="flex h-11 shrink-0 select-none items-center gap-0 border-b border-glass-border/50 bg-popover/70 backdrop-blur-2xl">
          {/* Traffic lights */}
          <div className="flex items-center gap-1.5 px-4">
            <button
              onClick={onClose}
              className="group flex size-3 cursor-pointer items-center justify-center rounded-full bg-[#ff5f57] transition-all hover:brightness-110"
              aria-label="Close"
            >
              <X className="size-[7px] text-[#6a0002] opacity-0 transition-opacity group-hover:opacity-100" />
            </button>
            <span className="size-3 rounded-full bg-white/10" />
            <span className="size-3 rounded-full bg-white/10" />
          </div>

          {/* Centered title */}
          <div className="flex flex-1 items-center justify-center">
            <span className="text-xs font-medium text-foreground/80">USB & Removable Storage</span>
          </div>

          {/* Right actions */}
          <div className="flex items-center justify-end gap-2 px-4">
            <button
              type="button"
              onClick={handleRefresh}
              disabled={isBusy}
              className="inline-flex h-6 items-center gap-1 rounded-md border border-glass-border bg-background/80 px-2 text-[11px] text-muted-foreground transition-colors hover:bg-background/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
            >
              <RefreshCw className="size-3" />
              Rescan
            </button>
          </div>
        </div>

        {/* Content */}
        <div className="relative min-h-[50vh] overflow-y-auto p-5">
          {drives.length === 0 ? (
            <div className="flex min-h-52 flex-col items-center justify-center gap-2 text-center text-sm text-muted-foreground">
              <HardDrive className="size-8 opacity-25" />
              <span>No removable drives detected.</span>
              <span className="text-xs opacity-60">Plug in a USB drive and click Rescan.</span>
            </div>
          ) : (
            <div className="flex flex-col gap-1.5">
              <p className="mb-2 text-[10px] uppercase tracking-[0.18em] text-muted-foreground/50">
                Detected drives
              </p>
              {drives.map((drive) => (
                <div
                  key={drive.id}
                  className={`flex items-center gap-3 rounded-xl border px-3 py-2.5 transition-colors ${
                    drive.isMounted
                      ? "border-emerald-500/20 bg-card/60"
                      : "border-glass-border bg-card/60"
                  }`}
                >
                  <div className="flex size-8 shrink-0 items-center justify-center rounded-lg border border-glass-border bg-background/55">
                    <Plug
                      className={`size-4 ${drive.isMounted ? "text-emerald-400" : "text-amber-400"}`}
                    />
                  </div>

                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-2">
                      <span className="truncate text-sm font-medium text-foreground">
                        {drive.label}
                      </span>
                      {drive.isMounted && (
                        <span className="shrink-0 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-medium text-emerald-400">
                          Mounted
                        </span>
                      )}
                    </div>
                    <div className="flex items-center gap-2 text-[10px] text-muted-foreground/60">
                      <span>{drive.size}</span>
                      {drive.partitions[0]?.fstype && (
                        <>
                          <span>·</span>
                          <span>{drive.partitions[0].fstype.toUpperCase()}</span>
                        </>
                      )}
                      {drive.isMounted && drive.mountpoint && (
                        <>
                          <span>·</span>
                          <button
                            type="button"
                            onClick={() => onNavigateToUsb(["Removable", drive.label])}
                            className="truncate hover:text-foreground hover:underline"
                          >
                            {drive.mountpoint}
                          </button>
                        </>
                      )}
                    </div>
                  </div>

                  <div className="flex shrink-0 items-center gap-1.5">
                    {isBusy ? (
                      <Loader2 className="size-4 animate-spin text-muted-foreground/40" />
                    ) : drive.isMounted ? (
                      <>
                        <button
                          type="button"
                          onClick={() => { unmount(drive.id); }}
                          className="rounded-lg bg-amber-500/15 px-3 py-1.5 text-xs font-medium text-amber-400 transition-colors hover:bg-amber-500/25"
                        >
                          Unmount
                        </button>
                        <button
                          type="button"
                          onClick={() => { eject(drive.id); onClose(); }}
                          title="Eject"
                          className="flex size-7 items-center justify-center rounded-lg text-muted-foreground/40 transition-colors hover:bg-background/50 hover:text-foreground"
                        >
                          <EjectIcon className="size-3.5" />
                        </button>
                      </>
                    ) : (
                      <button
                        type="button"
                        onClick={() => { mount(drive.id); }}
                        className="rounded-lg bg-sky-500/15 px-3 py-1.5 text-xs font-medium text-sky-400 transition-colors hover:bg-sky-500/25"
                      >
                        Mount
                      </button>
                    )}
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>
      </DialogContent>
    </Dialog>
  );
}
bytes
7,848
lines
178
Type
tsx
Latest commit
16 October 2026