Refactor components

This commit is contained in:
Gennady Grishkovtsov
2018-08-09 22:14:12 +03:00
parent 2e1a0a93f9
commit cf55cf8853
7 changed files with 265 additions and 178 deletions

View File

@@ -1,65 +1,11 @@
<style lang="scss">
.ar-icon-button {
fill: #747474;
border-radius: 50%;
border: 1px solid #05CBCD;
background-color: #FFFFFF;
padding: 5px;
cursor: pointer;
transition: .2s;
&--rec {
fill: white;
background-color: #FF6B64;
border-color: transparent;
}
&--pulse {
animation: ripple .5s linear infinite;
@keyframes ripple {
0% {
box-shadow:
0 0 0 0 rgba(red, 0.1),
0 0 0 1px rgba(red, 0.1),
0 0 0 5px rgba(red, 0.1);
}
100% {
box-shadow:
0 0 0 0 rgba(red, 0.1),
0 0 0 10px rgba(red, 0.1),
0 0 0 20px rgba(red, 0);
}
}
}
&--clicked {
fill: white;
background-color: #05CBCD;
}
&__sm {
width: 30px;
height: 30px;
}
&__lg {
width: 45px;
height: 45px;
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
}
}
</style>
<template> <template>
<div :class="iconClasses" v-html="icons[name]"></div> <div v-html="icons[name]"></div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
name: { type: String }, name: { type: String }
size: { type: String, default: 'sm' }
}, },
data: function () { data: function () {
return { return {
@@ -73,11 +19,6 @@
volume: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/><path d="M0 0h24v24H0z" fill="none"/></svg>' volume: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'
} }
} }
},
computed: {
iconClasses () {
return ['ar-icon-button', `ar-icon-button__${this.size}`]
}
} }
} }
</script> </script>

View File

@@ -1,6 +1,9 @@
<style lang="scss"> <style lang="scss">
.ar-line-control { .ar-line-control {
position: relative; position: relative;
height: 8px;
border-radius: 5px;
background-color: #E6E6E6;
&__head { &__head {
position: absolute; position: absolute;
@@ -12,24 +15,28 @@
</style> </style>
<template> <template>
<div :ref="refId" class="ar-line-control" @mousedown="onMouseDown"> <div
<div class="ar-line-control__head" :style="{width: percentageWidth}"></div> :ref="refId"
class="ar-line-control"
@mousedown="onMouseDown">
<div class="ar-line-control__head" :style="calculateSize"></div>
</div> </div>
</template> </template>
<script> <script>
import { calculateLineHeadPosition } from '../lib/utils.js' import { calculateLineHeadPosition } from '@/lib/utils.js'
export default { export default {
props: { props: {
refId: { type: String }, refId : { type: String },
eventName: { type: String }, eventName : { type: String },
percentage: { type: Number, default: 0 } percentage : { type: Number, default: 0 },
rowDirection : { type: Boolean, default: true}
}, },
methods: { methods: {
onMouseDown (ev) { onMouseDown (ev) {
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('changeLineHead', seekPos) this.$emit('change-linehead', seekPos)
document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onMouseUp) document.addEventListener('mouseup', this.onMouseUp)
}, },
@@ -37,17 +44,17 @@
document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mouseup', this.onMouseUp)
document.removeEventListener('mousemove', this.onMouseMove) document.removeEventListener('mousemove', this.onMouseMove)
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('changeLineHead', seekPos) this.$emit('change-linehead', seekPos)
}, },
onMouseMove (ev) { onMouseMove (ev) {
let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId]) let seekPos = calculateLineHeadPosition(ev, this.$refs[this.refId])
this.$emit('changeLineHead', seekPos) this.$emit('change-linehead', seekPos)
} }
}, },
computed: { computed: {
percentageWidth () { calculateSize () {
let width = this.percentage < 1 ? this.percentage * 100 : this.percentage let value = this.percentage < 1 ? this.percentage * 100 : this.percentage
return `${width}%` return `${this.rowDirection ? 'width' : 'height'}: ${value}%`
} }
} }
} }

View File

