1- import React , { useState , useCallback , useEffect , useSyncExternalStore } from 'react' ;
1+ import React , { useState , useCallback , useEffect , useRef , useSyncExternalStore } from 'react' ;
22import {
33 MessageCircle ,
44 Twitter ,
@@ -14,6 +14,7 @@ import {
1414 Radio ,
1515 Video ,
1616 VideoOff ,
17+ X ,
1718 type LucideIcon ,
1819} from 'lucide-react' ;
1920import ChatPanel from '../ChatPanel' ;
@@ -70,18 +71,59 @@ const Shell: React.FC = () => {
7071 const [ chatOpen , setChatOpen ] = useState ( true ) ;
7172 const [ reportEnabled , setReportEnabled ] = useState ( true ) ;
7273 const [ lang , setLang ] = useState < 'en' | 'zh' > ( 'en' ) ;
73- const [ liveWallpaper , setLiveWallpaper ] = useState ( true ) ;
74+ const [ liveWallpaper , setLiveWallpaper ] = useState ( false ) ;
7475 // eslint-disable-next-line @typescript-eslint/no-unused-vars
7576 const [ wallpaper , setWallpaper ] = useState ( VIDEO_WALLPAPER ) ;
77+ const [ pipPos , setPipPos ] = useState < { x : number ; y : number } | null > ( null ) ;
78+ const dragRef = useRef < { startX : number ; startY : number ; origX : number ; origY : number } | null > (
79+ null ,
80+ ) ;
81+ const pipRef = useRef < HTMLDivElement > ( null ) ;
82+ const barRef = useRef < HTMLDivElement > ( null ) ;
7683 const windows = useWindows ( ) ;
7784
78- const activeWallpaper = liveWallpaper
79- ? wallpaper
80- : isVideoUrl ( wallpaper )
81- ? STATIC_WALLPAPER
82- : wallpaper ;
85+ const bgWallpaper = isVideoUrl ( wallpaper ) ? STATIC_WALLPAPER : wallpaper ;
8386 const showVideo = liveWallpaper && isVideoUrl ( wallpaper ) ;
8487
88+ const PIP_W = 200 ;
89+ const PIP_H = 280 ;
90+
91+ useEffect ( ( ) => {
92+ if ( ! pipPos && barRef . current ) {
93+ const bar = barRef . current . getBoundingClientRect ( ) ;
94+ const barCenterX = bar . left + bar . width / 2 ;
95+ setPipPos ( {
96+ x : barCenterX - PIP_W / 2 ,
97+ y : bar . top - PIP_H - 16 ,
98+ } ) ;
99+ }
100+ } , [ pipPos ] ) ;
101+
102+ const handlePipMouseDown = useCallback (
103+ ( e : React . MouseEvent ) => {
104+ if ( ( e . target as HTMLElement ) . closest ( 'button' ) || ! pipPos ) return ;
105+ e . preventDefault ( ) ;
106+ dragRef . current = { startX : e . clientX , startY : e . clientY , origX : pipPos . x , origY : pipPos . y } ;
107+ const onMove = ( ev : MouseEvent ) => {
108+ if ( ! dragRef . current ) return ;
109+ const dx = ev . clientX - dragRef . current . startX ;
110+ const dy = ev . clientY - dragRef . current . startY ;
111+ setPipPos ( {
112+ x : Math . max ( 0 , Math . min ( window . innerWidth - PIP_W , dragRef . current . origX + dx ) ) ,
113+ y : Math . max ( 0 , Math . min ( window . innerHeight - PIP_H , dragRef . current . origY + dy ) ) ,
114+ } ) ;
115+ } ;
116+ const onUp = ( ) => {
117+ dragRef . current = null ;
118+ window . removeEventListener ( 'mousemove' , onMove ) ;
119+ window . removeEventListener ( 'mouseup' , onUp ) ;
120+ } ;
121+ window . addEventListener ( 'mousemove' , onMove ) ;
122+ window . addEventListener ( 'mouseup' , onUp ) ;
123+ } ,
124+ [ pipPos ] ,
125+ ) ;
126+
85127 const handleToggleReport = useCallback ( ( ) => {
86128 setReportEnabled ( ( prev ) => {
87129 const next = ! prev ;
@@ -115,18 +157,25 @@ const Shell: React.FC = () => {
115157 < div
116158 className = { styles . shell }
117159 data-testid = "shell"
118- style = {
119- activeWallpaper && ! showVideo
120- ? {
121- backgroundImage : `url(${ activeWallpaper } )` ,
122- backgroundSize : 'cover' ,
123- backgroundPosition : 'center' ,
124- }
125- : undefined
126- }
160+ style = { {
161+ backgroundImage : `url(${ bgWallpaper } )` ,
162+ backgroundSize : 'cover' ,
163+ backgroundPosition : 'center' ,
164+ } }
127165 >
128- { showVideo && (
129- < video className = { styles . videoBg } src = { wallpaper } autoPlay loop muted playsInline />
166+ { showVideo && pipPos && (
167+ < div
168+ ref = { pipRef }
169+ className = { styles . videoPip }
170+ style = { { left : pipPos . x , top : pipPos . y , bottom : 'auto' } }
171+ onMouseDown = { handlePipMouseDown }
172+ data-testid = "video-pip"
173+ >
174+ < video src = { wallpaper } autoPlay loop muted playsInline />
175+ < button className = { styles . pipClose } onClick = { ( ) => setLiveWallpaper ( false ) } title = "Close" >
176+ < X size = { 14 } />
177+ </ button >
178+ </ div >
130179 ) }
131180 { /* Desktop with app icons */ }
132181 < div className = { styles . desktop } data-testid = "desktop" >
@@ -162,7 +211,7 @@ const Shell: React.FC = () => {
162211 { /* Chat Panel — always mounted to preserve chat history */ }
163212 < ChatPanel onClose = { ( ) => setChatOpen ( false ) } visible = { chatOpen } />
164213
165- < div className = { `${ styles . bottomBar } ${ chatOpen ? styles . chatOpen : '' } ` } >
214+ < div ref = { barRef } className = { `${ styles . bottomBar } ${ chatOpen ? styles . chatOpen : '' } ` } >
166215 < button
167216 className = { `${ styles . barBtn } ${ liveWallpaper ? styles . liveOn : styles . liveOff } ` }
168217 onClick = { ( ) => setLiveWallpaper ( ( prev ) => ! prev ) }
0 commit comments