import { errorMessage } from "./host-remote"; import { useCallback, useEffect, useRef, useState } from "react"; import Guacamole from "guacamole-common-js"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { ArrowUp, Download, File as FileIcon, Folder, RefreshCw, Upload, X, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@termix/plugin-sdk/ui"; import { downloadFile, listDirectory, parentPath, saveBlobAs, uploadFile, describeUploadError, type RemoteFileEntry, } from "./guacamole-filesystem.ts"; interface GuacamoleFileBrowserProps { filesystem: Guacamole.Object; allowUpload: boolean; allowDownload: boolean; pendingUploads: File[]; onPendingUploadsHandled: () => void; onClose: () => void; } export function GuacamoleFileBrowser({ filesystem, allowUpload, allowDownload, pendingUploads, onPendingUploadsHandled, onClose, }: GuacamoleFileBrowserProps) { const { t } = useTranslation(); const [path, setPath] = useState("/"); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [busyName, setBusyName] = useState(null); const fileInputRef = useRef(null); const refresh = useCallback( async (target: string) => { setLoading(true); setError(null); try { setEntries(await listDirectory(filesystem, target)); } catch (err) { setEntries([]); setError(errorMessage(err, t("remoteDesktop.files.listFailed"))); } finally { setLoading(false); } }, [filesystem, t], ); useEffect(() => { void refresh(path); }, [refresh, path]); const upload = useCallback( async (files: File[]) => { for (const file of files) { setBusyName(file.name); try { await uploadFile(filesystem, path, file); toast.success(t("remoteDesktop.files.uploaded", { name: file.name })); } catch (err) { toast.error( describeUploadError(err, (key) => t(`remoteDesktop.files.${key}`, { name: file.name }), ), ); } finally { setBusyName(null); } } void refresh(path); }, [filesystem, path, refresh, t], ); // Files dropped on the display land here so they share the browser's current // directory rather than always going to the drive root. useEffect(() => { if (pendingUploads.length === 0) return; onPendingUploadsHandled(); if (!allowUpload) { toast.error(t("remoteDesktop.files.uploadDisabled")); return; } void upload(pendingUploads); }, [pendingUploads, onPendingUploadsHandled, allowUpload, upload, t]); const download = useCallback( async (entry: RemoteFileEntry) => { setBusyName(entry.name); try { const { blob, filename } = await downloadFile(filesystem, entry.path); saveBlobAs(blob, filename); } catch (err) { toast.error( err instanceof Error ? err.message : t("remoteDesktop.files.downloadFailed", { name: entry.name }), ); } finally { setBusyName(null); } }, [filesystem, t], ); return (
{t("remoteDesktop.files.title")}
{path}
{loading && (

{t("common.loading")}

)} {!loading && error && (

{error}

)} {!loading && !error && entries.length === 0 && (

{t("remoteDesktop.files.empty")}

)} {!loading && !error && entries.map((entry) => (
{entry.isDirectory ? ( ) : ( )} {!entry.isDirectory && allowDownload && ( )}
))}
{allowUpload && (
{ const files = Array.from(e.target.files ?? []); e.target.value = ""; if (files.length > 0) void upload(files); }} />
)}
); }