diff options
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r-- | app/javascript/flavours/glitch/features/audio/index.js | 51 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/video/index.js | 85 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/util/resize_image.js | 50 |
3 files changed, 171 insertions, 15 deletions
diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js index c050a63a9..ac0468f70 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -392,13 +392,59 @@ class Audio extends React.PureComponent { return this.props.foregroundColor || '#ffffff'; } + seekBy (time) { + const currentTime = this.audio.currentTime + time; + + if (!isNaN(currentTime)) { + this.setState({ currentTime }, () => { + this.audio.currentTime = currentTime; + }); + } + } + + handleAudioKeyDown = e => { + // On the audio element or the seek bar, we can safely use the space bar + // for playback control because there are no buttons to press + + if (e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + } + } + + handleKeyDown = e => { + switch(e.key) { + case 'k': + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + break; + case 'm': + e.preventDefault(); + e.stopPropagation(); + this.toggleMute(); + break; + case 'j': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-10); + break; + case 'l': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(10); + break; + } + } + render () { const { src, intl, alt, editable, autoPlay } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); return ( - <div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}> <audio src={src} ref={this.setAudioRef} @@ -412,12 +458,14 @@ class Audio extends React.PureComponent { <canvas role='button' + tabIndex='0' className='audio-player__canvas' width={this.state.width} height={this.state.height} style={{ width: '100%', position: 'absolute', top: 0, left: 0 }} ref={this.setCanvasRef} onClick={this.togglePlay} + onKeyDown={this.handleAudioKeyDown} title={alt} aria-label={alt} /> @@ -438,6 +486,7 @@ class Audio extends React.PureComponent { className={classNames('video-player__seek__handle', { active: dragging })} tabIndex='0' style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }} + onKeyDown={this.handleAudioKeyDown} /> </div> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 870812856..ea40b6073 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -182,10 +182,7 @@ class Video extends React.PureComponent { this.volume = c; } - handleMouseDownRoot = e => { - e.preventDefault(); - e.stopPropagation(); - } + handleClickRoot = e => e.stopPropagation(); handlePlay = () => { this.setState({ paused: false }); @@ -279,6 +276,81 @@ class Video extends React.PureComponent { } }, 15); + seekBy (time) { + const currentTime = this.video.currentTime + time; + + if (!isNaN(currentTime)) { + this.setState({ currentTime }, () => { + this.video.currentTime = currentTime; + }); + } + } + + handleVideoKeyDown = e => { + // On the video element or the seek bar, we can safely use the space bar + // for playback control because there are no buttons to press + + if (e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + } + } + + handleKeyDown = e => { + const frameTime = 1 / 25; + + switch(e.key) { + case 'k': + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + break; + case 'm': + e.preventDefault(); + e.stopPropagation(); + this.toggleMute(); + break; + case 'f': + e.preventDefault(); + e.stopPropagation(); + this.toggleFullscreen(); + break; + case 'j': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-10); + break; + case 'l': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(10); + break; + case ',': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-frameTime); + break; + case '.': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(frameTime); + break; + } + + // If we are in fullscreen mode, we don't want any hotkeys + // interacting with the UI that's not visible + + if (this.state.fullscreen) { + e.preventDefault(); + e.stopPropagation(); + + if (e.key === 'Escape') { + exitFullscreen(); + } + } + } + togglePlay = () => { if (this.state.paused) { this.setState({ paused: false }, () => this.video.play()); @@ -503,7 +575,8 @@ class Video extends React.PureComponent { ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} - onMouseDown={this.handleMouseDownRoot} + onClick={this.handleClickRoot} + onKeyDown={this.handleKeyDown} tabIndex={0} > <Blurhash @@ -528,6 +601,7 @@ class Video extends React.PureComponent { height={height} volume={volume} onClick={this.togglePlay} + onKeyDown={this.handleVideoKeyDown} onPlay={this.handlePlay} onPause={this.handlePause} onLoadedData={this.handleLoadedData} @@ -550,6 +624,7 @@ class Video extends React.PureComponent { className={classNames('video-player__seek__handle', { active: dragging })} tabIndex='0' style={{ left: `${progress}%` }} + onKeyDown={this.handleVideoKeyDown} /> </div> diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js index 8c89b2841..57fc6a6ec 100644 --- a/app/javascript/flavours/glitch/util/resize_image.js +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -41,6 +41,45 @@ const dropOrientationIfNeeded = (orientation) => new Promise(resolve => { } }); +// Some browsers don't allow reading from a canvas and instead return all-white +// or randomized data. Use a pre-defined image to check if reading the canvas +// works. +const checkCanvasReliability = () => new Promise((resolve, reject) => { + switch(_browser_quirks['canvas-read-unreliable']) { + case true: + reject('Canvas reading unreliable'); + break; + case false: + resolve(); + break; + default: + // 2×2 GIF with white, red, green and blue pixels + const testImageURL = + 'data:image/gif;base64,R0lGODdhAgACAKEDAAAA//8AAAD/AP///ywAAAAAAgACAAACA1wEBQA7'; + const refData = + [255, 255, 255, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0, 2, 2); + const imageData = context.getImageData(0, 0, 2, 2); + if (imageData.data.every((x, i) => refData[i] === x)) { + _browser_quirks['canvas-read-unreliable'] = false; + resolve(); + } else { + _browser_quirks['canvas-read-unreliable'] = true; + reject('Canvas reading unreliable'); + } + }; + img.onerror = () => { + _browser_quirks['canvas-read-unreliable'] = true; + reject('Failed to load test image'); + }; + img.src = testImageURL; + } +}); + const getImageUrl = inputFile => new Promise((resolve, reject) => { if (window.URL && URL.createObjectURL) { try { @@ -110,14 +149,6 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) = context.drawImage(img, 0, 0, width, height); - // The Tor Browser and maybe other browsers may prevent reading from canvas - // and return an all-white image instead. Assume reading failed if the resized - // image is perfectly white. - const imageData = context.getImageData(0, 0, width, height); - if (imageData.data.every(value => value === 255)) { - throw 'Failed to read from canvas'; - } - canvas.toBlob(resolve, type); }); @@ -127,7 +158,8 @@ const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) = const newWidth = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (width / height))); const newHeight = Math.round(Math.sqrt(MAX_IMAGE_PIXELS * (height / width))); - getOrientation(img, type) + checkCanvasReliability() + .then(getOrientation(img, type)) .then(orientation => processImage(img, { width: newWidth, height: newHeight, |