APlayer内部组件ts改造

This commit is contained in:
灌糖包子 2023-01-25 02:00:10 +08:00
parent 34b92fbd1a
commit b49bebf654
Signed by: sookie
GPG Key ID: 691E688C160D3188
15 changed files with 688 additions and 849 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -33,4 +33,10 @@ export interface MusicPlayerItem {
src: string src: string
pic: string pic: string
lrc?: string lrc?: string
}
export interface StatType {
duration: number,
loadedTime: number,
playedTime: number,
} }

View File

@ -2,7 +2,6 @@
<div <div
class="aplayer-bar-wrap" class="aplayer-bar-wrap"
@mousedown="onThumbMouseDown" @mousedown="onThumbMouseDown"
@touchstart="onThumbTouchStart"
ref="barWrap" ref="barWrap"
> >
<div class="aplayer-bar"> <div class="aplayer-bar">
@ -21,10 +20,8 @@
class="aplayer-thumb" class="aplayer-thumb"
:style="{borderColor: theme, backgroundColor: thumbHovered ? theme : '#fff'}" :style="{borderColor: theme, backgroundColor: thumbHovered ? theme : '#fff'}"
> >
<span class="aplayer-loading-icon" <span class="aplayer-loading-icon" :style="{backgroundColor: theme }">
:style="{backgroundColor: theme }" <player-icon type="loading"/>
>
<icon type="loading"/>
</span> </span>
</span> </span>
</div> </div>
@ -32,169 +29,125 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {getElementViewLeft} from '../utils' import { ref } from 'vue'
import Icon from './aplayer-icon.vue' import { getElementViewLeft } from '../utils'
import PlayerIcon from './aplayer-icon.vue'
export default { defineProps<{
components: { loadProgress: number,
Icon playProgress: number,
}, theme?: string
props: ['loadProgress', 'playProgress', 'theme'], }>()
data () {
return {
thumbHovered: false,
}
},
methods: {
onThumbMouseDown (e) {
const barWidth = this.$refs.barWrap.clientWidth
let percentage = (e.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('dragbegin', percentage) const emit = defineEmits(['dragbegin', 'dragging', 'dragend'])
document.addEventListener('mousemove', this.onDocumentMouseMove)
document.addEventListener('mouseup', this.onDocumentMouseUp)
},
onDocumentMouseMove (e) {
const barWidth = this.$refs.barWrap.clientWidth
let percentage = (e.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('dragging', percentage) const thumbHovered = ref(false)
}, const barWrap = ref<unknown>(null)
onDocumentMouseUp (e) {
document.removeEventListener('mouseup', this.onDocumentMouseUp)
document.removeEventListener('mousemove', this.onDocumentMouseMove)
const barWidth = this.$refs.barWrap.clientWidth const onThumbMouseDown = (e: MouseEvent) => {
let percentage = (e.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth const barWidth = (<HTMLElement>barWrap.value).clientWidth
percentage = percentage > 0 ? percentage : 0 let percentage = (e.clientX - getElementViewLeft(<HTMLElement>barWrap.value)) / barWidth
percentage = percentage < 1 ? percentage : 1 percentage = percentage > 0 ? percentage : 0
this.$emit('dragend', percentage) percentage = percentage < 1 ? percentage : 1
}, emit('dragbegin', percentage)
onThumbTouchStart (e) { document.addEventListener('mousemove', onDocumentMouseMove)
const barWidth = this.$refs.barWrap.clientWidth document.addEventListener('mouseup', onDocumentMouseUp)
let percentage = (e.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth }
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('dragbegin', percentage) const onDocumentMouseMove = (e: MouseEvent) => {
document.addEventListener('touchmove', this.onDocumentTouchMove) const barWidth = (<HTMLElement>barWrap.value).clientWidth
document.addEventListener('touchend', this.onDocumentTouchEnd) let percentage = (e.clientX - getElementViewLeft(<HTMLElement>barWrap.value)) / barWidth
}, percentage = percentage > 0 ? percentage : 0
onDocumentTouchMove (e) { percentage = percentage < 1 ? percentage : 1
const touch = e.changedTouches[0] emit('dragging', percentage)
const barWidth = this.$refs.barWrap.clientWidth }
let percentage = (touch.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('dragging', percentage) const onDocumentMouseUp = (e: MouseEvent) => {
}, document.removeEventListener('mouseup', onDocumentMouseUp)
onDocumentTouchEnd (e) { document.removeEventListener('mousemove', onDocumentMouseMove)
document.removeEventListener('touchend', this.onDocumentTouchEnd) const barWidth = (<HTMLElement>barWrap.value).clientWidth
document.removeEventListener('touchmove', this.onDocumentTouchMove) let percentage = (e.clientX - getElementViewLeft(<HTMLElement>barWrap.value)) / barWidth
percentage = percentage > 0 ? percentage : 0
const touch = e.changedTouches[0] percentage = percentage < 1 ? percentage : 1
const barWidth = this.$refs.barWrap.clientWidth emit('dragend', percentage)
let percentage = (touch.clientX - getElementViewLeft(this.$refs.barWrap)) / barWidth }
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('dragend', percentage)
},
},
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.aplayer-bar-wrap {
.aplayer-bar-wrap { margin: 0 0 0 5px;
margin: 0 0 0 5px; padding: 4px 0;
padding: 4px 0; cursor: pointer;
cursor: pointer; flex: 1;
flex: 1; .aplayer-bar {
position: relative;
.aplayer-bar { height: 2px;
position: relative; width: 100%;
background: #cdcdcd;
.aplayer-loaded {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background: #aaa;
height: 2px; height: 2px;
width: 100%; transition: all 0.5s ease;
background: #cdcdcd; will-change: width;
}
.aplayer-loaded { .aplayer-played {
position: absolute;
left: 0;
top: 0;
bottom: 0;
height: 2px;
transition: background-color .3s;
will-change: width;
.aplayer-thumb {
position: absolute; position: absolute;
left: 0;
top: 0; top: 0;
bottom: 0; right: 5px;
background: #aaa; margin-top: -5px;
height: 2px; margin-right: -10px;
transition: all 0.5s ease; width: 10px;
height: 10px;
will-change: width; border: 1px solid;
} transform: scale(.8);
will-change: transform;
.aplayer-played { transition: transform 300ms, background-color .3s, border-color .3s;
position: absolute; border-radius: 50%;
left: 0; background: #fff;
top: 0; cursor: pointer;
bottom: 0; &:hover {
height: 2px; transform: scale(1);
transition: background-color .3s; }
will-change: width; overflow: hidden;
.aplayer-loading-icon {
.aplayer-thumb { display: none;
position: absolute; width: 100%;
top: 0; height: 100%;
right: 5px; .icon-svg {
margin-top: -5px; position: absolute;
margin-right: -10px; animation: spin 1s linear infinite;
width: 10px;
height: 10px;
border: 1px solid;
transform: scale(.8);
will-change: transform;
transition: transform 300ms, background-color .3s, border-color .3s;
border-radius: 50%;
background: #fff;
cursor: pointer;
&:hover {
transform: scale(1);
}
overflow: hidden;
.aplayer-loading-icon {
display: none;
width: 100%;
height: 100%;
.icon-svg {
position: absolute;
animation: spin 1s linear infinite;
}
} }
} }
} }
} }
} }
}
.aplayer-loading { .aplayer-loading {
.aplayer-bar-wrap .aplayer-bar .aplayer-thumb .aplayer-loading-icon { .aplayer-bar-wrap .aplayer-bar .aplayer-thumb .aplayer-loading-icon {
display: block; display: block;
}
.aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb {
transform: scale(1);
}
} }
.aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb {
@keyframes spin { transform: scale(1);
0% {
transform: rotate(0)
}
100% {
transform: rotate(360deg)
}
} }
}
@keyframes spin {
0% {
transform: rotate(0)
}
100% {
transform: rotate(360deg)
}
}
</style> </style>

View File

@ -23,106 +23,95 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import IconButton from './aplayer-iconbutton.vue' import { computed, ref } from 'vue'
import {getElementViewTop} from '../utils' import IconButton from './aplayer-iconbutton.vue'
import {getElementViewTop} from '../utils'
const barHeight = 40 const props = defineProps<{
volume: number,
muted: boolean,
theme: string
}>()
const emit = defineEmits(['setvolume'])
const barHeight = 40
export default { const volumeIcon = computed(() => {
components: { if (props.muted || props.volume <= 0) return 'volume_off'
IconButton, if (props.volume >= 1) return 'volume_up'
}, return 'volume_down'
props: ['volume', 'muted', 'theme'], })
computed: {
volumeIcon () {
if (this.muted || this.volume <= 0) return 'volume_off'
if (this.volume >= 1) return 'volume_up'
return 'volume_down'
},
},
methods: {
adjustVolume (e) {
let percentage = (barHeight - e.clientY + getElementViewTop(this.$refs.bar)) / barHeight
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('setvolume', percentage)
},
onBarMouseDown () {
document.addEventListener('mousemove', this.onDocumentMouseMove)
document.addEventListener('mouseup', this.onDocumentMouseUp)
},
onDocumentMouseMove (e) {
let percentage = (barHeight - e.clientY + getElementViewTop(this.$refs.bar)) / barHeight
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
this.$emit('setvolume', percentage)
},
onDocumentMouseUp (e) {
document.removeEventListener('mouseup', this.onDocumentMouseUp)
document.removeEventListener('mousemove', this.onDocumentMouseMove)
let percentage = (barHeight - e.clientY + getElementViewTop(this.$refs.bar)) / barHeight const bar = ref<unknown>(null)
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1 const onBarMouseDown = () => {
this.$emit('setvolume', percentage) document.addEventListener('mousemove', onDocumentMouseMove)
} document.addEventListener('mouseup', onDocumentMouseUp)
} }
} const onDocumentMouseMove = (e: MouseEvent) => {
let percentage = (barHeight - e.clientY + getElementViewTop(<HTMLElement>bar.value)) / barHeight
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
emit('setvolume', percentage)
}
const onDocumentMouseUp = (e: MouseEvent) => {
document.removeEventListener('mouseup', onDocumentMouseUp)
document.removeEventListener('mousemove', onDocumentMouseMove)
let percentage = (barHeight - e.clientY + getElementViewTop(<HTMLElement>bar.value)) / barHeight
percentage = percentage > 0 ? percentage : 0
percentage = percentage < 1 ? percentage : 1
emit('setvolume', percentage)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.aplayer-volume-wrap {
.aplayer-volume-wrap { position: relative;
position: relative; cursor: pointer;
cursor: pointer; z-index: 0;
z-index: 0; &:hover .aplayer-volume-bar-wrap {
display: block;
&:hover .aplayer-volume-bar-wrap { }
display: block; .aplayer-volume-bar-wrap {
} display: none;
position: absolute;
.aplayer-volume-bar-wrap { bottom: 15px;
display: none; left: -4px;
right: -4px;
height: 40px;
z-index: -1;
transition: all .2s ease;
&::after {
content: '';
position: absolute; position: absolute;
bottom: 15px; bottom: -16px;
left: -4px; left: 0;
right: -4px; right: 0;
height: 62px;
background-color: #fff;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.07), 0 0 5px 0 rgba(0, 0, 0, 0.1);
}
.aplayer-volume-bar {
position: absolute;
bottom: 0;
left: 11px;
width: 5px;
height: 40px; height: 40px;
z-index: -1; background: #aaa;
transition: all .2s ease; border-radius: 2.5px;
overflow: hidden;
z-index: 1;
&::after { .aplayer-volume {
content: '';
position: absolute;
bottom: -16px;
left: 0;
right: 0;
height: 62px;
background-color: #fff;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.07), 0 0 5px 0 rgba(0, 0, 0, 0.1);
}
.aplayer-volume-bar {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 11px; left: 0;
width: 5px; right: 0;
height: 40px; transition: height 0.1s ease, background-color .3s;
background: #aaa; will-change: height;
border-radius: 2.5px;
overflow: hidden;
z-index: 1;
.aplayer-volume {
position: absolute;
bottom: 0;
left: 0;
right: 0;
transition: height 0.1s ease, background-color .3s;
will-change: height;
}
} }
} }
} }
}
</style> </style>

View File

@ -14,12 +14,12 @@
class="aplayer-dtime">{{secondToTime(stat.duration)}}</span> class="aplayer-dtime">{{secondToTime(stat.duration)}}</span>
</div> </div>
<volume <volume
v-if="!$parent.isMobile" v-if="!isMobile"
:volume="volume" :volume="volume"
:theme="theme" :theme="theme"
:muted="muted" :muted="muted"
@togglemute="$emit('togglemute')" @togglemute="$emit('togglemute')"
@setvolume="v => $emit('setvolume', v)" @setvolume="(v: number) => $emit('setvolume', v)"
/> />
<icon-button <icon-button
class="aplayer-icon-mode" class="aplayer-icon-mode"
@ -36,112 +36,94 @@
<icon-button <icon-button
class="aplayer-icon-menu" class="aplayer-icon-menu"
icon="menu" icon="menu"
:class="{ 'inactive': !$parent.showList }" :class="{ 'inactive': !showList }"
@click="$emit('togglelist')" @click="$emit('togglelist')"
/> />
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import IconButton from './aplayer-iconbutton.vue' import { computed } from 'vue'
import VProgress from './aplayer-controller-progress.vue' import IconButton from './aplayer-iconbutton.vue'
import Volume from './aplayer-controller-volume.vue' import VProgress from './aplayer-controller-progress.vue'
import Volume from './aplayer-controller-volume.vue'
import { StatType } from '@/model/api/music'
export default { const props = defineProps<{
components: { shuffle: boolean,
IconButton, repeat: string,
VProgress, stat: StatType,
Volume, theme: string
}, volume: number,
props: ['shuffle', 'repeat', 'stat', 'theme', 'volume', 'muted'], muted: boolean,
computed: { showList: boolean,
loadProgress () { isMobile: boolean
if (this.stat.duration === 0) return 0 }>()
return this.stat.loadedTime / this.stat.duration const loadProgress = computed(() => {
}, if (props.stat.duration === 0) return 0
playProgress () { return props.stat.loadedTime / props.stat.duration
if (this.stat.duration === 0) return 0 })
return this.stat.playedTime / this.stat.duration const playProgress = computed(() => {
}, if (props.stat.duration === 0) return 0
}, return props.stat.playedTime / props.stat.duration
methods: { })
secondToTime (second) {
if (isNaN(second)) {
return '00:00'
}
const pad0 = (num) => {
return num < 10 ? '0' + num : '' + num
}
const min = Math.trunc(second / 60) const secondToTime = (second: number) => {
const sec = Math.trunc(second - min * 60) if (isNaN(second)) {
const hours = Math.trunc(min / 60) return '00:00'
const minAdjust = Math.trunc((second / 60) - (60 * Math.trunc((second / 60) / 60)))
return second >= 3600 ? pad0(hours) + ':' + pad0(minAdjust) + ':' + pad0(sec) : pad0(min) + ':' + pad0(sec)
},
},
} }
const pad0 = (num: number) => {
return num < 10 ? '0' + num : '' + num
}
const min = Math.trunc(second / 60)
const sec = Math.trunc(second - min * 60)
const hours = Math.trunc(min / 60)
const minAdjust = Math.trunc((second / 60) - (60 * Math.trunc((second / 60) / 60)))
return second >= 3600 ? pad0(hours) + ':' + pad0(minAdjust) + ':' + pad0(sec) : pad0(min) + ':' + pad0(sec)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.aplayer-controller {
.aplayer-controller { display: flex;
align-items: center;
position: relative;
.aplayer-time {
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
height: 17px;
.aplayer-time { color: #999;
display: flex; font-size: 11px;
align-items: center; padding-left: 7px;
position: relative; .aplayer-volume-wrap {
height: 17px; margin-left: 4px;
color: #999; margin-right: 4px;
font-size: 11px; }
padding-left: 7px; .aplayer-icon {
cursor: pointer;
.aplayer-volume-wrap { transition: all 0.2s ease;
margin-left: 4px; margin-left: 4px;
margin-right: 4px; &.inactive {
opacity: .3;
} }
&.aplayer-icon-menu {
.aplayer-icon { display: none;
cursor: pointer;
transition: all 0.2s ease;
margin-left: 4px;
&.inactive {
opacity: .3;
}
.aplayer-fill {
fill: #666;
}
&:hover {
.aplayer-fill {
fill: #000;
}
}
&.aplayer-icon-menu {
display: none;
}
} }
.aplayer-volume-wrap + .aplayer-icon { }
margin-left: 0; .aplayer-volume-wrap + .aplayer-icon {
margin-left: 0;
}
&.aplayer-time-narrow {
.aplayer-icon-mode {
display: none;
} }
.aplayer-icon-menu {
&.aplayer-time-narrow { display: none;
.aplayer-icon-mode {
display: none;
}
.aplayer-icon-menu {
display: none;
}
} }
} }
} }
}
</style> </style>

View File

@ -2,26 +2,23 @@
<img class="icon-svg" :src="svg" :style="style" /> <img class="icon-svg" :src="svg" :style="style" />
</template> </template>
<script> <script lang="ts" setup>
const requireAssets = require.context('../assets', false, /\.svg$/) import { computed } from 'vue'
const SVGs = requireAssets.keys().reduce((svgs, path) => { const props = defineProps<{type: string}>()
const svgFile = requireAssets(path) const requireAssets = require.context('../assets', false, /\.svg$/)
svgs[path.match(/^.*\/(.+?)\.svg$/)[1]] = svgFile const SVGs = requireAssets.keys().reduce((svgs: {[propName:string]: string}, path: string) => {
return svgs const svgFile = requireAssets(path)
}, {}) const fileNameMatch = path.match(/^.*\/(.+?)\.svg$/)
export default { if (fileNameMatch) {
props: ['type'], svgs[fileNameMatch[1]] = svgFile
computed: {
svg () {
return SVGs[this.type] || {}
},
style () {
if (this.type === 'next') {
return {
transform: 'rotate(180deg)',
}
}
}
}
} }
return svgs
}, {})
const svg = computed(() => SVGs[props.type])
const style = computed(() => {
if (props.type === 'next') {
return { transform: 'rotate(180deg)' }
}
})
</script> </script>

View File

@ -3,43 +3,34 @@
type="button" type="button"
class="aplayer-icon" class="aplayer-icon"
> >
<icon :type="icon"/> <player-icon :type="icon"/>
</button> </button>
</template> </template>
<script> <script lang="ts" setup>
import Icon from './aplayer-icon.vue' import PlayerIcon from './aplayer-icon.vue'
defineProps<{icon: string}>()
export default {
components: {
Icon,
},
props: ['icon'],
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.aplayer-icon { .aplayer-icon {
width: 15px; width: 15px;
height: 15px; height: 15px;
border: none; border: none;
background-color: transparent; background-color: transparent;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
opacity: .8; opacity: .8;
vertical-align: middle; vertical-align: middle;
padding: 0; padding: 0;
font-size: 12px; font-size: 12px;
margin: 0; margin: 0;
display: inline; display: inline;
&:hover {
&:hover { opacity: 1;
opacity: 1;
}
.aplayer-fill {
transition: all .2s ease-in-out;
}
} }
.aplayer-fill {
transition: all .2s ease-in-out;
}
}
</style> </style>

View File

@ -6,10 +6,7 @@
ref="list" ref="list"
v-show="show" v-show="show"
> >
<ol <ol ref="ol" :style="listHeightStyle">
ref="ol"
:style="listHeightStyle"
>
<li <li
v-for="(aMusic, index) of musicList" v-for="(aMusic, index) of musicList"
:key="index" :key="index"
@ -19,148 +16,120 @@
<span class="aplayer-list-cur" :style="{background: theme}"></span> <span class="aplayer-list-cur" :style="{background: theme}"></span>
<span class="aplayer-list-index">{{ index + 1}}</span> <span class="aplayer-list-index">{{ index + 1}}</span>
<span class="aplayer-list-title">{{ aMusic.title || 'Untitled' }}</span> <span class="aplayer-list-title">{{ aMusic.title || 'Untitled' }}</span>
<span class="aplayer-list-author">{{ aMusic.artist || aMusic.author || 'Unknown' }}</span> <span class="aplayer-list-author">{{ aMusic.artist || 'Unknown' }}</span>
</li> </li>
</ol> </ol>
</div> </div>
</transition> </transition>
</template> </template>
<script> <script lang="ts" setup>
export default { import { computed } from 'vue'
props: { import { MusicPlayerItem } from '@/model/api/music'
show: {
type: Boolean, const props = defineProps<{
default: true, show: boolean,
}, currentMusic: MusicPlayerItem,
currentMusic: Object, musicList: MusicPlayerItem[],
musicList: { theme?: string,
type: Array, listMaxHeight?: string
default () { }>()
return [] const listHeightStyle = computed(() => {
} return {
}, height: `${33 * props.musicList.length - 1}px`,
playIndex: { maxHeight: props.listMaxHeight || ''
type: Number,
default: 0,
},
theme: String,
listMaxHeight: String,
},
computed: {
listHeightStyle () {
return {
height: `${33 * this.musicList.length - 1}px`,
maxHeight: this.listMaxHeight || ''
}
}
}
} }
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.aplayer-list {
.aplayer-list { overflow: hidden;
overflow: hidden; &.slide-v-enter-active,
&.slide-v-leave-active {
&.slide-v-enter-active, transition: height 500ms ease;
&.slide-v-leave-active { will-change: height;
transition: height 500ms ease; }
will-change: height; &.slide-v-enter,
&.slide-v-leave-to {
height: 0 !important;
}
ol {
list-style-type: none;
margin: 0;
padding: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 5px;
} }
&::-webkit-scrollbar-track {
&.slide-v-enter, background-color: #f9f9f9;
&.slide-v-leave-to {
height: 0 !important;
} }
&::-webkit-scrollbar-thumb {
ol { border-radius: 3px;
list-style-type: none; background-color: #eee;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #ccc;
}
&:hover {
li.aplayer-list-light:not(:hover) {
background-color: inherit;
transition: inherit;
}
}
&:not(:hover) {
li.aplayer-list-light {
transition: background-color .6s ease;
}
}
li {
position: relative;
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 12px;
border-top: 1px solid #e9e9e9;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
margin: 0; margin: 0;
padding: 0; text-align: start;
overflow-y: auto; display: flex;
&:first-child {
&::-webkit-scrollbar { border-top: none;
width: 5px;
} }
&::-webkit-scrollbar-track {
background-color: #f9f9f9;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: #eee;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #ccc;
}
&:hover { &:hover {
li.aplayer-list-light:not(:hover) { background: #efefef;
background-color: inherit;
transition: inherit;
}
} }
&.aplayer-list-light {
&:not(:hover) { background: #efefef;
li.aplayer-list-light {
transition: background-color .6s ease;
}
}
li {
position: relative;
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 12px;
border-top: 1px solid #e9e9e9;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
margin: 0;
text-align: start;
display: flex;
&:first-child {
border-top: none;
}
&:hover {
background: #efefef;
}
&.aplayer-list-light {
background: #efefef;
.aplayer-list-cur {
display: inline-block;
}
}
.aplayer-list-cur { .aplayer-list-cur {
display: none; display: inline-block;
width: 3px;
height: 22px;
position: absolute;
left: 0;
top: 5px;
transition: background-color .3s;
}
.aplayer-list-index {
color: #666;
margin-right: 12px;
}
.aplayer-list-title {
flex-grow: 1;
}
.aplayer-list-author {
flex-shrink: 0;
color: #666;
float: right;
} }
} }
.aplayer-list-cur {
display: none;
width: 3px;
height: 22px;
position: absolute;
left: 0;
top: 5px;
transition: background-color .3s;
}
.aplayer-list-index {
color: #666;
margin-right: 12px;
}
.aplayer-list-title {
flex-grow: 1;
}
.aplayer-list-author {
flex-shrink: 0;
color: #666;
float: right;
}
} }
} }
}
</style> </style>

View File

@ -15,150 +15,114 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {parseLrc} from '../utils' import { computed, watch, ref } from 'vue'
import { parseLrc } from '../utils'
import { MusicPlayerItem, StatType } from '@/model/api/music'
export default { const props = defineProps<{
props: { currentMusic: MusicPlayerItem,
currentMusic: { playStat: StatType
type: Object, }>()
required: true
}, const displayLrc = ref('')
playStat: { const currentLineIndex = ref(0)
type: Object,
required: true const lrcLines = computed(() => parseLrc(displayLrc.value))
}
}, const transformStyle = computed(() => {
data () { return {
return { transform: `translateY(${-currentLineIndex.value * 16}px)`,
displayLrc: '', webkitTransform: `translateY(${-currentLineIndex.value * 16}px)`,
currentLineIndex: 0, }
} })
},
computed: { const applyLrc = (lrc: string) => {
lrcLines () { if (/^https?:\/\//.test(lrc)) {
return parseLrc(this.displayLrc) fetch(lrc)
}, .then(response => response.text())
currentLine () { .then((lrc) => displayLrc.value = lrc)
if (this.currentLineIndex > this.lrcLines.length - 1) { } else {
return null displayLrc.value = lrc
} }
return this.lrcLines[this.currentLineIndex] }
},
transformStyle () { watch(props.currentMusic, (music: MusicPlayerItem) => {
// transform: translateY(0); -webkit-transform: translateY(0); currentLineIndex.value = 0
return { if (music.lrc) {
transform: `translateY(${-this.currentLineIndex * 16}px)`, applyLrc(music.lrc)
webkitTransform: `translateY(${-this.currentLineIndex * 16}px)`, } else {
} displayLrc.value = ''
}, }
}, }, { immediate: true })
methods: {
applyLrc (lrc) { watch(() => props.playStat.playedTime, (playedTime: number) => {
if (/^https?:\/\//.test(lrc)) { for (let i = 0; i < lrcLines.value.length; i++) {
this.fetchLrc(lrc) const line = lrcLines.value[i]
} else { const nextLine = lrcLines.value[i + 1]
this.displayLrc = lrc if (playedTime >= line[0] && (!nextLine || playedTime < nextLine[0])) {
} currentLineIndex.value = i
},
fetchLrc (src) {
fetch(src)
.then(response => response.text())
.then((lrc) => {
this.displayLrc = lrc
})
},
hideLrc () {
this.displayLrc = ''
},
},
watch: {
currentMusic: {
immediate: true,
handler (music) {
this.currentLineIndex = 0
if (music.lrc) {
this.applyLrc(music.lrc)
} else {
this.hideLrc()
}
}
},
'playStat.playedTime' (playedTime) {
for (let i = 0; i < this.lrcLines.length; i++) {
const line = this.lrcLines[i]
const nextLine = this.lrcLines[i + 1]
if (playedTime >= line[0] && (!nextLine || playedTime < nextLine[0])) {
this.currentLineIndex = i
}
}
},
} }
} }
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import "../less/variables"; @import "../less/variables";
.aplayer-lrc {
.aplayer-lrc { position: relative;
position: relative; height: @lrc-height;
height: @lrc-height; text-align: center;
text-align: center; overflow: hidden;
margin-bottom: 7px;
&:before {
position: absolute;
top: 0;
z-index: 1;
display: block;
overflow: hidden; overflow: hidden;
margin-bottom: 7px; width: 100%;
height: 10%;
&:before { content: ' ';
position: absolute; background: -moz-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
top: 0; background: -webkit-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
z-index: 1; background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
display: block; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0);
overflow: hidden; }
width: 100%; &:after {
height: 10%; position: absolute;
content: ' '; bottom: 0;
background: -moz-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); z-index: 1;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); display: block;
background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); overflow: hidden;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0); width: 100%;
} height: 33%;
content: ' ';
&:after { background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%);
position: absolute; background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%);
bottom: 0; background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%);
z-index: 1; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ccffffff', GradientType=0);
display: block; }
overflow: hidden; p {
width: 100%; font-size: 12px;
height: 33%; color: #666;
content: ' '; line-height: 16px;
background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%); height: 16px;
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%); padding: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%); margin: 0;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ccffffff', GradientType=0); transition: all 0.5s ease-out;
} opacity: 0.4;
overflow: hidden;
p { &.aplayer-lrc-current {
font-size: 12px; opacity: 1;
color: #666; overflow: visible;
line-height: 16px; height: initial;
height: 16px;
padding: 0;
margin: 0;
transition: all 0.5s ease-out;
opacity: 0.4;
overflow: hidden;
&.aplayer-lrc-current {
opacity: 1;
overflow: visible;
height: initial;
}
}
.aplayer-lrc-contents {
width: 100%;
transition: all 0.5s ease-out;
user-select: text;
cursor: default;
} }
} }
.aplayer-lrc-contents {
width: 100%;
transition: all 0.5s ease-out;
user-select: text;
cursor: default;
}
}
</style> </style>

