import { Download, Loader2, PackageOpen } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { CollapsibleSection } from "@/components/common/collapsible-section"; import { formatHeaders } from "@/lib/api"; import { useFileStore } from "@/stores/file-store"; import type { SplitMode } from "@/stores/split-store"; import { useSplitStore } from "@/stores/split-store"; const MODES: Array<{ id: SplitMode; label: string }> = [ { id: "grid", label: "Grid" }, { id: "tile-size", label: "Tile Size" }, ]; const PRESETS = [ { label: "2x1", c: 2, r: 1, desc: "Horizontal half" }, { label: "1x2", c: 1, r: 2, desc: "Vertical half" }, { label: "2x2", c: 2, r: 2, desc: "Quarters" }, { label: "3x1", c: 3, r: 1, desc: "Horizontal strip" }, { label: "1x3", c: 1, r: 3, desc: "Vertical strip" }, { label: "3x3", c: 3, r: 3, desc: "9-tile grid" }, { label: "2x3", c: 2, r: 3, desc: "6-tile portrait" }, { label: "3x2", c: 3, r: 2, desc: "6-tile landscape" }, { label: "4x4", c: 4, r: 4, desc: "16-tile grid" }, ]; const OUTPUT_FORMATS = [ { value: "original", label: "Keep Original" }, { value: "png", label: "PNG" }, { value: "jpg", label: "JPG" }, { value: "webp", label: "WebP" }, { value: "avif", label: "AVIF" }, { value: "jxl", label: "JXL" }, ] as const; const LOSSY_FORMATS = new Set(["jpg", "webp", "avif", "jxl"]); export function SplitSettings() { const { files, processing: fileStoreProcessing } = useFileStore(); const { mode, columns, rows, tileWidth, tileHeight, outputFormat, quality, imageDimensions, processing, error, tiles, zipBlobUrl, setMode, setColumns, setRows, setTileWidth, setTileHeight, setOutputFormat, setQuality, setProcessing, setError, setTiles, setZipBlobUrl, applyPreset, getEffectiveGrid, getComputedTileDimensions, } = useSplitStore(); const [downloadingIndex, setDownloadingIndex] = useState(null); const hasFile = files.length > 0; const grid = getEffectiveGrid(); const tileCount = grid.columns * grid.rows; const tileDims = getComputedTileDimensions(); const isLossy = LOSSY_FORMATS.has(outputFormat); const hasTiles = tiles.length > 0; const tileWarning = tileDims && (tileDims.width < 50 || tileDims.height < 50) ? `Tiles will be very small (${tileDims.width}x${tileDims.height}px)` : null; // biome-ignore lint/correctness/useExhaustiveDependencies: files is a store value that triggers reset when changed useEffect(() => { setTiles([]); setZipBlobUrl(null); setError(null); }, [files, setTiles, setZipBlobUrl, setError]); const handleProcess = useCallback(async () => { if (files.length === 0) return; setProcessing(true); setError(null); setTiles([]); setZipBlobUrl(null); try { const effectiveGrid = getEffectiveGrid(); const settings: Record = { columns: effectiveGrid.columns, rows: effectiveGrid.rows, outputFormat, }; if (mode === "tile-size") { settings.tileWidth = tileWidth; settings.tileHeight = tileHeight; } if (LOSSY_FORMATS.has(outputFormat)) { settings.quality = quality; } const settingsJson = JSON.stringify(settings); const JSZip = (await import("jszip")).default; const combinedZip = new JSZip(); const previewTiles: Array<{ row: number; col: number; blobUrl: string | null }> = []; const multiFile = files.length > 1; for (let fi = 0; fi < files.length; fi++) { const file = files[fi]; const formData = new FormData(); formData.append("file", file); formData.append("settings", settingsJson); const res = await fetch("/api/v1/tools/split", { method: "POST", headers: formatHeaders(), body: formData, }); if (!res.ok) { const text = await res.text(); throw new Error(`Failed to split ${file.name}: ${text || res.status}`); } const blob = await res.blob(); const fileZip = await JSZip.loadAsync(blob); const baseName = file.name.replace(/\.[^.]+$/, ""); const prefix = multiFile ? `${baseName}/` : ""; const fileNames = Object.keys(fileZip.files).filter((n) => !fileZip.files[n].dir); fileNames.sort(); for (const name of fileNames) { const data = await fileZip.files[name].async("uint8array"); combinedZip.file(`${prefix}${name}`, data); if (fi === 0) { const tileBlob = new Blob([data as BlobPart]); const tileBlobUrl = URL.createObjectURL(tileBlob); const match = name.match(/_r(\d+)_c(\d+)/); previewTiles.push({ row: match ? Number.parseInt(match[1], 10) : 0, col: match ? Number.parseInt(match[2], 10) : 0, blobUrl: tileBlobUrl, }); } } } const combinedBlob = await combinedZip.generateAsync({ type: "blob" }); setZipBlobUrl(URL.createObjectURL(combinedBlob)); previewTiles.sort((a, b) => a.row - b.row || a.col - b.col); setTiles( previewTiles.map((t, i) => ({ row: t.row, col: t.col, label: `${i + 1}`, width: 0, height: 0, blobUrl: t.blobUrl, })), ); } catch (err) { setError(err instanceof Error ? err.message : "Split failed"); } finally { setProcessing(false); } }, [ files, mode, outputFormat, quality, tileWidth, tileHeight, getEffectiveGrid, setProcessing, setError, setTiles, setZipBlobUrl, ]); const handleDownloadZip = useCallback(() => { if (!zipBlobUrl) return; const a = document.createElement("a"); a.href = zipBlobUrl; const baseName = files.length > 1 ? "split-batch" : (files[0]?.name?.replace(/\.[^.]+$/, "") ?? "split"); a.download = `${baseName}-${grid.columns}x${grid.rows}.zip`; a.click(); }, [zipBlobUrl, files, grid]); const handleDownloadTile = useCallback( (index: number) => { const tile = tiles[index]; if (!tile?.blobUrl) return; setDownloadingIndex(index); const a = document.createElement("a"); a.href = tile.blobUrl; const baseName = files[0]?.name?.replace(/\.[^.]+$/, "") ?? "tile"; const ext = outputFormat === "original" ? (files[0]?.name?.split(".").pop() ?? "png") : outputFormat; a.download = `${baseName}_r${tile.row}_c${tile.col}.${ext}`; a.click(); setTimeout(() => setDownloadingIndex(null), 500); }, [tiles, files, outputFormat], ); return (

Split Mode

{MODES.map((m) => ( ))}
{mode === "grid" && ( <>

Presets

{PRESETS.map((p) => ( ))}
setColumns(Number(e.target.value))} min={1} max={20} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums" />
setRows(Number(e.target.value))} min={1} max={20} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums" />
)} {mode === "tile-size" && (
setTileWidth(Number(e.target.value))} min={10} max={imageDimensions?.width ?? 10000} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums" />
setTileHeight(Number(e.target.value))} min={10} max={imageDimensions?.height ?? 10000} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground tabular-nums" />
{imageDimensions && (

Image: {imageDimensions.width}x{imageDimensions.height}px. Grid: {grid.columns}x {grid.rows} = {tileCount} tiles

)}
)} {mode === "grid" && (
{grid.columns}x{grid.rows} = {tileCount} tiles {tileDims && ( ~{tileDims.width}x{tileDims.height}px each )}
)} {tileWarning &&

{tileWarning}

}
{OUTPUT_FORMATS.map((f) => ( ))}
{isLossy && (
{quality}
setQuality(Number(e.target.value))} className="w-full mt-1" />
)}
{error &&

{error}

} {hasTiles && (

{files.length > 1 ? `${files.length} images split (${tiles.length} tiles each)` : `${tiles.length} Tiles Generated`}

{tiles.map((tile, i) => ( ))}
)}
); }