refactor(app state): reduce global state size

This commit is contained in:
wryk 2019-04-05 17:46:36 +02:00 committed by Tixie
parent 4e5a57c72c
commit c361e6b8ac
9 changed files with 145 additions and 174 deletions

View file

@ -5,20 +5,12 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'
export default { export default {
name: 'souvenir', name: 'souvenir',
computed: {
...mapState([
'welcomed',
'downloading'
])
},
methods: { methods: {
handleVisibilityChange (event) { handleVisibilityChange (event) {
if (document.hidden) { if (document.hidden) {
this.$store.commit('stopCamera') this.$store.commit('updateCamera', null)
} }
} }
}, },

View file

@ -8,7 +8,7 @@ import {
GIF_FRAME_RATE GIF_FRAME_RATE
} from '/constants.js' } from '/constants.js'
export function capture (mediaStream, duration, facingMode) { export function capture ({ mediaStream, facingMode }, duration) {
const emitter = new EventEmitter() const emitter = new EventEmitter()
Promise.resolve().then(async () => { Promise.resolve().then(async () => {

View file

@ -61,8 +61,11 @@ export function encode ({ imageDataList, imageWidth, imageHeight, delayTime }) {
case 'done': case 'done':
const byteArray = new Uint8Array(payload.buffer) const byteArray = new Uint8Array(payload.buffer)
const blob = new Blob([byteArray], { type: 'image/gif' }) const blob = new Blob([byteArray], { type: 'image/gif' })
const objectUrl = URL.createObjectURL(blob)
emitter.emit('done', objectUrl) emitter.emit('done', {
blob,
createdAt: new Date()
})
break break
} }
} }

View file

@ -2,145 +2,55 @@ import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { getCamera } from '/services/camera.js' import { getCamera } from '/services/camera.js'
import { capture } from '/services/capture.js'
import { encode } from '/services/encode.js'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
state: { state: {
welcomed: false, cameraShouldFaceUser: true,
mediaStream: null,
facingMode: null,
timer: { timer: {
selected: 2, selected: 2,
list: [2, 3, 5] list: [2, 3, 5]
}, },
capturing: { camera: null,
status: false, capture: null,
shouldFaceUser: true, gif: null
state: 0
},
encoding: {
status: false,
state: 0
},
downloading: {
status: false,
objectUrl: null,
timestamp: null
}
}, },
mutations: { mutations: {
updateWelcomed (state, welcome) { updateCameraShouldFaceUser (state, cameraShouldFaceUser) {
state.welcomed = welcome state.cameraShouldFaceUser = cameraShouldFaceUser
},
startCamera (state, { mediaStream, facingMode }) {
state.mediaStream = mediaStream
state.facingMode = facingMode
},
stopCamera (state) {
if (state.mediaStream) {
state.mediaStream.getTracks().forEach(track => track.stop())
}
state.mediaStream = null
state.facingMode = null
},
updateFacingMode (state, facingMode) {
state.facingMode = facingMode
},
inverseFacingMode (state) {
state.capturing.shouldFaceUser = !state.capturing.shouldFaceUser
}, },
updateTimer (state, time) { updateTimer (state, time) {
state.timer.selected = time state.timer.selected = time
}, },
startCapture (state) { updateCamera (state, camera) {
state.capturing.status = true if (state.camera) {
}, state.camera.mediaStream.getTracks().forEach(track => track.stop())
stopCapture (state) {
state.capturing.status = false
},
updateCaptureState (state, percent) {
state.capturing.state = percent
},
startEncoding (state) {
state.encoding.status = true
},
stopEncoding (state) {
state.encoding.status = false
},
updateEncodingState (state, percent) {
state.encoding.state = percent
},
startDownloading (state, objectUrl) {
state.downloading.status = true
state.downloading.objectUrl = objectUrl
state.downloading.timestamp = Date.now()
},
stopDownloading (state) {
if (state.downloading.objectUrl) {
URL.revokeObjectURL(state.downloading.objectUrl)
} }
state.downloading.status = false state.camera = camera
state.downloading.objectUrl = null },
state.downloading.timestamp = null updateCapture (state, capture) {
state.capture = capture
},
updateGif (state, gif) {
state.gif = gif
} }
}, },
actions: { actions: {
async requestCamera ({ state, commit }, inverseFacingMode) { async requestCamera ({ state, commit }, inverseFacingMode) {
commit('stopCamera') commit('updateCamera', null)
const shouldFaceUser = inverseFacingMode const shouldFaceUser = inverseFacingMode
? !state.capturing.shouldFaceUser ? !state.cameraShouldFaceUser
: state.capturing.shouldFaceUser : state.cameraShouldFaceUser
commit('updateCamera', await getCamera(shouldFaceUser))
commit('startCamera', await getCamera(shouldFaceUser))
if (inverseFacingMode) { if (inverseFacingMode) {
commit('inverseFacingMode') commit('updateCameraShouldFaceUser', shouldFaceUser)
} }
},
capture ({ state, commit, dispatch }) {
return new Promise((resolve, reject) => {
commit('startCapture')
const capturing = capture(state.mediaStream, state.timer.selected * 1000, state.facingMode)
capturing.once('error', error => {
console.error(error)
reject(error)
})
capturing.on('progress', value => commit('updateCaptureState', Math.round(value * 100)))
capturing.once('done', captureData => {
commit('stopCapture')
commit('updateCaptureState', 0)
resolve(captureData)
})
})
},
encode ({ commit }, captureData) {
return new Promise((resolve, reject) => {
commit('startEncoding')
const encoding = encode(captureData)
encoding.once('error', error => {
console.error(error)
reject(error)
})
encoding.on('progress', value => commit('updateEncodingState', Math.round(value * 100)))
encoding.once('done', objectUrl => {
commit('stopEncoding')
commit('updateEncodingState', 0)
commit('startDownloading', objectUrl)
resolve(objectUrl)
})
})
} }
} }
}) })

