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>
<script>
import { mapState } from 'vuex'
export default {
name: 'souvenir',
computed: {
...mapState([
'welcomed',
'downloading'
])
},
methods: {
handleVisibilityChange (event) {
if (document.hidden) {
this.$store.commit('stopCamera')
this.$store.commit('updateCamera', null)
}
}
},

View file

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

View file

@ -61,8 +61,11 @@ export function encode ({ imageDataList, imageWidth, imageHeight, delayTime }) {
case 'done':
const byteArray = new Uint8Array(payload.buffer)
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
}
}

View file

@ -2,145 +2,55 @@ import Vue from 'vue'
import Vuex from 'vuex'
import { getCamera } from '/services/camera.js'
import { capture } from '/services/capture.js'
import { encode } from '/services/encode.js'
Vue.use(Vuex)
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
state: {
welcomed: false,
mediaStream: null,
facingMode: null,
cameraShouldFaceUser: true,
timer: {
selected: 2,
list: [2, 3, 5]
},
capturing: {
status: false,
shouldFaceUser: true,
state: 0
},
encoding: {
status: false,
state: 0
},
downloading: {
status: false,
objectUrl: null,
timestamp: null
}
camera: null,
capture: null,
gif: null
},
mutations: {
updateWelcomed (state, welcome) {
state.welcomed = welcome
},
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
updateCameraShouldFaceUser (state, cameraShouldFaceUser) {
state.cameraShouldFaceUser = cameraShouldFaceUser
},
updateTimer (state, time) {
state.timer.selected = time
},
startCapture (state) {
state.capturing.status = true
},
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)
updateCamera (state, camera) {
if (state.camera) {
state.camera.mediaStream.getTracks().forEach(track => track.stop())
}
state.downloading.status = false
state.downloading.objectUrl = null
state.downloading.timestamp = null
state.camera = camera
},
updateCapture (state, capture) {
state.capture = capture
},
updateGif (state, gif) {
state.gif = gif
}
},
actions: {
async requestCamera ({ state, commit }, inverseFacingMode) {
commit('stopCamera')
commit('updateCamera', null)
const shouldFaceUser = inverseFacingMode
? !state.capturing.shouldFaceUser
: state.capturing.shouldFaceUser
? !state.cameraShouldFaceUser
: state.cameraShouldFaceUser
commit('updateCamera', await getCamera(shouldFaceUser))
commit('startCamera', await getCamera(shouldFaceUser))
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">
<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">
{{ timeLabel(time) }}
</option>
</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>
</template>
@ -21,8 +21,7 @@ export default {
},
computed: {
...mapState([
'timer',
'encoding'
'timer'
])
},
methods: {

View file

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

View file

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

View file

@ -1,17 +1,17 @@
<template lang="html">
<div class="capture">
<div v-if="capturing.status" class="capture-progress">
<capture-progress></capture-progress>
<div v-if="capturing" class="capture-progress">
<capture-progress :value="capturingProgress"></capture-progress>
</div>
<capture-options v-else></capture-options>
<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>
<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>
</template>
@ -19,6 +19,8 @@
import captureOptions from '/views/components/capture-options'
import captureProgress from '/views/components/capture-progress'
import encodingOverlay from '/views/components/encoding'
import { capture } from '/services/capture.js'
import { encode } from '/services/encode.js'
import 'objectFitPolyfill'
@ -31,26 +33,81 @@ export default {
captureProgress,
encodingOverlay
},
data: () => ({
capturing: false,
capturingProgress: 0,
encoding: false,
encodingProgress: 0
}),
computed: {
...mapState([
'mediaStream',
'facingMode',
'capturing',
'camera',
'timer',
'encoding'
'capture'
]),
flipActive () {
return this.facingMode === 'user' || this.facingMode === 'unknow'
shouldFlip () {
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: {
async startCapture () {
const captureData = await this.$store.dispatch('capture')
await this.$store.dispatch('encode', captureData)
this.$router.push({ name: 'download' })
startCapturing () {
this.capturing = true
const capturing = capture(this.camera, this.timer.selected * 1000)
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 () {
if (!this.mediaStream) {
startEncoding () {
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 {
await this.$store.dispatch('requestCamera', false)
} catch (error) {
@ -60,17 +117,18 @@ export default {
}
}
},
updatePreviewMediaStream () {
const mediaStream = this.camera ? this.camera.mediaStream : null
this.$refs.preview.srcObject = mediaStream
},
handleVisibilityChange (event) {
if (!document.hidden) {
this.ensureCameraStarted()
this.ensureCamera()
}
},
updatePreviewMediaStream () {
this.$refs.preview.srcObject = this.mediaStream
}
},
watch: {
mediaStream: function (mediaStream) {
camera: function () {
this.updatePreviewMediaStream()
}
},
@ -80,10 +138,7 @@ export default {
document.body.classList.add('capture-body')
window.objectFitPolyfill(this.$refs.preview)
this.ensureCameraStarted()
},
updated: function () {
this.updatePreviewMediaStream()
this.ensureCamera()
},
destroyed: function () {
document.body.classList.remove('capture-body')

View file

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