@@ -7,15 +7,18 @@ import {
77 isFunction as _isFunction ,
88 isNil as _isNil ,
99 isString as _isString ,
10+ throttle ,
1011} from "lodash" ;
11-
12- import Draggable from "react-draggable " ;
12+ import Draggable , { DraggableData , DraggableEvent } from "react-draggable" ;
13+ import { ResizableBox , ResizeCallbackData } from "react-resizable " ;
1314import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
1415import {
1516 faArrowsAlt ,
1617 faWindowMaximize ,
1718 faWindowMinimize ,
1819 faTimes ,
20+ faExpand ,
21+ faCompress ,
1922} from "@fortawesome/free-solid-svg-icons" ;
2023import { faYoutube } from "@fortawesome/free-brands-svg-icons" ;
2124import { Link } from "react-router" ;
@@ -35,8 +38,25 @@ export type YoutubePlayerProps = DataSourceProps & {
3538
3639type YoutubePlayerState = {
3740 hidePlayer ?: boolean ;
41+ isExpanded ?: boolean ;
42+ width : number ;
43+ height : number ;
44+ x : number ;
45+ y : number ;
46+ isInteracting : boolean ;
3847} ;
3948
49+ const DEFAULT_WIDTH = 350 ;
50+ const DEFAULT_HEIGHT = 200 ;
51+ const SIDEBAR_WIDTH = 200 ;
52+ const PLAYER_HEIGHT = 60 ;
53+ const BUTTON_HEIGHT = 30 ;
54+ const SIDEBAR_BREAKPOINT = 992 ;
55+ const EXPANDED_WIDTH = 1280 ;
56+ const EXPANDED_HEIGHT = 720 ;
57+ const PADDING = 10 ;
58+ const PADDING_TOP = 30 ;
59+
4060// For some reason Youtube types do not document getVideoData,
4161// which we need to determine if there was no search results
4262type ExtendedYoutubePlayer = {
@@ -123,10 +143,27 @@ export default class YoutubePlayer
123143 public iconColor = dataSourcesInfo . youtube . color ;
124144 youtubePlayer ?: ExtendedYoutubePlayer ;
125145 checkVideoLoadedTimerId ?: NodeJS . Timeout ;
146+ handleWindowResizeThrottle : ( ( ) => void ) & {
147+ cancel : ( ) => void ;
148+ flush : ( ) => void ;
149+ } ;
126150
127151 constructor ( props : YoutubePlayerProps ) {
128152 super ( props ) ;
129- this . state = { hidePlayer : false } ;
153+ this . state = {
154+ hidePlayer : false ,
155+ isExpanded : false ,
156+ width : DEFAULT_WIDTH ,
157+ height : DEFAULT_HEIGHT ,
158+ x : 0 ,
159+ y : 0 ,
160+ isInteracting : false ,
161+ } ;
162+ this . handleWindowResizeThrottle = throttle ( this . handleWindowResize , 100 ) ;
163+ }
164+
165+ componentDidMount ( ) : void {
166+ window . addEventListener ( "resize" , this . handleWindowResizeThrottle ) ;
130167 }
131168
132169 componentDidUpdate ( prevProps : DataSourceProps ) {
@@ -140,6 +177,11 @@ export default class YoutubePlayer
140177 }
141178 }
142179
180+ componentWillUnmount ( ) : void {
181+ window . removeEventListener ( "resize" , this . handleWindowResizeThrottle ) ;
182+ this . handleWindowResizeThrottle . cancel ( ) ;
183+ }
184+
143185 stop = ( ) => {
144186 this . youtubePlayer ?. stopVideo ( ) ;
145187 // Clear playlist
@@ -377,8 +419,111 @@ export default class YoutubePlayer
377419 } ) ;
378420 } ;
379421
422+ getMaxAvailableWidth = ( ) : number => {
423+ let maxWidth = window . innerWidth - PADDING * 2 ;
424+
425+ if ( window . innerWidth > SIDEBAR_BREAKPOINT ) {
426+ maxWidth -= SIDEBAR_WIDTH ;
427+ }
428+ return maxWidth ;
429+ } ;
430+
431+ calculateDimensions = ( ) => {
432+ const maxWidth = this . getMaxAvailableWidth ( ) ;
433+ const targetWidth = Math . min ( EXPANDED_WIDTH , maxWidth ) ;
434+ const calculatedHeight = ( targetWidth * 9 ) / 16 + BUTTON_HEIGHT ;
435+ const maxHeight =
436+ window . innerHeight - ( PLAYER_HEIGHT + BUTTON_HEIGHT + PADDING_TOP ) ;
437+ const targetHeight = Math . min (
438+ EXPANDED_HEIGHT + BUTTON_HEIGHT ,
439+ calculatedHeight ,
440+ maxHeight
441+ ) ;
442+ return {
443+ width : targetWidth ,
444+ height : targetHeight ,
445+ } ;
446+ } ;
447+
448+ handleExpandToggle = ( ) => {
449+ this . setState ( ( prev ) => {
450+ const isNowExpanded = ! prev . isExpanded ;
451+
452+ if ( isNowExpanded ) {
453+ const { width, height } = this . calculateDimensions ( ) ;
454+ return {
455+ isExpanded : true ,
456+ width,
457+ height,
458+ x : 0 ,
459+ y : 0 ,
460+ } ;
461+ }
462+ return {
463+ isExpanded : false ,
464+ width : DEFAULT_WIDTH ,
465+ height : DEFAULT_HEIGHT ,
466+ x : 0 ,
467+ y : 0 ,
468+ } ;
469+ } ) ;
470+ } ;
471+
472+ handleWindowResize = ( ) => {
473+ const { isExpanded, width, height } = this . state ;
474+ if ( isExpanded ) {
475+ const { width : newWidth , height : newHeight } = this . calculateDimensions ( ) ;
476+ this . setState ( {
477+ width : newWidth ,
478+ height : newHeight ,
479+ } ) ;
480+ } else {
481+ const maxSafeWidth = this . getMaxAvailableWidth ( ) ;
482+ const maxSafeHeight =
483+ window . innerHeight - ( PLAYER_HEIGHT + BUTTON_HEIGHT + PADDING_TOP ) ;
484+ if ( width > maxSafeWidth || height > maxSafeHeight ) {
485+ this . setState ( {
486+ width : Math . min ( width , maxSafeWidth ) ,
487+ height : Math . min ( height , maxSafeHeight ) ,
488+ } ) ;
489+ }
490+ }
491+ } ;
492+
493+ onResize = ( event : React . SyntheticEvent , data : ResizeCallbackData ) => {
494+ this . setState ( {
495+ width : data . size . width ,
496+ height : data . size . height ,
497+ isExpanded : false ,
498+ } ) ;
499+ } ;
500+
501+ onInteractionStart = ( ) => {
502+ this . setState ( { isInteracting : true } ) ;
503+ } ;
504+
505+ onInteractionStop = ( ) => {
506+ this . setState ( { isInteracting : false } ) ;
507+ } ;
508+
509+ onDrag = ( event : DraggableEvent , data : DraggableData ) => {
510+ this . setState ( {
511+ x : data . x ,
512+ y : data . y ,
513+ } ) ;
514+ } ;
515+
380516 render ( ) {
381- const { hidePlayer } = this . state ;
517+ const {
518+ hidePlayer,
519+ isExpanded,
520+ width,
521+ height,
522+ x,
523+ y,
524+ isInteracting,
525+ } = this . state ;
526+
382527 const options : Options = {
383528 playerVars : {
384529 controls : 0 ,
@@ -392,18 +537,27 @@ export default class YoutubePlayer
392537 width : "100%" ,
393538 height : "100%" ,
394539 } ;
540+
395541 const draggableBoundPadding = 10 ;
396542 // width of screen - padding on each side - youtube player width
397543 const leftBound =
398- document . body . clientWidth - draggableBoundPadding * 2 - 350 ;
399-
544+ document . body . clientWidth - draggableBoundPadding * 2 - width ;
400545 const isCurrentDataSource =
401546 store . get ( currentDataSourceNameAtom ) === this . name ;
402547 const isPlayerVisible = isCurrentDataSource && ! hidePlayer ;
548+ const maxResizableWidth = this . getMaxAvailableWidth ( ) ;
549+ const maxResizableHeight =
550+ window . innerHeight - PLAYER_HEIGHT - BUTTON_HEIGHT - PADDING_TOP ;
403551
404552 return (
405553 < Draggable
406554 handle = ".youtube-drag-handle"
555+ position = { { x, y } }
556+ disabled = { isInteracting }
557+ cancel = ".react-resizable-handle"
558+ onDrag = { this . onDrag }
559+ onStart = { this . onInteractionStart }
560+ onStop = { this . onInteractionStop }
407561 bounds = { {
408562 left : - leftBound ,
409563 right : - draggableBoundPadding ,
@@ -420,21 +574,45 @@ export default class YoutubePlayer
420574 >
421575 < FontAwesomeIcon icon = { faArrowsAlt } />
422576 </ button >
577+ < button
578+ className = "btn btn-sm youtube-button"
579+ type = "button"
580+ onClick = { this . handleExpandToggle }
581+ title = { isExpanded ? "Restore size" : "Expand video" }
582+ aria-label = { isExpanded ? "Restore size" : "Expand video" }
583+ >
584+ < FontAwesomeIcon icon = { isExpanded ? faCompress : faExpand } />
585+ </ button >
423586 < button
424587 className = "btn btn-sm youtube-button"
425588 type = "button"
426589 onClick = { this . handleHide }
427590 >
428591 < FontAwesomeIcon icon = { faTimes } />
429592 </ button >
430- < YouTube
431- className = "youtube-player"
432- opts = { options }
433- onError = { this . onError }
434- onStateChange = { this . handlePlayerStateChanged }
435- onReady = { this . onReady }
436- videoId = ""
437- />
593+ < ResizableBox
594+ width = { width }
595+ height = { height }
596+ onResizeStart = { this . onInteractionStart }
597+ onResize = { this . onResize }
598+ onResizeStop = { this . onInteractionStop }
599+ resizeHandles = { [ "nw" ] }
600+ minConstraints = { [ DEFAULT_WIDTH , DEFAULT_HEIGHT ] }
601+ maxConstraints = { [ maxResizableWidth , maxResizableHeight ] }
602+ axis = "both"
603+ className = "youtube-resizable-container"
604+ >
605+ < YouTube
606+ className = { `youtube-player${
607+ isInteracting ? " no-video-interaction" : ""
608+ } `}
609+ opts = { options }
610+ onError = { this . onError }
611+ onStateChange = { this . handlePlayerStateChanged }
612+ onReady = { this . onReady }
613+ videoId = ""
614+ />
615+ </ ResizableBox >
438616 </ div >
439617 </ Draggable >
440618 ) ;
0 commit comments