Skip to content

Commit

Permalink
v1.2.0 Release
Browse files Browse the repository at this point in the history
  • Loading branch information
tohjustin authored Jan 22, 2017
2 parents f3a1842 + 62a4081 commit 3ccb122
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 439 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pomodoro-timer-pwa",
"version": "1.1.0",
"version": "1.2.0",
"description": "Pomodoro Timer as a Progressive Web App (PWA)",
"author": "Justin Toh <tohjustin@hotmail.com>",
"private": true,
Expand Down
155 changes: 69 additions & 86 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
<mu-raised-button label="RESET" :color="primaryButton.bgColor" class="resetButton" v-on:click="resetTimer"/>
<mu-raised-button :label="primaryButton.text" :backgroundColor="primaryButton.bgColor" class="primaryButton" v-on:click="primaryButton.callbackFn"/>
</div>
<audio class="audio" ref="audio" src="/static/alarm.mp3" preload="auto" type="audio/mpeg"></audio>
</div>
<transition name="slide">
<div class="SettingsView" v-show="showSettingsView">
<settings ref="settings" :workDuration="workDuration" :breakDuration="breakDuration" :allowMelody="allowMelody" :allowVibration="allowVibration" v-on:change="switchToMainView"/>
<settings ref="settings" :workDuration="workDuration" :breakDuration="breakDuration" :allowNotification="allowNotification" v-on:change="switchToMainView"/>
</div>
</transition>
</div>
Expand All @@ -31,20 +30,39 @@ Vue.use(MuseUI)
import * as _ from 'lodash'
import radialBar from './components/radialBar'
import settings from './components/SettingsView'
import notificationHelper from './js/notificationHelper'
// Helper Object to manage STATE
// helper object to manage our app's state
const STATE = {
WORK_START: 'WORK_START_STATE',
WORK: 'WORK_STATE',
WORK_PAUSED: 'WORK_PAUSED_STATE',
BREAK_START: 'BREAK_START_STATE',
BREAK: 'BREAK_STATE',
BREAK_PAUSE: 'BREAK_PAUSE_STATE',
GET_MODE: (inputState) => { return (inputState === STATE.WORK_START || inputState === STATE.WORK || inputState === STATE.WORK_PAUSED) ? 'WORK' : 'BREAK' },
START: (inputState) => { return (STATE.GET_MODE(inputState) === 'WORK') ? STATE.WORK : STATE.BREAK },
PAUSE: (inputState) => { return (STATE.GET_MODE(inputState) === 'WORK') ? STATE.WORK_PAUSED : STATE.BREAK_PAUSED },
RESET: (inputState) => { return (STATE.GET_MODE(inputState) === 'WORK') ? STATE.WORK_START : STATE.BREAK_START },
SWITCH: (inputState) => { return (STATE.GET_MODE(inputState) === 'BREAK') ? STATE.WORK_START : STATE.BREAK_START }
WORK_START: { mode: 'work', status: 'init' },
WORK: { mode: 'work', status: 'active' },
WORK_PAUSED: { mode: 'work', status: 'inactive' },
BREAK_START: { mode: 'break', status: 'init' },
BREAK: { mode: 'break', status: 'active' },
BREAK_PAUSED: { mode: 'break', status: 'inactive' },
GET_MODE: (inputState) => { return inputState.mode },
GET_STATUS: (inputState) => { return inputState.status },
START: (inputState) => {
let temp = _.clone(inputState)
temp.status = 'active'
return temp
},
PAUSE: (inputState) => {
let temp = _.clone(inputState)
temp.status = 'inactive'
return temp
},
RESET: (inputState) => {
let temp = _.clone(inputState)
temp.status = 'init'
return temp
},
SWITCH: (inputState) => {
let temp = _.clone(inputState)
temp.mode = (temp.mode === 'break') ? 'work' : 'break'
temp.status = 'init'
return temp
}
}
export default {
Expand All @@ -62,78 +80,73 @@ export default {
// app Settings
workDuration: 1500,
breakDuration: 300,
allowMelody: true,
allowVibration: true,
// Worker objects for our timer
allowNotification: true && !notificationHelper.isBlocked(),
// worker objects for our timer
timerWorker: null,
alarmWorker: null,
// Data used for responsiveness
radialBarSize: 300
// data used for responsiveness
radialBarSize: 300,
// variable to control timer speed
clockDelay: 1000
}
},
computed: {
overlayText: function () {
return _.chain(this.timeRemaining / 60).floor().padStart(2, '0') + ':' + _.chain(this.timeRemaining % 60).padStart(2, '0')
},
fractionOfTimeLeft: function () {
if (this.state === STATE.WORK_START || this.state === STATE.WORK || this.state === STATE.WORK_PAUSED) {
return this.timeRemaining / this.workDuration
} else {
return this.timeRemaining / this.breakDuration
}
return (this.state.mode === 'work') ? (this.timeRemaining / this.workDuration) : (this.timeRemaining / this.breakDuration)
},
// Computes the data of the primaryButton at any given [state]
// computes the data of the primaryButton at any given [state]
primaryButton: function () {
switch (this.state) {
case STATE.WORK_START:
return { text: 'START WORKING', bgColor: '#2196F3', callbackFn: () => { this.startTimer() } }
case STATE.WORK:
return { text: 'STOP WORKING', bgColor: '#2196F3', callbackFn: () => { this.pauseTimer() } }
case STATE.WORK_PAUSED:
return { text: 'RESUME WORKING', bgColor: '#2196F3', callbackFn: () => { this.startTimer() } }
case STATE.BREAK_START:
return { text: 'START MY BREAK', bgColor: '#7CB342', callbackFn: () => { this.startTimer() } }
case STATE.BREAK:
return { text: 'STOP MY BREAK', bgColor: '#7CB342', callbackFn: () => { this.pauseTimer() } }
case STATE.BREAK_PAUSED:
return { text: 'RESUME MY BREAK', bgColor: '#7CB342', callbackFn: () => { this.startTimer() } }
default:
return { text: 'ERROR', bgColor: '#FFFFFF', callbackFn: () => {} } // Error
let primaryBtn = {}
switch (this.state.status) {
case 'init': primaryBtn.text = 'START'; break
case 'active': primaryBtn.text = 'STOP'; break
case 'inactive': primaryBtn.text = 'RESUME'; break
}
primaryBtn.text += (this.state.mode === 'work') ? ' WORKING' : ' MY BREAK'
primaryBtn.bgColor = (this.state.mode === 'work') ? '#2196F3' : '#7CB342'
primaryBtn.callbackFn = (this.state.status === 'active') ? () => { this.pauseTimer() } : () => { this.startTimer() }
return primaryBtn
}
},
methods: {
startTimer: function () {
// Workaround to get browsers to play audio on mobile devices (requires user interaction to download sound file)
this._preloadAudio()
this.state = STATE.START(this.state)
this._stopAlarm()
if (this.timerWorker === null) {
this.timerWorker = setInterval(() => {
this.timeRemaining--
if (this.timeRemaining <= 0) {
this.triggerAlarm()
}
}, 1000)
}, this.clockDelay)
}
},
pauseTimer: function () {
this._clearWorker()
this._clearTimeWorker()
this.state = STATE.PAUSE(this.state)
},
resetTimer: function () {
this._clearWorker()
this._clearTimeWorker()
this.state = STATE.RESET(this.state)
this.timeRemaining = (STATE.GET_MODE(this.state) === 'WORK') ? this.workDuration : this.breakDuration
this._stopAlarm()
this.timeRemaining = (this.state.mode === 'work') ? this.workDuration : this.breakDuration
},
triggerAlarm: function () {
this._clearWorker()
this._clearTimeWorker()
this.state = STATE.SWITCH(this.state)
this.timeRemaining = (STATE.GET_MODE(this.state) === 'WORK') ? this.workDuration : this.breakDuration
this._ringAlarm()
this.timeRemaining = (this.state.mode === 'work') ? this.workDuration : this.breakDuration
var startBreakMessage = { title: 'Stop working already!', body: 'Time to start your break...' }
var backToWorkMessage = { title: 'Your break is over!', body: 'Time to get back to work...' }
if (this.allowNotification === true) {
notificationHelper.requestPermission()
notificationHelper.sendNotification(
(this.state.mode === 'work') ? backToWorkMessage.title : startBreakMessage.title,
(this.state.mode === 'work') ? backToWorkMessage.body : startBreakMessage.body
)
}
},
switchToSettingsView: function () {
this.pauseTimer()
Expand All @@ -142,48 +155,21 @@ export default {
switchToMainView: function (event) {
this.workDuration = event.workDuration
this.breakDuration = event.breakDuration
this.allowMelody = event.allowMelody
this.allowVibration = event.allowVibration
this.allowNotification = event.allowNotification
this.resetTimer()
this.showSettingsView = false
},
// [PRIVATE] terminates the timerWorker
_clearWorker: function () {
_clearTimeWorker: function () {
clearInterval(this.timerWorker)
this.timerWorker = null
},
// [PRIVATE] helper method to access STATE object
_getStateHelper: function () {
return STATE
},
// [PRIVATE] triggers the alarmWorker (audio + vibration)
_ringAlarm: function () {
let ring = () => {
if (this.allowMelody) {
this.$refs.audio.pause()
this.$refs.audio.currentTime = 0
this.$refs.audio.play()
}
if (this.allowVibration && navigator.vibrate) {
navigator.vibrate(1500)
}
}
this.alarmWorker = setInterval(ring, 4000)
ring()
},
// [PRIVATE] terminates the alarmWorker (audio + vibration)
_stopAlarm: function () {
clearInterval(this.alarmWorker)
this.alarmWorker = null
this.$refs.audio.pause()
if (navigator.vibrate) {
navigator.vibrate(0)
}
},
// [PRIVATE] workaround to get browsers to play audio on mobile devices (requires user interaction to download sound file)
_preloadAudio () {
this.$refs.audio.play()
this.$refs.audio.pause()
// [PRIVATE] sends browser notification
_sendNotification: function (msg) {
},
// [PRIVATE] callback for WINDOW_RESIZE event, resizes the radialBar component for the new viewport dimension
_handleResize (event) {
Expand All @@ -195,9 +181,6 @@ export default {
}
},
created: function () {
// enable vibration support
navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate
// bind event handlers to the `_handleResize` method
window.addEventListener('resize', this._handleResize)
this._handleResize()
Expand Down
51 changes: 29 additions & 22 deletions src/components/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
<mu-slider :step="60" :min="60" :max="3600" v-model="myBreakDuration"/>
</mu-list-item>
<mu-divider/>
<mu-list-item title="Play Alarm Melody">
<mu-switch slot="right" v-model="myAllowMelody"/>
</mu-list-item>
<mu-divider/>
<mu-list-item title="Vibrate on alarm" :describeText="vibrationSupported ? 'Your browser supports vibration' : 'Your browser doesn\'t support vibration'">
<mu-switch slot="right" v-model="myAllowVibration" :disabled="!vibrationSupported"/>
<mu-list-item title="Push Notifications" :describeText="notificationSettingDescription">
<mu-switch slot="right" v-model="myAllowNotification" :disabled="disableNotificationSetting"/>
</mu-list-item>
<mu-divider/>
</mu-list>
Expand All @@ -33,30 +29,46 @@ import MuseUI from 'muse-ui'
import 'muse-ui/dist/muse-ui.css'
Vue.use(MuseUI)
import notificationHelper from '../js/notificationHelper'
export default {
name: 'settings',
props: {
workDuration: { type: Number, required: true },
breakDuration: { type: Number, required: true },
allowMelody: { type: Boolean, required: true },
allowVibration: { type: Boolean, required: true }
allowNotification: { type: Boolean, required: true }
},
data () {
return {
myWorkDuration: this.workDuration,
myBreakDuration: this.breakDuration,
myAllowMelody: this.allowMelody,
myAllowVibration: this.allowVibration,
vibrationSupported: false
myAllowNotification: this.allowNotification
}
},
computed: {
disableNotificationSetting: function () {
return !notificationHelper.supported() || notificationHelper.isBlocked()
},
notificationSettingDescription: function () {
if (!notificationHelper.supported()) {
return 'Your browser does not support notifications'
} else if (notificationHelper.isBlocked()) {
return 'Please unblock notifications via your browser settings'
} else {
if (this.myAllowNotification === true) {
return 'You will be notified when timer reaches zero'
} else if (this.myAllowNotification === false) {
return 'You will NOT be notified when timer reaches zero'
}
}
}
},
methods: {
backToAppView () {
let settings = {
var settings = {
workDuration: this.myWorkDuration,
breakDuration: this.myBreakDuration,
allowMelody: this.myAllowMelody,
allowVibration: this.myAllowVibration
allowNotification: this.myAllowNotification
}
this.$emit('change', settings)
}
Expand All @@ -65,14 +77,6 @@ export default {
toMinutes: function (value, text) {
return `${text} (${value / 60} min)`
}
},
created: function () {
// Check if browser supports HTML5 Vibration API
navigator.vibrate = navigator.vibrate || navigator.webkitVibrate || navigator.mozVibrate || navigator.msVibrate
let vibrationSupported = ('vibrate' in navigator && navigator.vibrate !== undefined)
this.vibrationSupported = vibrationSupported
this.myAllowVibration = this.myAllowVibration && vibrationSupported
}
}
</script>
Expand Down Expand Up @@ -102,6 +106,9 @@ export default {
padding-top: 12px !important;
padding-bottom: 12px !important;
}
.mu-item-text {
word-break: normal;
}
.mu-slider {
margin-bottom: 0px !important;
}
Expand Down
46 changes: 46 additions & 0 deletions src/js/notificationHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Configuration Data
var vibratePattern = [100, 50, 100, 50, 100]
var notificationTag = 'pomodori-notification'
var notificationIcon = '/static/icons/icon-192x192.png'
var renotifyOption = 'true'

var _supported = function () { return ('Notification' in window && window.Notification !== undefined) }
var _isBlocked = function () { return ('Notification' in window && window.Notification.permission === 'denied') }
var _isEnabled = function () { return ('Notification' in window && window.Notification.permission === 'granted') }

exports.supported = function () { return _supported() }
exports.isBlocked = function () { return _isBlocked() }
exports.isEnabled = function () { return _isEnabled() }

exports.requestPermission = function () {
if (_supported() && window.Notification.permission === 'default') {
window.Notification.requestPermission()
}
}

exports.sendNotification = function (msgTitle, msgBody) {
if (_supported() && window.Notification.permission === 'granted') {
// send notification via service workers (for browsers w/ service worker installed)
if (navigator.serviceWorker.controller !== null) {
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(msgTitle, {
body: msgBody,
icon: notificationIcon,
vibrate: vibratePattern,
tag: notificationTag,
renotify: renotifyOption
})
})
} else {
new window.Notification(msgTitle, { // eslint-disable-line no-new
'body': msgBody,
'icon': notificationIcon,
'vibrate': vibratePattern,
'tag': notificationTag,
'renotify': renotifyOption
})
}
return true
}
return false
}
Loading

0 comments on commit 3ccb122

Please sign in to comment.