Skip to content

Commit 8910593

Browse files
authored
Merge pull request #3441 from celestiancoder/LB-1488-ALLOW-YT-VIDEO-TO-BE-RESIZED
LB-1488: Allow Youtube video to be resized
2 parents cad180c + cf7d3a9 commit 8910593

File tree

4 files changed

+286
-24
lines changed

4 files changed

+286
-24
lines changed

frontend/css/sass/brainzplayer.scss

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@use "sass:map";
22
@use "sass:color";
33

4+
@import "~react-resizable/css/styles.css";
5+
46
$primary-color: $listenbrainz-blue;
57
$brainzplayer-height: 60px;
68
$brainzplayer-padding: 0px;
@@ -13,7 +15,8 @@ $white-background: $white;
1315
$youtube-player-height: 200px;
1416
$youtube-player-width: 350px;
1517
$youtube-button-size: 30px;
16-
$youtube-resize-transition: height 0.25s ease-out;
18+
$youtube-resize-transition: width 0.15s ease-in-out, height 0.15s ease-in-out;
19+
$youtube-expanded-width: 1280px;
1720

1821
#brainz-player {
1922
position: fixed;
@@ -59,7 +62,7 @@ $youtube-resize-transition: height 0.25s ease-out;
5962
height: $cover-art-size;
6063
width: $cover-art-size;
6164
position: absolute;
62-
// transition: height, width 0.4s;
65+
transition: height, width 0.4s;
6366
bottom: 0;
6467
}
6568

@@ -75,12 +78,42 @@ $youtube-resize-transition: height 0.25s ease-out;
7578
position: absolute;
7679
bottom: $brainzplayer-height + 10px;
7780
right: 10px;
78-
height: $youtube-player-height + $youtube-button-size;
79-
width: $youtube-player-width;
81+
height: auto;
82+
width: auto;
8083
max-width: calc(100vw - 20px);
84+
@media (min-width: $offscreen-sidenav-breakpoint) {
85+
max-width: calc(100vw - 200px - 20px);
86+
}
8187
z-index: 4;
8288
text-align: right; // to align the move and reduce buttons
83-
transition: $youtube-resize-transition;
89+
90+
.react-resizable-handle {
91+
z-index: 100;
92+
position: absolute;
93+
94+
&.react-resizable-handle-nw {
95+
top: 0;
96+
left: 0;
97+
cursor: nw-resize;
98+
width: 20px;
99+
height: 20px;
100+
z-index: 2;
101+
position: absolute;
102+
&::after {
103+
content: "";
104+
width: 12px;
105+
height: 12px;
106+
top: 4px;
107+
left: 4px;
108+
position: absolute;
109+
border-left: 2px solid rgba(255, 255, 255, 0.978);
110+
border-top: 2px solid rgba(255, 255, 255, 0.978);
111+
}
112+
&:hover::after {
113+
border-color: #fff;
114+
}
115+
}
116+
}
84117

85118
&.reduced {
86119
height: $youtube-button-size;
@@ -89,8 +122,13 @@ $youtube-resize-transition: height 0.25s ease-out;
89122
}
90123
}
91124

125+
.no-video-interaction {
126+
pointer-events: none;
127+
}
128+
92129
.youtube-player {
93-
height: $youtube-player-height;
130+
height: 100%;
131+
width: 100%;
94132
border-radius: 8px;
95133
border-top-right-radius: 0; // intersection with the square drag handle
96134
overflow: hidden;
@@ -116,6 +154,14 @@ $youtube-resize-transition: height 0.25s ease-out;
116154
cursor: grabbing;
117155
}
118156
}
157+
158+
.youtube-resizable-container {
159+
position: relative;
160+
> div {
161+
height: 100%;
162+
width: 100%;
163+
}
164+
}
119165
}
120166
}
121167
}

frontend/js/src/common/brainzplayer/YoutubePlayer.tsx

Lines changed: 192 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
1314
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1415
import {
1516
faArrowsAlt,
1617
faWindowMaximize,
1718
faWindowMinimize,
1819
faTimes,
20+
faExpand,
21+
faCompress,
1922
} from "@fortawesome/free-solid-svg-icons";
2023
import { faYoutube } from "@fortawesome/free-brands-svg-icons";
2124
import { Link } from "react-router";
@@ -35,8 +38,25 @@ export type YoutubePlayerProps = DataSourceProps & {
3538

3639
type 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
4262
type 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

Comments
 (0)