mirror of
https://github.com/GuerillaStudio/souvenir.git
synced 2024-11-08 11:11:53 +00:00
refactor(app state): reduce global state size
This commit is contained in:
parent
4e5a57c72c
commit
c361e6b8ac
9 changed files with 145 additions and 174 deletions
10
src/App.vue
10
src/App.vue
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
134
src/store.js
134
src/store.js
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue