update music request footer

This commit is contained in:
2023-12-28 12:25:27 +08:00
parent 8c482f8f19
commit 0baa458e89
6 changed files with 333 additions and 218 deletions

View File

@@ -1,8 +1,15 @@
import mitt, { Emitter } from 'mitt'
import { Music } from './store/useMusicRequest';
declare type MittType<T = any> = {
onOpenTemplateSettings: {
template: string,
},
onMusicRequestPlayerEnded: {
music: Music
}
onMusicRequestPlayNextWaitingMusic: {
}
};

View File

@@ -226,7 +226,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'manage-musicRequest',
component: () => import('@/views/open_live/MusicRequest.vue'),
meta: {
title: '点歌 (放歌',
title: '点歌 (点播',
keepAlive: true,
danmaku: true,
},

View File

@@ -0,0 +1,160 @@
import { DanmakuUserInfo, SongFrom, SongsInfo } from '@/api/api-models'
import { useStorage } from '@vueuse/core'
import { useMessage } from 'naive-ui'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export type WaitMusicInfo = {
from: DanmakuUserInfo
music: SongsInfo
}
export type Music = {
id: number
title: string
artist: string
src: string
pic: string
lrc: string
}
export type MusicRequestSettings = {
playMusicWhenFree: boolean
repeat: 'repeat-one' | 'repeat-all' | 'no-repeat'
listMaxHeight: string
shuffle: boolean
volume: number
orderPrefix: string
orderCooldown?: number
orderMusicFirst: boolean
platform: 'netease' | 'kugou'
deviceId?: string
blacklist: string[]
}
export const useMusicRequestProvider = defineStore('MusicRequest', () => {
const waitingMusics = useStorage<WaitMusicInfo[]>('Setting.MusicRequest.Waiting', [])
const originMusics = ref<SongsInfo[]>([])
const aplayerMusics = computed(() => originMusics.value.map((m) => songToMusic(m)))
const currentMusic = ref<Music>({
id: -1,
title: '',
artist: '',
src: '',
pic: '',
lrc: '',
} as Music)
const currentOriginMusic = ref<WaitMusicInfo>()
const isPlayingOrderMusic = ref(false)
const aplayerRef = ref()
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
playMusicWhenFree: true,
repeat: 'repeat-all',
listMaxHeight: '300',
shuffle: true,
volume: 0.5,
orderPrefix: '点歌',
orderCooldown: 600,
orderMusicFirst: true,
platform: 'netease',
blacklist: [],
})
const message = useMessage()
function addWaitingMusic(info: WaitMusicInfo) {
if ((settings.value.orderMusicFirst && !isPlayingOrderMusic.value) || originMusics.value.length == 0 || aplayerRef.value?.audio.paused) {
playMusic(info.music)
console.log(`正在播放 [${info.from.name}] 点的 ${info.music.name} - ${info.music.author?.join('/')}`)
} else {
waitingMusics.value.push(info)
message.success(`[${info.from.name}] 点了一首 ${info.music.name} - ${info.music.author?.join('/')}`)
}
}
function onMusicEnd() {
if (!playWaitingMusic()) {
isPlayingOrderMusic.value = false
if (currentOriginMusic) {
currentOriginMusic.value = undefined
}
setTimeout(() => {
if (!settings.value.playMusicWhenFree) {
message.info('根据配置,已暂停播放音乐')
currentMusic.value = aplayerMusics.value[0]
pauseMusic()
}
}, 1)
}
}
function onMusicPlay() {}
function playWaitingMusic() {
const info = waitingMusics.value.shift()
if (info) {
message.success(`正在播放 [${info.from.name}] 点的 ${info.music.name}`)
console.log(`正在播放 [${info.from.name}] 点的 ${info.music.name}`)
setTimeout(() => {
isPlayingOrderMusic.value = true
const index = waitingMusics.value.indexOf(info)
if (index > -1) {
waitingMusics.value.splice(index, 1)
}
currentOriginMusic.value = info
aplayerRef.value?.pause()
playMusic(info.music)
}, 10)
return true
} else {
return false
}
}
function playMusic(music: SongsInfo) {
//pauseMusic()
currentMusic.value = songToMusic(music)
aplayerRef.value?.thenPlay()
}
function pauseMusic() {
if (!aplayerRef.value?.audio.paused) {
aplayerRef.value?.pause()
}
}
function songToMusic(s: SongsInfo) {
return {
id: s.id,
title: s.name,
artist: s.author?.join('/'),
src: s.from == SongFrom.Netease ? `https://music.163.com/song/media/outer/url?id=${s.id}.mp3` : s.url,
pic: s.cover ?? '',
lrc: '',
} as Music
}
function setSinkId() {
try {
aplayerRef.value?.audio.setSinkId(settings.value.deviceId ?? 'default')
console.log('设置音频输出设备为 ' + (settings.value.deviceId ?? '默认'))
} catch (err) {
console.error(err)
message.error('设置音频输出设备失败: ' + err)
}
}
return {
waitingMusics,
originMusics,
aplayerMusics,
currentMusic,
currentOriginMusic,
isPlayingOrderMusic,
settings,
setSinkId,
playWaitingMusic,
playMusic,
addWaitingMusic,
onMusicEnd,
onMusicPlay,
pauseMusic,
aplayerRef,
}
})

