mirror of
https://github.com/GuerillaStudio/souvenir.git
synced 2025-01-20 22:10:20 +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>
|
</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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
134
src/store.js
134
src/store.js
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue