Skip to content

Commit 4f73f3c

Browse files
Retsommclaude
andcommitted
fix(annotation): 修正 epub 底線 SVG 消失問題
根本原因:patchIframeViewPrototype 的 underline 用 try-catch 包住 target.apply(),但 epub.js IframeView.underline() 本身不 crash, 而是建立 new Underline(null range) 並加進 pane。真正崩潰的是 後續 pane.render() 呼叫 range.getClientRects() 時,此時 marks-pane 已清空整個 <g>,迭代中途 crash,所有 SVG line 全部消失。 修正:改回在建立 Underline 前先呼叫 contents.range(cfi), 回傳 null 則直接 return null,防止壞 Underline 進入 pane。 同時移除所有錯誤的 sectionIndex fix code: 這些程式碼把 _annotationsBySectionIndex[n](實為 hash string 陣列) 當成 CFI 為 key 的 object 操作,雖然實際上是 no-op, 但增加了不必要的複雜度,一併清除。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2c0ecb2 commit 4f73f3c

8 files changed

Lines changed: 530 additions & 128 deletions

File tree

pwa/src/components/NotePanel.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const exportAnnotations = (selected: Annotation[], bookTitle: string) => {
5353
const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bookTitle, embedded }: Props) => {
5454
const { annotations } = useAnnotationStore()
5555
const [pickerOpenId, setPickerOpenId] = useState<string | null>(null)
56+
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
5657
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
5758
const selectAllRef = useRef<HTMLInputElement>(null)
5859

@@ -151,15 +152,14 @@ const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bo
151152
aria-label="更換顏色"
152153
/>
153154
<button
154-
style={{ width: 22, height: 22, borderRadius: 6, fontSize: 11, display: 'flex', alignItems: 'center', justifyContent: 'center', color: ink3Col, cursor: 'pointer', transition: 'all .12s' }}
155+
style={{ width: 22, height: 22, borderRadius: 6, fontSize: 11, display: 'flex', alignItems: 'center', justifyContent: 'center', color: pendingDeleteId === a.id ? '#ef4444' : ink3Col, background: pendingDeleteId === a.id ? 'rgba(239,68,68,0.08)' : 'transparent', cursor: 'pointer', transition: 'all .12s' }}
155156
onClick={(e) => {
156157
e.stopPropagation()
157-
onRemoveAnnotation(a.id)
158-
setSelectedIds((prev) => { const n = new Set(prev); n.delete(a.id); return n })
159-
if (pickerOpenId === a.id) setPickerOpenId(null)
158+
setPickerOpenId(null)
159+
setPendingDeleteId(pendingDeleteId === a.id ? null : a.id)
160160
}}
161161
onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; e.currentTarget.style.background = 'rgba(239,68,68,0.08)' }}
162-
onMouseLeave={(e) => { e.currentTarget.style.color = ink3Col; e.currentTarget.style.background = 'transparent' }}
162+
onMouseLeave={(e) => { e.currentTarget.style.color = pendingDeleteId === a.id ? '#ef4444' : ink3Col; e.currentTarget.style.background = pendingDeleteId === a.id ? 'rgba(239,68,68,0.08)' : 'transparent' }}
163163
aria-label="刪除此註記"
164164
></button>
165165
</div>
@@ -176,6 +176,24 @@ const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bo
176176
))}
177177
</div>
178178
)}
179+
{pendingDeleteId === a.id && (
180+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, paddingLeft: 24 }} onClick={(e) => e.stopPropagation()}>
181+
<span style={{ fontFamily: MONO, fontSize: 11, color: '#ef4444', letterSpacing: '0.02em', flexShrink: 0 }}>確定刪除?</span>
182+
<button
183+
style={{ height: 22, padding: '0 8px', borderRadius: 5, fontFamily: MONO, fontSize: 11, color: ink3Col, background: darkMode ? '#2a2520' : '#ede8e0', cursor: 'pointer', transition: 'all .12s' }}
184+
onClick={() => setPendingDeleteId(null)}
185+
>取消</button>
186+
<button
187+
style={{ height: 22, padding: '0 8px', borderRadius: 5, fontFamily: MONO, fontSize: 11, color: '#fff', background: '#ef4444', cursor: 'pointer', transition: 'all .12s' }}
188+
onClick={() => {
189+
onRemoveAnnotation(a.id)
190+
setSelectedIds((prev) => { const n = new Set(prev); n.delete(a.id); return n })
191+
setPendingDeleteId(null)
192+
setPickerOpenId(null)
193+
}}
194+
>刪除</button>
195+
</div>
196+
)}
179197
</div>
180198
</li>
181199
))}

pwa/src/components/Reader.tsx

Lines changed: 250 additions & 54 deletions
Large diffs are not rendered by default.

pwa/src/components/Toolbar.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,16 @@ const IconBookmarkFill = () => (
8080
</svg>
8181
)
8282

83-
export type ActivePanel = 'notes' | 'chapters' | 'settings' | 'bookinfo' | 'mobilepanel' | null
83+
const IconBookmarkList = () => (
84+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
85+
<path d="M13 21l-5-4-5 4V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v16z" />
86+
<line x1="17" y1="7" x2="21" y2="7" />
87+
<line x1="17" y1="11" x2="21" y2="11" />
88+
<line x1="17" y1="15" x2="21" y2="15" />
89+
</svg>
90+
)
91+
92+
export type ActivePanel = 'notes' | 'chapters' | 'settings' | 'bookinfo' | 'mobilepanel' | 'bookmarks' | null
8493