View File

@ -13,139 +13,121 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import IconButton from './aplayer-iconbutton.vue' import { computed, ref } from 'vue'
import IconButton from './aplayer-iconbutton.vue'
export default { const props = withDefaults(defineProps<{
components: { pic?: string,
IconButton, theme?: string,
}, playing: boolean
props: { enableDrag: boolean
pic: String, }>(), {
theme: String, playing: false,
playing: { enableDrag: false
type: Boolean, })
default: false,
},
enableDrag: {
type: Boolean,
default: false
}
},
data () {
return {
hasMovedSinceMouseDown: false,
dragStartX: 0,
dragStartY: 0
}
},
computed: {
currentPicStyleObj () {
if (!this.pic) return {}
return {
backgroundImage: `url(${this.pic})`,
backgroundColor: this.theme
}
},
},
methods: {
onDragBegin (e) {
if (this.enableDrag) {
this.hasMovedSinceMouseDown = false
this.$emit('dragbegin')
this.dragStartX = e.clientX
this.dragStartY = e.clientY
document.addEventListener('mousemove', this.onDocumentMouseMove)
document.addEventListener('mouseup', this.onDocumentMouseUp)
}
},
onDocumentMouseMove (e) {
this.hasMovedSinceMouseDown = true
this.$emit('dragging', {offsetLeft: e.clientX - this.dragStartX, offsetTop: e.clientY - this.dragStartY})
},
onDocumentMouseUp (e) {
document.removeEventListener('mouseup', this.onDocumentMouseUp)
document.removeEventListener('mousemove', this.onDocumentMouseMove)
this.$emit('dragend') const emit = defineEmits(['dragbegin', 'dragging', 'dragend', 'toggleplay'])
},
onClick () { const hasMovedSinceMouseDown = ref(false)
if (!this.hasMovedSinceMouseDown) { const dragStartX = ref(0)
this.$emit('toggleplay') const dragStartY = ref(0)
}
} const currentPicStyleObj = computed(() => {
} if (!props.pic) return {}
return {
backgroundImage: `url(${props.pic})`,
backgroundColor: props.theme
} }
})
const onDragBegin = (e: MouseEvent) => {
if (props.enableDrag) {
hasMovedSinceMouseDown.value = false
emit('dragbegin')
dragStartX.value = e.clientX
dragStartY.value = e.clientY
document.addEventListener('mousemove', onDocumentMouseMove)
document.addEventListener('mouseup', onDocumentMouseUp)
}
}
const onDocumentMouseMove = (e: MouseEvent) => {
hasMovedSinceMouseDown.value = true
emit('dragging', {offsetLeft: e.clientX - dragStartX.value, offsetTop: e.clientY - dragStartY.value})
}
const onDocumentMouseUp = () => {
document.removeEventListener('mouseup', onDocumentMouseUp)
document.removeEventListener('mousemove', onDocumentMouseMove)
emit('dragend')
}
const onClick = () => {
if (!hasMovedSinceMouseDown.value) {
emit('toggleplay')
}
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import "../less/variables"; @import "../less/variables";
.aplayer-float {
.aplayer-float { .aplayer-pic:active {
.aplayer-pic:active { cursor: move;
cursor: move;
}
} }
}
.aplayer-pic { .aplayer-pic {
flex-shrink: 0; flex-shrink: 0;
position: relative;
position: relative; height: @aplayer-height;
height: @aplayer-height; width: @aplayer-height;
width: @aplayer-height; background-image: url(../default.jpg);
background-image: url(../default.jpg); background-size: cover;
background-size: cover; transition: all 0.3s ease;
transition: all 0.3s ease; cursor: pointer;
cursor: pointer; &:hover {
&:hover {
.aplayer-button {
opacity: 1;
}
}
.aplayer-button { .aplayer-button {
position: absolute; opacity: 1;
border-radius: 50%;
opacity: 0.8;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.2);
transition: all 0.1s ease;
.aplayer-fill {
fill: #fff;
}
}
.aplayer-play {
width: 26px;
height: 26px;
border: 2px solid #fff;
bottom: 50%;
right: 50%;
margin: 0 -15px -15px 0;
.aplayer-icon-play {
position: absolute;
top: 3px;
left: 4px;
height: 20px;
width: 20px;
}
}
.aplayer-pause {
width: 16px;
height: 16px;
border: 2px solid #fff;
bottom: 4px;
right: 4px;
.aplayer-icon-pause {
position: absolute;
top: 2px;
left: 2px;
height: 12px;
width: 12px;
}
} }
} }
.aplayer-button {
position: absolute;
border-radius: 50%;
opacity: 0.8;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.2);
transition: all 0.1s ease;
.aplayer-fill {
fill: #fff;
}
}
.aplayer-play {
width: 26px;
height: 26px;
border: 2px solid #fff;
bottom: 50%;
right: 50%;
margin: 0 -15px -15px 0;
.aplayer-icon-play {
position: absolute;
top: 3px;
left: 4px;
height: 20px;
width: 20px;
}
}
.aplayer-pause {
width: 16px;
height: 16px;
border: 2px solid #fff;
bottom: 4px;
right: 4px;
.aplayer-icon-pause {
position: absolute;
top: 2px;
left: 2px;
height: 12px;
width: 12px;
}
}
}
</style> </style>

View File

@ -1,75 +0,0 @@
/**
* Parse lrc, suppose multiple time tag
* @see https://github.com/MoePlayer/APlayer/blob/master/src/js/lrc.js#L83
* @author DIYgod(https://github.com/DIYgod)
*
* @param {String} lrc_s - Format:
* [mm:ss]lyric
* [mm:ss.xx]lyric
* [mm:ss.xxx]lyric
* [mm:ss.xx][mm:ss.xx][mm:ss.xx]lyric
* [mm:ss.xx]<mm:ss.xx>lyric
*
* @return {String} [[time, text], [time, text], [time, text], ...]
*/
export function parseLrc (lrc_s) {
if (lrc_s) {
lrc_s = lrc_s.replace(/([^\]^\n])\[/g, (match, p1) => p1 + '\n[')
const lyric = lrc_s.split('\n')
const lrc = []
const lyricLen = lyric.length
for (let i = 0; i < lyricLen; i++) {
// match lrc time
const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g)
// match lrc text
const lrcText = lyric[i].replace(/.*\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g, '').replace(/<(\d{2}):(\d{2})(\.(\d{2,3}))?>/g, '').replace(/^\s+|\s+$/g, '')
if (lrcTimes) {
// handle multiple time tag
const timeLen = lrcTimes.length
for (let j = 0; j < timeLen; j++) {
const oneTime = /\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/.exec(lrcTimes[j])
const min2sec = oneTime[1] * 60
const sec2sec = parseInt(oneTime[2])
const msec2sec = oneTime[4] ? parseInt(oneTime[4]) / ((oneTime[4] + '').length === 2 ? 100 : 1000) : 0
const lrcTime = min2sec + sec2sec + msec2sec
lrc.push([lrcTime, lrcText])
}
}
}
// sort by time
lrc.sort((a, b) => a[0] - b[0])
return lrc
}
else {
return []
}
}
export function warn (message) {
return console.warn(`[Vue-APlayer] ${message}`)
}
export function getElementViewLeft (element) {
let actualLeft = element.offsetLeft
let current = element.offsetParent
let elementScrollLeft
while (current !== null) {
actualLeft += current.offsetLeft
current = current.offsetParent
}
elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft
return actualLeft - elementScrollLeft
}
export function getElementViewTop (element) {
let actualTop = element.offsetTop
let current = element.offsetParent
let elementScrollTop
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop
return actualTop - elementScrollTop
}

