Fbrowser/src/components/ArchivePanel.tsx
stan44 565be4e1e7 Migrate Fbrowser to Rust and Tauri desktop app
- Replace the Python/Docker setup with a Rust workspace and Tauri frontend
- Add core crates for archive, audio, MIDI, plugin, and desktop UI layers
- Refresh the app scaffolding, build config, and documentation
2026-03-30 16:18:26 -05:00

157 lines
6.2 KiB
TypeScript

import { useState } from "react";
import { open, save } from "@tauri-apps/plugin-dialog";
import type { ArchiveJobResult } from "../lib/types";
interface ArchivePanelProps {
onExtract: (source: string, destination: string) => Promise<ArchiveJobResult>;
onCompress: (source: string, destination: string) => Promise<ArchiveJobResult>;
}
type ArchiveAction = "extract" | "compress" | null;
export function ArchivePanel({ onExtract, onCompress }: ArchivePanelProps) {
const [source, setSource] = useState("");
const [destination, setDestination] = useState("");
const [lastResult, setLastResult] = useState<ArchiveJobResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [pendingAction, setPendingAction] = useState<ArchiveAction>(null);
async function runAction(action: Exclude<ArchiveAction, null>) {
if (!source || !destination) {
setError("Choose both a source and destination before running an archive job.");
return;
}
setPendingAction(action);
setError(null);
try {
const result = action === "extract" ? await onExtract(source, destination) : await onCompress(source, destination);
setLastResult(result);
} catch (cause) {
setError(cause instanceof Error ? cause.message : "Archive job failed.");
} finally {
setPendingAction(null);
}
}
return (
<div className="glass rounded-[32px] p-6">
<div className="text-xs uppercase tracking-[0.2em] text-textMuted">Archive utilities</div>
<h2 className="mt-2 text-2xl font-semibold text-white">Extract and package sample crates</h2>
<p className="mt-3 max-w-2xl text-sm text-textMuted">
Supports <span className="text-text">ZIP</span>, <span className="text-text">TAR</span>, and{" "}
<span className="text-text">TAR.GZ / TGZ</span>. Extraction expects an archive file and a target
folder. Compression accepts either a file or directory and writes a new archive.
</p>
<div className="mt-6 grid gap-4">
<div className="rounded-3xl border border-line/35 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.2em] text-textMuted">Source</div>
<div className="mt-3 flex gap-3">
<input
value={source}
onChange={(event) => setSource(event.target.value)}
className="flex-1 rounded-2xl border border-line/40 bg-panel/70 px-4 py-3 text-text outline-none"
placeholder="Archive file to extract, or file/folder to compress"
/>
<button
type="button"
className="rounded-2xl border border-line/45 px-4 py-3 text-text"
onClick={async () => {
const picked = await open({
directory: false,
multiple: false,
filters: [{ name: "Archives", extensions: ["zip", "tar", "gz", "tgz"] }],
});
if (typeof picked === "string") setSource(picked);
}}
>
File
</button>
<button
type="button"
className="rounded-2xl border border-line/45 px-4 py-3 text-text"
onClick={async () => {
const picked = await open({ directory: true, multiple: false });
if (typeof picked === "string") setSource(picked);
}}
>
Folder
</button>
</div>
</div>
<div className="rounded-3xl border border-line/35 bg-white/5 p-4">
<div className="text-xs uppercase tracking-[0.2em] text-textMuted">Destination</div>
<div className="mt-3 flex gap-3">
<input
value={destination}
onChange={(event) => setDestination(event.target.value)}
className="flex-1 rounded-2xl border border-line/40 bg-panel/70 px-4 py-3 text-text outline-none"
placeholder="Target folder for extract, or archive path like crate.tar.gz"
/>
<button
type="button"
className="rounded-2xl border border-line/45 px-4 py-3 text-text"
onClick={async () => {
const picked = await open({ directory: true, multiple: false });
if (typeof picked === "string") setDestination(picked);
}}
>
Folder
</button>
<button
type="button"
className="rounded-2xl border border-line/45 px-4 py-3 text-text"
onClick={async () => {
const picked = await save({
filters: [
{ name: "ZIP", extensions: ["zip"] },
{ name: "TAR", extensions: ["tar"] },
{ name: "TAR.GZ", extensions: ["tar.gz", "tgz"] },
],
});
if (typeof picked === "string") setDestination(picked);
}}
>
Save as
</button>
</div>
</div>
</div>
<div className="mt-5 flex gap-3">
<button
type="button"
onClick={() => void runAction("extract")}
disabled={pendingAction !== null}
className="rounded-2xl bg-accent px-5 py-3 font-semibold text-black disabled:cursor-not-allowed disabled:opacity-50"
>
{pendingAction === "extract" ? "Extracting..." : "Extract"}
</button>
<button
type="button"
onClick={() => void runAction("compress")}
disabled={pendingAction !== null}
className="rounded-2xl border border-line/45 px-5 py-3 text-text disabled:cursor-not-allowed disabled:opacity-50"
>
{pendingAction === "compress" ? "Compressing..." : "Create archive"}
</button>
</div>
{error ? (
<div className="mt-5 rounded-2xl border border-red-400/35 bg-red-500/10 p-4 text-sm text-red-100">
{error}
</div>
) : null}
{lastResult ? (
<div className="mt-5 rounded-2xl border border-line/35 bg-white/5 p-4 text-sm text-text">
Output: {lastResult.output_path}
<div className="mt-2 text-textMuted">{lastResult.processed_entries} items processed</div>
</div>
) : null}
</div>
);
}