[Glitch] Add pop-out player for audio/video in web UI
port d88a79b456
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
This commit is contained in:
committed by
Thibaut Girka
parent
9c88792f0a
commit
8f950e540b
@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { reduceMotion } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const obfuscatedCount = count => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
export default class AnimatedNumber extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
obfuscate: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props;
|
||||
const { value, obfuscate } = this.props;
|
||||
const { direction } = this.state;
|
||||
|
||||
if (reduceMotion) {
|
||||
return <FormattedNumber value={value} />;
|
||||
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
|
||||
}
|
||||
|
||||
const styles = [{
|
||||
@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
||||
{items => (
|
||||
<span className='animated-number'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
|
||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
|
@ -4,6 +4,7 @@ import spring from 'react-motion/lib/spring';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
|
||||
|
||||
export default class IconButton extends React.PureComponent {
|
||||
|
||||
@ -27,6 +28,8 @@ export default class IconButton extends React.PureComponent {
|
||||
overlay: PropTypes.bool,
|
||||
tabIndex: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
counter: PropTypes.number,
|
||||
obfuscateCount: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -104,6 +107,8 @@ export default class IconButton extends React.PureComponent {
|
||||
pressed,
|
||||
tabIndex,
|
||||
title,
|
||||
counter,
|
||||
obfuscateCount,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -120,6 +125,10 @@ export default class IconButton extends React.PureComponent {
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
if (typeof counter !== 'undefined') {
|
||||
style.width = 'auto';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
@ -135,7 +144,7 @@ export default class IconButton extends React.PureComponent {
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
|
||||
{this.props.label}
|
||||
</button>
|
||||
);
|
||||
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default @connect()
|
||||
class PictureInPicturePlaceholder extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
width: PropTypes.number,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: this.props.width,
|
||||
height: this.props.width && (this.props.width / (16/9)),
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(removePictureInPicture());
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.node.offsetWidth;
|
||||
const height = width / (16/9);
|
||||
|
||||
this.setState({ width, height });
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}, 250, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
render () {
|
||||
const { height } = this.state;
|
||||
|
||||
return (
|
||||
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
|
||||
<Icon id='window-restore' />
|
||||
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,7 @@ import classNames from 'classnames';
|
||||
import { autoUnfoldCW } from 'flavours/glitch/util/content_warning';
|
||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||
import { displayMedia } from 'flavours/glitch/util/initial_state';
|
||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
@ -97,6 +98,8 @@ class Status extends ImmutablePureComponent {
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
scrollKey: PropTypes.string,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
usingPiP: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -123,6 +126,7 @@ class Status extends ImmutablePureComponent {
|
||||
'hidden',
|
||||
'expanded',
|
||||
'unread',
|
||||
'usingPiP',
|
||||
]
|
||||
|
||||
updateOnStates = [
|
||||
@ -394,6 +398,12 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleDeployPictureInPicture = (type, mediaProps) => {
|
||||
const { deployPictureInPicture, status } = this.props;
|
||||
|
||||
deployPictureInPicture(status, type, mediaProps);
|
||||
}
|
||||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.props.onReply(this.props.status, this.context.router.history);
|
||||
@ -496,6 +506,7 @@ class Status extends ImmutablePureComponent {
|
||||
hidden,
|
||||
unread,
|
||||
featured,
|
||||
usingPiP,
|
||||
...other
|
||||
} = this.props;
|
||||
const { isExpanded, isCollapsed, forceFilter } = this.state;
|
||||
@ -576,6 +587,9 @@ class Status extends ImmutablePureComponent {
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
mediaIcon = 'tasks';
|
||||
} else if (usingPiP) {
|
||||
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||
mediaIcon = 'video-camera';
|
||||
} else if (attachments.size > 0) {
|
||||
if (muted || attachments.some(item => item.get('type') === 'unknown')) {
|
||||
media = (
|
||||
@ -601,6 +615,7 @@ class Status extends ImmutablePureComponent {
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
@ -624,6 +639,7 @@ class Status extends ImmutablePureComponent {
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
width={this.props.cachedMediaWidth}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>)}
|
||||
|
@ -40,16 +40,6 @@ const messages = defineMessages({
|
||||
hide: { id: 'status.hide', defaultMessage: 'Hide toot' },
|
||||
});
|
||||
|
||||
const obfuscatedCount = count => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
@ -284,10 +274,14 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
);
|
||||
if (showReplyCount) {
|
||||
replyButton = (
|
||||
<div className='status__action-bar__counter'>
|
||||
{replyButton}
|
||||
<span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
className='status__action-bar-button'
|
||||
title={replyTitle}
|
||||
icon={replyIcon}
|
||||
onClick={this.handleReplyClick}
|
||||
counter={status.get('replies_count')}
|
||||
obfuscateCount
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user