View File

@ -0,0 +1,69 @@
/**
* Parse lrc, suppose multiple time tag
* @param {string} lrc_s - Format:
* [mm:ss]lyric
* [mm:ss.xx]lyric
* [mm:ss.xxx]lyric
* [mm:ss.xx][mm:ss.xx][mm:ss.xx]lyric
* [mm:ss.xx]<mm:ss.xx>lyric
*
* @return {Array} [[time, text], [time, text], [time, text], ...]
*/
export function parseLrc (lrc_s: string): Array<[number, string]> {
if (!lrc_s) return []
lrc_s = lrc_s.replace(/([^\]^\n])\[/g, (match, p1) => p1 + '\n[')
const lyric = lrc_s.split('\n')
const lrc: Array<[number, string]> = []
const lyricLen = lyric.length
for (let i = 0; i < lyricLen; i++) {
// match lrc time
const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g)
// match lrc text
const lrcText = lyric[i].replace(/.*\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/g, '').replace(/<(\d{2}):(\d{2})(\.(\d{2,3}))?>/g, '').replace(/^\s+|\s+$/g, '')
if (lrcTimes) {
// handle multiple time tag
const timeLen = lrcTimes.length
for (let j = 0; j < timeLen; j++) {
const oneTime = /\[(\d{2}):(\d{2})(\.(\d{2,3}))?]/.exec(lrcTimes[j])
if (!oneTime) continue
const min2sec = parseInt(oneTime[1]) * 60
const sec2sec = parseInt(oneTime[2])
const msec2sec = oneTime[4] ? parseInt(oneTime[4]) / ((oneTime[4] + '').length === 2 ? 100 : 1000) : 0
const lrcTime = min2sec + sec2sec + msec2sec
lrc.push([lrcTime, lrcText])
}
}
}
// sort by time
lrc.sort((item1, item2) => item1[0] - item2[0])
return lrc
}
export function warn (message: string) {
return console.warn(`[Vue-APlayer] ${message}`)
}
export function getElementViewLeft (element: HTMLElement): number {
let actualLeft = element.offsetLeft
let current = <HTMLElement>element.offsetParent
let elementScrollLeft
while (current !== null) {
actualLeft += current.offsetLeft
current = <HTMLElement>current.offsetParent
}
elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft
return actualLeft - elementScrollLeft
}
export function getElementViewTop (element: HTMLElement): number {
let actualTop = element.offsetTop
let current = <HTMLElement>element.offsetParent
let elementScrollTop
while (current !== null) {
actualTop += current.offsetTop
current = <HTMLElement>current.offsetParent
}
elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop
return actualTop - elementScrollTop
}

