mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-10 20:36:55 +08:00
feat: implement speech synthesis for live danmaku events with customizable templates and API support
This commit is contained in:
@@ -14,12 +14,14 @@ import {
|
||||
BarChartOutline,
|
||||
CheckmarkCircleOutline,
|
||||
CloseCircleOutline,
|
||||
CloudDownloadOutline,
|
||||
HardwareChipOutline,
|
||||
HelpCircle,
|
||||
LogInOutline,
|
||||
LogOutOutline,
|
||||
PeopleOutline,
|
||||
PersonCircleOutline,
|
||||
RefreshOutline,
|
||||
TimeOutline,
|
||||
TimerOutline,
|
||||
TrendingUpOutline,
|
||||
@@ -63,6 +65,7 @@ import {
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NPopconfirm,
|
||||
NQrCode,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
@@ -155,6 +158,35 @@ const cookieCloud = useTauriStore().getTarget<CookieCloudConfig>(COOKIE_CLOUD_KE
|
||||
const cookieCloudData = ref((await cookieCloud.get())!)
|
||||
const isLoadingCookiecloud = ref(false)
|
||||
|
||||
// Cookie Cloud 手动操作相关状态
|
||||
const isSyncingFromCloud = ref(false)
|
||||
const isCheckingCookie = ref(false)
|
||||
const lastSyncTime = ref<number>(0)
|
||||
const lastCheckTime = ref<number>(0)
|
||||
const COOLDOWN_DURATION = 5 * 1000 // 30秒冷却时间
|
||||
|
||||
// 计算剩余冷却时间
|
||||
const syncCooldownRemaining = ref(0)
|
||||
const checkCooldownRemaining = ref(0)
|
||||
let syncCooldownTimer: number | undefined
|
||||
let checkCooldownTimer: number | undefined
|
||||
|
||||
// 更新冷却时间显示
|
||||
function updateCooldowns() {
|
||||
const now = Date.now()
|
||||
syncCooldownRemaining.value = Math.max(0, Math.ceil((lastSyncTime.value + COOLDOWN_DURATION - now) / 1000))
|
||||
checkCooldownRemaining.value = Math.max(0, Math.ceil((lastCheckTime.value + COOLDOWN_DURATION - now) / 1000))
|
||||
}
|
||||
|
||||
// 启动冷却计时器
|
||||
function startCooldownTimers() {
|
||||
if (syncCooldownTimer) clearInterval(syncCooldownTimer)
|
||||
if (checkCooldownTimer) clearInterval(checkCooldownTimer)
|
||||
|
||||
syncCooldownTimer = window.setInterval(updateCooldowns, 1000)
|
||||
checkCooldownTimer = window.setInterval(updateCooldowns, 1000)
|
||||
}
|
||||
|
||||
async function setCookieCloud() {
|
||||
try {
|
||||
isLoadingCookiecloud.value = true
|
||||
@@ -167,6 +199,60 @@ async function setCookieCloud() {
|
||||
}
|
||||
}
|
||||
|
||||
// 手动从 Cookie Cloud 同步
|
||||
async function manualSyncFromCloud() {
|
||||
const now = Date.now()
|
||||
if (now - lastSyncTime.value < COOLDOWN_DURATION) {
|
||||
message.warning(`请等待 ${syncCooldownRemaining.value} 秒后再试`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSyncingFromCloud.value = true
|
||||
await biliCookie.check(true) // 强制从 CookieCloud 同步
|
||||
lastSyncTime.value = Date.now()
|
||||
updateCooldowns()
|
||||
|
||||
if (biliCookie.isCookieValid) {
|
||||
message.success('Cookie 同步成功')
|
||||
} else {
|
||||
message.error('Cookie 同步失败或无效')
|
||||
}
|
||||
} catch (err: any) {
|
||||
logError(`手动同步 Cookie 失败: ${err}`)
|
||||
message.error(`同步失败: ${err.message || '未知错误'}`)
|
||||
} finally {
|
||||
isSyncingFromCloud.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 手动检查 Cookie 有效性
|
||||
async function manualCheckCookie() {
|
||||
const now = Date.now()
|
||||
if (now - lastCheckTime.value < COOLDOWN_DURATION) {
|
||||
message.warning(`请等待 ${checkCooldownRemaining.value} 秒后再试`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingCookie.value = true
|
||||
await biliCookie.check(false) // 只检查本地 Cookie
|
||||
lastCheckTime.value = Date.now()
|
||||
updateCooldowns()
|
||||
|
||||
if (biliCookie.isCookieValid) {
|
||||
message.success('Cookie 有效')
|
||||
} else {
|
||||
message.error('Cookie 已失效')
|
||||
}
|
||||
} catch (err: any) {
|
||||
logError(`手动检查 Cookie 失败: ${err}`)
|
||||
message.error(`检查失败: ${err.message || '未知错误'}`)
|
||||
} finally {
|
||||
isCheckingCookie.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Timers ---
|
||||
let uptimeTimer: number | undefined
|
||||
let epsTimer: number | undefined
|
||||
@@ -206,8 +292,8 @@ const danmakuClientStateText = computed(() => {
|
||||
const danmakuClientStateType = computed(() => {
|
||||
switch (webfetcher.danmakuClientState) { // Replace with actual exposed state
|
||||
case 'connected': return 'success'
|
||||
case 'connecting': 'info'
|
||||
case 'stopped': 'error'
|
||||
case 'connecting': return 'info'
|
||||
case 'stopped': return 'error'
|
||||
default: return 'default'
|
||||
}
|
||||
})
|
||||
@@ -594,6 +680,9 @@ onMounted(async () => {
|
||||
|
||||
// Initialize statistics logic (ensure it runs)
|
||||
// initInfo(); // Assuming this is called elsewhere or on app startup
|
||||
|
||||
// 启动冷却计时器
|
||||
startCooldownTimers()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -603,6 +692,9 @@ onUnmounted(() => {
|
||||
clearInterval(timer.value)
|
||||
clearTimeout(expiredTimer.value)
|
||||
clearInterval(countdownTimer.value)
|
||||
// Clean up cooldown timers
|
||||
clearInterval(syncCooldownTimer)
|
||||
clearInterval(checkCooldownTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -940,8 +1032,14 @@ onUnmounted(() => {
|
||||
style="width: 100%; max-width: 800px; margin-bottom: 1rem;"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag>
|
||||
{{ }}
|
||||
<NTag
|
||||
:type="biliCookie.cookieCloudState === 'valid' ? 'success' : biliCookie.cookieCloudState === 'syncing' ? 'info' : biliCookie.cookieCloudState === 'invalid' ? 'error' : 'default'"
|
||||
>
|
||||
{{
|
||||
biliCookie.cookieCloudState === 'valid' ? '已配置' :
|
||||
biliCookie.cookieCloudState === 'syncing' ? '同步中' :
|
||||
biliCookie.cookieCloudState === 'invalid' ? '配置无效' : '未配置'
|
||||
}}
|
||||
</NTag>
|
||||
</template>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@@ -980,30 +1078,67 @@ onUnmounted(() => {
|
||||
placeholder="请输入 Host (可选)"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NButton
|
||||
v-if="biliCookie.cookieCloudState === 'invalid' || biliCookie.cookieCloudState === 'unset'"
|
||||
type="primary"
|
||||
:loading="isLoadingCookiecloud"
|
||||
@click="setCookieCloud"
|
||||
>
|
||||
保存配置
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
v-else
|
||||
type="error"
|
||||
@positive-click="async () => {
|
||||
await biliCookie.clearCookieCloudConfig();
|
||||
cookieCloudData = { key: '', password: '' };
|
||||
message.success('配置已清除');
|
||||
}"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton type="error">
|
||||
清除配置
|
||||
</NButton>
|
||||
</template>
|
||||
确定要清除配置吗?
|
||||
</NPopconfirm>
|
||||
<NFlex gap="small">
|
||||
<NButton
|
||||
v-if="biliCookie.cookieCloudState === 'invalid' || biliCookie.cookieCloudState === 'unset'"
|
||||
type="primary"
|
||||
:loading="isLoadingCookiecloud"
|
||||
@click="setCookieCloud"
|
||||
>
|
||||
保存配置
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
v-else
|
||||
type="error"
|
||||
@positive-click="async () => {
|
||||
await biliCookie.clearCookieCloudConfig();
|
||||
cookieCloudData = { key: '', password: '', host: 'https://cookie.vtsuru.live' };
|
||||
message.success('配置已清除');
|
||||
}"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton type="error">
|
||||
清除配置
|
||||
</NButton>
|
||||
</template>
|
||||
确定要清除配置吗?
|
||||
</NPopconfirm>
|
||||
</NFlex>
|
||||
<NDivider style="margin: 8px 0;">
|
||||
手动操作
|
||||
</NDivider>
|
||||
<NFlex gap="small">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:disabled="biliCookie.cookieCloudState !== 'valid' || syncCooldownRemaining > 0"
|
||||
:loading="isSyncingFromCloud"
|
||||
@click="manualSyncFromCloud"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="CloudDownloadOutline" />
|
||||
</template>
|
||||
{{ syncCooldownRemaining > 0 ? `同步 Cookie (${syncCooldownRemaining}s)` : '从云端同步 Cookie' }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ biliCookie.cookieCloudState !== 'valid' ? '请先配置有效的 Cookie Cloud' : syncCooldownRemaining > 0 ? `请等待 ${syncCooldownRemaining} 秒` : '手动从 Cookie Cloud 拉取最新的 Cookie' }}
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:disabled="!biliCookie.hasBiliCookie || checkCooldownRemaining > 0"
|
||||
:loading="isCheckingCookie"
|
||||
@click="manualCheckCookie"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="RefreshOutline" />
|
||||
</template>
|
||||
{{ checkCooldownRemaining > 0 ? `检查状态 (${checkCooldownRemaining}s)` : '检查 Cookie 状态' }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ !biliCookie.hasBiliCookie ? '当前没有 Cookie' : checkCooldownRemaining > 0 ? `请等待 ${checkCooldownRemaining} 秒` : '手动检查当前 Cookie 的有效性' }}
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</div>
|
||||
</NCard>
|
||||
</NTabPane>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { MenuOption } from 'naive-ui'
|
||||
// 引入 Tauri 插件
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
|
||||
import { Chat24Filled, CloudArchive24Filled, FlashAuto24Filled, Settings24Filled } from '@vicons/fluent'
|
||||
import { Chat24Filled, CloudArchive24Filled, FlashAuto24Filled, Mic24Filled, Settings24Filled } from '@vicons/fluent'
|
||||
import { CheckmarkCircle, CloseCircle, Home } from '@vicons/ionicons5'
|
||||
import { NA, NButton, NCard, NInput, NLayout, NLayoutContent, NLayoutSider, NMenu, NSpace, NSpin, NText, NTooltip } from 'naive-ui'
|
||||
|
||||
@@ -116,6 +116,12 @@ const menuOptions = computed(() => {
|
||||
key: 'danmaku-auto-action-manage',
|
||||
icon: () => h(FlashAuto24Filled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: { name: 'client-read-danmaku' } }, () => '读弹幕'),
|
||||
key: 'read-danmaku',
|
||||
icon: () => h(Mic24Filled),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: { name: 'client-settings' } }, () => '设置'),
|
||||
|
||||
1246
src/client/ClientReadDanmaku.vue
Normal file
1246
src/client/ClientReadDanmaku.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -173,7 +173,7 @@ const highlightPatterns = computed(() => {
|
||||
return allPatterns
|
||||
})
|
||||
|
||||
const MAX_LENGTH = 20
|
||||
const MAX_LENGTH = 40
|
||||
const WARNING_THRESHOLD = 16
|
||||
|
||||
function evaluateTemplateForUI(template: string): string {
|
||||
|
||||
@@ -36,7 +36,9 @@ let updateNotificationRef: any = null
|
||||
|
||||
async function sendHeartbeat() {
|
||||
try {
|
||||
await invoke('heartbeat')
|
||||
await invoke('heartbeat', undefined, {
|
||||
headers: [['Origin', location.host]]
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('发送心跳失败:', error)
|
||||
}
|
||||
@@ -88,10 +90,10 @@ async function checkUpdatePeriodically() {
|
||||
try {
|
||||
info('[更新检查] 开始检查更新...')
|
||||
const update = await check()
|
||||
|
||||
|
||||
if (update) {
|
||||
info(`[更新检查] 发现新版本: ${update.version}`)
|
||||
|
||||
|
||||
// 发送 Windows 通知
|
||||
const permissionGranted = await isPermissionGranted()
|
||||
if (permissionGranted) {
|
||||
@@ -100,7 +102,7 @@ async function checkUpdatePeriodically() {
|
||||
body: `发现新版本 ${update.version},点击通知查看详情`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 显示不可关闭的 NaiveUI notification
|
||||
if (!updateNotificationRef) {
|
||||
updateNotificationRef = window.$notification.warning({
|
||||
@@ -113,9 +115,11 @@ async function checkUpdatePeriodically() {
|
||||
'button',
|
||||
{
|
||||
class: 'n-button n-button--primary-type n-button--small-type',
|
||||
onClick: () => { void handleUpdateInstall(update) },
|
||||
onClick: () => {
|
||||
void handleUpdateInstall(update)
|
||||
},
|
||||
},
|
||||
'立即更新'
|
||||
'立即更新',
|
||||
),
|
||||
h(
|
||||
'button',
|
||||
@@ -123,7 +127,7 @@ async function checkUpdatePeriodically() {
|
||||
class: 'n-button n-button--default-type n-button--small-type',
|
||||
onClick: () => handleUpdateDismiss(),
|
||||
},
|
||||
'稍后提醒'
|
||||
'稍后提醒',
|
||||
),
|
||||
])
|
||||
},
|
||||
@@ -146,7 +150,7 @@ async function handleUpdateInstall(update: any) {
|
||||
updateNotificationRef.destroy()
|
||||
updateNotificationRef = null
|
||||
}
|
||||
|
||||
|
||||
// 显示下载进度通知
|
||||
let downloaded = 0
|
||||
let contentLength = 0
|
||||
@@ -156,7 +160,7 @@ async function handleUpdateInstall(update: any) {
|
||||
closable: false,
|
||||
duration: 0,
|
||||
})
|
||||
|
||||
|
||||
info('[更新] 开始下载并安装更新')
|
||||
await update.downloadAndInstall((event: any) => {
|
||||
switch (event.event) {
|
||||
@@ -177,16 +181,16 @@ async function handleUpdateInstall(update: any) {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
progressNotification.destroy()
|
||||
info('[更新] 更新安装完成,准备重启应用')
|
||||
|
||||
|
||||
window.$notification.success({
|
||||
title: '更新完成',
|
||||
content: '应用将在 3 秒后重启',
|
||||
duration: 3000,
|
||||
})
|
||||
|
||||
|
||||
// 延迟 3 秒后重启
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
await relaunch()
|
||||
@@ -328,13 +332,13 @@ export async function initAll(isOnBoot: boolean) {
|
||||
useAutoAction().init()
|
||||
useBiliFunction().init()
|
||||
|
||||
//startHeartbeat()
|
||||
|
||||
// startHeartbeat()
|
||||
|
||||
// 启动定期更新检查
|
||||
if (!isDev) {
|
||||
startUpdateCheck()
|
||||
}
|
||||
|
||||
|
||||
clientInited.value = true
|
||||
}
|
||||
export function OnClientUnmounted() {
|
||||
|
||||
Reference in New Issue
Block a user