8594
interface Props {
8695
onBack: () => void
@@ -97,6 +106,7 @@ interface Props {
97106
activePanel: ActivePanel
98107
isBookmarked: boolean
99108
onToggleBookmark: () => void
109+
onToggleBookmarkList: () => void
100110
}
101111

102112
const SERIF = '"Source Serif 4", "Noto Serif TC", Georgia, serif'
@@ -117,6 +127,7 @@ const Toolbar = ({
117127
activePanel,
118128
isBookmarked,
119129
onToggleBookmark,
130+
onToggleBookmarkList,
120131
}: Props) => {
121132
const paperBg = darkMode ? '#1a1816' : '#f9f7f2'
122133
const borderCol = darkMode ? '#3a3430' : '#e4ddd0'
@@ -214,6 +225,7 @@ const Toolbar = ({
214225
{btn(activePanel === 'settings', onToggleSettings, <IconSettings />, '排版與語音設定')}
215226
{btn(activePanel === 'chapters', onToggleChapters, <IconChapters />, '章節目錄')}
216227
{btn(activePanel === 'notes', onToggleNotes, <IconNotes />, '我的註記')}
228+
{btn(activePanel === 'bookmarks', onToggleBookmarkList, <IconBookmarkList />, '書籤清單')}
217229
</div>
218230
{btn(false, onToggleDark, darkMode ? <IconSun /> : <IconMoon />, darkMode ? '切換淺色模式' : '切換深色模式')}
219231
</div>

renderer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
},
2323
"devDependencies": {
2424
"@babel/core": "^7.29.0",
25-
"@playwright/test": "^1.58.2",
25+
"@playwright/test": "^1.59.1",
2626
"@rolldown/plugin-babel": "^0.2.3",
2727
"@tailwindcss/vite": "^4.2.2",
2828
"@types/react": "^18.2.55",

renderer/src/components/NotePanel.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const exportAnnotations = (selected: Annotation[], bookTitle: string) => {
5252
const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bookTitle }: Props) => {
5353
const { annotations } = useAnnotationStore()
5454
const [pickerOpenId, setPickerOpenId] = useState<string | null>(null)
55+
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
5556
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
5657
const selectAllRef = useRef<HTMLInputElement>(null)
5758

@@ -165,16 +166,17 @@ const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bo
165166
style={{
166167
width: 22, height: 22, borderRadius: 6, fontSize: 11,
167168
display: 'flex', alignItems: 'center', justifyContent: 'center',
168-
color: ink3Col, cursor: 'pointer', transition: 'all .12s',
169+
color: pendingDeleteId === a.id ? '#ef4444' : ink3Col,
170+
background: pendingDeleteId === a.id ? 'rgba(239,68,68,0.08)' : 'transparent',
171+
cursor: 'pointer', transition: 'all .12s',
169172
}}
170173
onClick={(e) => {
171174
e.stopPropagation()
172-
onRemoveAnnotation(a.id)
173-
setSelectedIds((prev) => { const n = new Set(prev); n.delete(a.id); return n })
174-
if (pickerOpenId === a.id) setPickerOpenId(null)
175+
setPickerOpenId(null)
176+
setPendingDeleteId(pendingDeleteId === a.id ? null : a.id)
175177
}}
176178
onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; e.currentTarget.style.background = 'rgba(239,68,68,0.08)' }}
177-
onMouseLeave={(e) => { e.currentTarget.style.color = ink3Col; e.currentTarget.style.background = 'transparent' }}
179+
onMouseLeave={(e) => { e.currentTarget.style.color = pendingDeleteId === a.id ? '#ef4444' : ink3Col; e.currentTarget.style.background = pendingDeleteId === a.id ? 'rgba(239,68,68,0.08)' : 'transparent' }}
178180
aria-label="刪除此註記"
179181
></button>
180182
</div>
@@ -197,6 +199,25 @@ const NotePanel = ({ onNavigate, onChangeColor, onRemoveAnnotation, darkMode, bo
197199
))}
198200
</div>
199201
)}
202+
{/* Delete confirmation */}
203+
{pendingDeleteId === a.id && (
204+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, paddingLeft: 24 }} onClick={(e) => e.stopPropagation()}>
205+
<span style={{ fontFamily: MONO, fontSize: 11, color: '#ef4444', letterSpacing: '0.02em', flexShrink: 0 }}>確定刪除?</span>
206+
<button
207+
style={{ height: 22, padding: '0 8px', borderRadius: 5, fontFamily: MONO, fontSize: 11, color: ink3Col, background: darkMode ? '#2a2520' : '#ede8e0', cursor: 'pointer' }}
208+
onClick={() => setPendingDeleteId(null)}
209+
>取消</button>
210+
<button
211+
style={{ height: 22, padding: '0 8px', borderRadius: 5, fontFamily: MONO, fontSize: 11, color: '#fff', background: '#ef4444', cursor: 'pointer' }}
212+
onClick={() => {
213+
onRemoveAnnotation(a.id)
214+
setSelectedIds((prev) => { const n = new Set(prev); n.delete(a.id); return n })
215+
setPendingDeleteId(null)
216+
setPickerOpenId(null)
217+
}}
218+
>刪除</button>
219+
</div>
220+
)}
200221
</div>
201222
</li>
202223
))}

0 commit comments

Comments
 (0)