forked from akai/readest
209 lines
6.3 KiB
TypeScript
209 lines
6.3 KiB
TypeScript
import { create } from 'zustand';
|
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
|
|
import type {
|
|
ReviewGptConfig,
|
|
ReviewRow,
|
|
ReviewSessionPayload,
|
|
} from '@/services/reviewEditorService';
|
|
|
|
export type ReviewBookState = {
|
|
enabled: boolean;
|
|
loading: boolean;
|
|
error: string;
|
|
baseUrl: string;
|
|
sessionId: string | null;
|
|
reviewRoot?: string;
|
|
version?: string;
|
|
rows: ReviewRow[];
|
|
session: ReviewSessionPayload | null;
|
|
gptConfig: ReviewGptConfig | null;
|
|
selectedRowId: string;
|
|
};
|
|
|
|
const DEFAULT_PANEL_WIDTH = '32%';
|
|
const DEFAULT_PANEL_HEIGHT = '68vh';
|
|
const MIN_PANEL_WIDTH_PERCENT = 24;
|
|
const MAX_PANEL_WIDTH_PERCENT = 48;
|
|
const MIN_PANEL_HEIGHT_VH = 35;
|
|
const MAX_PANEL_HEIGHT_VH = 92;
|
|
|
|
type ReviewModeStore = {
|
|
activeBookKey: string | null;
|
|
isPanelVisible: boolean;
|
|
isPanelPinned: boolean;
|
|
panelWidth: string;
|
|
panelHeight: string;
|
|
books: Record<string, ReviewBookState>;
|
|
getBookState: (bookKey: string | null) => ReviewBookState | null;
|
|
setActiveBookKey: (bookKey: string | null) => void;
|
|
setPanelVisible: (visible: boolean) => void;
|
|
togglePanelPin: () => void;
|
|
setPanelWidth: (width: string) => void;
|
|
setPanelHeight: (height: string) => void;
|
|
setBookLoading: (bookKey: string, loading: boolean) => void;
|
|
setBookError: (bookKey: string, error: string) => void;
|
|
setBookEnabled: (bookKey: string, enabled: boolean) => void;
|
|
setBookData: (
|
|
bookKey: string,
|
|
data: Partial<Omit<ReviewBookState, 'enabled' | 'loading' | 'error'>>,
|
|
) => void;
|
|
selectRow: (bookKey: string, rowId: string) => void;
|
|
updateRow: (bookKey: string, row: ReviewRow) => void;
|
|
clearBook: (bookKey: string) => void;
|
|
};
|
|
|
|
const emptyBookState = (): ReviewBookState => ({
|
|
enabled: false,
|
|
loading: false,
|
|
error: '',
|
|
baseUrl: '',
|
|
sessionId: null,
|
|
rows: [],
|
|
session: null,
|
|
gptConfig: null,
|
|
selectedRowId: '',
|
|
});
|
|
|
|
const formatSizedValue = (value: number, unit: '%' | 'vh') =>
|
|
`${Math.round(value * 100) / 100}${unit}`;
|
|
|
|
const normalizeSizedValue = (value: unknown, unit: '%' | 'vh', min: number, max: number) => {
|
|
if (typeof value !== 'string') return null;
|
|
const match = value.trim().match(new RegExp(`^(-?\\d+(?:\\.\\d+)?)${unit}$`));
|
|
if (!match) return null;
|
|
const numericValue = Number(match[1]);
|
|
if (!Number.isFinite(numericValue)) return null;
|
|
return formatSizedValue(Math.min(max, Math.max(min, numericValue)), unit);
|
|
};
|
|
|
|
const normalizePersistedLayout = (state: unknown) => {
|
|
if (!state || typeof state !== 'object') return {};
|
|
const persisted = state as Partial<ReviewModeStore>;
|
|
const panelWidth = normalizeSizedValue(
|
|
persisted.panelWidth,
|
|
'%',
|
|
MIN_PANEL_WIDTH_PERCENT,
|
|
MAX_PANEL_WIDTH_PERCENT,
|
|
);
|
|
const panelHeight = normalizeSizedValue(
|
|
persisted.panelHeight,
|
|
'vh',
|
|
MIN_PANEL_HEIGHT_VH,
|
|
MAX_PANEL_HEIGHT_VH,
|
|
);
|
|
|
|
return {
|
|
...(typeof persisted.isPanelPinned === 'boolean'
|
|
? { isPanelPinned: persisted.isPanelPinned }
|
|
: {}),
|
|
...(panelWidth ? { panelWidth } : {}),
|
|
...(panelHeight ? { panelHeight } : {}),
|
|
};
|
|
};
|
|
|
|
const withBookState = (
|
|
books: Record<string, ReviewBookState>,
|
|
bookKey: string,
|
|
patch: Partial<ReviewBookState>,
|
|
) => ({
|
|
...books,
|
|
[bookKey]: {
|
|
...(books[bookKey] || emptyBookState()),
|
|
...patch,
|
|
},
|
|
});
|
|
|
|
export const useReviewModeStore = create<ReviewModeStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
activeBookKey: null,
|
|
isPanelVisible: false,
|
|
isPanelPinned: false,
|
|
panelWidth: DEFAULT_PANEL_WIDTH,
|
|
panelHeight: DEFAULT_PANEL_HEIGHT,
|
|
books: {},
|
|
getBookState: (bookKey) => (bookKey ? get().books[bookKey] || null : null),
|
|
setActiveBookKey: (bookKey) => set({ activeBookKey: bookKey }),
|
|
setPanelVisible: (visible) => set({ isPanelVisible: visible }),
|
|
togglePanelPin: () => set((state) => ({ isPanelPinned: !state.isPanelPinned })),
|
|
setPanelWidth: (width) => set({ panelWidth: width }),
|
|
setPanelHeight: (height) => set({ panelHeight: height }),
|
|
setBookLoading: (bookKey, loading) =>
|
|
set((state) => ({
|
|
books: withBookState(
|
|
state.books,
|
|
bookKey,
|
|
loading ? { loading, error: '' } : { loading },
|
|
),
|
|
})),
|
|
setBookError: (bookKey, error) =>
|
|
set((state) => ({
|
|
books: withBookState(state.books, bookKey, { error, loading: false }),
|
|
})),
|
|
setBookEnabled: (bookKey, enabled) =>
|
|
set((state) => ({
|
|
activeBookKey: enabled
|
|
? bookKey
|
|
: state.activeBookKey === bookKey
|
|
? null
|
|
: state.activeBookKey,
|
|
isPanelVisible: enabled
|
|
? true
|
|
: state.activeBookKey === bookKey
|
|
? false
|
|
: state.isPanelVisible,
|
|
books: withBookState(state.books, bookKey, { enabled }),
|
|
})),
|
|
setBookData: (bookKey, data) =>
|
|
set((state) => ({
|
|
books: withBookState(state.books, bookKey, {
|
|
...data,
|
|
loading: false,
|
|
error: '',
|
|
}),
|
|
})),
|
|
selectRow: (bookKey, rowId) =>
|
|
set((state) => ({
|
|
activeBookKey: bookKey,
|
|
isPanelVisible: true,
|
|
books: withBookState(state.books, bookKey, {
|
|
selectedRowId: rowId,
|
|
}),
|
|
})),
|
|
updateRow: (bookKey, row) =>
|
|
set((state) => {
|
|
const current = state.books[bookKey] || emptyBookState();
|
|
return {
|
|
books: withBookState(state.books, bookKey, {
|
|
rows: current.rows.map((item) => (item.id === row.id ? row : item)),
|
|
}),
|
|
};
|
|
}),
|
|
clearBook: (bookKey) =>
|
|
set((state) => {
|
|
const books = { ...state.books };
|
|
delete books[bookKey];
|
|
return {
|
|
books,
|
|
activeBookKey: state.activeBookKey === bookKey ? null : state.activeBookKey,
|
|
isPanelVisible: state.activeBookKey === bookKey ? false : state.isPanelVisible,
|
|
};
|
|
}),
|
|
}),
|
|
{
|
|
name: 'readest-review-panel-layout',
|
|
storage: createJSONStorage(() => window.localStorage),
|
|
partialize: (state) => ({
|
|
isPanelPinned: state.isPanelPinned,
|
|
panelWidth: state.panelWidth,
|
|
panelHeight: state.panelHeight,
|
|
}),
|
|
merge: (persistedState, currentState) => ({
|
|
...currentState,
|
|
...normalizePersistedLayout(persistedState),
|
|
}),
|
|
},
|
|
),
|
|
);
|