The code powering m.abunchtell.com https://m.abunchtell.com
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

236 行
6.6 KiB

  1. import ImmutablePropTypes from 'react-immutable-proptypes';
  2. import PureRenderMixin from 'react-addons-pure-render-mixin';
  3. import IconButton from './icon_button';
  4. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  5. import { isIOS } from '../is_mobile';
  6. const messages = defineMessages({
  7. toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
  8. toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
  9. expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
  10. });
  11. const videoStyle = {
  12. position: 'relative',
  13. zIndex: '1',
  14. width: '100%',
  15. height: '100%',
  16. objectFit: 'cover',
  17. top: '50%',
  18. transform: 'translateY(-50%)'
  19. };
  20. const muteStyle = {
  21. position: 'absolute',
  22. top: '4px',
  23. right: '4px',
  24. color: 'white',
  25. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  26. opacity: '0.8',
  27. zIndex: '5'
  28. };
  29. const spoilerStyle = {
  30. marginTop: '8px',
  31. textAlign: 'center',
  32. height: '100%',
  33. cursor: 'pointer',
  34. display: 'flex',
  35. alignItems: 'center',
  36. justifyContent: 'center',
  37. flexDirection: 'column',
  38. position: 'relative'
  39. };
  40. const spoilerSpanStyle = {
  41. display: 'block',
  42. fontSize: '14px'
  43. };
  44. const spoilerSubSpanStyle = {
  45. display: 'block',
  46. fontSize: '11px',
  47. fontWeight: '500'
  48. };
  49. const spoilerButtonStyle = {
  50. position: 'absolute',
  51. top: '4px',
  52. left: '4px',
  53. color: 'white',
  54. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  55. zIndex: '100'
  56. };
  57. const expandButtonStyle = {
  58. position: 'absolute',
  59. bottom: '4px',
  60. right: '4px',
  61. color: 'white',
  62. textShadow: "0px 1px 1px black, 1px 0px 1px black",
  63. zIndex: '100'
  64. };
  65. const VideoPlayer = React.createClass({
  66. propTypes: {
  67. media: ImmutablePropTypes.map.isRequired,
  68. width: React.PropTypes.number,
  69. height: React.PropTypes.number,
  70. sensitive: React.PropTypes.bool,
  71. intl: React.PropTypes.object.isRequired,
  72. autoplay: React.PropTypes.bool,
  73. onOpenVideo: React.PropTypes.func.isRequired
  74. },
  75. getDefaultProps () {
  76. return {
  77. width: 239,
  78. height: 110
  79. };
  80. },
  81. getInitialState () {
  82. return {
  83. visible: !this.props.sensitive,
  84. preview: true,
  85. muted: true,
  86. hasAudio: true
  87. };
  88. },
  89. mixins: [PureRenderMixin],
  90. handleClick () {
  91. this.setState({ muted: !this.state.muted });
  92. },
  93. handleVideoClick (e) {
  94. e.stopPropagation();
  95. const node = ReactDOM.findDOMNode(this).querySelector('video');
  96. if (node.paused) {
  97. node.play();
  98. } else {
  99. node.pause();
  100. }
  101. },
  102. handleOpen () {
  103. this.setState({ preview: !this.state.preview });
  104. },
  105. handleVisibility () {
  106. this.setState({
  107. visible: !this.state.visible,
  108. preview: true
  109. });
  110. },
  111. handleExpand () {
  112. this.video.pause();
  113. this.props.onOpenVideo(this.props.media, this.video.currentTime);
  114. },
  115. setRef (c) {
  116. this.video = c;
  117. },
  118. handleLoadedData () {
  119. if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
  120. this.setState({ hasAudio: false });
  121. }
  122. },
  123. componentDidMount () {
  124. if (!this.video) {
  125. return;
  126. }
  127. this.video.addEventListener('loadeddata', this.handleLoadedData);
  128. },
  129. componentDidUpdate () {
  130. if (!this.video) {
  131. return;
  132. }
  133. this.video.addEventListener('loadeddata', this.handleLoadedData);
  134. },
  135. componentWillUnmount () {
  136. if (!this.video) {
  137. return;
  138. }
  139. this.video.removeEventListener('loadeddata', this.handleLoadedData);
  140. },
  141. render () {
  142. const { media, intl, width, height, sensitive, autoplay } = this.props;
  143. let spoilerButton = (
  144. <div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
  145. <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
  146. </div>
  147. );
  148. let expandButton = (
  149. <div style={expandButtonStyle} >
  150. <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
  151. </div>
  152. );
  153. let muteButton = '';
  154. if (this.state.hasAudio) {
  155. muteButton = (
  156. <div style={muteStyle}>
  157. <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
  158. </div>
  159. );
  160. }
  161. if (!this.state.visible) {
  162. if (sensitive) {
  163. return (
  164. <div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
  165. {spoilerButton}
  166. <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
  167. <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
  168. </div>
  169. );
  170. } else {
  171. return (
  172. <div role='button' tabIndex='0' style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
  173. {spoilerButton}
  174. <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
  175. <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
  176. </div>
  177. );
  178. }
  179. }
  180. if (this.state.preview && !autoplay) {
  181. return (
  182. <div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
  183. {spoilerButton}
  184. <div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
  185. </div>
  186. );
  187. }
  188. return (
  189. <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
  190. {spoilerButton}
  191. {muteButton}
  192. {expandButton}
  193. <video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
  194. </div>
  195. );
  196. }
  197. });
  198. export default injectIntl(VideoPlayer);