Files
SnapOtter/apps/web/src/stores/split-store.ts
T

136 lines
4.0 KiB
TypeScript
Raw Normal View History

import { create } from "zustand";
export type SplitMode = "grid" | "tile-size";
export interface TileInfo {
row: number;
col: number;
label: string;
width: number;
height: number;
blobUrl: string | null;
}
interface SplitState {
// Split configuration
mode: SplitMode;
columns: number;
rows: number;
tileWidth: number;
tileHeight: number;
outputFormat: "original" | "png" | "jpg" | "webp";
quality: number;
// Image dimensions (set when image loads in the canvas)
imageDimensions: { width: number; height: number } | null;
// Processing state
processing: boolean;
error: string | null;
tiles: TileInfo[];
zipBlobUrl: string | null;
// Actions
setMode: (mode: SplitMode) => void;
setColumns: (n: number) => void;
setRows: (n: number) => void;
setTileWidth: (n: number) => void;
setTileHeight: (n: number) => void;
setOutputFormat: (f: "original" | "png" | "jpg" | "webp") => void;
setQuality: (q: number) => void;
setImageDimensions: (d: { width: number; height: number } | null) => void;
setProcessing: (p: boolean) => void;
setError: (e: string | null) => void;
setTiles: (tiles: TileInfo[]) => void;
setZipBlobUrl: (url: string | null) => void;
applyPreset: (cols: number, rows: number) => void;
reset: () => void;
// Derived helpers (computed in selectors)
getEffectiveGrid: () => { columns: number; rows: number };
getTileCount: () => number;
getComputedTileDimensions: () => { width: number; height: number } | null;
}
export const useSplitStore = create<SplitState>((set, get) => ({
mode: "grid",
columns: 3,
rows: 3,
tileWidth: 200,
tileHeight: 200,
outputFormat: "original",
quality: 90,
imageDimensions: null,
processing: false,
error: null,
tiles: [],
zipBlobUrl: null,
setMode: (mode) => set({ mode, tiles: [], zipBlobUrl: null, error: null }),
setColumns: (columns) =>
set({ columns: Math.max(1, Math.min(20, columns)), tiles: [], zipBlobUrl: null }),
setRows: (rows) => set({ rows: Math.max(1, Math.min(20, rows)), tiles: [], zipBlobUrl: null }),
setTileWidth: (tileWidth) =>
set({ tileWidth: Math.max(10, tileWidth), tiles: [], zipBlobUrl: null }),
setTileHeight: (tileHeight) =>
set({ tileHeight: Math.max(10, tileHeight), tiles: [], zipBlobUrl: null }),
setOutputFormat: (outputFormat) => set({ outputFormat }),
setQuality: (quality) => set({ quality }),
setImageDimensions: (imageDimensions) => set({ imageDimensions }),
setProcessing: (processing) => set({ processing }),
setError: (error) => set({ error }),
setTiles: (tiles) => set({ tiles }),
setZipBlobUrl: (url) => {
const prev = get().zipBlobUrl;
if (prev) URL.revokeObjectURL(prev);
set({ zipBlobUrl: url });
},
applyPreset: (cols, rows) =>
set({ mode: "grid", columns: cols, rows, tiles: [], zipBlobUrl: null, error: null }),
reset: () => {
const prev = get();
if (prev.zipBlobUrl) URL.revokeObjectURL(prev.zipBlobUrl);
for (const t of prev.tiles) {
if (t.blobUrl) URL.revokeObjectURL(t.blobUrl);
}
set({
tiles: [],
zipBlobUrl: null,
processing: false,
error: null,
imageDimensions: null,
});
},
getEffectiveGrid: () => {
const { mode, columns, rows, tileWidth, tileHeight, imageDimensions } = get();
if (mode === "tile-size" && imageDimensions) {
return {
columns: Math.max(1, Math.ceil(imageDimensions.width / tileWidth)),
rows: Math.max(1, Math.ceil(imageDimensions.height / tileHeight)),
};
}
return { columns, rows };
},
getTileCount: () => {
const { columns, rows } = get().getEffectiveGrid();
return columns * rows;
},
getComputedTileDimensions: () => {
const { imageDimensions, mode, tileWidth, tileHeight } = get();
if (!imageDimensions) return null;
const { columns, rows } = get().getEffectiveGrid();
if (mode === "tile-size") {
return { width: tileWidth, height: tileHeight };
}
return {
width: Math.floor(imageDimensions.width / columns),
height: Math.floor(imageDimensions.height / rows),
};
},
}));