diff --git a/ui/src/components/library/LibraryResultPanel.css b/ui/src/components/library/LibraryResultPanel.css new file mode 100644 index 0000000..2641f7b --- /dev/null +++ b/ui/src/components/library/LibraryResultPanel.css @@ -0,0 +1,67 @@ +.library-result { + background: var(--c-ink-2); + border: 1px solid var(--c-ink-5); + border-radius: var(--r-3); + padding: var(--s-4); + margin-bottom: var(--s-3); + display: flex; + flex-direction: column; + gap: var(--s-3); +} + +.library-result.error { + border-color: var(--c-danger); +} + +.library-result header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.library-result dl { + margin: 0; + display: grid; + grid-template-columns: max-content 1fr; + gap: var(--s-2) var(--s-4); + font-size: var(--fs-small); +} + +.library-result dl > div { + display: contents; +} + +.library-result dt { + color: var(--c-ink-9); +} + +.library-result dd { + margin: 0; +} + +.library-result dd.mono { + font-family: var(--font-mono); + word-break: break-all; +} + +.library-result details { + font-size: var(--fs-small); +} + +.library-result details summary { + cursor: pointer; + color: var(--c-warn); +} + +.library-result details ul { + margin: var(--s-2) 0 0; + padding-left: var(--s-5); +} + +.library-result-error { + margin: 0; + font-family: var(--font-mono); + font-size: var(--fs-small); + white-space: pre-wrap; + color: var(--c-danger); +} diff --git a/ui/src/screens/LibraryScreen.tsx b/ui/src/screens/LibraryScreen.tsx index 04d3731..ae7da7d 100644 --- a/ui/src/screens/LibraryScreen.tsx +++ b/ui/src/screens/LibraryScreen.tsx @@ -1,6 +1,7 @@ import { open, save } from "@tauri-apps/plugin-dialog"; import { useCallback, useMemo, useState } from "react"; +import "@/components/library/LibraryResultPanel.css"; import { FormatPickerModal } from "@/components/library/FormatPickerModal"; import { ipc, @@ -87,10 +88,23 @@ export function LibraryScreen({ const [pickerMode, setPickerMode] = useState<"export" | "import" | null>( null, ); + const [libraryBusy, setLibraryBusy] = useState<"export" | "import" | null>( + null, + ); const [libraryResult, setLibraryResult] = useState< - | { kind: "export"; report: LibraryExportReport } - | { kind: "import"; report: LibraryImportReport } - | { kind: "error"; message: string } + | { + kind: "export"; + format: FormatInfo; + destination: string; + report: LibraryExportReport; + } + | { + kind: "import"; + format: FormatInfo; + source: string; + report: LibraryImportReport; + } + | { kind: "error"; mode: "export" | "import"; message: string } | null >(null); @@ -102,20 +116,24 @@ export function LibraryScreen({ if (mode === "export") { const destination = await pickExportDestination(f); if (!destination) return; + setLibraryBusy("export"); const report = await ipc.libraryExport({ format: f.format, destination, }); - setLibraryResult({ kind: "export", report }); + setLibraryResult({ kind: "export", format: f, destination, report }); } else { const source = await pickImportSource(f); if (!source) return; + setLibraryBusy("import"); const report = await ipc.libraryImport({ format: f.format, source }); - setLibraryResult({ kind: "import", report }); + setLibraryResult({ kind: "import", format: f, source, report }); await refresh(); } } catch (e) { - setLibraryResult({ kind: "error", message: String(e) }); + setLibraryResult({ kind: "error", mode, message: String(e) }); + } finally { + setLibraryBusy(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pickerMode]); @@ -199,17 +217,18 @@ export function LibraryScreen({ {filtered.length} @@ -264,61 +283,16 @@ export function LibraryScreen({

)} + {libraryBusy && ( +

+ {libraryBusy === "export" ? "Exporting library…" : "Importing library…"} +

+ )} {libraryResult && ( -
- {libraryResult.kind === "export" && ( - <> - Exported {libraryResult.report.tracks_written} track(s) as{" "} - {libraryResult.report.format} - {libraryResult.report.warnings.length > 0 && ( - <>. {libraryResult.report.warnings.length} warning(s) - )} - .{" "} - - - )} - {libraryResult.kind === "import" && ( - <> - Imported {libraryResult.report.tracks_imported} new,{" "} - {libraryResult.report.tracks_updated} updated,{" "} - {libraryResult.report.tracks_skipped} skipped ( - {libraryResult.report.format}).{" "} - - - )} - {libraryResult.kind === "error" && ( - <> - Library operation failed: {libraryResult.message}{" "} - - - )} -
+ setLibraryResult(null)} + /> )} void; +}) { + if (result.kind === "error") { + return ( +
+
+ + {result.mode === "export" ? "Export failed" : "Import failed"} + + +
+
{result.message}
+
+ ); + } + if (result.kind === "export") { + return ( +
+
+ Export complete · {result.format.label} + +
+
+
+
Destination
+
{result.destination}
+
+
+
Tracks
+
{result.report.tracks_written}
+
+
+
Bytes
+
{formatBytes(result.report.bytes_written)}
+
+
+ {result.report.warnings.length > 0 && ( +
+ + {result.report.warnings.length} warning + {result.report.warnings.length === 1 ? "" : "s"} + +
    + {result.report.warnings.map((w, i) => ( +
  • {w}
  • + ))} +
+
+ )} +
+ ); + } + // import + return ( +
+
+ Import complete · {result.format.label} + +
+
+
+
Source
+
{result.source}
+
+
+
Imported
+
{result.report.tracks_imported}
+
+
+
Updated
+
{result.report.tracks_updated}
+
+
+
Skipped
+
{result.report.tracks_skipped}
+
+
+ {result.report.warnings.length > 0 && ( +
+ + {result.report.warnings.length} warning + {result.report.warnings.length === 1 ? "" : "s"} + +
    + {result.report.warnings.map((w, i) => ( +
  • {w}
  • + ))} +
+
+ )} +
+ ); +} + function formatBytes(n: number): string { if (!Number.isFinite(n) || n <= 0) return "0 B"; const units = ["B", "KiB", "MiB", "GiB", "TiB"];