@@ -44,6 +44,30 @@ declare global {
4444
4545const qualityOptions = [ "原画" , "高清" , "标清" ] as const ;
4646
47+ const PLAYER_DRAG_EXCLUDED_SELECTOR = [
48+ // App chrome / topbar (主播信息栏 & 关闭/关注按钮等)
49+ ".player-topbar" ,
50+ ".player-window-controls" ,
51+ // Stream error overlay (中间刷新按钮)
52+ ".retry-btn" ,
53+ // Generic interactive elements
54+ "button" ,
55+ "a" ,
56+ "input" ,
57+ "textarea" ,
58+ "select" ,
59+ "[role='button']" ,
60+ "[role='link']" ,
61+ "[contenteditable='true']" ,
62+ // xgplayer controls / popups (播放器控制栏及其菜单)
63+ ".xgplayer-controls" ,
64+ ".xgplayer-controls *" ,
65+ ".xgplayer-danmu-block-panel" ,
66+ ".xgplayer-danmu-settings-panel" ,
67+ ".xgplayer-quality-dropdown" ,
68+ ".xgplayer-line-dropdown"
69+ ] . join ( ", " ) ;
70+
4771const DEFAULT_DANMU_SETTINGS : DanmuUserSettings = {
4872 color : "#ffffff" ,
4973 strokeColor : "#444444" ,
@@ -161,6 +185,8 @@ export function MainPlayer({
161185 const { setIsland, clearIsland, setFullscreen } = usePlayerUi ( ) ;
162186 const { ensureProxyStarted, getAvatarSrc } = useImageProxy ( ) ;
163187 const pageRef = useRef < HTMLDivElement | null > ( null ) ;
188+ const dragStartArmedRef = useRef ( false ) ;
189+ const dragCandidateRef = useRef < null | { pointerId : number ; x : number ; y : number } > ( null ) ;
164190
165191 const playerContainerRef = useRef < HTMLDivElement | null > ( null ) ;
166192 const playerRef = useRef < any > ( null ) ;
@@ -354,6 +380,79 @@ export function MainPlayer({
354380 }
355381 } , [ ] ) ;
356382
383+ const startWindowDragging = useCallback ( async ( ) => {
384+ try {
385+ const { getCurrentWindow } = await import ( "@tauri-apps/api/window" ) ;
386+ await getCurrentWindow ( ) . startDragging ( ) ;
387+ } catch {
388+ // ignore
389+ }
390+ } , [ ] ) ;
391+
392+ const isDragExcludedTarget = useCallback ( ( target : HTMLElement ) => {
393+ return ! ! target . closest ( PLAYER_DRAG_EXCLUDED_SELECTOR ) ;
394+ } , [ ] ) ;
395+
396+ const onPlayerPointerDownCapture = useCallback (
397+ ( e : React . PointerEvent ) => {
398+ if ( e . button !== 0 ) return ;
399+ if ( e . altKey || e . ctrlKey || e . metaKey || e . shiftKey ) return ;
400+ const target = e . target as HTMLElement | null ;
401+ if ( ! target ) return ;
402+
403+ const root = pageRef . current ;
404+ if ( ! root || ! root . contains ( target ) ) return ;
405+ if ( isDragExcludedTarget ( target ) ) return ;
406+
407+ // Defer dragging until the pointer moves a bit, so normal "click-to-toggle" behaviors still work.
408+ dragCandidateRef . current = { pointerId : e . pointerId , x : e . clientX , y : e . clientY } ;
409+ } ,
410+ [ isDragExcludedTarget ]
411+ ) ;
412+
413+ const onPlayerPointerMoveCapture = useCallback (
414+ ( e : React . PointerEvent ) => {
415+ const candidate = dragCandidateRef . current ;
416+ if ( ! candidate ) return ;
417+ if ( candidate . pointerId !== e . pointerId ) return ;
418+ if ( dragStartArmedRef . current ) return ;
419+
420+ const dx = e . clientX - candidate . x ;
421+ const dy = e . clientY - candidate . y ;
422+ if ( dx * dx + dy * dy < 36 ) return ; // 6px threshold
423+
424+ const target = e . target as HTMLElement | null ;
425+ const root = pageRef . current ;
426+ if ( ! target || ! root || ! root . contains ( target ) ) {
427+ dragCandidateRef . current = null ;
428+ return ;
429+ }
430+
431+ if ( isDragExcludedTarget ( target ) ) {
432+ dragCandidateRef . current = null ;
433+ return ;
434+ }
435+
436+ dragStartArmedRef . current = true ;
437+ dragCandidateRef . current = null ;
438+ e . preventDefault ( ) ;
439+
440+ void startWindowDragging ( ) . finally ( ( ) => {
441+ window . setTimeout ( ( ) => {
442+ dragStartArmedRef . current = false ;
443+ } , 300 ) ;
444+ } ) ;
445+ } ,
446+ [ isDragExcludedTarget , startWindowDragging ]
447+ ) ;
448+
449+ const onPlayerPointerUpCapture = useCallback ( ( e : React . PointerEvent ) => {
450+ const candidate = dragCandidateRef . current ;
451+ if ( ! candidate ) return ;
452+ if ( candidate . pointerId !== e . pointerId ) return ;
453+ dragCandidateRef . current = null ;
454+ } , [ ] ) ;
455+
357456 useEffect ( ( ) => {
358457 const armHide = ( ) => {
359458 if ( hideChromeTimerRef . current ) window . clearTimeout ( hideChromeTimerRef . current ) ;
@@ -1217,7 +1316,14 @@ export function MainPlayer({
12171316 } , [ platform , playerAnchorName , playerAvatar , playerTitle , roomId ] ) ;
12181317
12191318 return (
1220- < div className = { `player-page${ chromeHiddenClass } ` } ref = { pageRef } >
1319+ < div
1320+ className = { `player-page${ chromeHiddenClass } ` }
1321+ ref = { pageRef }
1322+ onPointerDownCapture = { onPlayerPointerDownCapture }
1323+ onPointerMoveCapture = { onPlayerPointerMoveCapture }
1324+ onPointerUpCapture = { onPlayerPointerUpCapture }
1325+ onPointerCancelCapture = { onPlayerPointerUpCapture }
1326+ >
12211327 { isWindows ? (
12221328 < div className = "player-window-controls" data-tauri-drag-region = "false" aria-label = "窗口控制" >
12231329 < button type = "button" className = "window-btn" data-tauri-drag-region = "false" aria-label = "最小化" onClick = { ( ) => void minimizeWindow ( ) } >
0 commit comments