]>
cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/features/video/index.js
1 import React
from 'react';
2 import PropTypes
from 'prop-types';
3 import { defineMessages
, injectIntl
, FormattedMessage
} from 'react-intl';
4 import { fromJS
} from 'immutable';
5 import { throttle
} from 'lodash';
6 import classNames
from 'classnames';
7 import { isFullscreen
, requestFullscreen
, exitFullscreen
} from '../ui/util/fullscreen';
8 import { displayMedia
} from '../../initial_state';
10 const messages
= defineMessages({
11 play: { id: 'video.play', defaultMessage: 'Play' },
12 pause: { id: 'video.pause', defaultMessage: 'Pause' },
13 mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
14 unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
15 hide: { id: 'video.hide', defaultMessage: 'Hide video' },
16 expand: { id: 'video.expand', defaultMessage: 'Expand video' },
17 close: { id: 'video.close', defaultMessage: 'Close video' },
18 fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
19 exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
22 const formatTime
= secondsNum
=> {
23 let hours
= Math
.floor(secondsNum
/ 3600);
24 let minutes
= Math
.floor((secondsNum
- (hours
* 3600)) / 60);
25 let seconds
= secondsNum
- (hours
* 3600) - (minutes
* 60);
27 if (hours
< 10) hours
= '0' + hours
;
28 if (minutes
< 10) minutes
= '0' + minutes
;
29 if (seconds
< 10) seconds
= '0' + seconds
;
31 return (hours
=== '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
34 export const findElementPosition
= el
=> {
37 if (el
.getBoundingClientRect
&& el
.parentNode
) {
38 box
= el
.getBoundingClientRect();
48 const docEl
= document
.documentElement
;
49 const body
= document
.body
;
51 const clientLeft
= docEl
.clientLeft
|| body
.clientLeft
|| 0;
52 const scrollLeft
= window
.pageXOffset
|| body
.scrollLeft
;
53 const left
= (box
.left
+ scrollLeft
) - clientLeft
;
55 const clientTop
= docEl
.clientTop
|| body
.clientTop
|| 0;
56 const scrollTop
= window
.pageYOffset
|| body
.scrollTop
;
57 const top
= (box
.top
+ scrollTop
) - clientTop
;
60 left: Math
.round(left
),
65 export const getPointerPosition
= (el
, event
) => {
67 const box
= findElementPosition(el
);
68 const boxW
= el
.offsetWidth
;
69 const boxH
= el
.offsetHeight
;
71 const boxX
= box
.left
;
73 let pageY
= event
.pageY
;
74 let pageX
= event
.pageX
;
76 if (event
.changedTouches
) {
77 pageX
= event
.changedTouches
[0].pageX
;
78 pageY
= event
.changedTouches
[0].pageY
;
81 position
.y
= Math
.max(0, Math
.min(1, (pageY
- boxY
) / boxH
));
82 position
.x
= Math
.max(0, Math
.min(1, (pageX
- boxX
) / boxW
));
87 export default @injectIntl
88 class Video
extends React
.PureComponent
{
91 preview: PropTypes
.string
,
92 src: PropTypes
.string
.isRequired
,
93 alt: PropTypes
.string
,
94 width: PropTypes
.number
,
95 height: PropTypes
.number
,
96 sensitive: PropTypes
.bool
,
97 startTime: PropTypes
.number
,
98 onOpenVideo: PropTypes
.func
,
99 onCloseVideo: PropTypes
.func
,
100 detailed: PropTypes
.bool
,
101 inline: PropTypes
.bool
,
102 intl: PropTypes
.object
.isRequired
,
110 containerWidth: false,
114 revealed: displayMedia
!== 'hide_all' && !this.props
.sensitive
|| displayMedia
=== 'show_all',
117 setPlayerRef
= c
=> {
122 containerWidth: c
.offsetWidth
,
135 handleClickRoot
= e
=> e
.stopPropagation();
138 this.setState({ paused: false });
141 handlePause
= () => {
142 this.setState({ paused: true });
145 handleTimeUpdate
= () => {
147 currentTime: Math
.floor(this.video
.currentTime
),
148 duration: Math
.floor(this.video
.duration
),
152 handleMouseDown
= e
=> {
153 document
.addEventListener('mousemove', this.handleMouseMove
, true);
154 document
.addEventListener('mouseup', this.handleMouseUp
, true);
155 document
.addEventListener('touchmove', this.handleMouseMove
, true);
156 document
.addEventListener('touchend', this.handleMouseUp
, true);
158 this.setState({ dragging: true });
160 this.handleMouseMove(e
);
166 handleMouseUp
= () => {
167 document
.removeEventListener('mousemove', this.handleMouseMove
, true);
168 document
.removeEventListener('mouseup', this.handleMouseUp
, true);
169 document
.removeEventListener('touchmove', this.handleMouseMove
, true);
170 document
.removeEventListener('touchend', this.handleMouseUp
, true);
172 this.setState({ dragging: false });
176 handleMouseMove
= throttle(e
=> {
177 const { x
} = getPointerPosition(this.seek
, e
);
178 const currentTime
= Math
.floor(this.video
.duration
* x
);
180 if (!isNaN(currentTime
)) {
181 this.video
.currentTime
= currentTime
;
182 this.setState({ currentTime
});
187 if (this.state
.paused
) {
194 toggleFullscreen
= () => {
195 if (isFullscreen()) {
198 requestFullscreen(this.player
);
202 componentDidMount () {
203 document
.addEventListener('fullscreenchange', this.handleFullscreenChange
, true);
204 document
.addEventListener('webkitfullscreenchange', this.handleFullscreenChange
, true);
205 document
.addEventListener('mozfullscreenchange', this.handleFullscreenChange
, true);
206 document
.addEventListener('MSFullscreenChange', this.handleFullscreenChange
, true);
209 componentWillUnmount () {
210 document
.removeEventListener('fullscreenchange', this.handleFullscreenChange
, true);
211 document
.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange
, true);
212 document
.removeEventListener('mozfullscreenchange', this.handleFullscreenChange
, true);
213 document
.removeEventListener('MSFullscreenChange', this.handleFullscreenChange
, true);
216 handleFullscreenChange
= () => {
217 this.setState({ fullscreen: isFullscreen() });
220 handleMouseEnter
= () => {
221 this.setState({ hovered: true });
224 handleMouseLeave
= () => {
225 this.setState({ hovered: false });
229 this.video
.muted
= !this.video
.muted
;
230 this.setState({ muted: this.video
.muted
});
233 toggleReveal
= () => {
234 if (this.state
.revealed
) {
238 this.setState({ revealed: !this.state
.revealed
});
241 handleLoadedData
= () => {
242 if (this.props
.startTime
) {
243 this.video
.currentTime
= this.props
.startTime
;
248 handleProgress
= () => {
249 if (this.video
.buffered
.length
> 0) {
250 this.setState({ buffer: this.video
.buffered
.end(0) / this.video
.duration
* 100 });
254 handleOpenVideo
= () => {
255 const { src
, preview
, width
, height
, alt
} = this.props
;
256 const media
= fromJS({
259 preview_url: preview
,
266 this.props
.onOpenVideo(media
, this.video
.currentTime
);
269 handleCloseVideo
= () => {
271 this.props
.onCloseVideo();
275 const { preview
, src
, inline
, startTime
, onOpenVideo
, onCloseVideo
, intl
, alt
, detailed
, sensitive
} = this.props
;
276 const { containerWidth
, currentTime
, duration
, buffer
, dragging
, paused
, fullscreen
, hovered
, muted
, revealed
} = this.state
;
277 const progress
= (currentTime
/ duration
) * 100;
278 const playerStyle
= {};
280 let { width
, height
} = this.props
;
282 if (inline
&& containerWidth
) {
283 width
= containerWidth
;
284 height
= containerWidth
/ (16/9);
286 playerStyle
.width
= width
;
287 playerStyle
.height
= height
;
291 if (startTime
|| fullscreen
|| dragging
) {
293 } else if (detailed
) {
294 preload
= 'metadata';
301 warning
= <FormattedMessage id
='status.sensitive_warning' defaultMessage
='Sensitive content' />;
303 warning
= <FormattedMessage id
='status.media_hidden' defaultMessage
='Media hidden' />;
309 className
={classNames('video-player', { inactive: !revealed
, detailed
, inline: inline
&& !fullscreen
, fullscreen
})}
311 ref
={this.setPlayerRef
}
312 onMouseEnter
={this.handleMouseEnter
}
313 onMouseLeave
={this.handleMouseLeave
}
314 onClick
={this.handleClickRoot
}
318 ref
={this.setVideoRef
}
329 onClick
={this.togglePlay
}
330 onPlay
={this.handlePlay
}
331 onPause
={this.handlePause
}
332 onTimeUpdate
={this.handleTimeUpdate
}
333 onLoadedData
={this.handleLoadedData
}
334 onProgress
={this.handleProgress
}
337 <button type
='button' className
={classNames('video-player__spoiler', { active: !revealed
})} onClick
={this.toggleReveal
}>
338 <span className
='video-player__spoiler__title'>{warning
}</span
>
339 <span className
='video-player__spoiler__subtitle'><FormattedMessage id
='status.sensitive_toggle' defaultMessage
='Click to view' /></span>
342 <div className
={classNames('video-player__controls', { active: paused
|| hovered
})}>
343 <div className
='video-player__seek' onMouseDown
={this.handleMouseDown
} ref
={this.setSeekRef
}>
344 <div className
='video-player__seek__buffer' style
={{ width: `${buffer}%` }} />
345 <div className
='video-player__seek__progress' style
={{ width: `${progress}%` }} />
348 className
={classNames('video-player__seek__handle', { active: dragging
})}
350 style
={{ left: `${progress}%` }}
354 <div className
='video-player__buttons-bar'>
355 <div className
='video-player__buttons left'>
356 <button type
='button' aria
-label
={intl
.formatMessage(paused
? messages
.play : messages
.pause
)} onClick
={this.togglePlay
}><i className
={classNames('fa fa-fw', { 'fa-play': paused
, 'fa-pause': !paused
})} /></button
>
357 <button type
='button' aria
-label
={intl
.formatMessage(muted
? messages
.unmute : messages
.mute
)} onClick
={this.toggleMute
}><i className
={classNames('fa fa-fw', { 'fa-volume-off': muted
, 'fa-volume-up': !muted
})} /></button
>
359 {!onCloseVideo
&& <button type
='button' aria
-label
={intl
.formatMessage(messages
.hide
)} onClick
={this.toggleReveal
}><i className
='fa fa-fw fa-eye' /></button
>}
361 {(detailed
|| fullscreen
) &&
363 <span className
='video-player__time-current'>{formatTime(currentTime
)}</span
>
364 <span className
='video-player__time-sep'>/</span
>
365 <span className
='video-player__time-total'>{formatTime(duration
)}</span
>
370 <div className
='video-player__buttons right'>
371 {(!fullscreen
&& onOpenVideo
) && <button type
='button' aria
-label
={intl
.formatMessage(messages
.expand
)} onClick
={this.handleOpenVideo
}><i className
='fa fa-fw fa-expand' /></button
>}
372 {onCloseVideo
&& <button type
='button' aria
-label
={intl
.formatMessage(messages
.close
)} onClick
={this.handleCloseVideo
}><i className
='fa fa-fw fa-compress' /></button
>}
373 <button type
='button' aria
-label
={intl
.formatMessage(fullscreen
? messages
.exit_fullscreen : messages
.fullscreen
)} onClick
={this.toggleFullscreen
}><i className
={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen
, 'fa-compress': fullscreen
})} /></button
>
This page took 0.167691 seconds and 5 git commands to generate.