@@ -1,14 +1,23 @@
<style lang="scss"> <style lang="scss">
.ar-player { .ar-player {
width: 100%; width: 380px;
height: 120px; height: 120px;
border: 1px solid #E8E8E8; border: 1px solid #E8E8E8;
border-radius: 24px; border-radius: 24px;
background-color: #FAFAFA;
display: flex; display: flex;
flex-direction: column; flex-direction: column-reverse;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #FAFAFA;
font-family: 'Roboto', sans-serif;
&-bar {
display: flex;
align-items: center;
height: 38px;
padding: 0 12px;
margin: 0 5px;
}
&-actions { &-actions {
width: 55%; width: 55%;
@@ -17,46 +26,35 @@
justify-content: space-around; justify-content: space-around;
} }
&-volume { &--compact {
display: flex; height: unset;
align-items: center; flex-direction: row;
line-height: 10px;
&-bar {
width: 50px;
height: 8px;
background: #E6E6E6;
border-radius: 4px;
position: relative;
}
&__icon {
fill: #747474;
width: 24px;
height: 24px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
padding: 0;
background-color: unset; background-color: unset;
margin-right: 3px;
& > .ar-player-actions {
width: unset;
& > #download,
& > #upload {
display: none;
} }
} }
&-bar { & > .ar-player-bar {
display: flex; border: 1px solid #E8E8E8;
align-items: center; border-radius: 24px;
margin-bottom: 2px; margin: 0 0 0 5px;
}
&--active { & > .ar-player__progress {
background-color: white; width: 125px;
}
}
} }
&__progress { &__progress {
width: 160px; width: 160px;
height: 8px;
border-radius: 5px;
background-color: #E6E6E6;
margin: 0 8px; margin: 0 8px;
} }
@@ -65,75 +63,93 @@
font-size: 16px; font-size: 16px;
width: 41px; width: 41px;
} }
&__play {
width: 45px;
height: 45px;
background-color: #FFFFFF;
box-shadow: 0 2px 11px 11px rgba(0,0,0,0.07);
&--active {
fill: white !important;
background-color: #05CBCD !important;
} }
}
}
@import '../scss/icons';
</style> </style>
<template> <template>
<div class="ar-player" :class="{'ar-player--active': record.url}"> <div class="ar-player" :class="{'ar-player--compact': compact}">
<div class="ar-player-actions">
<icon-button
id="download"
class="ar-icon ar-icon__sm"
name="download"
@click.native="download"/>
<icon-button
id="play"
class="ar-icon ar-icon__lg ar-player__play"
:name="playBtnIcon"
:class="{'ar-player__play--active': isPlaying}"
@click.native="playback"/>
<icon-button
id="upload"
class="ar-icon ar-icon__sm"
name="save"
@click.native="upload"/>
</div>
<div class="ar-player-bar"> <div class="ar-player-bar">
<div class="ar-player__time">{{playedTime}}</div> <div class="ar-player__time">{{playedTime}}</div>
<line-control <line-control
class="ar-player__progress" class="ar-player__progress"
ref-id="progress" ref-id="progress"
:percentage="progress" :percentage="progress"
@changeLineHead="_onUpdateProgress"/> @change-linehead="_onUpdateProgress"/>
<div class="ar-player__time">{{duration}}</div> <div class="ar-player__time">{{duration}}</div>
<volume-control @change-volume="_onChangeVolume"/>
<div class="ar-player-volume">
<icon-button class="ar-player-volume__icon" name="volume"/>
<line-control
class="ar-player-volume-bar"
ref-id="volume"
:percentage="volume"
@changeLineHead="_onUpdateVolume"/>
</div>
</div> </div>
<div class="ar-player-actions"> <audio :id="playerUniqId" :src="audioSource"></audio>
<icon-button name="download" @click.native="download"/>
<icon-button
size="lg"
:name="playBtnIcon"
:class="{'ar-icon-button--clicked': isPlaying}"
@click.native="playback"/>
<icon-button name="save" @click.native="upload"/>
</div>
<audio id="audio-recorder-player" :src="record.url"></audio>
</div> </div>
</template> </template>
<script> <script>
import IconButton from './icon-button.vue' import IconButton from './icon-button'
import LineControl from './line-control.vue' import LineControl from './line-control'
import { convertTimeMMSS } from '../lib/utils.js' import VolumeControl from './volume-control'
import { convertTimeMMSS } from '@/lib/utils.js'
export default { export default {
props: { props: {
uploadUrl: { type: String }, src : { type: String },
record: { type: Object }, uploadUrl : { type: String },
startUpload: { type: Function }, record : { type: Object },
successfulUpload: { type: Function }, compact : { type: Boolean, default: true },
failedUpload: { type: Function } startUpload : { type: Function },
successfulUpload : { type: Function },
failedUpload : { type: Function }
}, },
data () { data () {
return { return {
isPlaying: false, isPlaying: false,
duration: convertTimeMMSS(0), duration: convertTimeMMSS(0),
playedTime: convertTimeMMSS(0), playedTime: convertTimeMMSS(0),
progress: 0, progress: 0
volume: 0.8
} }
}, },
components: { components: {
IconButton, IconButton,
LineControl LineControl,
VolumeControl
}, },
mounted: function() { mounted: function() {
this.player = document.getElementById('audio-recorder-player') this.player = document.getElementById(this.playerUniqId)
this.player.addEventListener('ended', () => { this.player.addEventListener('ended', () => {
this.isPlaying = false this.isPlaying = false
@@ -149,11 +165,17 @@
computed: { computed: {
playBtnIcon () { playBtnIcon () {
return this.isPlaying ? 'pause' : 'play' return this.isPlaying ? 'pause' : 'play'
},
audioSource () {
return this.src || this.record.url
},
playerUniqId () {
return `audio-player${this._uid}`
} }
}, },
methods: { methods: {
playback () { playback () {
if (!this.record.url) { if (!this.audioSource) {
return return
} }
@@ -166,7 +188,7 @@
this.isPlaying = !this.isPlaying this.isPlaying = !this.isPlaying
}, },
upload () { upload () {
if (!this.record.url) { if (!this.audioSource) {
return return
} }
@@ -194,7 +216,7 @@
}) })
}, },
download () { download () {
if (!this.record.url) { if (!this.audioSource) {
return return
} }
@@ -216,10 +238,9 @@
this.player.currentTime = pos * this.player.duration this.player.currentTime = pos * this.player.duration
} }
}, },
_onUpdateVolume (val) { _onChangeVolume (val) {
if (val) { if (val) {
this.player.volume = val this.player.volume = val
this.volume = val
} }
} }
} }

View File

@@ -145,6 +145,8 @@
} }
} }
} }
@import '../scss/icons';
</style> </style>
<template> <template>
@@ -160,15 +162,15 @@
<div class="ar-content" :class="{'ar__blur': isUploading}"> <div class="ar-content" :class="{'ar__blur': isUploading}">
<div class="ar-recorder"> <div class="ar-recorder">
<icon-button <icon-button
size="lg" class="ar-icon ar-icon__lg"
:name="iconButtonType" :name="iconButtonType"
:class="{ :class="{
'ar-icon-button--rec': isRecording, 'ar-icon--rec': isRecording,
'ar-icon-button--pulse': isRecording && volume > 0.02 'ar-icon--pulse': isRecording && volume > 0.02
}" }"
@click.native="toggleRecorder"/> @click.native="toggleRecorder"/>
<icon-button <icon-button
class="ar-recorder__stop" class="ar-icon ar-icon__sm ar-recorder__stop"
name="stop" name="stop"
@click.native="stopRecorder"/> @click.native="stopRecorder"/>
</div> </div>
@@ -188,14 +190,15 @@
</div> </div>
</div> </div>
<record-player <audio-player
:compact="compact"
:record="selectedRecord" :record="selectedRecord"
:upload-url="uploadUrl" :upload-url="uploadUrl"
:start-upload="startUpload" :start-upload="startUpload"
:successful-upload="successfulUpload" :successful-upload="successfulUpload"
:failed-upload="failedUpload" :failed-upload="failedUpload"
@on-start-upload="onStartUpload" @start-upload="onStartUpload"
@on-end-upload="onEndUpload"/> @end-upload="onEndUpload"/>
<div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div> <div :class="uploadStatusClasses" v-if="uploadStatus">{{message}}</div>
</div> </div>
@@ -203,27 +206,28 @@
</template> </template>
<script> <script>
import IconButton from './components/icon-button.vue' import AudioPlayer from './player.vue'
import Recorder from './lib/recorder.js' import IconButton from './icon-button.vue'
import RecordPlayer from './components/record-player.vue' import Recorder from '@/lib/recorder.js'
import { convertTimeMMSS } from './lib/utils.js' import { convertTimeMMSS } from '@/lib/utils.js'
export default { export default {
props: { props: {
attempts: { type: Number }, attempts : { type: Number },
time: { type: Number }, compact : { type: Boolean, default: false },
uploadUrl: { type: String }, time : { type: Number },
uploadUrl : { type: String },
attemptsLimit: { type: Function }, attemptsLimit : { type: Function },
failedUpload: { type: Function }, failedUpload : { type: Function },
micFailed: { type: Function }, micFailed : { type: Function },
startRecord: { type: Function }, startRecord : { type: Function },
startUpload: { type: Function }, startUpload : { type: Function },
stopRecord: { type: Function }, stopRecord : { type: Function },
successfulUpload: { type: Function }, successfulUpload : { type: Function },
successfulUploadMsg: { type: String, default: 'Upload successful' }, successfulUploadMsg : { type: String, default: 'Upload successful' },
failedUploadMsg: { type: String, default: 'Upload fail' } failedUploadMsg : { type: String, default: 'Upload fail' }
}, },
data () { data () {
return { return {
@@ -245,8 +249,8 @@
} }
}, },
components: { components: {
IconButton, AudioPlayer,
RecordPlayer IconButton
}, },
methods: { methods: {
toggleRecorder () { toggleRecorder () {

View File

@@ -0,0 +1,60 @@
<style lang="scss">
.ar-volume {
display: flex;
align-items: center;
line-height: 10px;
&-bar {
width: 50px;
height: 6px;
background: #E6E6E6;
border-radius: 4px;
position: relative;
}
&__icon {
fill: #747474;
width: 24px;
height: 24px;
border: 0;
border-radius: 0;
padding: 0;
background-color: unset;
margin-right: 3px;
}
}
</style>
<template>
<div class="ar-volume">
<icon-button class="ar-volume__icon" name="volume"/>
<line-control
class="ar-volume-bar"
ref-id="volume"
:percentage="volume"
@change-linehead="onChangeLinehead"/>
</div>
</template>
<script>
import IconButton from './icon-button.vue'
import LineControl from './line-control.vue'
export default {
data () {
return {
volume: 0.8
}
},
components: {
IconButton,
LineControl
},
methods: {
onChangeLinehead (val) {
this.$emit('change-volume', val)
this.volume = val
}
}
}
</script>

View File

@@ -1,13 +1,22 @@
import AudioRecorder from './audio-recorder.vue' import AudioPlayer from './components/player.vue'
import AudioRecorder from './components/recorder.vue'
export default { const components = {
install: function (Vue) { AudioPlayer,
AudioRecorder,
install (Vue) {
if (this.installed) { if (this.installed) {
return return
} }
this.installed = true this.installed = true
Vue.component('audio-player', AudioPlayer)
Vue.component('audio-recorder', AudioRecorder) Vue.component('audio-recorder', AudioRecorder)
} }
} }
export default components
export { AudioPlayer, AudioRecorder }

45
src/scss/icons.scss Normal file
View File

@@ -0,0 +1,45 @@
.ar-icon {
fill: #747474;
border-radius: 50%;
border: 1px solid #05CBCD;
background-color: #FFFFFF;
padding: 5px;
cursor: pointer;
transition: .2s;
&--rec {
fill: white;
background-color: #FF6B64;
border-color: transparent;
}
&--pulse {
animation: ripple .5s linear infinite;
@keyframes ripple {
0% {
box-shadow:
0 0 0 0 rgba(red, 0.1),
0 0 0 1px rgba(red, 0.1),
0 0 0 5px rgba(red, 0.1);
}
100% {
box-shadow:
0 0 0 0 rgba(red, 0.1),
0 0 0 10px rgba(red, 0.1),
0 0 0 20px rgba(red, 0);
}
}
}
&__sm {
width: 30px;
height: 30px;
}
&__lg {
width: 45px;
height: 45px;
box-shadow: 0 2px 5px 1px rgba(158,158,158,0.5);
}
}