mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
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),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
}));
|