@@ -107,6 +107,7 @@ const copyTextToClipboard = async (text: string) => {
107107
108108const TTS_HIGHLIGHT_ID = 'tit-tts-progress'
109109const TTS_HIGHLIGHT_STYLE_ID = 'tit-tts-progress-style'
110+ const TTS_HIGHLIGHT_OVERLAY_ID = 'tit-tts-progress-overlay'
110111const TTS_HIGHLIGHT_INTERVAL = 80
111112const TTS_USER_INPUT_GRACE = 180
112113const TTS_HIGHLIGHT_LENGTH = 4
@@ -143,6 +144,7 @@ const ensureTTSHighlightStyle = (doc: Document) => {
143144const clearTTSHighlight = ( doc : Document | null | undefined ) => {
144145 const highlights = ( doc ?. defaultView as any ) ?. CSS ?. highlights
145146 highlights ?. delete ?.( TTS_HIGHLIGHT_ID )
147+ doc ?. getElementById ( TTS_HIGHLIGHT_OVERLAY_ID ) ?. remove ( )
146148}
147149
148150const clearTTSHighlights = ( docs : Iterable < Document | null | undefined > ) => {
@@ -279,6 +281,57 @@ const createRangeFromTextOffset = (doc: Document, start: number, length = TTS_HI
279281 return range
280282}
281283
284+
285+ const paintTTSHighlightOverlay = ( doc : Document , range : Range ) => {
286+ const body = doc . body
287+ if ( ! body ) return false
288+
289+ let overlay = doc . getElementById ( TTS_HIGHLIGHT_OVERLAY_ID )
290+ if ( ! overlay ) {
291+ overlay = doc . createElement ( 'div' )
292+ overlay . id = TTS_HIGHLIGHT_OVERLAY_ID
293+ Object . assign ( overlay . style , {
294+ position : 'fixed' ,
295+ inset : '0' ,
296+ pointerEvents : 'none' ,
297+ zIndex : '2147483647' ,
298+ overflow : 'hidden' ,
299+ contain : 'layout style paint' ,
300+ } )
301+ body . appendChild ( overlay )
302+ }
303+
304+ const viewportWidth = doc . documentElement . clientWidth || doc . defaultView ?. innerWidth || 0
305+ const viewportHeight = doc . documentElement . clientHeight || doc . defaultView ?. innerHeight || 0
306+ const rects = Array . from ( range . getClientRects ( ) )
307+ . filter ( ( rect ) =>
308+ rect . width > 0 &&
309+ rect . height > 0 &&
310+ rect . bottom >= 0 &&
311+ rect . top <= viewportHeight &&
312+ rect . right >= 0 &&
313+ rect . left <= viewportWidth
314+ )
315+
316+ overlay . replaceChildren ( )
317+ for ( const rect of rects ) {
318+ const mark = doc . createElement ( 'div' )
319+ Object . assign ( mark . style , {
320+ position : 'fixed' ,
321+ left : `${ Math . max ( 0 , rect . left ) } px` ,
322+ top : `${ Math . max ( 0 , rect . top ) } px` ,
323+ width : `${ Math . max ( 1 , Math . min ( rect . width , viewportWidth - Math . max ( 0 , rect . left ) ) ) } px` ,
324+ height : `${ Math . max ( 1 , Math . min ( rect . height , viewportHeight - Math . max ( 0 , rect . top ) ) ) } px` ,
325+ background : 'rgba(245, 158, 11, 0.32)' ,
326+ borderRadius : '2px' ,
327+ pointerEvents : 'none' ,
328+ } )
329+ overlay . appendChild ( mark )
330+ }
331+
332+ return rects . length > 0
333+ }
334+
282335const getTTSRangeViewportState = ( doc : Document , range : Range | null ) : TTSRangeViewportState => {
283336 const viewportWidth = doc . documentElement . clientWidth || doc . defaultView ?. innerWidth || 0
284337 const viewportHeight = doc . documentElement . clientHeight || doc . defaultView ?. innerHeight || 0
@@ -421,6 +474,7 @@ interface Props {
421474 darkMode : boolean
422475 onToggleDark : ( ) => void
423476 onUpdateProgress ?: ( pct : number ) => void
477+ onApplyLatestVersion : ( ) => void | Promise < void >
424478}
425479
426480const SERIF = '"Source Serif 4", "Noto Serif TC", Georgia, serif'
@@ -630,7 +684,7 @@ const patchIframeViewPrototype = (proto: Record<string, unknown>) => {
630684 }
631685}
632686
633- const Reader = ( { bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMode, onToggleDark, onUpdateProgress } : Props ) => {
687+ const Reader = ( { bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMode, onToggleDark, onUpdateProgress, onApplyLatestVersion } : Props ) => {
634688 const viewerRef = useRef < HTMLDivElement > ( null )
635689 const bookRef = useRef < Book | null > ( null )
636690 const renditionRef = useRef < Rendition | null > ( null )
@@ -1442,6 +1496,31 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
14421496 } , 50 )
14431497 }
14441498
1499+
1500+ const getVisibleContentDocument = ( ) : Document | null => {
1501+ const viewer = viewerRef . current
1502+ if ( ! viewer ) return null
1503+
1504+ const iframe = viewer . querySelector ( 'iframe' ) as HTMLIFrameElement | null
1505+ if ( iframe ?. contentDocument ?. body ) return iframe . contentDocument
1506+
1507+ const viewerRect = viewer . getBoundingClientRect ( )
1508+ let best : { doc : Document ; area : number } | null = null
1509+ for ( const frame of Array . from ( viewer . querySelectorAll ( 'iframe' ) ) as HTMLIFrameElement [ ] ) {
1510+ try {
1511+ const doc = frame . contentDocument
1512+ if ( ! doc ?. body ) continue
1513+ const rect = frame . getBoundingClientRect ( )
1514+ const width = Math . max ( 0 , Math . min ( rect . right , viewerRect . right ) - Math . max ( rect . left , viewerRect . left ) )
1515+ const height = Math . max ( 0 , Math . min ( rect . bottom , viewerRect . bottom ) - Math . max ( rect . top , viewerRect . top ) )
1516+ const area = width * height
1517+ if ( area > 0 && ( ! best || area > best . area ) ) best = { doc, area }
1518+ } catch { /* ignore cross-origin iframe */ }
1519+ }
1520+
1521+ return best ?. doc ?? currentDocRef . current
1522+ }
1523+
14451524 // 字體家族(獨立,不影響其他設定)
14461525 useEffect ( ( ) => {
14471526 if ( ! ready ) return
@@ -1929,7 +2008,7 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
19292008
19302009 const updateTTSHighlight = ( absoluteOffset : number , allowAutoFollow = true , source : TTSProgressSource = 'boundary' ) => {
19312010 ttsLastAbsoluteOffsetRef . current = absoluteOffset
1932- const doc = currentDocRef . current
2011+ const doc = getVisibleContentDocument ( ) ?? currentDocRef . current
19332012 if ( ! doc ) {
19342013 if ( DEBUG_TTS_FOLLOW ) console . log ( '[TTS:follow] skip highlight: no current doc' , { absoluteOffset } )
19352014 return
@@ -1955,16 +2034,13 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
19552034 return
19562035 }
19572036
1958- const highlights = ( doc . defaultView as any ) ?. CSS ?. highlights
1959- const HighlightCtor = ( doc . defaultView as any ) ?. Highlight
1960- if ( highlights && HighlightCtor ) {
1961- clearOtherTTSHighlights ( doc )
1962- highlights . delete ( TTS_HIGHLIGHT_ID )
1963- highlights . set ( TTS_HIGHLIGHT_ID , new HighlightCtor ( range ) )
2037+ clearOtherTTSHighlights ( doc )
2038+ const painted = paintTTSHighlightOverlay ( doc , range )
2039+ if ( painted ) {
19642040 ttsHighlightedDocRef . current = doc
19652041 } else {
1966- clearAllTTSHighlights ( )
1967- if ( DEBUG_TTS_FOLLOW ) console . log ( '[TTS:follow] Custom Highlight API unavailable, only auto-follow by progress ' , { absoluteOffset } )
2042+ clearTTSHighlight ( doc )
2043+ if ( DEBUG_TTS_FOLLOW ) console . log ( '[TTS:follow] overlay 無可用 rect,僅自動跟隨進度 ' , { absoluteOffset } )
19682044 }
19692045 followTTSRange ( range , doc , absoluteOffset , allowAutoFollow , source )
19702046 }
@@ -2117,10 +2193,10 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
21172193
21182194 const speakCurrentPage = ( ) => {
21192195 if ( ! viewerRef . current ) return
2120- const iframe = viewerRef . current . querySelector ( 'iframe' )
2121- if ( ! iframe ?. contentDocument ?. body ) return
2196+ const visibleDoc = getVisibleContentDocument ( )
2197+ if ( ! visibleDoc ?. body ) return
21222198
2123- const textIndex = getTextIndex ( iframe . contentDocument )
2199+ const textIndex = getTextIndex ( visibleDoc )
21242200 const fullText = textIndex ?. text ?? ''
21252201 if ( ! fullText ) return
21262202
@@ -2196,11 +2272,11 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
21962272 }
21972273 ttsVisibleSpineIndexRef . current = currentSpineIdx
21982274 ttsVisibleStartOffsetRef . current = startOffset
2199- ttsCurrentPageStartOffsetRef . current = measureCurrentPageStartOffset ( iframe . contentDocument , loc )
2275+ ttsCurrentPageStartOffsetRef . current = measureCurrentPageStartOffset ( visibleDoc , loc )
22002276 ttsLastAbsoluteOffsetRef . current = startOffset
22012277 ttsChapterTextLengthRef . current = fullText . length
22022278 ttsChapterPageTotalRef . current = totalPages
2203- ttsVisiblePageEndOffsetRef . current = measureCurrentPageEndOffset ( iframe . contentDocument , loc )
2279+ ttsVisiblePageEndOffsetRef . current = measureCurrentPageEndOffset ( visibleDoc , loc )
22042280 updateTTSHighlight ( startOffset , false )
22052281 console . log ( '[TTS] speakCurrentPage 開始' , {
22062282 currentSpineIdx,
@@ -2459,6 +2535,7 @@ const Reader = ({ bookPath, bookId, bookRecord, getCoverDataUrl, onBack, darkMod
24592535 isBookmarked = { isBookmarked }
24602536 onToggleBookmark = { handleToggleBookmark }
24612537 onToggleBookmarkList = { ( ) => togglePanel ( 'bookmarks' ) }
2538+ onApplyLatestVersion = { onApplyLatestVersion }
24622539 />
24632540 < div className = "flex flex-1 overflow-hidden" >
24642541 < div
0 commit comments