View File

@ -35,6 +35,8 @@
:volume="audioVolume" :volume="audioVolume"
:muted="isAudioMuted" :muted="isAudioMuted"
:theme="currentTheme" :theme="currentTheme"
:showList="showList"
:isMobile="isMobile"
@toggleshuffle="shouldShuffle = !shouldShuffle" @toggleshuffle="shouldShuffle = !shouldShuffle"
@togglelist="showList = !showList" @togglelist="showList = !showList"
@togglemute="toggleMute" @togglemute="toggleMute"
@ -490,7 +492,6 @@
}, },
onProgressDragEnd (val) { onProgressDragEnd (val) {
this.isSeeking = false this.isSeeking = false
if (this.wasPlayingBeforeSeeking) { if (this.wasPlayingBeforeSeeking) {
this.thenPlay() this.thenPlay()
} }

View File

@ -11,7 +11,11 @@
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"allowJs": true "allowJs": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
} }

View File

@ -1,3 +1,4 @@
const path = require('path')
const AutoImport = require('unplugin-auto-import/webpack') const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack') const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers') const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
@ -12,6 +13,9 @@ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
productionSourceMap: false, productionSourceMap: false,
configureWebpack: { configureWebpack: {
resolve: {
alias: { '@': path.resolve(__dirname, './src') }
},
plugins: [ plugins: [
AutoImport({ AutoImport({
resolvers: [ElementPlusResolver()], resolvers: [ElementPlusResolver()],