251 lines
7.1 KiB
TypeScript
251 lines
7.1 KiB
TypeScript
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> = 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<PageRef[]>([]);
|
|
const [selectedPageIds, setSelectedPageIdsState] = useState<string[]>([]);
|
|
const [lastSelectedVisualIndex, setLastSelectedVisualIndexState] = useState<
|
|
number | null
|
|
>(null);
|
|
const [workspaceDirty, setWorkspaceDirty] = useState(false);
|
|
const [workspaceMessage, setWorkspaceMessage] = useState<string | null>(null);
|
|
const [workspaceHistory, setWorkspaceHistory] = useState<
|
|
WorkspaceCommandRecord[]
|
|
>([]);
|
|
const [redoHistory, setRedoHistory] = useState<WorkspaceCommandRecord[]>([]);
|
|
|
|
const latestPagesRef = useRef<PageRef[]>([]);
|
|
const selectedPageIdsRef = useRef<string[]>([]);
|
|
const lastSelectedVisualIndexRef = useRef<number | null>(null);
|
|
|
|
const setPages = useCallback((action: SetStateAction<PageRef[]>) => {
|
|
setPagesState((previous) => {
|
|
const next = typeof action === 'function' ? action(previous) : action;
|
|
latestPagesRef.current = next;
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const setSelectedPageIds = useCallback((action: SetStateAction<string[]>) => {
|
|
setSelectedPageIdsState((previous) => {
|
|
const next = typeof action === 'function' ? action(previous) : action;
|
|
selectedPageIdsRef.current = next;
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const setLastSelectedVisualIndex = useCallback(
|
|
(action: SetStateAction<number | null>) => {
|
|
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<string, unknown>;
|
|
}): 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,
|
|
};
|
|
}
|