feat: 部分完成音乐播放器

This commit is contained in:
539943419 committed 2024-09-30 17:07:11 +08:00
1 parent 3b09d4c1cb
commit 1f42785b4a
13 files changed
+659 -37

No files matched your search

+1
View File
@@ -29,6 +29,7 @@
"axios": "^1.7.3",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"naive-ui": "^2.39.0",
+22
View File
@@ -59,6 +59,9 @@ importers:
copy-to-clipboard:
specifier: ^3.3.3
version: 3.3.3
localforage:
specifier: ^1.10.0
version: 1.10.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -890,6 +893,9 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@4.3.7:
resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
@@ -929,9 +935,15 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
localforage@1.10.0:
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@@ -2219,6 +2231,8 @@ snapshots:
- supports-color
optional: true
immediate@3.0.6: {}
immutable@4.3.7: {}
inflight@1.0.6:
@@ -2250,10 +2264,18 @@ snapshots:
dependencies:
is-docker: 2.2.1
lie@3.1.1:
dependencies:
immediate: 3.0.6
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
localforage@1.10.0:
dependencies:
lie: 3.1.1
lodash-es@4.17.21: {}
lodash@4.17.21: {}
+1 -1
View File
@@ -8,7 +8,7 @@ const pluginConfig: registerComponentOption = {
name: '音乐',
icon: Disc,
getComponent: async () =>
(await import('./miaoMusicPlayerDemo.vue')).default,
(await import('./miaoMusicPlayer.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
@@ -1,11 +1,12 @@
import { createApp, Reactive } from "vue";
import aplayerListNew from "../src/aplayerListNew.vue";
import aplayerListNew from "../src/components/aplayerListNew.vue";
import apType, { audioType } from "../types/ap";
type aplayerMiaoOption = {
export type aplayerMiaoOption = {
onDragItemStart?: () => void
onDragItemEnd?: () => void
onPlayVideo?: (element: audioType) => void
onSaveCollection?: (audios: audioType[]) => void
}
/**
@@ -2,14 +2,20 @@
<miao-drop-handler @on-virtual-files="handleDropVFiles">
<miao-alert-tip-provider ref="miaoMessageRef">
<div class="miaoMusic-container">
<NScrollbar>
<miao-music-main v-if="ap" :ap="ap"></miao-music-main>
</NScrollbar>
<miao-music-main
v-if="ap"
:ap="ap"
v-model:data="data"
:updateLocalData="updateLocalData"
@new-collection="saveCurrPlayList"></miao-music-main>
<miao-mask
:show="isAPlayerListShow"
style="z-index: 5"
@click="() => ap?.list.hide()"></miao-mask>
<div ref="aplayer" style="z-index: 10" />
<miao-mask
:show="isSavingCollection"
@click="isSavingCollection = false"></miao-mask>
</div>
</miao-alert-tip-provider>
</miao-drop-handler>
@@ -18,8 +24,16 @@
<script setup lang="ts">
import useAplayerMiao from './hooks/aplayerMiao'
import { VirtualFile } from '@/class/VirtualDirectory'
import { App, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { NScrollbar } from 'naive-ui'
import {
App,
onBeforeMount,
onMounted,
onUnmounted,
reactive,
ref,
toRaw,
watch
} from 'vue'
import './src/APlayer.fix.css'
// @ts-ignore
import APlayer from 'APlayer'
@@ -33,6 +47,9 @@ import usePluginCenter from '@/hooks/usePluginCenter'
import VirtualPage from '@/class/VirtualPage'
import miaoMask from '@/components/miaoMask.vue'
import miaoMusicMain from './src/main.vue'
import collectionType from './types/collection'
import cloneDeep from 'lodash/cloneDeep'
import localForage from 'localforage'
const pluginCenter = usePluginCenter()
let ap = ref<apType>()
@@ -41,6 +58,7 @@ let vueApp: App<Element>
const miaoMessageRef = ref<InstanceType<typeof miaoAlertTipProvider>>()
const aplayer = ref<any>(null)
const isAPlayerListShow = ref(true)
const isSavingCollection = ref(false)
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
@@ -50,6 +68,18 @@ const props = defineProps<{
view: VirtualPage
}>()
const data = ref<collectionType[]>([])
watch(
data,
() => {
updateLocalData()
},
{
deep: true
}
)
watch(
() => currentFiles.value,
(value, oldValue) => {
@@ -80,6 +110,23 @@ const handleDropVFiles = (files: VirtualFile[]) => {
currentFiles.value = uniq([...currentFiles.value, ...files])
}
const saveCurrPlayList = () => {
if (ap.value) {
data.value.unshift({
id: Math.random(),
name: `新建歌单-${new Date()}`,
intro: 'none',
createTime: new Date(),
coverUrl: '',
audios: cloneDeep(ap.value.list.audios)
})
miaoMessageRef.value?.alertTip('已创建新的歌单', {
timeout: 1000,
type: 'success'
})
}
}
const initAPlayerMiao = () => {
const aplayerOption = {
container: aplayer.value,
@@ -103,7 +150,8 @@ const initAPlayerMiao = () => {
) as VirtualFile
]
)
}
},
onSaveCollection: saveCurrPlayList
})
ap.value.on('listremove', (...args: any) => {
const { index } = args[0] as { index: number }
@@ -122,7 +170,18 @@ const initAPlayerMiao = () => {
})
}
onMounted(() => {
const updateLocalData = () => {
// console.log('updateLocalData', toRaw(data.value))
localForage.setItem('miaoMusic-storage-musicCollections', toRaw(data.value))
}
const getLocalData = async () => {
data.value =
(await localForage.getItem('miaoMusic-storage-musicCollections')) ?? []
}
onBeforeMount(async () => {
await getLocalData()
})
onMounted(async () => {
currentFiles.value = [...currentFiles.value.reverse()]
initAPlayerMiao()
})
@@ -331,7 +331,8 @@
}
.aplayer .aplayer-info .aplayer-music .aplayer-title {
font-size: 14px
font-size: 14px;
user-select: none;
}
.aplayer .aplayer-info .aplayer-music .aplayer-author {
@@ -0,0 +1,299 @@
<template>
<div class="collection-info-container">
<div class="collection-info-container-top">
<div
class="collection-info-container-top-exit"
@click="emit('exit')">
<ArrowBackOutline></ArrowBackOutline>
</div>
<div class="collection-info-container-top-title">
<span :tooltip="false" v-if="!isEditing">
{{ collection.name }}
</span>
<n-input
v-if="isEditing"
v-model:value="collection.name"></n-input>
</div>
<div
class="collection-info-container-top-delete"
v-if="isEditing"
@click="
clickDeleteCount !== 2
? clickDeleteCount++
: emit('deleteSelf')
">
<TrashOutline></TrashOutline>
<span>
{{
clickDeleteCount === 0
? `删除`
: clickDeleteCount === 1
? '确认删除'
: '再次确认'
}}
</span>
</div>
</div>
<div class="collection-info-container-controller">
<div
class="collection-info-container-controller-play"
@click="emit('playAll', collection)">
<div class="collection-info-container-controller-play-icon">
<CaretForwardOutline></CaretForwardOutline>
</div>
<div class="collection-info-container-controller-play-text">
{{ `全部播放` }}
</div>
</div>
<div class="collection-info-container-controller-right">
<div
class="collection-info-container-controller-edit"
@click="isEditing = !isEditing">
<component
:is="
isEditing ? CheckmarkOutline : CreateOutline
"></component>
</div>
</div>
</div>
<NScrollbar>
<div class="collection-info-container-audioList">
<VueDraggable
v-model="collection.audios"
:animation="150"
handle=".collection-info-container-audioList-item-controller-item-drag"
ghostClass="iSDragging">
<div
v-for="(audio, index) in collection.audios"
class="collection-info-container-audioList-item">
<div
class="collection-info-container-audioList-item-index">
<div>{{ index + 1 }}</div>
</div>
<div
class="collection-info-container-audioList-item-info">
<div
v-if="!isEditing"
class="collection-info-container-audioList-item-info-name">
<NEllipsis :tooltip="false">
{{ audio.name }}
</NEllipsis>
</div>
<n-input
v-model:value="audio.name"
v-if="isEditing"></n-input>
</div>
<div
class="collection-info-container-audioList-item-controller">
<PlayOutline
class="collection-info-container-audioList-item-controller-item"
v-if="!isEditing"
@click="
emit('playAll', collection, index)
"></PlayOutline>
<TrashOutline
class="collection-info-container-audioList-item-controller-item"
v-if="isEditing"
@click="handleDelete(index)"></TrashOutline>
<ReorderFourOutline
v-if="isEditing"
class="collection-info-container-audioList-item-controller-item collection-info-container-audioList-item-controller-item-drag"></ReorderFourOutline>
</div>
</div>
</VueDraggable>
</div>
</NScrollbar>
</div>
</template>
<script setup lang="ts">
import { NScrollbar, NEllipsis, NInput } from 'naive-ui'
import { VueDraggable } from 'vue-draggable-plus'
import {
ArrowBackOutline,
CaretForwardOutline,
ReorderFourOutline,
PlayOutline,
CreateOutline,
CheckmarkOutline,
TrashOutline
} from '@vicons/ionicons5'
import collectionType from '../types/collection'
import { ref, watch } from 'vue'
const props = defineProps<{
collection: collectionType
}>()
const isEditing = ref(false)
const clickDeleteCount = ref(0)
watch(isEditing, (v) => v && (clickDeleteCount.value = 0))
const handleDelete = (index: number) => {
props.collection.audios.splice(index, 1)
}
const emit = defineEmits<{
exit: []
deleteSelf: []
playAll: [collection: collectionType, index?: number]
}>()
</script>
<style lang="scss" scoped>
$ig: linear-gradient(90deg, #a1ee5e 0%, #00e3ae 100%);
.collection-info-container {
width: 100%;
height: 100%;
background-color: #ffffff;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
&-top {
height: 70px;
padding: 0 15px;
background: $ig;
display: flex;
align-items: center;
&-exit {
width: 30px;
aspect-ratio: 1;
color: #063b2a;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg {
width: 25px;
aspect-ratio: 1;
transition: width 0.25s ease;
}
&:hover {
svg {
width: 30px;
}
}
}
&-title {
display: flex;
margin-left: 10px;
&-confirm {
margin-left: 10px;
width: 32px;
// color: #ffffff;
}
span {
user-select: none;
font-size: larger;
}
}
&-delete {
display: flex;
height: 25px;
position: absolute;
right: 28px;
color: red;
user-select: none;
cursor: pointer;
& > span {
position: relative;
top: 3px;
margin-left: 3px;
}
}
}
&-controller {
height: 50px;
padding: 0 15px;
display: flex;
align-items: center;
&-play {
display: flex;
align-items: center;
& > div {
cursor: pointer;
}
&-icon {
height: 30px;
width: 45px;
background: $ig;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 30px;
height: 25px;
color: aliceblue;
}
}
&-text {
user-select: none;
margin-left: 10px;
font-size: larger;
}
}
&-right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
& > div {
margin: 0 10px;
cursor: pointer;
height: 25px;
width: 25px;
color: #002911;
}
}
}
&-audioList {
flex: 1;
color: #002911;
padding-bottom: 88px;
&-item {
// padding: 0 20px;
padding-right: 7px;
display: flex;
height: 70px;
align-items: center;
&-index {
width: 50px;
user-select: none;
font-size: 24px;
text-align: right;
& > div {
margin: auto;
width: fit-content;
}
}
&-info {
flex: 1;
display: flex;
user-select: none;
overflow: hidden;
&-name {
overflow: hidden;
}
}
&-controller {
display: flex;
align-items: center;
&-item {
width: 30px;
cursor: pointer;
margin: 0 5px;
}
}
}
}
}
.isDragging {
opacity: 0.5;
}
</style>
@@ -9,7 +9,10 @@
</span>
</div>
<div class="aplayer-list-top-buttons">
<component :is="AddCircleOutline" class="aplayer-list-top-buttons-icon"></component>
<AddCircleOutline
v-if="onSaveCollection"
class="aplayer-list-top-buttons-icon"
@click="onSaveCollection(ap.list.audios)"></AddCircleOutline>
</div>
<span></span>
</div>
@@ -43,7 +46,7 @@
<div
v-if="isVideo(element)"
class="aplayer-list-control-btn aplayer-list-control-btn-hover aplayer-list-control-btn-play"
@click.stop="onPlayVideo(element)">
@click.stop="onPlayVideo && onPlayVideo(element)">
<VideocamOutline class="aplayer-list-control-btn-svg" />
</div>
<div
@@ -120,14 +123,14 @@ import {
import { VueDraggable } from 'vue-draggable-plus'
// 与vue-draggable冲突
// import miaoLazyDiv from '@/components/miaoLazyDiv.vue';
import apType, { audioType } from '../types/ap'
import apType, { audioType } from '../../types/ap'
import { aplayerMiaoOption } from '../../hooks/aplayerMiao'
const props = defineProps<{
ap: apType
onDragItemStart: () => void
onDragItemEnd: () => void
onPlayVideo: (element: audioType) => void
}>()
const props = defineProps<
{
ap: apType
} & aplayerMiaoOption
>()
const apRef = ref(props.ap)
@@ -396,6 +399,7 @@ onMounted(() => {
&-icon {
width: 20px;
aspect-ratio: 1;
cursor: pointer;
}
&:hover {
scale: 1.1;
@@ -0,0 +1,123 @@
<template>
<div class="collection" @click="emit('onenter')">
<div class="collection-cover" @click.stop="emit('onPlay')">
<img
v-if="item.coverUrl"
class="collection-cover-img"
:src="item.coverUrl" />
<div class="collection-cover-play">
<Play></Play>
</div>
</div>
<div class="collection-info">
<div class="collection-info-name">
<span class="collection-info-name-ellipsis">
<span>
{{ item.name }}
</span>
</span>
</div>
<div class="collection-info-stats">
<span class="collection-info-stats-time">{{ dateStr }}</span>
<span class="collection-info-stats-count">
{{ `共${item.audios.length}首` }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import collectionType from '../../types/collection'
import dateFormatter from '@/hooks/dateFormatter'
import { Play } from '@vicons/ionicons5'
const props = defineProps<{
item: collectionType
}>()
const emit = defineEmits<{
onPlay: []
onenter: []
}>()
const dateStr = computed(() =>
dateFormatter(new Date(props.item.createTime), 'yyyy-MM-dd')
)
</script>
<style lang="scss" scoped>
.collection {
height: 50px;
position: relative;
display: flex;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
border-radius: 5px;
overflow: hidden;
transition: height 0.25s ease;
cursor: pointer;
user-select: none;
&:not(&:last-child) {
margin-bottom: 15px;
}
.collection-cover {
width: 50px;
height: 50px;
aspect-ratio: 1;
background-color: #60e8a2;
margin-right: 5px;
.collection-cover-play {
width: 25px;
height: 15px;
background-color: #d1d1d1b2;
position: absolute;
bottom: 5px;
left: 5px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 3px;
svg {
height: 15px;
color: #fff;
}
}
}
.collection-info {
position: relative;
width: calc(100% - 60px);
display: flex;
flex-direction: column;
justify-content: center;
.collection-info-name {
max-width: 100%;
overflow: hidden;
&-ellipsis {
overflow: hidden;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
max-width: 100%;
text-overflow: ellipsis;
span {
white-space: nowrap;
text-wrap: nowrap;
text-overflow: ellipsis;
}
}
}
.collection-info-stats {
text-wrap: nowrap;
span {
font-size: smaller;
}
.collection-info-stats-time {
margin-right: 10px;
}
}
}
}
</style>
@@ -1,11 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 29 32">
<path
d="M9.333 9.333h13.333v4l5.333-5.333-5.333-5.333v4h-16v8h2.667v-5.333zM22.667 22.667h-13.333v-4l-5.333 5.333 5.333 5.333v-4h16v-8h-2.667v5.333z"></path>
</svg>
</template>
<script setup lang="ts">
</script>
<style scoped></style>
+114 -6
View File
@@ -1,25 +1,133 @@
<template>
<div class="miaoMusic-main">
<div class="fd" v-if="false">
{{ fakeData }}
</div>
<NScrollbar>
<div class="miaoMusic-main-collectionsList">
<div class="miaoMusic-main-collectionsList-title">自建歌单</div>
<VueDraggable
v-if="collections"
v-model="collections"
:animation="150"
:delay="100"
ghostClass="iSDragging">
<collection
v-for="item in collections"
:item="item"
@on-play="handleCollectionsPlay(item)"
@onenter="handleCollectionEnter(item)"></collection>
</VueDraggable>
<n-empty
description="空空如也~"
size="huge"
style="margin-top: 200px"
v-if="!data || data?.length === 0">
<template #icon>
<n-icon>
<HandLeftOutline />
</n-icon>
</template>
<template #extra>
<n-button
size="small"
type="primary"
@click="emit('newCollection')">
立刻创建
</n-button>
</template>
</n-empty>
</div>
</NScrollbar>
<Transition name="slide-fade">
<collectionInfo
class="miaoMusic-main-collection"
:collection="currCollection"
v-if="currCollection"
@exit="handleCollectionEnter(undefined)"
@delete-self="handleCollectionDelete()"
@play-all="handleCollectionsPlay"></collectionInfo>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import apType from '../types/ap'
import collection from './components/collection.vue'
import collectionInfo from './collectionInfo.vue'
import collectionType from '../types/collection'
import { VueDraggable } from 'vue-draggable-plus'
import { NScrollbar, NEmpty, NButton } from 'naive-ui'
import { HandLeftOutline } from '@vicons/ionicons5'
const props = defineProps<{
ap: apType
}>()
const fakeData = ref(new Array(10000).fill('我会一直盯着你'))
console.log(props.ap)
const emit = defineEmits<{
newCollection: []
}>()
const collections = defineModel<collectionType[]>('data')
const currCollection = ref<collectionType>()
const playCollection = (collection: collectionType, index?: number) => {
const ap = props.ap
console.log({ collection, ap })
// clear有bug
while (ap.list.audios.length !== 0) {
ap.list.remove(0)
}
ap.list.add(collection.audios)
index && ap.list.switch(index)
ap.play()
}
const handleCollectionsPlay = (item: collectionType, index?: number) => {
playCollection(item, index)
}
const handleCollectionEnter = (item: collectionType | undefined) => {
currCollection.value = item
}
const handleCollectionDelete = () => {
collections.value &&
collections.value.splice(
collections.value.findIndex((v) => v === currCollection.value),
1
)
currCollection.value = undefined
}
</script>
<style lang="scss" scoped>
.miaoMusic-main {
padding-bottom: 88px;
position: relative;
max-height: 100%;
height: 100%;
.miaoMusic-main-collectionsList {
padding: 15px 20px 88px;
&-title {
font-size: larger;
letter-spacing: 1px;
margin-bottom: 10px;
user-select: none;
}
}
}
.slide-fade-enter-active {
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(200px);
opacity: 0;
}
.isDragging {
opacity: 0.5;
}
</style>
+1
View File
@@ -18,6 +18,7 @@ type apType = {
add: (audios: audioType[]) => void
refreshAudioIndex: () => void
hide: () => void
clear: () => void
}
container: HTMLDivElement
on: (event: string, callback: () => void) => void
@@ -0,0 +1,14 @@
import type { audioType } from './ap'
export type collectionItemType = {} & audioType
export type collectionType = {
id: number
name: string
intro: string
createTime: Date
coverUrl?: string
audios: collectionItemType[]
}
export default collectionType