461 lines
12 KiB
JavaScript
461 lines
12 KiB
JavaScript
import React, { Component } from 'react';
|
|
import Icon from '/imports/ui/components/common/icon/component';
|
|
import TimerService from '/imports/ui/components/timer/service';
|
|
import logger from '/imports/startup/client/logger';
|
|
import Tooltip from '/imports/ui/components/common/tooltip/component';
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
import PropTypes from 'prop-types';
|
|
import Styled from './styles';
|
|
|
|
const CDN = Meteor.settings.public.app.cdn;
|
|
const BASENAME = Meteor.settings.public.app.basename;
|
|
const HOST = CDN + BASENAME;
|
|
const trackName = Meteor.settings.public.timer.music;
|
|
const TAB_TIMER_INDICATOR = Meteor.settings.public.timer.tabIndicator;
|
|
|
|
const propTypes = {
|
|
timer: PropTypes.shape({
|
|
stopwatch: PropTypes.bool,
|
|
running: PropTypes.bool,
|
|
time: PropTypes.number,
|
|
accumulated: PropTypes.number,
|
|
timestamp: PropTypes.number,
|
|
}).isRequired,
|
|
isTimerActive: PropTypes.bool.isRequired,
|
|
isMusicActive: PropTypes.bool.isRequired,
|
|
sidebarNavigationIsOpen: PropTypes.bool.isRequired,
|
|
sidebarContentIsOpen: PropTypes.bool.isRequired,
|
|
timeOffset: PropTypes.number.isRequired,
|
|
isModerator: PropTypes.bool.isRequired,
|
|
currentTrack: PropTypes.bool.isRequired,
|
|
};
|
|
|
|
const intlMessages = defineMessages({
|
|
toolTipTimerStopped: {
|
|
id: 'app.timer.toolTipTimerStopped',
|
|
},
|
|
toolTipTimerRunning: {
|
|
id: 'app.timer.toolTipTimerRunning',
|
|
},
|
|
toolTipStopwatchStopped: {
|
|
id: 'app.timer.toolTipStopwatchStopped',
|
|
},
|
|
toolTipStopwatchRunning: {
|
|
id: 'app.timer.toolTipStopwatchRunning',
|
|
},
|
|
toolTipTimerStoppedMod: {
|
|
id: 'app.timer.toolTipTimerStoppedMod',
|
|
},
|
|
toolTipTimerRunningMod: {
|
|
id: 'app.timer.toolTipTimerRunningMod',
|
|
},
|
|
toolTipStopwatchStoppedMod: {
|
|
id: 'app.timer.toolTipStopwatchStoppedMod',
|
|
},
|
|
toolTipStopwatchRunningMod: {
|
|
id: 'app.timer.toolTipStopwatchRunningMod',
|
|
},
|
|
});
|
|
|
|
class Indicator extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.timeRef = React.createRef();
|
|
this.interval = null;
|
|
|
|
this.alarm = null;
|
|
this.music = null;
|
|
|
|
// We need to avoid trigger on mount
|
|
this.triggered = true;
|
|
|
|
this.alreadyNotified = false;
|
|
|
|
this.updateTime = this.updateTime.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
const { timer } = this.props;
|
|
const { running } = timer;
|
|
|
|
this.alarm = new Audio(`${HOST}/resources/sounds/alarm.mp3`);
|
|
this.setUpMusic();
|
|
|
|
this.triggered = this.initTriggered();
|
|
|
|
const { current } = this.timeRef;
|
|
if (current && running) {
|
|
this.interval = setInterval(this.updateTime, TimerService.getInterval());
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { timer, isTimerActive } = this.props;
|
|
const { timer: prevTimer, isTimerActive: prevTimerActive } = prevProps;
|
|
|
|
if (this.shouldPlayMusic()) {
|
|
this.playMusic();
|
|
}
|
|
|
|
if (!isTimerActive && prevTimerActive) {
|
|
this.updateTabTitleTimer(true, this.getTime());
|
|
}
|
|
|
|
this.updateInterval(prevTimer, timer);
|
|
this.updateAlarmTrigger(prevTimer, timer);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
clearInterval(this.interval);
|
|
this.stopMusic();
|
|
}
|
|
|
|
getTime() {
|
|
const {
|
|
timer,
|
|
timeOffset,
|
|
} = this.props;
|
|
|
|
const {
|
|
stopwatch,
|
|
running,
|
|
time,
|
|
accumulated,
|
|
timestamp,
|
|
} = timer;
|
|
|
|
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
|
|
|
|
let updatedTime;
|
|
if (stopwatch) {
|
|
updatedTime = elapsedTime;
|
|
} else {
|
|
updatedTime = Math.max(time - elapsedTime, 0);
|
|
}
|
|
|
|
if (this.shouldNotifyTimerEnded(updatedTime)) {
|
|
TimerService.timerEnded();
|
|
}
|
|
|
|
if (this.shouldStopMusic(updatedTime)) {
|
|
this.stopMusic();
|
|
}
|
|
|
|
if (this.soundAlarm(updatedTime)) {
|
|
this.play();
|
|
}
|
|
|
|
return TimerService.getTimeAsString(updatedTime, stopwatch);
|
|
}
|
|
|
|
setUpMusic() {
|
|
const { currentTrack } = this.props;
|
|
if (trackName[currentTrack] === undefined) return;
|
|
if (this.music === null) {
|
|
this.music = new Audio(`${HOST}/resources/sounds/${trackName[currentTrack]}.mp3`);
|
|
this.music.volume = TimerService.getMusicVolume();
|
|
this.music.addEventListener('timeupdate', () => {
|
|
const buffer = 0.19;
|
|
// Start playing the music before it ends to make the loop gapless
|
|
if (this.music.currentTime > this.music.duration - buffer) {
|
|
this.music.currentTime = 0;
|
|
this.music.play();
|
|
}
|
|
});
|
|
} else {
|
|
this.music.src = `${HOST}/resources/sounds/${trackName[currentTrack]}.mp3`;
|
|
}
|
|
this.music.track = currentTrack;
|
|
}
|
|
|
|
updateInterval(prevTimer, timer) {
|
|
const { running } = timer;
|
|
const { running: prevRunning } = prevTimer;
|
|
|
|
if (!prevRunning && running) {
|
|
this.interval = setInterval(this.updateTime, TimerService.getInterval());
|
|
}
|
|
|
|
if (prevRunning && !running) {
|
|
clearInterval(this.interval);
|
|
}
|
|
}
|
|
|
|
updateAlarmTrigger(prevTimer, timer) {
|
|
const {
|
|
accumulated,
|
|
timestamp,
|
|
} = timer;
|
|
|
|
const { timestamp: prevTimestamp } = prevTimer;
|
|
|
|
const reseted = timestamp !== prevTimestamp && accumulated === 0;
|
|
|
|
if (reseted) {
|
|
this.triggered = false;
|
|
this.alreadyNotified = false;
|
|
}
|
|
}
|
|
|
|
initTriggered() {
|
|
const {
|
|
timer,
|
|
timeOffset,
|
|
} = this.props;
|
|
|
|
const {
|
|
stopwatch,
|
|
running,
|
|
} = timer;
|
|
|
|
if (stopwatch || !running) return false;
|
|
|
|
const {
|
|
time,
|
|
accumulated,
|
|
timestamp,
|
|
} = timer;
|
|
|
|
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
|
|
const updatedTime = Math.max(time - elapsedTime, 0);
|
|
|
|
if (updatedTime === 0) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
play() {
|
|
if (this.alarm && !this.triggered) {
|
|
this.triggered = true;
|
|
this.alarm.play().catch((error) => {
|
|
logger.error({
|
|
logCode: 'timer_sound_error',
|
|
extraInfo: { error },
|
|
}, `Timer beep failed: ${error}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
soundAlarm(time) {
|
|
const { timer } = this.props;
|
|
const {
|
|
running,
|
|
stopwatch,
|
|
} = timer;
|
|
|
|
const enabled = TimerService.isAlarmEnabled();
|
|
const zero = time === 0;
|
|
|
|
return enabled && running && zero && !stopwatch;
|
|
}
|
|
|
|
playMusic() {
|
|
const handleUserInteraction = () => {
|
|
this.music.play();
|
|
window.removeEventListener('click', handleUserInteraction);
|
|
window.removeEventListener('auxclick', handleUserInteraction);
|
|
window.removeEventListener('keydown', handleUserInteraction);
|
|
window.removeEventListener('touchstart', handleUserInteraction);
|
|
};
|
|
|
|
const playMusicAfterUserInteraction = () => {
|
|
window.addEventListener('click', handleUserInteraction);
|
|
window.addEventListener('auxclick', handleUserInteraction);
|
|
window.addEventListener('keydown', handleUserInteraction);
|
|
window.addEventListener('touchstart', handleUserInteraction);
|
|
};
|
|
|
|
this.handleUserInteraction = handleUserInteraction;
|
|
|
|
if (this.music !== null) {
|
|
this.music.play()
|
|
.catch((error) => {
|
|
if (error.name === 'NotAllowedError') {
|
|
playMusicAfterUserInteraction();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
stopMusic() {
|
|
if (this.music !== null) {
|
|
this.music.pause();
|
|
this.music.currentTime = 0;
|
|
window.removeEventListener('click', this.handleUserInteraction);
|
|
window.removeEventListener('auxclick', this.handleUserInteraction);
|
|
window.removeEventListener('keydown', this.handleUserInteraction);
|
|
window.removeEventListener('touchstart', this.handleUserInteraction);
|
|
}
|
|
}
|
|
|
|
shouldPlayMusic() {
|
|
const {
|
|
timer,
|
|
isMusicActive,
|
|
} = this.props;
|
|
|
|
const {
|
|
running,
|
|
stopwatch,
|
|
} = timer;
|
|
const validMusic = this.music != null;
|
|
if (!validMusic) return false;
|
|
|
|
const musicIsPlaying = !this.music.paused;
|
|
|
|
return !musicIsPlaying && isMusicActive && running && !stopwatch;
|
|
}
|
|
|
|
shouldStopMusic(time) {
|
|
const {
|
|
timer,
|
|
isTimerActive,
|
|
isMusicActive,
|
|
currentTrack,
|
|
} = this.props;
|
|
|
|
const {
|
|
running,
|
|
stopwatch,
|
|
} = timer;
|
|
|
|
const zero = time === 0;
|
|
const validMusic = this.music != null;
|
|
const musicIsPlaying = validMusic && !this.music.paused;
|
|
const trackChanged = this.music?.track !== currentTrack;
|
|
|
|
const reachedZeroOrStopped = (running && zero) || (!running);
|
|
|
|
return musicIsPlaying
|
|
&& (!isMusicActive || stopwatch || reachedZeroOrStopped || !isTimerActive || trackChanged);
|
|
}
|
|
|
|
shouldNotifyTimerEnded(time) {
|
|
const { timer } = this.props;
|
|
const {
|
|
running,
|
|
stopwatch,
|
|
} = timer;
|
|
|
|
if (stopwatch || !running) return false;
|
|
|
|
const reachedZero = time === 0;
|
|
|
|
if (reachedZero && !this.alreadyNotified) {
|
|
this.alreadyNotified = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
updateTime() {
|
|
const { current } = this.timeRef;
|
|
if (current) {
|
|
current.textContent = this.getTime();
|
|
this.updateTabTitleTimer(false, current.textContent);
|
|
}
|
|
}
|
|
|
|
updateTabTitleTimer(deactivation, timeString) {
|
|
if (!TAB_TIMER_INDICATOR) return;
|
|
|
|
const matchTimerString = /\[[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\]/g;
|
|
|
|
if (deactivation) {
|
|
document.title = document.title.replace(matchTimerString, '');
|
|
} else {
|
|
if (RegExp(matchTimerString).test(document.title)) {
|
|
document.title = document.title.replace(matchTimerString, '');
|
|
document.title = '[' + timeString + '] ' + document.title;
|
|
} else {
|
|
document.title = '[' + timeString + '] ' + document.title;
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { isTimerActive } = this.props;
|
|
const time = this.getTime();
|
|
if (!isTimerActive) return null;
|
|
|
|
const {
|
|
isModerator,
|
|
timer,
|
|
sidebarNavigationIsOpen,
|
|
sidebarContentIsOpen,
|
|
currentTrack,
|
|
intl,
|
|
} = this.props;
|
|
const { stopwatch, running } = timer;
|
|
const trackChanged = this.music?.track !== currentTrack;
|
|
if (trackChanged) {
|
|
this.setUpMusic();
|
|
}
|
|
|
|
const onClick = running ? TimerService.stopTimer : TimerService.startTimer;
|
|
this.updateTabTitleTimer(false, time);
|
|
|
|
const title = () => {
|
|
if (isModerator) {
|
|
if (stopwatch && !running) {
|
|
return intl.formatMessage(intlMessages.toolTipStopwatchStoppedMod);
|
|
} if (stopwatch && running) {
|
|
return intl.formatMessage(intlMessages.toolTipStopwatchRunningMod);
|
|
} if (!stopwatch) {
|
|
if (running) {
|
|
return intl.formatMessage(intlMessages.toolTipTimerRunningMod);
|
|
} if (!running) {
|
|
return intl.formatMessage(intlMessages.toolTipTimerStoppedMod);
|
|
}
|
|
}
|
|
}
|
|
if (stopwatch && !running) {
|
|
return intl.formatMessage(intlMessages.toolTipStopwatchStopped);
|
|
} if (stopwatch && running) {
|
|
return intl.formatMessage(intlMessages.toolTipStopwatchRunning);
|
|
} if (!stopwatch) {
|
|
if (running) {
|
|
return intl.formatMessage(intlMessages.toolTipTimerRunning);
|
|
} if (!running) {
|
|
return intl.formatMessage(intlMessages.toolTipTimerStopped);
|
|
}
|
|
}
|
|
};
|
|
return (
|
|
<Styled.TimerWrapper>
|
|
<Tooltip
|
|
title={title}
|
|
>
|
|
<Styled.Timer>
|
|
<Styled.TimerButton
|
|
running={running}
|
|
disabled={!isModerator}
|
|
hide={sidebarNavigationIsOpen && sidebarContentIsOpen}
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={isModerator ? onClick : null}
|
|
>
|
|
<Styled.TimerContent>
|
|
<Styled.TimerIcon>
|
|
<Icon iconName="time" />
|
|
</Styled.TimerIcon>
|
|
<Styled.TimerTime
|
|
aria-hidden
|
|
ref={this.timeRef}
|
|
>
|
|
{time}
|
|
</Styled.TimerTime>
|
|
</Styled.TimerContent>
|
|
</Styled.TimerButton>
|
|
</Styled.Timer>
|
|
</Tooltip>
|
|
</Styled.TimerWrapper>
|
|
);
|
|
}
|
|
}
|
|
|
|
Indicator.propTypes = propTypes;
|
|
|
|
export default injectIntl(Indicator);
|