@@ -211,33 +211,18 @@ export function Reader({ article }: ReaderProps) {
211211 } catch ( e ) { }
212212
213213 if ( videoId ) {
214- const iframeHtml = `
215- <div style="margin-bottom: 24px;">
216- <iframe
217- width="100%"
218- height="auto"
219- style="aspect-ratio: 16/9; border-radius: 12px;"
220- src="https://www.youtube.com/embed/${ videoId } ?autoplay=1&playsinline=1&modestbranding=1&rel=0"
221- frameborder="0"
222- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
223- allowfullscreen>
224- </iframe>
225- </div>
226- <hr style="margin: 24px 0; border-color: #3f3f46;" />
227- ` ;
228-
229- setContent ( prev => {
230- if ( prev . includes ( videoId ! ) ) return prev ;
231- return iframeHtml + prev ;
232- } ) ;
214+ // We render the video separately now to allow for position tracking
215+ setYoutubeVideoId ( videoId ) ;
233216 }
234217 }
235218 } ;
236219
237220 // Defer enhancement to next tick to allow initial render
238221 const timer = setTimeout ( enhanceContent , 0 ) ;
239222 return ( ) => clearTimeout ( timer ) ;
240- } , [ article . id , article . url , article . mediaKind , article . readerHTML ] ) ; // Removed 'content' dependency to avoid loops
223+ } , [ article . id , article . url , article . mediaKind , article . readerHTML ] ) ;
224+
225+ const [ youtubeVideoId , setYoutubeVideoId ] = useState < string | null > ( null ) ;
241226
242227 const toggleReaderMode = async ( ) => {
243228 if ( isReaderMode ) {
@@ -463,6 +448,17 @@ export function Reader({ article }: ReaderProps) {
463448 ` } </ style >
464449
465450 { /* Content */ }
451+ { youtubeVideoId && (
452+ < div className = "mb-8" >
453+ < YouTubePlayer
454+ videoId = { youtubeVideoId }
455+ articleId = { article . id }
456+ initialPosition = { article . playbackPosition }
457+ />
458+ < hr className = "my-8 border-zinc-200 dark:border-zinc-800" />
459+ </ div >
460+ ) }
461+
466462 < div
467463 className = "reader-content prose prose-zinc dark:prose-invert prose-lg max-w-none"
468464 style = { { fontSize : `${ zoom } %` } }
@@ -480,3 +476,90 @@ export function Reader({ article }: ReaderProps) {
480476 </ div >
481477 ) ;
482478}
479+
480+ function YouTubePlayer ( { videoId, articleId, initialPosition = 0 } : { videoId : string ; articleId : string ; initialPosition ?: number } ) {
481+ const playerRef = useRef < any > ( null ) ;
482+ const containerRef = useRef < HTMLDivElement > ( null ) ;
483+
484+ useEffect ( ( ) => {
485+ // Load YouTube IFrame API if not already loaded
486+ if ( ! window . YT ) {
487+ const tag = document . createElement ( 'script' ) ;
488+ tag . src = "https://www.youtube.com/iframe_api" ;
489+ const firstScriptTag = document . getElementsByTagName ( 'script' ) [ 0 ] ;
490+ firstScriptTag . parentNode ?. insertBefore ( tag , firstScriptTag ) ;
491+ }
492+
493+ const initPlayer = ( ) => {
494+ playerRef . current = new window . YT . Player ( containerRef . current , {
495+ videoId : videoId ,
496+ playerVars : {
497+ autoplay : 1 ,
498+ playsinline : 1 ,
499+ modestbranding : 1 ,
500+ rel : 0 ,
501+ start : Math . floor ( initialPosition ) ,
502+ } ,
503+ events : {
504+ onStateChange : ( event : any ) => {
505+ // When video is playing, periodically save position
506+ if ( event . data === window . YT . PlayerState . PLAYING ) {
507+ const interval = setInterval ( ( ) => {
508+ if ( playerRef . current && playerRef . current . getCurrentTime ) {
509+ const currentTime = playerRef . current . getCurrentTime ( ) ;
510+ db . articles . update ( articleId , { playbackPosition : currentTime } ) ;
511+ } else {
512+ clearInterval ( interval ) ;
513+ }
514+ } , 5000 ) ; // Save every 5 seconds
515+
516+ // Store interval on player object to clear it later
517+ ( playerRef . current as any ) . _posInterval = interval ;
518+ } else {
519+ if ( ( playerRef . current as any ) . _posInterval ) {
520+ clearInterval ( ( playerRef . current as any ) . _posInterval ) ;
521+ }
522+ // Save final position when paused/ended
523+ if ( playerRef . current && playerRef . current . getCurrentTime ) {
524+ const currentTime = playerRef . current . getCurrentTime ( ) ;
525+ db . articles . update ( articleId , { playbackPosition : currentTime } ) ;
526+ }
527+ }
528+ }
529+ }
530+ } ) ;
531+ } ;
532+
533+ if ( window . YT && window . YT . Player ) {
534+ initPlayer ( ) ;
535+ } else {
536+ const previousOnYouTubeIframeAPIReady = window . onYouTubeIframeAPIReady ;
537+ window . onYouTubeIframeAPIReady = ( ) => {
538+ if ( previousOnYouTubeIframeAPIReady ) previousOnYouTubeIframeAPIReady ( ) ;
539+ initPlayer ( ) ;
540+ } ;
541+ }
542+
543+ return ( ) => {
544+ if ( playerRef . current ) {
545+ if ( ( playerRef . current as any ) . _posInterval ) {
546+ clearInterval ( ( playerRef . current as any ) . _posInterval ) ;
547+ }
548+ playerRef . current . destroy ( ) ;
549+ }
550+ } ;
551+ } , [ videoId , articleId ] ) ;
552+
553+ return (
554+ < div className = "relative w-full aspect-video rounded-xl overflow-hidden bg-black shadow-lg" >
555+ < div ref = { containerRef } className = "w-full h-full" />
556+ </ div >
557+ ) ;
558+ }
559+
560+ declare global {
561+ interface Window {
562+ YT : any ;
563+ onYouTubeIframeAPIReady : ( ) => void ;
564+ }
565+ }
0 commit comments