View File

@@ -5,6 +5,7 @@ import { ThemeType } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
import { ACCOUNT_API_URL } from '@/data/constants'
import { useMusicRequestProvider } from '@/store/useMusicRequest'
import {
CalendarClock24Filled,
Chat24Filled,
@@ -28,6 +29,7 @@ import {
NIcon,
NLayout,
NLayoutContent,
NLayoutFooter,
NLayoutHeader,
NLayoutSider,
NMenu,
@@ -37,12 +39,14 @@ import {
NSpace,
NSpin,
NSwitch,
NTag,
NText,
NTooltip,
useMessage,
} from 'naive-ui'
import { computed, h, onMounted, ref } from 'vue'
import { computed, h, onMounted, ref, watch } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import APlayer from 'vue3-aplayer'
import DanmakuLayout from './manage/DanmakuLayout.vue'
const accountInfo = useAccount()
@@ -60,9 +64,18 @@ const type = computed(() => {
return ''
})
const cookie = useStorage('JWT_Token', '')
const musicRquestStore = useMusicRequestProvider()
const canResendEmail = ref(false)
const aplayerHeight = computed(() => {
return musicRquestStore.originMusics.length == 0 ? '0' : '80'
})
const aplayer = ref()
watch(aplayer, () => {
musicRquestStore.aplayerRef = aplayer.value
})
function renderIcon(icon: unknown) {
return () => h(NIcon, null, { default: () => h(icon as any) })
}
@@ -276,7 +289,7 @@ const menuOptions = [
},
),
]),
default: () => accountInfo.value?.isBiliVerified ? '需要使用直播弹幕的功能' : '你尚未进行 Bilibili 认证, 请前往面板进行绑定',
default: () => (accountInfo.value?.isBiliVerified ? '需要使用直播弹幕的功能' : '你尚未进行 Bilibili 认证, 请前往面板进行绑定'),
},
),
key: 'manage-danmaku',
@@ -428,82 +441,103 @@ onMounted(() => {
</template>
</NPageHeader>
</NLayoutHeader>
<NScrollbar x-scrollable>
<NLayout has-sider>
<NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false">
<NSpace justify="center" style="margin-top: 16px">
<NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%">
<template #icon>
<NIcon :component="BrowsersOutline" />
</template>
<template v-if="width >= 180"> 面板 </template>
</NButton>
<NTooltip v-if="width >= 180">
<template #trigger>
<NButton @click="$router.push({ name: 'manage-feedback' })">
<template #icon>
<NIcon :component="PersonFeedback24Filled" />
<NLayout has-sider>
<NLayoutSider ref="sider" bordered show-trigger collapse-mode="width" :default-collapsed="windowWidth < 750" :collapsed-width="64" :width="180" :native-scrollbar="false">
<NSpace justify="center" style="margin-top: 16px">
<NButton @click="$router.push({ name: 'manage-index' })" type="info" style="width: 100%">
<template #icon>
<NIcon :component="BrowsersOutline" />
</template>
<template v-if="width >= 180"> 面板 </template>
</NButton>
<NTooltip v-if="width >= 180">
<template #trigger>
<NButton @click="$router.push({ name: 'manage-feedback' })">
<template #icon>
<NIcon :component="PersonFeedback24Filled" />
</template>
</NButton>
</template>
反馈
</NTooltip>
</NSpace>
<NMenu
style="margin-top: 12px"
:disabled="accountInfo?.isEmailVerified != true"
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
/>
<NSpace v-if="width > 150" justify="center" align="center" vertical>
<NText depth="3">
有更多功能建议请
<NButton text type="info" @click="$router.push({ name: 'manage-feedback' })"> 反馈 </NButton>
</NText>
<NText depth="3">
<NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton>
</NText>
</NSpace>
</NLayoutSider>
<NLayout>
<NScrollbar :style="`height: calc(100vh - 50px - ${aplayerHeight}px)`">
<NLayoutContent style="box-sizing: border-box; padding: 20px; min-width: 300px">
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
<KeepAlive>
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
<Suspense v-else>
<component :is="Component" />
<template #fallback>
<NSpin show />
</template>
</NButton>
</template>
反馈
</NTooltip>
</NSpace>
<NMenu
style="margin-top: 12px"
:disabled="accountInfo?.isEmailVerified != true"
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
/>
<NSpace v-if="width > 150" justify="center" align="center" vertical>
<NText depth="3">
有更多功能建议请
<NButton text type="info" @click="$router.push({ name: 'manage-feedback' })"> 反馈 </NButton>
</NText>
<NText depth="3">
<NButton text type="info" @click="$router.push({ name: 'about' })"> 关于本站 </NButton>
</NText>
</NSpace>
</NLayoutSider>
<NScrollbar style="height: calc(100vh - 50px)">
<NLayout>
<div style="box-sizing: border-box; padding: 20px; min-width: 300px">
<RouterView v-if="accountInfo?.isEmailVerified" v-slot="{ Component, route }">
<KeepAlive>
<DanmakuLayout v-if="route.meta.danmaku" :component="Component" />
<Suspense v-else>
<component :is="Component" />
<template #fallback>
<NSpin show />
</template>
</Suspense>
</KeepAlive>
</RouterView>
<template v-else>
<NAlert type="info">
请进行邮箱验证
<br /><br />
<NSpace>
<NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail"> 重新发送验证邮件 </NButton>
<NCountdown v-if="!canResendEmail" :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()" @finish="canResendEmail = true" />
</Suspense>
</KeepAlive>
</RouterView>
<template v-else>
<NAlert type="info">
请进行邮箱验证
<br /><br />
<NSpace>
<NButton size="small" type="info" :disabled="!canResendEmail" @click="resendEmail"> 重新发送验证邮件 </NButton>
<NCountdown v-if="!canResendEmail" :duration="(accountInfo?.nextSendEmailTime ?? 0) - Date.now()" @finish="canResendEmail = true" />
<NPopconfirm @positive-click="logout" size="small">
<template #trigger>
<NButton type="error"> 登出 </NButton>
</template>
确定登出?
</NPopconfirm>
</NSpace>
</NAlert>
</template>
<NBackTop />
</div>
</NLayout>
<NPopconfirm @positive-click="logout" size="small">
<template #trigger>
<NButton type="error"> 登出 </NButton>
</template>
确定登出?
</NPopconfirm>
</NSpace>
</NAlert>
</template>
<NBackTop />
</NLayoutContent>
</NScrollbar>
<NLayoutFooter :style="`height: ${aplayerHeight}px;overflow: auto`">
<div style="display: flex; align-items: center; margin: 0 10px 0 10px">
<APlayer
v-if="musicRquestStore.aplayerMusics.length > 0"
ref="aplayer"
:list="musicRquestStore.aplayerMusics"
v-model:music="musicRquestStore.currentMusic"
v-model:volume="musicRquestStore.settings.volume"
v-model:shuffle="musicRquestStore.settings.shuffle"
v-model:repeat="musicRquestStore.settings.repeat"
:listMaxHeight="'200'"
mutex
listFolded
@ended="musicRquestStore.onMusicEnd"
@play="musicRquestStore.onMusicPlay"
style="flex: 1;min-width: 400px;"
/>
<NSpace vertical>
<NTag :bordered="false" type="info" size="small"> 队列: {{ musicRquestStore.waitingMusics.length }} </NTag>
<NButton size="small" type="info" @click="musicRquestStore.waitingMusics.length > 0 ? musicRquestStore.onMusicEnd() : musicRquestStore.aplayerRef?.onAudioEnded()"> 下一首 </NButton>
</NSpace>
</div>
</NLayoutFooter>
</NLayout>
</NScrollbar>
</NLayout>
</NLayout>
<template v-else>
<NLayoutContent style="display: flex; justify-content: center; align-items: center; flex-direction: column; padding: 50px; height: 100%; box-sizing: border-box">

View File

@@ -4,6 +4,7 @@ import { DanmakuUserInfo, EventModel, FunctionTypes, SongFrom, SongsInfo } from
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import DanmakuClient, { RoomAuthInfo } from '@/data/DanmakuClient'
import { MUSIC_REQUEST_API_URL, SONG_API_URL } from '@/data/constants'
import { MusicRequestSettings, useMusicRequestProvider } from '@/store/useMusicRequest'
import { useStorage } from '@vueuse/core'
import { List } from 'linqts'
import {
@@ -37,27 +38,10 @@ import {
SelectOption,
useMessage,
} from 'naive-ui'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import APlayer from 'vue3-aplayer'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { clearInterval, setInterval } from 'worker-timers'
import MusicRequestOBS from '../obs/MusicRequestOBS.vue'
type MusicRequestSettings = {
playMusicWhenFree: boolean
repeat: 'repeat-one' | 'repeat-all' | 'no-repeat'
listMaxHeight: string
shuffle: boolean
volume: number
orderPrefix: string
orderCooldown?: number
orderMusicFirst: boolean
platform: 'netease' | 'kugou'
deviceId?: string
blacklist: string[]
}
type Music = {
id: number
title: string
@@ -71,21 +55,11 @@ type WaitMusicInfo = {
music: SongsInfo
}
const settings = useStorage<MusicRequestSettings>('Setting.MusicRequest', {
playMusicWhenFree: true,
repeat: 'repeat-all',
listMaxHeight: '300',
shuffle: true,
volume: 0.5,
orderPrefix: '点歌',
orderCooldown: 600,
orderMusicFirst: true,
platform: 'netease',
blacklist: [],
const settings = computed(() => {
return musicRquestStore.settings
})
const cooldown = useStorage<{ [id: number]: number }>('Setting.MusicRequest.Cooldown', {})
const musicRquestStore = useMusicRequestProvider()
const props = defineProps<{
client: DanmakuClient
@@ -99,28 +73,9 @@ const deviceList = ref<SelectOption[]>([])
const accountInfo = useAccount()
const message = useMessage()
const aplayer = ref()
const listening = ref(false)
const originMusics = ref<SongsInfo[]>(await get())
const waitingMusics = useStorage<WaitMusicInfo[]>('Setting.MusicRequest.Waiting', [])
const musics = computed(() => {
return originMusics.value.map((s) => songToMusic(s))
})
const currentMusic = ref<Music>({
id: -1,
title: '',
artist: '',
src: '',
pic: '',
lrc: '',
} as Music)
const currentOriginMusic = ref<WaitMusicInfo>()
watch(currentOriginMusic, (music) => {
if (music) {
currentMusic.value = songToMusic(music.music)
}
const originMusics = computed(() => {
return musicRquestStore.originMusics
})
const isLoading = ref(false)
@@ -242,7 +197,7 @@ function delMusic(song: SongsInfo) {
.then((data) => {
if (data.code == 200) {
message.success('已删除')
originMusics.value = originMusics.value.filter((s) => s.key != song.key)
musicRquestStore.originMusics = originMusics.value.filter((s) => s.key != song.key)
} else {
message.error('删除失败: ' + data.message)
}
@@ -256,7 +211,7 @@ function clearMusic() {
.then((data) => {
if (data.code == 200) {
message.success('已清空')
originMusics.value = []
musicRquestStore.originMusics = []
} else {
message.error('清空失败: ' + data.message)
}
@@ -284,7 +239,7 @@ async function downloadConfig() {
if (data.msg) {
message.error('获取失败: ' + data.msg)
} else {
settings.value = data.data
musicRquestStore.settings = data.data ?? ({} as MusicRequestSettings)
message.success('已获取配置文件')
}
})
@@ -304,10 +259,9 @@ function stopListen() {
listening.value = false
message.success('已停止监听')
}
const isPlayingOrderMusic = ref(false)
async function onGetEvent(data: EventModel) {
if (!listening.value || !checkMessage(data.msg)) return
if (settings.value.orderCooldown && cooldown.value[data.uid]) {
if (settings.value.orderCooldown && cooldown.value[data.uid] && data.uid != (accountInfo.value?.biliId ?? -1)) {
const lastRequest = cooldown.value[data.uid]
if (Date.now() - lastRequest < settings.value.orderCooldown * 1000) {
message.info(`[${data.name}] 冷却中,距离下次点歌还有 ${((settings.value.orderCooldown * 1000 - (Date.now() - lastRequest)) / 1000).toFixed(1)}`)
@@ -333,63 +287,13 @@ async function onGetEvent(data: EventModel) {
},
music: result,
} as WaitMusicInfo
if ((settings.value.orderMusicFirst && !isPlayingOrderMusic.value) || originMusics.value.length == 0 || aplayer.value?.audio.paused) {
playWaitingMusic(music)
console.log(`正在播放 [${data.name}] 点的 ${result.name} - ${result.author?.join('/')}`)
} else {
waitingMusics.value.push(music)
message.success(`[${data.name}] 点了一首 ${result.name} - ${result.author?.join('/')}`)
}
musicRquestStore.addWaitingMusic(music)
}
}
function checkMessage(msg: string) {
return msg.trim().toLowerCase().startsWith(settings.value.orderPrefix.trimStart())
}
function onMusicEnd() {
const data = waitingMusics.value.shift()
if (data) {
setTimeout(() => {
playWaitingMusic(data)
message.success(`正在播放 [${data.from.name}] 点的 ${data.music.name}`)
console.log(`正在播放 [${data.from.name}] 点的 ${data.music.name}`)
}, 10) //逆天
} else {
isPlayingOrderMusic.value = false
currentOriginMusic.value = undefined
setTimeout(() => {
if (!settings.value.playMusicWhenFree) {
message.info('根据配置,已暂停播放音乐')
pauseMusic()
}
}, 10)
}
}
function playWaitingMusic(music: WaitMusicInfo) {
isPlayingOrderMusic.value = true
const index = waitingMusics.value.indexOf(music)
if (index > -1) {
waitingMusics.value.splice(index, 1)
}
pauseMusic()
currentOriginMusic.value = music
nextTick(() => {
aplayer.value?.play()
})
}
function pauseMusic() {
if (!aplayer.value?.audio.paused) {
aplayer.value?.pause()
}
}
function setSinkId() {
try {
aplayer.value?.audio.setSinkId(settings.value.deviceId ?? 'default')
console.log('设置音频输出设备为 ' + (settings.value.deviceId ?? '默认'))
} catch (err) {
console.error(err)
message.error('设置音频输出设备失败: ' + err)
}
}
function songToMusic(s: SongsInfo) {
return {
id: s.id,
@@ -417,32 +321,34 @@ async function getOutputDevice() {
}
function blockMusic(song: SongsInfo) {
settings.value.blacklist.push(song.name)
waitingMusics.value.splice(
waitingMusics.value.findIndex((m) => m.music == song),
musicRquestStore.waitingMusics.splice(
musicRquestStore.waitingMusics.findIndex((m) => m.music == song),
1,
)
message.success(`[${song.name}] 已添加到黑名单`)
}
function updateWaiting() {
QueryPostAPI(MUSIC_REQUEST_API_URL + 'update-waiting', {
playing: currentOriginMusic.value,
waiting: waitingMusics.value,
playing: musicRquestStore.currentOriginMusic,
waiting: musicRquestStore.waitingMusics,
})
}
let timer: number
onMounted(async () => {
props.client.onEvent('danmaku', onGetEvent)
if (musicRquestStore.originMusics.length == 0) {
musicRquestStore.originMusics = await get()
}
if (originMusics.value.length > 0) {
currentMusic.value = songToMusic(originMusics.value[0])
musicRquestStore.currentMusic = songToMusic(originMusics.value[0])
}
await getOutputDevice()
if (deviceList.value.length > 0) {
if (!deviceList.value.find((d) => d.value == settings.value.deviceId)) {
settings.value.deviceId = undefined
} else {
setSinkId()
musicRquestStore.setSinkId()
}
}
timer = setInterval(updateWaiting, 2000)
@@ -483,26 +389,14 @@ onUnmounted(() => {
</NPopconfirm>
</NSpace>
<NDivider />
<APlayer
v-if="musics.length > 0 || currentMusic.src"
:list="musics"
v-model:music="currentMusic"
ref="aplayer"
v-model:volume="settings.volume"
v-model:shuffle="settings.shuffle"
v-model:repeat="settings.repeat"
:listMaxHeight="'200'"
mutex
@ended="onMusicEnd"
/>
<NCollapse :default-expanded-names="['1']">
<NCollapseItem title="队列" name="1">
<NEmpty v-if="waitingMusics.length == 0"> 暂无 </NEmpty>
<NEmpty v-if="musicRquestStore.waitingMusics.length == 0"> 暂无 </NEmpty>
<NList v-else size="small" bordered>
<NListItem v-for="item in waitingMusics">
<NListItem v-for="item in musicRquestStore.waitingMusics">
<NSpace align="center">
<NButton @click="playWaitingMusic(item)" type="primary" secondary size="small"> 播放 </NButton>
<NButton @click="waitingMusics.splice(waitingMusics.indexOf(item), 1)" type="error" secondary size="small"> 取消 </NButton>
<NButton @click="musicRquestStore.playMusic(item.music)" type="primary" secondary size="small"> 播放 </NButton>
<NButton @click="musicRquestStore.waitingMusics.splice(musicRquestStore.waitingMusics.indexOf(item), 1)" type="error" secondary size="small"> 取消 </NButton>
<NButton @click="blockMusic(item.music)" type="warning" secondary size="small"> 拉黑 </NButton>
<span>
<NTag v-if="item.music.from == SongFrom.Netease" type="success" size="small"> 网易</NTag>
@@ -558,7 +452,13 @@ onUnmounted(() => {
</NSpace>
<NSpace>
<NButton @click="getOutputDevice"> 获取输出设备 </NButton>
<NSelect v-model:value="settings.deviceId" :options="deviceList" :fallback-option="() => ({ label: '未选择', value: '' })" style="min-width: 200px" @update:value="setSinkId" />
<NSelect
v-model:value="settings.deviceId"
:options="deviceList"
:fallback-option="() => ({ label: '未选择', value: '' })"
style="min-width: 200px"
@update:value="musicRquestStore.setSinkId"
/>
</NSpace>
</NSpace>
</NTabPane>
@@ -573,7 +473,7 @@ onUnmounted(() => {
<NButton @click="showNeteaseModal = true"> 从网易云歌单导入 </NButton>
</NSpace>
<NDivider style="margin: 15px 0 10px 0" />
<NEmpty v-if="musics.length == 0"> 暂无 </NEmpty>
<NEmpty v-if="musicRquestStore.originMusics.length == 0"> 暂无 </NEmpty>
<NVirtualList v-else :style="`max-height: 1000px`" :item-size="30" :items="originMusics" item-resizable>
<template #default="{ item, index }">
<p :style="`min-height: ${30}px;width:97%;display:flex;align-items:center;`">
@@ -584,6 +484,8 @@ onUnmounted(() => {
</template>
确定删除?
</NPopconfirm>
<NButton type="info" secondary size="small" @click="musicRquestStore.playMusic(item)"> 播放 </NButton>
<NText> {{ item.name }} - {{ item.author?.join('/') }} </NText>
</NSpace>
</p>

View File

@@ -117,6 +117,7 @@ const voiceOptions = computed(() => {
.ToArray()
})
const isSpeaking = ref(false)
const speakingText = ref('')
const speakQueue = ref<{ updateAt: number; combineCount?: number; data: EventModel }[]>([])
const isVtsuruVoiceAPI = computed(() => {
return settings.value.voiceType == 'api' && settings.value.voiceAPI?.toLowerCase().trim().startsWith('voice.vtsuru.live')
@@ -191,6 +192,7 @@ async function speak() {
if (text) {
isSpeaking.value = true
readedDanmaku.value++
speakingText.value = text
console.log(`[TTS] 正在朗读: ${text}`)
if (checkTimer) {
clearInterval(checkTimer)
@@ -258,7 +260,7 @@ function speakFromAPI(text: string) {
.replace(/\{\{\s*text\s*\}\}/, encodeURIComponent(text))}`
const tempURL = new URL(url)
if (isVtsuruVoiceAPI.value && splitter.countGraphemes(tempURL.searchParams.get('text') ?? '') > 50) {
message.error('本站提供的测试接口字数不允许超过 50 字. 内容: [' + tempURL.searchParams.get('text') + ']')
message.error('本站提供的测试接口字数不允许超过 100 字. 内容: [' + tempURL.searchParams.get('text') + ']')
cancelSpeech()
return
}
@@ -266,6 +268,10 @@ function speakFromAPI(text: string) {
nextTick(() => {
//apiAudio.value?.load()
apiAudio.value?.play().catch((err) => {
if (err.toString().startsWith('AbortError')) {
return
}
console.log(err)
console.log(err)
message.error('无法播放语音:' + err)
cancelSpeech()
@@ -283,8 +289,14 @@ function cancelSpeech() {
checkTimer = undefined
}
isApiAudioLoading.value = false
apiAudio.value?.pause()
pauseAPI()
EasySpeech.cancel()
speakingText.value = ''
}
function pauseAPI() {
if (!apiAudio.value?.paused) {
apiAudio.value?.pause()
}
}
function onGetEvent(data: EventModel) {
if (!canSpeech.value) {
@@ -506,7 +518,7 @@ onUnmounted(() => {
<NAlert v-if="!speechSynthesisInfo || !speechSynthesisInfo.speechSynthesis" type="error"> 你的浏览器不支持语音功能 </NAlert>
<template v-else>
<NSpace vertical>
<NAlert v-if="settings.voiceType == 'local'" type="info" closeable >
<NAlert v-if="settings.voiceType == 'local'" type="info" closeable>
建议在 Edge 浏览器使用
<NTooltip>
<template #trigger>
@@ -671,7 +683,7 @@ onUnmounted(() => {
<NAlert v-if="isVtsuruVoiceAPI" type="success" closable>
看起来你正在使用本站提供的测试API (voice.vtsuru.live), 这个接口将会返回
<NButton text type="info" tag="a" href="https://space.bilibili.com/5859321" target="_blank"> Xz乔希 </NButton>
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试 侵删
训练的 Taffy 模型结果, 不支持部分英文, 仅用于测试, 不保证可用性. 侵删
</NAlert>
</NSpace>
<br />
@@ -689,7 +701,7 @@ onUnmounted(() => {
placeholder="API 地址, 例如 xxx.com/voice/bert-vits2?text={{text}}&id=0 (前面不要带https://)"
:status="/^(?:https?:\/\/)/.test(settings.voiceAPI?.toLowerCase() ?? '') ? 'error' : 'success'"
/>
<NButton @click="testAPI" type="info"> 测试 </NButton>
<NButton @click="testAPI" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
</NInputGroup>
<br /><br />
<NSpace vertical>
@@ -720,22 +732,22 @@ onUnmounted(() => {
<NInputGroup>
<NInputGroupLabel> 弹幕模板 </NInputGroupLabel>
<NInput v-model:value="settings.danmakuTemplate" placeholder="弹幕消息" />
<NButton @click="test(EventDataTypes.Message)" type="info"> 测试 </NButton>
<NButton @click="test(EventDataTypes.Message)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
</NInputGroup>
<NInputGroup>
<NInputGroupLabel> 礼物模板 </NInputGroupLabel>
<NInput v-model:value="settings.giftTemplate" placeholder="礼物消息" />
<NButton @click="test(EventDataTypes.Gift)" type="info"> 测试 </NButton>
<NButton @click="test(EventDataTypes.Gift)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
</NInputGroup>
<NInputGroup>
<NInputGroupLabel> SC模板 </NInputGroupLabel>
<NInput v-model:value="settings.scTemplate" placeholder="SC消息" />
<NButton @click="test(EventDataTypes.SC)" type="info"> 测试 </NButton>
<NButton @click="test(EventDataTypes.SC)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
</NInputGroup>
<NInputGroup>
<NInputGroupLabel> 上舰模板 </NInputGroupLabel>
<NInput v-model:value="settings.guardTemplate" placeholder="上舰消息" />
<NButton @click="test(EventDataTypes.Guard)" type="info"> 测试 </NButton>
<NButton @click="test(EventDataTypes.Guard)" type="info" :loading="isApiAudioLoading"> 测试 </NButton>
</NInputGroup>
</NSpace>
<NDivider> 设置 </NDivider>