View file

@ -1,11 +1,11 @@
<template lang="html"> <template lang="html">
<div class="options"> <div class="options">
<select v-model="timer.selected" class="options__select" :disabled="encoding.status" @change="updateTimer(timer.selected)"> <select v-model="timer.selected" class="options__select" @change="updateTimer(timer.selected)">
<option v-for="time in timer.list" :key="time" :value="time"> <option v-for="time in timer.list" :key="time" :value="time">
{{ timeLabel(time) }} {{ timeLabel(time) }}
</option> </option>
</select> </select>
<button class="options__btn" :disabled="encoding.status" @click="switchCamera"><icon-switch></icon-switch>switch</button> <button class="options__btn" @click="switchCamera"><icon-switch></icon-switch>switch</button>
</div> </div>
</template> </template>
@ -21,8 +21,7 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'timer', 'timer'
'encoding'
]) ])
}, },
methods: { methods: {

View file

@ -1,18 +1,19 @@
<template lang="html"> <template lang="html">
<div class="progressBar"> <div class="progressBar">
<div class="progressBar__state" :style="'width: ' + capturing.state + '%;'"></div> <div class="progressBar__state" :style="'width: ' + percentage + '%;'"></div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'
export default { export default {
name: 'progressBar', name: 'progressBar',
props: {
value: Number
},
computed: { computed: {
...mapState([ percentage () {
'capturing' return this.value * 100
]) }
} }
} }
</script> </script>

View file

@ -5,21 +5,25 @@
</div> </div>
<div class="encoding-label">Encoding</div> <div class="encoding-label">Encoding</div>
<div class="encoding-progressBar"> <div class="encoding-progressBar">
<div class="encoding-progressBar__state" :style="'width: ' + encoding.state + '%;'"></div> <div class="encoding-progressBar__state" :style="'width: ' + percentage + '%;'"></div>
</div> </div>
<div class="encoding-percent">{{ encoding.state }}%</div> <div class="encoding-percent">{{ roundedPercentage }}%</div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'
export default { export default {
name: 'encodingOverlay', name: 'encodingOverlay',
props: {
value: Number
},
computed: { computed: {
...mapState([ percentage () {
'encoding' return this.value * 100
]) },
roundedPercentage () {
return Math.round(this.percentage)
}
} }
} }
</script> </script>

View file

@ -1,17 +1,17 @@
<template lang="html"> <template lang="html">
<div class="capture"> <div class="capture">
<div v-if="capturing.status" class="capture-progress"> <div v-if="capturing" class="capture-progress">
<capture-progress></capture-progress> <capture-progress :value="capturingProgress"></capture-progress>
</div> </div>
<capture-options v-else></capture-options> <capture-options v-else></capture-options>
<div class="preview"> <div class="preview">
<video ref="preview" class="preview-visual" :class="{ 'preview--flip': flipActive }" preload="yes" autoplay muted playsinline webkit-playsinline></video> <video ref="preview" class="preview-visual" :class="{ 'preview--flip': shouldFlip }" preload="yes" autoplay muted playsinline webkit-playsinline></video>
</div> </div>
<button class="capture-btn" :class="{ 'capture-btn--capturing': capturing.status }" :disabled="!mediaStream" @click.prevent="startCapture">Capture</button> <button class="capture-btn" :class="{ 'capture-btn--capturing': capturing }" :disabled="!camera || encoding" @click.prevent="startCapturing">Capture</button>
<encoding-overlay v-if="encoding.status"></encoding-overlay> <encoding-overlay v-if="encoding" :value="encodingProgress"></encoding-overlay>
</div> </div>
</template> </template>
@ -19,6 +19,8 @@
import captureOptions from '/views/components/capture-options' import captureOptions from '/views/components/capture-options'
import captureProgress from '/views/components/capture-progress' import captureProgress from '/views/components/capture-progress'
import encodingOverlay from '/views/components/encoding' import encodingOverlay from '/views/components/encoding'
import { capture } from '/services/capture.js'
import { encode } from '/services/encode.js'
import 'objectFitPolyfill' import 'objectFitPolyfill'
@ -31,26 +33,81 @@ export default {
captureProgress, captureProgress,
encodingOverlay encodingOverlay
}, },
data: () => ({
capturing: false,
capturingProgress: 0,
encoding: false,
encodingProgress: 0
}),
computed: { computed: {
...mapState([ ...mapState([
'mediaStream', 'camera',
'facingMode',
'capturing',
'timer', 'timer',
'encoding' 'capture'
]), ]),
flipActive () { shouldFlip () {
return this.facingMode === 'user' || this.facingMode === 'unknow' if (this.camera) {
switch (this.camera.facingMode) {
default:
throw new Error('Unhandled case')
case 'user':
case 'unknow':
return true
case 'environment':
return false
}
} else {
return false
}
} }
}, },
methods: { methods: {
async startCapture () { startCapturing () {
const captureData = await this.$store.dispatch('capture') this.capturing = true
await this.$store.dispatch('encode', captureData) const capturing = capture(this.camera, this.timer.selected * 1000)
this.$router.push({ name: 'download' })
capturing.once('error', error => {
console.error(error)
this.capturing = false
this.capturingProgress = 0
})
capturing.on('progress', value => {
this.capturingProgress = value
})
capturing.once('done', captureData => {
this.capturing = false
this.capturingProgress = 0
this.$store.commit('updateCapture', captureData)
this.startEncoding()
})
}, },
async ensureCameraStarted () { startEncoding () {
if (!this.mediaStream) { this.encoding = true
const encoding = encode(this.capture)
encoding.once('error', error => {
console.error(error)
this.encoding = false
this.encodingProgress = 0
})
encoding.on('progress', value => {
this.encodingProgress = value
})
encoding.once('done', gif => {
this.encoding = false
this.encodingProgress = 0
this.$store.commit('updateGif', gif)
this.$router.push({ name: 'download' })
})
},
async ensureCamera () {
if (!this.camera) {
try { try {
await this.$store.dispatch('requestCamera', false) await this.$store.dispatch('requestCamera', false)
} catch (error) { } catch (error) {
@ -60,17 +117,18 @@ export default {
} }
} }
}, },
updatePreviewMediaStream () {
const mediaStream = this.camera ? this.camera.mediaStream : null
this.$refs.preview.srcObject = mediaStream
},
handleVisibilityChange (event) { handleVisibilityChange (event) {
if (!document.hidden) { if (!document.hidden) {
this.ensureCameraStarted() this.ensureCamera()
} }
},
updatePreviewMediaStream () {
this.$refs.preview.srcObject = this.mediaStream
} }
}, },
watch: { watch: {
mediaStream: function (mediaStream) { camera: function () {
this.updatePreviewMediaStream() this.updatePreviewMediaStream()
} }
}, },
@ -80,10 +138,7 @@ export default {
document.body.classList.add('capture-body') document.body.classList.add('capture-body')
window.objectFitPolyfill(this.$refs.preview) window.objectFitPolyfill(this.$refs.preview)
this.ensureCameraStarted() this.ensureCamera()
},
updated: function () {
this.updatePreviewMediaStream()
}, },
destroyed: function () { destroyed: function () {
document.body.classList.remove('capture-body') document.body.classList.remove('capture-body')

View file

@ -6,10 +6,10 @@
</div> </div>
<div class="preview preview--novideo"> <div class="preview preview--novideo">
<img class="preview-visualImg" :src="downloading.objectUrl" alt=""> <img class="preview-visualImg" :src="objectUrl" alt="">
</div> </div>
<a class="download-btn btn btn--primary w100" :href="downloading.objectUrl" :download="`souvenir${downloading.timestamp}.gif`">Download GIF</a> <a class="download-btn btn btn--primary w100" :href="objectUrl" :download="`souvenir${timestamp}.gif`">Download GIF</a>
</div> </div>
</template> </template>
@ -18,24 +18,31 @@ import { mapState } from 'vuex'
export default { export default {
name: 'download', name: 'download',
data: () => ({
objectUrl: null
}),
computed: { computed: {
...mapState([ ...mapState([
'downloading' 'gif'
]) ]),
timestamp () {
return this.gif.createdAt.getTime()
}
}, },
methods: { methods: {
back () { back () {
this.$store.commit('stopDownloading')
this.$router.push({ name: 'capture' }) this.$router.push({ name: 'capture' })
} }
}, },
created () { created () {
if (this.downloading.objectUrl === null) { if (!this.gif) {
this.$router.push({ name: 'home' }) this.$router.push({ name: 'home' })
} }
this.objectUrl = URL.createObjectURL(this.gif.blob)
}, },
destroyed () { destroyed () {
this.$store.commit('stopDownloading') URL.revokeObjectURL(this.objectUrl)
} }
} }
</script> </script>