From 75293bd1a4512419fdfb2a5b3de84e77582d25be Mon Sep 17 00:00:00 2001 From: akai Date: Thu, 9 Jul 2026 19:16:52 +0800 Subject: [PATCH] refactor: harden inline review panel layout --- .../__tests__/store/review-mode-store.test.ts | 66 ++++ .../src/app/reader/components/ReviewPanel.tsx | 289 ++++-------------- .../app/reader/hooks/useReviewPanelDrag.ts | 124 ++++++++ .../hooks/useReviewPanelFloatingResize.ts | 197 ++++++++++++ .../src/hooks/useIsMobileViewport.ts | 19 ++ apps/readest-app/src/store/reviewModeStore.ts | 204 ++++++++----- .../tools/epub-review-editor/MAINTENANCE.md | 2 +- .../tools/epub-review-editor/README.md | 4 +- .../epub-review-editor/static/index.html | 6 +- .../tools/epub-review-editor/version.py | 2 +- 10 files changed, 605 insertions(+), 308 deletions(-) create mode 100644 apps/readest-app/src/app/reader/hooks/useReviewPanelDrag.ts create mode 100644 apps/readest-app/src/app/reader/hooks/useReviewPanelFloatingResize.ts create mode 100644 apps/readest-app/src/hooks/useIsMobileViewport.ts diff --git a/apps/readest-app/src/__tests__/store/review-mode-store.test.ts b/apps/readest-app/src/__tests__/store/review-mode-store.test.ts index 2af098f3..8ade035d 100644 --- a/apps/readest-app/src/__tests__/store/review-mode-store.test.ts +++ b/apps/readest-app/src/__tests__/store/review-mode-store.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, test } from 'vitest'; import { useReviewModeStore } from '@/store/reviewModeStore'; beforeEach(() => { + localStorage.clear(); useReviewModeStore.setState({ activeBookKey: null, isPanelVisible: false, @@ -99,4 +100,69 @@ describe('reviewModeStore', () => { expect(state.activeBookKey).toBeNull(); expect(state.isPanelVisible).toBe(false); }); + + test('persists only panel layout preferences', () => { + useReviewModeStore.getState().setPanelWidth('41%'); + useReviewModeStore.getState().setPanelHeight('77vh'); + useReviewModeStore.getState().togglePanelPin(); + useReviewModeStore.getState().setBookData('book-a', { + baseUrl: 'http://127.0.0.1:5177', + sessionId: 'session-a', + rows: [ + { + id: 'R00001', + file: 'chapter.xhtml', + jp_html: '原文', + jp_text: '原文', + cn_html: '译文', + current_html: '译文', + }, + ], + }); + + const stored = JSON.parse(localStorage.getItem('readest-review-panel-layout') || '{}'); + + expect(stored.state).toEqual({ + isPanelPinned: true, + panelWidth: '41%', + panelHeight: '77vh', + }); + expect(stored.state.books).toBeUndefined(); + expect(stored.state.activeBookKey).toBeUndefined(); + }); + + test('ignores invalid persisted layout values during hydration', async () => { + useReviewModeStore.setState({ + activeBookKey: null, + isPanelVisible: false, + isPanelPinned: false, + panelWidth: '32%', + panelHeight: '68vh', + books: {}, + }); + + useReviewModeStore.persist.clearStorage(); + localStorage.setItem( + 'readest-review-panel-layout', + JSON.stringify({ + state: { + isPanelPinned: true, + panelWidth: '999%', + panelHeight: 'not-a-height', + books: { + stale: { enabled: true }, + }, + }, + version: 0, + }), + ); + await useReviewModeStore.persist.rehydrate(); + + const state = useReviewModeStore.getState(); + expect(state.isPanelPinned).toBe(true); + expect(state.panelWidth).toBe('48%'); + expect(state.panelHeight).toBe('68vh'); + expect(state.books).toEqual({}); + expect(state.activeBookKey).toBeNull(); + }); }); diff --git a/apps/readest-app/src/app/reader/components/ReviewPanel.tsx b/apps/readest-app/src/app/reader/components/ReviewPanel.tsx index 0d1cb404..107ce81f 100644 --- a/apps/readest-app/src/app/reader/components/ReviewPanel.tsx +++ b/apps/readest-app/src/app/reader/components/ReviewPanel.tsx @@ -13,17 +13,11 @@ import { Sparkles, X, } from 'lucide-react'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, - type ReactNode, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { Overlay } from '@/components/Overlay'; import { useEnv } from '@/context/EnvContext'; +import { useIsMobileViewport } from '@/hooks/useIsMobileViewport'; import { useTranslation } from '@/hooks/useTranslation'; import { usePanelResize } from '@/hooks/usePanelResize'; import { useBookDataStore } from '@/store/bookDataStore'; @@ -32,6 +26,8 @@ import { useReviewModeStore } from '@/store/reviewModeStore'; import { useThemeStore } from '@/store/themeStore'; import { getPanelTopInset } from '@/utils/insets'; import { openExternalUrl } from '@/utils/open'; +import { useReviewPanelDrag } from '../hooks/useReviewPanelDrag'; +import { useReviewPanelFloatingResize } from '../hooks/useReviewPanelFloatingResize'; import { exportReviewedEpub, generateReviewFeedback, @@ -54,6 +50,7 @@ const MAX_REVIEW_PANEL_WIDTH = 0.48; const MIN_FLOATING_PANEL_HEIGHT = 0.35; const MAX_FLOATING_PANEL_HEIGHT = 0.92; const FLOATING_PANEL_MARGIN = 12; +const DEFAULT_REVIEW_PANEL_WIDTH = 0.32; const cnHtmlPurifyOptions = { ALLOWED_TAGS: [ @@ -110,10 +107,7 @@ const emptyGptForm: GptForm = { const sanitizeInlineHtml = (html: string) => DOMPurify.sanitize(html || '', cnHtmlPurifyOptions); const escapeInlineHtml = (value: string) => - value - .replace(/&/g, '&') - .replace(//g, '>'); + value.replace(/&/g, '&').replace(//g, '>'); const rowTitle = (row: { document_title?: string; file_label?: string; file: string }) => row.document_title || row.file_label || row.file; @@ -209,7 +203,7 @@ function GptConfigPanel({ {open ? '收起' : '展开'} {open ? ( -
+