import { useCallback, useRef, useState } from 'react'; import type { PageRef } from '../pdf/pdfTypes'; import type { WorkspaceCommand, WorkspaceCommandRecord, WorkspaceCommandState, } from './workspaceCommands'; import { createSnapshotCommand, reviveWorkspaceCommand, toWorkspaceCommandRecord, } from './workspaceCommands'; function createId(prefix: string): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`; } export function createWorkspaceId(): string { return createId('workspace'); } export function createPdfId(): string { return createId('pdf'); } export function defaultWorkspaceNameFromPdfName(pdfName: string): string { return pdfName.replace(/\.pdf$/i, '') || 'Untitled workspace'; } export function createPageRefId(): string { return createId('page'); } export function createInitialPageRefs(pageCount: number): PageRef[] { return Array.from({ length: pageCount }, (_, sourcePageIndex) => ({ id: createPageRefId(), sourcePageIndex, rotation: 0, })); } export function normalizeRotation(rotation: number | undefined): number { return (((rotation ?? 0) % 360) + 360) % 360; } type SetStateAction = T | ((previous: T) => T); interface UseWorkspaceStateOptions { onContentChanged?: () => void; } interface ReplaceWorkspaceStateOptions { pages?: PageRef[]; selectedPageIds?: string[]; lastSelectedVisualIndex?: number | null; history?: WorkspaceCommandRecord[]; redoHistory?: WorkspaceCommandRecord[]; dirty?: boolean; message?: string | null; } export function useWorkspaceState({ onContentChanged, }: UseWorkspaceStateOptions = {}) { const [pages, setPagesState] = useState([]); const [selectedPageIds, setSelectedPageIdsState] = useState([]); const [lastSelectedVisualIndex, setLastSelectedVisualIndexState] = useState< number | null >(null); const [workspaceDirty, setWorkspaceDirty] = useState(false); const [workspaceMessage, setWorkspaceMessage] = useState(null); const [workspaceHistory, setWorkspaceHistory] = useState< WorkspaceCommandRecord[] >([]); const [redoHistory, setRedoHistory] = useState([]); const latestPagesRef = useRef([]); const selectedPageIdsRef = useRef([]); const lastSelectedVisualIndexRef = useRef(null); const setPages = useCallback((action: SetStateAction) => { setPagesState((previous) => { const next = typeof action === 'function' ? action(previous) : action; latestPagesRef.current = next; return next; }); }, []); const setSelectedPageIds = useCallback((action: SetStateAction) => { setSelectedPageIdsState((previous) => { const next = typeof action === 'function' ? action(previous) : action; selectedPageIdsRef.current = next; return next; }); }, []); const setLastSelectedVisualIndex = useCallback( (action: SetStateAction) => { setLastSelectedVisualIndexState((previous) => { const next = typeof action === 'function' ? action(previous) : action; lastSelectedVisualIndexRef.current = next; return next; }); }, [] ); const getCurrentCommandState = useCallback( (): WorkspaceCommandState => ({ pages: latestPagesRef.current, selectedPageIds: selectedPageIdsRef.current, lastSelectedVisualIndex: lastSelectedVisualIndexRef.current, }), [] ); const applyCommandState = useCallback( (state: WorkspaceCommandState) => { setPages(state.pages); setSelectedPageIds(state.selectedPageIds); setLastSelectedVisualIndex(state.lastSelectedVisualIndex); }, [setLastSelectedVisualIndex, setPages, setSelectedPageIds] ); const markWorkspaceChanged = useCallback(() => { setWorkspaceDirty(true); setWorkspaceMessage(null); onContentChanged?.(); }, [onContentChanged]); const createWorkspaceCommand = useCallback( (params: { type: string; label: string; before: WorkspaceCommandState; after: WorkspaceCommandState; details?: Record; }): WorkspaceCommand => createSnapshotCommand({ id: createId('command'), type: params.type, label: params.label, before: params.before, after: params.after, details: params.details, }), [] ); const executeWorkspaceCommand = useCallback( (command: WorkspaceCommand) => { const nextState = command.do(getCurrentCommandState()); applyCommandState(nextState); setWorkspaceHistory((previous) => [ ...previous, toWorkspaceCommandRecord(command), ]); setRedoHistory([]); markWorkspaceChanged(); }, [applyCommandState, getCurrentCommandState, markWorkspaceChanged] ); const handleUndo = useCallback(() => { const record = workspaceHistory[workspaceHistory.length - 1]; if (!record) return; const command = reviveWorkspaceCommand(record); const previousState = command.undo(getCurrentCommandState()); applyCommandState(previousState); setWorkspaceHistory((previous) => previous.slice(0, -1)); setRedoHistory((previous) => [...previous, record]); markWorkspaceChanged(); }, [ applyCommandState, getCurrentCommandState, markWorkspaceChanged, workspaceHistory, ]); const handleRedo = useCallback(() => { const record = redoHistory[redoHistory.length - 1]; if (!record) return; const command = reviveWorkspaceCommand(record); const nextState = command.do(getCurrentCommandState()); applyCommandState(nextState); setRedoHistory((previous) => previous.slice(0, -1)); setWorkspaceHistory((previous) => [...previous, record]); markWorkspaceChanged(); }, [ applyCommandState, getCurrentCommandState, markWorkspaceChanged, redoHistory, ]); const replaceWorkspaceState = useCallback( (state: ReplaceWorkspaceStateOptions = {}) => { setPages(state.pages ?? []); setSelectedPageIds(state.selectedPageIds ?? []); setLastSelectedVisualIndex(state.lastSelectedVisualIndex ?? null); setWorkspaceHistory(state.history ?? []); setRedoHistory(state.redoHistory ?? []); setWorkspaceDirty(state.dirty ?? false); setWorkspaceMessage(state.message ?? null); }, [setLastSelectedVisualIndex, setPages, setSelectedPageIds] ); const resetWorkspaceState = useCallback(() => { replaceWorkspaceState(); }, [replaceWorkspaceState]); return { pages, setPages, selectedPageIds, setSelectedPageIds, lastSelectedVisualIndex, setLastSelectedVisualIndex, latestPagesRef, workspaceDirty, setWorkspaceDirty, workspaceMessage, setWorkspaceMessage, workspaceHistory, setWorkspaceHistory, redoHistory, setRedoHistory, getCurrentCommandState, applyCommandState, createWorkspaceCommand, executeWorkspaceCommand, handleUndo, handleRedo, replaceWorkspaceState, resetWorkspaceState, }; }