import { createStore } from "solid-js/store"; import yaml from "js-yaml"; import { calculateDimensions } from "./dimensions"; import { loadCSVFromPath, CSV } from "../../utils/csv-loader"; import { formatLayers } from "./layer-parser"; import * as layerCrud from "./layer-crud"; import type { CardData, LayerConfig, Dimensions, CardSide, CardShape, } from "../types"; export const DECK_DEFAULTS = { SIZE_W: 54, SIZE_H: 86, GRID_W: 5, GRID_H: 8, BLEED: 1, PADDING: 2, CORNER_RADIUS: 3, } as const; export interface DraggingState { layer: number; action: "drag" | "resize-corner" | "resize-edge"; anchor?: "nw" | "ne" | "sw" | "se"; edge?: "n" | "s" | "e" | "w"; startX: number; startY: number; startGrid: { x1: number; y1: number; x2: number; y2: number }; } export interface DeckState { sizeW: number; sizeH: number; gridW: number; gridH: number; bleed: number; padding: number; cornerRadius: number; shape: CardShape; fixed: boolean; /** True when the deck was configured via a yaml role=tag codeblock (data-config). */ isYamlBlock: boolean; src: string; rawSrc: string; dimensions: Dimensions | null; cards: CSV; activeTab: number; frontLayerConfigs: LayerConfig[]; backLayerConfigs: LayerConfig[]; isEditing: boolean; selectedLayer: number | null; activeSide: CardSide; isSelecting: boolean; selectStart: { x: number; y: number } | null; selectEnd: { x: number; y: number } | null; draggingState: DraggingState | null; isLoading: boolean; error: string | null; isExporting: boolean; exportProgress: number; exportError: string | null; printOrientation: "portrait" | "landscape"; printFrontOddPageOffsetX: number; printFrontOddPageOffsetY: number; printDoubleSided: boolean; pltDoubleCut: boolean; } export interface DeckActions { setSizeW: (size: number) => void; setSizeH: (size: number) => void; setGridW: (grid: number) => void; setGridH: (grid: number) => void; setBleed: (bleed: number) => void; setPadding: (padding: number) => void; setCornerRadius: (cornerRadius: number) => void; setShape: (shape: CardShape) => void; setIsYamlBlock: (isYamlBlock: boolean) => void; setCards: (cards: CSV) => void; setActiveTab: (index: number) => void; updateCardData: (index: number, key: string, value: string) => void; setFrontLayerConfigs: (configs: LayerConfig[]) => void; updateFrontLayerConfig: ( index: number, updates: Partial, ) => void; toggleFrontLayerVisible: (index: number) => void; setBackLayerConfigs: (configs: LayerConfig[]) => void; updateBackLayerConfig: (index: number, updates: Partial) => void; toggleBackLayerVisible: (index: number) => void; addLayer: (side: CardSide, prop: string) => void; removeLayer: (side: CardSide, index: number) => void; reorderLayers: (side: CardSide, fromIndex: number, toIndex: number) => void; setIsEditing: (editing: boolean) => void; setSelectedLayer: (layer: number | null) => void; setActiveSide: (side: CardSide) => void; setIsSelecting: (selecting: boolean) => void; setSelectStart: (pos: { x: number; y: number } | null) => void; setSelectEnd: (pos: { x: number; y: number } | null) => void; cancelSelection: () => void; setDraggingState: (state: DraggingState | null) => void; moveLayer: ( index: number, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }, ) => void; resizeLayerCorner: ( index: number, anchor: "nw" | "ne" | "sw" | "se", dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }, ) => void; resizeLayerEdge: ( index: number, edge: "n" | "s" | "e" | "w", delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }, ) => void; loadCardsFromPath: ( path: string, rawSrc: string, frontLayers?: LayerConfig[], backLayers?: LayerConfig[], ) => Promise; setError: (error: string | null) => void; clearError: () => void; generateCode: (backLayersStr?: string) => string; copyCode: (fallback?: (code: string) => void) => Promise; setExporting: (exporting: boolean) => void; exportDeck: () => void; setExportProgress: (progress: number) => void; setExportError: (error: string | null) => void; clearExportError: () => void; setPrintOrientation: (orientation: "portrait" | "landscape") => void; setPrintFrontOddPageOffsetX: (offset: number) => void; setPrintFrontOddPageOffsetY: (offset: number) => void; setPrintDoubleSided: (doubleSided: boolean) => void; setPltDoubleCut: (doubleCut: boolean) => void; } export interface DeckStore { state: DeckState; actions: DeckActions; } export function createDeckStore(initialSrc: string = ""): DeckStore { const [state, setState] = createStore({ sizeW: DECK_DEFAULTS.SIZE_W, sizeH: DECK_DEFAULTS.SIZE_H, gridW: DECK_DEFAULTS.GRID_W, gridH: DECK_DEFAULTS.GRID_H, bleed: DECK_DEFAULTS.BLEED, padding: DECK_DEFAULTS.PADDING, cornerRadius: DECK_DEFAULTS.CORNER_RADIUS, shape: "rectangle", fixed: false, isYamlBlock: false, src: initialSrc, rawSrc: initialSrc, dimensions: null, cards: [] as any, activeTab: 0, frontLayerConfigs: [], backLayerConfigs: [], isEditing: false, selectedLayer: null, activeSide: "front", isSelecting: false, selectStart: null, selectEnd: null, draggingState: null, isLoading: false, error: null, isExporting: false, exportProgress: 0, exportError: null, printOrientation: "portrait", printFrontOddPageOffsetX: 0, printFrontOddPageOffsetY: 0, printDoubleSided: false, pltDoubleCut: false, }); const updateDimensions = () => { const dims = calculateDimensions({ sizeW: state.sizeW, sizeH: state.sizeH, gridW: state.gridW, gridH: state.gridH, bleed: state.bleed, padding: state.padding, }); setState({ dimensions: dims }); }; const setSizeW = (size: number) => { setState({ sizeW: size }); updateDimensions(); }; const setSizeH = (size: number) => { setState({ sizeH: size }); updateDimensions(); }; const setGridW = (grid: number) => { setState({ gridW: grid }); updateDimensions(); }; const setGridH = (grid: number) => { setState({ gridH: grid }); updateDimensions(); }; const setBleed = (bleed: number) => { setState({ bleed }); updateDimensions(); }; const setPadding = (padding: number) => { setState({ padding }); updateDimensions(); }; const setCornerRadius = (cornerRadius: number) => { setState({ cornerRadius }); }; const setShape = (shape: CardShape) => { setState({ shape }); }; const setIsYamlBlock = (isYamlBlock: boolean) => { setState({ isYamlBlock }); }; const setCards = (cards: CSV) => setState({ cards, activeTab: 0 }); const setActiveTab = (index: number) => setState({ activeTab: index }); const updateCardData = (index: number, key: string, value: string) => { setState("cards", index, key, value); }; const setFrontLayerConfigs = (configs: LayerConfig[]) => setState({ frontLayerConfigs: configs }); const updateFrontLayerConfig = ( index: number, updates: Partial, ) => { setState("frontLayerConfigs", index, (config) => ({ ...config, ...updates, })); }; const toggleFrontLayerVisible = (index: number) => { setState("frontLayerConfigs", index, "visible", (v) => !v); }; const setBackLayerConfigs = (configs: LayerConfig[]) => setState({ backLayerConfigs: configs }); const updateBackLayerConfig = ( index: number, updates: Partial, ) => { setState("backLayerConfigs", index, (config) => ({ ...config, ...updates, })); }; const toggleBackLayerVisible = (index: number) => { setState("backLayerConfigs", index, "visible", (v) => !v); }; const getLayerConfigs = (side: CardSide): LayerConfig[] => side === "front" ? state.frontLayerConfigs : state.backLayerConfigs; const setLayerConfigs = (side: CardSide, configs: LayerConfig[]) => { if (side === "front") setFrontLayerConfigs(configs); else setBackLayerConfigs(configs); }; const addLayer = (side: CardSide, prop: string) => { const configs = getLayerConfigs(side); setLayerConfigs(side, layerCrud.addLayer(configs, prop)); setState({ selectedLayer: layerCrud.adjustSelectedLayer( state.selectedLayer, "add", configs.length, ), }); }; const removeLayer = (side: CardSide, index: number) => { setLayerConfigs(side, layerCrud.removeLayer(getLayerConfigs(side), index)); setState({ selectedLayer: layerCrud.adjustSelectedLayer( state.selectedLayer, "remove", index, ), }); }; const reorderLayers = (side: CardSide, from: number, to: number) => { setLayerConfigs( side, layerCrud.reorderLayers(getLayerConfigs(side), from, to), ); setState({ selectedLayer: layerCrud.adjustSelectedLayer( state.selectedLayer, "reorder", from, to, ), }); }; const setIsEditing = (editing: boolean) => setState({ isEditing: editing }); const setSelectedLayer = (layer: number | null) => setState({ selectedLayer: layer }); const setActiveSide = (side: CardSide) => setState({ activeSide: side }); const setIsSelecting = (selecting: boolean) => setState({ isSelecting: selecting }); const setSelectStart = (pos: { x: number; y: number } | null) => setState({ selectStart: pos }); const setSelectEnd = (pos: { x: number; y: number } | null) => setState({ selectEnd: pos }); const cancelSelection = () => { setState({ isSelecting: false, selectStart: null, selectEnd: null }); }; const setDraggingState = (draggingState: DraggingState | null) => setState({ draggingState }); const getLayerConfig = (index: number): LayerConfig | undefined => { const configs = state.activeSide === "front" ? state.frontLayerConfigs : state.backLayerConfigs; return configs[index]; }; const updateLayerConfig = (index: number, updates: Partial) => { if (state.activeSide === "front") { updateFrontLayerConfig(index, updates); } else { updateBackLayerConfig(index, updates); } }; const moveLayer = ( index: number, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }, ) => { const layer = getLayerConfig(index); if (!layer) return; const grid = startGrid ?? { x1: layer.x1, y1: layer.y1, x2: layer.x2, y2: layer.y2, }; updateLayerConfig(index, { x1: grid.x1 + dxGrid, x2: grid.x2 + dxGrid, y1: grid.y1 + dyGrid, y2: grid.y2 + dyGrid, }); }; const resizeLayerCorner = ( index: number, anchor: "nw" | "ne" | "sw" | "se", dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }, ) => { const layer = getLayerConfig(index); if (!layer) return; const updates: Partial = {}; if (anchor === "nw") { updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid); updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid); } else if (anchor === "ne") { updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid); updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid); } else if (anchor === "sw") { updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid); updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid); } else if (anchor === "se") { updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid); updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid); } updateLayerConfig(index, updates); }; const resizeLayerEdge = ( index: number, edge: "n" | "s" | "e" | "w", delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }, ) => { const layer = getLayerConfig(index); if (!layer) return; const updates: Partial = {}; if (edge === "n") updates.y1 = Math.min(startGrid.y2, Math.max(1, startGrid.y1 + delta)); if (edge === "s") updates.y2 = Math.max( startGrid.y1, Math.min(state.gridH, startGrid.y2 + delta), ); if (edge === "w") updates.x1 = Math.min(startGrid.x2, Math.max(1, startGrid.x1 + delta)); if (edge === "e") updates.x2 = Math.max( startGrid.x1, Math.min(state.gridW, startGrid.x2 + delta), ); updateLayerConfig(index, updates); }; const loadCardsFromPath = async ( path: string, rawSrc: string, frontLayers: LayerConfig[] = [], backLayers: LayerConfig[] = [], ) => { if (!path) { setState({ error: "未指定 CSV 文件路径" }); return; } setState({ isLoading: true, error: null, src: path, rawSrc: rawSrc }); try { const data = await loadCSVFromPath(path); if (data.length === 0) { setState({ error: `CSV ${path} 文件为空或格式不正确`, isLoading: false, }); return; } setState({ cards: data, activeTab: 0, frontLayerConfigs: layerCrud.withKeys(frontLayers), backLayerConfigs: layerCrud.withKeys(backLayers), isLoading: false, }); updateDimensions(); } catch (err) { setState({ error: `加载 CSV ${path} 失败:${err instanceof Error ? err.message : "未知错误"}`, isLoading: false, }); } }; const setError = (error: string | null) => setState({ error }); const clearError = () => setState({ error: null }); const generateCode = (backLayersStr?: string) => { if (state.isYamlBlock) { return generateYamlCode(); } const frontLayersStr = formatLayers(state.frontLayerConfigs); const backLayersString = backLayersStr || formatLayers(state.backLayerConfigs); const parts = [ `:md-deck[${state.rawSrc || state.src}]`, `{size="${state.sizeW}x${state.sizeH}" `, `grid="${state.gridW}x${state.gridH}" `, ]; if (state.bleed !== DECK_DEFAULTS.BLEED) { parts.push(`bleed="${state.bleed}" `); } if (state.padding !== DECK_DEFAULTS.PADDING) { parts.push(`padding="${state.padding}" `); } if (state.shape !== "rectangle") { parts.push(`shape="${state.shape}" `); } parts.push(`layers="${frontLayersStr}" `); if (backLayersString) { parts.push(`back-layers="${backLayersString}" `); } parts.push("}"); return parts.join(""); }; /** Serialize the deck back to a yaml codeblock (round-trips templates). */ const generateYamlCode = () => { const toYamlLayer = (l: LayerConfig) => { const out: Record = {}; if (l.template) { out.template = l.template; } else { out.prop = l.prop ?? ""; } out.pos = `${l.x1},${l.y1}-${l.x2},${l.y2}`; if (l.fontSize) out.font = l.fontSize; if (l.orientation && l.orientation !== "n") out.orientation = l.orientation; if (l.align) out.align = l.align; if (!l.visible) out.visible = false; return out; }; const config: Record = { size: `${state.sizeW}x${state.sizeH}`, grid: `${state.gridW}x${state.gridH}`, layers: state.frontLayerConfigs.map(toYamlLayer), }; if (state.bleed !== DECK_DEFAULTS.BLEED) config.bleed = state.bleed; if (state.padding !== DECK_DEFAULTS.PADDING) config.padding = state.padding; if (state.shape !== "rectangle") config.shape = state.shape; if (state.backLayerConfigs.length > 0) { config.back_layers = state.backLayerConfigs.map(toYamlLayer); } const doc = { tag: "md-deck", body: state.rawSrc || state.src, "data-config": config, }; const yamlStr = yaml.dump(doc, { indent: 2, lineWidth: -1, noRefs: true, }); const fence = "```yaml role=tag"; const backtick = "`"; return `${fence}\n${yamlStr}${backtick.repeat(3)}`; }; const copyCode = async (fallback?: (code: string) => void) => { const code = generateCode(); try { await navigator.clipboard.writeText(code); alert("已复制到剪贴板!"); } catch (err) { console.error("复制失败:", err); if (fallback) { fallback(code); } else { alert("复制失败,请手动复制"); } } }; const setExporting = (exporting: boolean) => setState({ isExporting: exporting }); const exportDeck = () => { setState({ isExporting: true, exportProgress: 0, exportError: null }); }; const setExportProgress = (progress: number) => setState({ exportProgress: progress }); const setExportError = (error: string | null) => setState({ exportError: error }); const clearExportError = () => setState({ exportError: null }); const setPrintOrientation = (orientation: "portrait" | "landscape") => { setState({ printOrientation: orientation }); }; const setPrintFrontOddPageOffsetX = (offset: number) => { setState({ printFrontOddPageOffsetX: offset }); }; const setPrintFrontOddPageOffsetY = (offset: number) => { setState({ printFrontOddPageOffsetY: offset }); }; const setPrintDoubleSided = (doubleSided: boolean) => { setState({ printDoubleSided: doubleSided }); }; const setPltDoubleCut = (doubleCut: boolean) => { setState({ pltDoubleCut: doubleCut }); }; const actions: DeckActions = { setSizeW, setSizeH, setGridW, setGridH, setBleed, setPadding, setCornerRadius, setShape, setIsYamlBlock, setCards, setActiveTab, updateCardData, setFrontLayerConfigs, updateFrontLayerConfig, toggleFrontLayerVisible, setBackLayerConfigs, updateBackLayerConfig, toggleBackLayerVisible, addLayer, removeLayer, reorderLayers, setIsEditing, setSelectedLayer, setActiveSide, setIsSelecting, setSelectStart, setSelectEnd, cancelSelection, setDraggingState, moveLayer, resizeLayerCorner, resizeLayerEdge, loadCardsFromPath, setError, clearError, generateCode, copyCode, setExporting, exportDeck, setExportProgress, setExportError, clearExportError, setPrintOrientation, setPrintFrontOddPageOffsetX, setPrintFrontOddPageOffsetY, setPrintDoubleSided, setPltDoubleCut, }; return { state, actions }; }