- 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
157 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|