feat: 添加弹幕窗口管理功能;优化弹幕客户端连接逻辑;实现自动滚动和设置更新; 修复浏览页页面切换的问题

This commit is contained in:
2025-04-14 17:05:13 +08:00
parent c13fcb90c8
commit ff755afd99
11 changed files with 195 additions and 139 deletions

View File

@@ -3,7 +3,8 @@ import { useDanmakuClient } from '@/store/useDanmakuClient';
import { DANMAKU_WINDOW_BROADCAST_CHANNEL, DanmakuWindowBCData, DanmakuWindowSettings } from './store/useDanmakuWindow'; import { DANMAKU_WINDOW_BROADCAST_CHANNEL, DanmakuWindowBCData, DanmakuWindowSettings } from './store/useDanmakuWindow';
import { NSpin, NEmpty, NIcon } from 'naive-ui'; import { NSpin, NEmpty, NIcon } from 'naive-ui';
import { EventDataTypes, EventModel } from '@/api/api-models'; import { EventDataTypes, EventModel } from '@/api/api-models';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; // Import nextTick
import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
import { TransitionGroup } from 'vue'; import { TransitionGroup } from 'vue';
import { Money24Regular, VehicleShip24Filled } from '@vicons/fluent'; import { Money24Regular, VehicleShip24Filled } from '@vicons/fluent';
@@ -11,6 +12,8 @@ let bc: BroadcastChannel | undefined = undefined;
const setting = ref<DanmakuWindowSettings>(); const setting = ref<DanmakuWindowSettings>();
const danmakuList = ref<EventModel[]>([]); const danmakuList = ref<EventModel[]>([]);
const maxItems = computed(() => setting.value?.maxDanmakuCount || 50); const maxItems = computed(() => setting.value?.maxDanmakuCount || 50);
// Ref for the scroll container
const scrollContainerRef = ref<HTMLElement | null>(null);
const isConnected = computed(() => { const isConnected = computed(() => {
return setting.value !== undefined; return setting.value !== undefined;
@@ -49,8 +52,8 @@ function formatUsername(item: EventModel): string {
function addDanmaku(data: EventModel) { function addDanmaku(data: EventModel) {
if (!setting.value) return; if (!setting.value) return;
// 检查是否是需要过滤的消息类型 // Map EventDataTypes enum values to the string values used in filterTypes
const typeMap: Record<number, string> = { const typeToStringMap: { [key in EventDataTypes]?: string } = {
[EventDataTypes.Message]: "Message", [EventDataTypes.Message]: "Message",
[EventDataTypes.Gift]: "Gift", [EventDataTypes.Gift]: "Gift",
[EventDataTypes.SC]: "SC", [EventDataTypes.SC]: "SC",
@@ -58,12 +61,30 @@ function addDanmaku(data: EventModel) {
[EventDataTypes.Enter]: "Enter" [EventDataTypes.Enter]: "Enter"
}; };
const typeStr = typeMap[data.type]; const typeStr = typeToStringMap[data.type];
// Check if the type should be filtered out
if (!typeStr || !setting.value.filterTypes.includes(typeStr)) { if (!typeStr || !setting.value.filterTypes.includes(typeStr)) {
return; return; // Don't add if filtered
} }
// 维护最大消息数量 // --- Auto Scroll Logic ---
const el = scrollContainerRef.value;
let shouldScroll = false;
if (el) {
const threshold = 5; // Pixels threshold to consider "at the end"
if (setting.value?.reverseOrder) {
// Check if scrolled to the top before adding
shouldScroll = el.scrollTop <= threshold;
} else {
// Check if scrolled to the bottom before adding
shouldScroll = el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
}
}
// --- End Auto Scroll Logic ---
// Maintain max message count
if (setting.value.reverseOrder) { if (setting.value.reverseOrder) {
danmakuList.value.unshift(data); danmakuList.value.unshift(data);
if (danmakuList.value.length > maxItems.value) { if (danmakuList.value.length > maxItems.value) {
@@ -75,18 +96,47 @@ function addDanmaku(data: EventModel) {
danmakuList.value.shift(); danmakuList.value.shift();
} }
} }
// --- Auto Scroll Execution ---
if (shouldScroll && el) {
nextTick(() => {
if (setting.value?.reverseOrder) {
el.scrollTop = 0; // Scroll to top
} else {
el.scrollTop = el.scrollHeight; // Scroll to bottom
}
});
}
// --- End Auto Scroll Execution ---
} }
onMounted(() => { onMounted(() => {
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL); bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
console.log(`[DanmakuWindow] BroadcastChannel 已创建: ${DANMAKU_WINDOW_BROADCAST_CHANNEL}`);
bc.postMessage({
type: 'window-ready',
})
bc.onmessage = (event) => { bc.onmessage = (event) => {
const data = event.data as DanmakuWindowBCData; const data = event.data as DanmakuWindowBCData;
switch (data.type) { switch (data.type) {
case 'danmaku': case 'danmaku':
addDanmaku(data.data); addDanmaku(data.data); // addDanmaku now handles scrolling
// console.log('[DanmakuWindow] 收到弹幕:', data.data); // Keep console logs minimal if not debugging
break; break;
case 'update-setting': case 'update-setting':
setting.value = data.data; setting.value = data.data;
console.log('[DanmakuWindow] 设置已更新:', data.data);
// Adjust scroll on setting change if needed (e.g., reverseOrder changes)
nextTick(() => {
const el = scrollContainerRef.value;
if (el) {
if (setting.value?.reverseOrder) {
el.scrollTop = 0;
} else {
el.scrollTop = el.scrollHeight;
}
}
});
break; break;
} }
}; };
@@ -143,6 +193,7 @@ function formatMessage(item: EventModel): string {
}" }"
> >
<TransitionGroup <TransitionGroup
ref="scrollContainerRef"
:class="['danmaku-list', {'reverse': setting?.reverseOrder}]" :class="['danmaku-list', {'reverse': setting?.reverseOrder}]"
name="danmaku-list" name="danmaku-list"
tag="div" tag="div"
@@ -158,6 +209,7 @@ function formatMessage(item: EventModel): string {
<div <div
v-for="(item, index) in danmakuList" v-for="(item, index) in danmakuList"
:key="`${item.time}-${index}`" :key="`${item.time}-${index}`"
:data-type="item.type"
class="danmaku-item" class="danmaku-item"
:style="{ :style="{
marginBottom: `${setting?.itemSpacing || 5}px`, marginBottom: `${setting?.itemSpacing || 5}px`,
@@ -235,7 +287,11 @@ function formatMessage(item: EventModel): string {
</div> </div>
</template> </template>
<style scoped> <style>
html, body{
background: transparent;
}
.danmaku-list-enter-active, .danmaku-list-enter-active,
.danmaku-list-leave-active { .danmaku-list-leave-active {
transition: all 0.3s ease; transition: all 0.3s ease;
@@ -283,6 +339,10 @@ function formatMessage(item: EventModel): string {
color: #9d78c1; color: #9d78c1;
} }
.danmaku-item[data-type="3"] { /* Guard */
color: #9d78c1;
}
.danmaku-item[data-type="4"] { /* Enter */ .danmaku-item[data-type="4"] { /* Enter */
color: #4caf50; color: #4caf50;
} }

View File

@@ -28,6 +28,12 @@ async function testNotification() {
> >
测试通知 测试通知
</NButton> </NButton>
<NButton
type="primary"
@click="$router.push({ name: 'client-danmaku-window-manage'})"
>
弹幕机
</NButton>
<LabelItem label="关闭弹幕客户端"> <LabelItem label="关闭弹幕客户端">
<NSwitch <NSwitch
v-model:value="setting.settings.dev_disableDanmakuClient" v-model:value="setting.settings.dev_disableDanmakuClient"

View File

@@ -58,33 +58,17 @@ const presets = {
// 应用预设 // 应用预设
function applyPreset(preset: 'dark' | 'light' | 'transparent') { function applyPreset(preset: 'dark' | 'light' | 'transparent') {
const presetData = presets[preset]; const presetData = presets[preset];
danmakuWindow.updateSetting('backgroundColor', presetData.backgroundColor); danmakuWindow.danmakuWindowSetting.backgroundColor = presetData.backgroundColor;
danmakuWindow.updateSetting('textColor', presetData.textColor); danmakuWindow.danmakuWindowSetting.textColor = presetData.textColor;
danmakuWindow.updateSetting('shadowColor', presetData.shadowColor); danmakuWindow.danmakuWindowSetting.shadowColor = presetData.shadowColor;
message.success(`已应用${preset === 'dark' ? '暗黑' : preset === 'light' ? '明亮' : '透明'}主题预设`); message.success(`已应用${preset === 'dark' ? '暗黑' : preset === 'light' ? '明亮' : '透明'}主题预设`);
} }
// 重置位置到屏幕中央 // 重置位置到屏幕中央
async function resetPosition() { async function resetPosition() {
// 假设屏幕尺寸为 1920x1080将窗口居中 danmakuWindow.setDanmakuWindowPosition(0, 0);
const width = danmakuWindow.danmakuWindowSetting.width;
const height = danmakuWindow.danmakuWindowSetting.height;
// 计算居中位置
const x = Math.floor((1920 - width) / 2);
const y = Math.floor((1080 - height) / 2);
danmakuWindow.setDanmakuWindowPosition(x, y);
message.success('窗口位置已重置'); message.success('窗口位置已重置');
} }
// 更新设置包装了updateSetting方法
function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting>(
key: K,
value: typeof danmakuWindow.danmakuWindowSetting[K]
) {
danmakuWindow.updateSetting(key, value);
}
</script> </script>
<template> <template>
@@ -95,7 +79,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<template #header-extra> <template #header-extra>
<NButton <NButton
:type="danmakuWindow.isDanmakuWindowOpen ? 'warning' : 'primary'" :type="danmakuWindow.isDanmakuWindowOpen ? 'warning' : 'primary'"
@click="danmakuWindow.isDanmakuWindowOpen ? danmakuWindow.closeWindow() : danmakuWindow.createWindow()" @click="danmakuWindow.isDanmakuWindowOpen ? danmakuWindow.closeWindow() : danmakuWindow.openWindow()"
> >
{{ danmakuWindow.isDanmakuWindowOpen ? '关闭弹幕窗口' : '打开弹幕窗口' }} {{ danmakuWindow.isDanmakuWindowOpen ? '关闭弹幕窗口' : '打开弹幕窗口' }}
</NButton> </NButton>
@@ -136,7 +120,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
v-model:value="danmakuWindow.danmakuWindowSetting.width" v-model:value="danmakuWindow.danmakuWindowSetting.width"
:min="200" :min="200"
:max="2000" :max="2000"
@update:value="val => updateSetting('width', val || 400)" @update:value="(value) => danmakuWindow.setDanmakuWindowSize(value as number, danmakuWindow.danmakuWindowSetting.height)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -146,7 +130,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
v-model:value="danmakuWindow.danmakuWindowSetting.height" v-model:value="danmakuWindow.danmakuWindowSetting.height"
:min="200" :min="200"
:max="2000" :max="2000"
@update:value="val => updateSetting('height', val || 600)" @update:value="(value) => danmakuWindow.setDanmakuWindowSize(danmakuWindow.danmakuWindowSetting.width, value as number)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -155,7 +139,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NInputNumber <NInputNumber
v-model:value="danmakuWindow.danmakuWindowSetting.x" v-model:value="danmakuWindow.danmakuWindowSetting.x"
:min="0" :min="0"
@update:value="val => updateSetting('x', val || 0)" @update:value="() => danmakuWindow.updateWindowPosition()"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -164,7 +148,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NInputNumber <NInputNumber
v-model:value="danmakuWindow.danmakuWindowSetting.y" v-model:value="danmakuWindow.danmakuWindowSetting.y"
:min="0" :min="0"
@update:value="val => updateSetting('y', val || 0)" @update:value="() => danmakuWindow.updateWindowPosition()"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -188,13 +172,11 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NFormItem label="总是置顶"> <NFormItem label="总是置顶">
<NSwitch <NSwitch
v-model:value="danmakuWindow.danmakuWindowSetting.alwaysOnTop" v-model:value="danmakuWindow.danmakuWindowSetting.alwaysOnTop"
@update:value="val => updateSetting('alwaysOnTop', val)"
/> />
</NFormItem> </NFormItem>
<NFormItem label="鼠标穿透"> <NFormItem label="鼠标穿透">
<NSwitch <NSwitch
v-model:value="danmakuWindow.danmakuWindowSetting.interactive" v-model:value="danmakuWindow.danmakuWindowSetting.interactive"
@update:value="val => updateSetting('interactive', val)"
/> />
</NFormItem> </NFormItem>
</NSpace> </NSpace>
@@ -217,7 +199,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NColorPicker <NColorPicker
v-model:value="danmakuWindow.danmakuWindowSetting.backgroundColor" v-model:value="danmakuWindow.danmakuWindowSetting.backgroundColor"
:show-alpha="true" :show-alpha="true"
@update:value="val => updateSetting('backgroundColor', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -226,7 +207,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NColorPicker <NColorPicker
v-model:value="danmakuWindow.danmakuWindowSetting.textColor" v-model:value="danmakuWindow.danmakuWindowSetting.textColor"
:show-alpha="true" :show-alpha="true"
@update:value="val => updateSetting('textColor', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -237,7 +217,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:min="0.1" :min="0.1"
:max="1" :max="1"
:step="0.05" :step="0.05"
@update:value="val => updateSetting('opacity', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -248,7 +227,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:min="10" :min="10"
:max="24" :max="24"
:step="1" :step="1"
@update:value="val => updateSetting('fontSize', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -259,7 +237,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:min="0" :min="0"
:max="20" :max="20"
:step="1" :step="1"
@update:value="val => updateSetting('borderRadius', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -270,7 +247,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:min="0" :min="0"
:max="20" :max="20"
:step="1" :step="1"
@update:value="val => updateSetting('itemSpacing', val)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -284,7 +260,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NFormItem label="启用阴影"> <NFormItem label="启用阴影">
<NSwitch <NSwitch
v-model:value="danmakuWindow.danmakuWindowSetting.enableShadow" v-model:value="danmakuWindow.danmakuWindowSetting.enableShadow"
@update:value="val => updateSetting('enableShadow', val)"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
@@ -294,7 +269,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NColorPicker <NColorPicker
v-model:value="danmakuWindow.danmakuWindowSetting.shadowColor" v-model:value="danmakuWindow.danmakuWindowSetting.shadowColor"
:show-alpha="true" :show-alpha="true"
@update:value="val => updateSetting('shadowColor', val)"
/> />
</NFormItem> </NFormItem>
</NSpace> </NSpace>
@@ -331,15 +305,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:key="option.value" :key="option.value"
:value="option.value" :value="option.value"
:label="option.label" :label="option.label"
@update:checked="val => {
let types = [...danmakuWindow.danmakuWindowSetting.filterTypes];
if (val) {
if (!types.includes(option.value)) types.push(option.value);
} else {
types = types.filter(t => t !== option.value);
}
updateSetting('filterTypes', types);
}"
/> />
</NSpace> </NSpace>
</NCheckboxGroup> </NCheckboxGroup>
@@ -351,25 +316,21 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NSpace> <NSpace>
<NCheckbox <NCheckbox
:checked="danmakuWindow.danmakuWindowSetting.showAvatar" :checked="danmakuWindow.danmakuWindowSetting.showAvatar"
@update:checked="val => updateSetting('showAvatar', val)"
> >
显示头像 显示头像
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox
:checked="danmakuWindow.danmakuWindowSetting.showUsername" :checked="danmakuWindow.danmakuWindowSetting.showUsername"
@update:checked="val => updateSetting('showUsername', val)"
> >
显示用户名 显示用户名
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox
:checked="danmakuWindow.danmakuWindowSetting.showFansMedal" :checked="danmakuWindow.danmakuWindowSetting.showFansMedal"
@update:checked="val => updateSetting('showFansMedal', val)"
> >
显示粉丝牌 显示粉丝牌
</NCheckbox> </NCheckbox>
<NCheckbox <NCheckbox
:checked="danmakuWindow.danmakuWindowSetting.showGuardIcon" :checked="danmakuWindow.danmakuWindowSetting.showGuardIcon"
@update:checked="val => updateSetting('showGuardIcon', val)"
> >
显示舰长图标 显示舰长图标
</NCheckbox> </NCheckbox>
@@ -383,7 +344,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
<NText>从上往下</NText> <NText>从上往下</NText>
<NSwitch <NSwitch
v-model:value="danmakuWindow.danmakuWindowSetting.reverseOrder" v-model:value="danmakuWindow.danmakuWindowSetting.reverseOrder"
@update:value="val => updateSetting('reverseOrder', val)"
/> />
<NText>从下往上</NText> <NText>从下往上</NText>
</NSpace> </NSpace>
@@ -396,7 +356,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
v-model:value="danmakuWindow.danmakuWindowSetting.maxDanmakuCount" v-model:value="danmakuWindow.danmakuWindowSetting.maxDanmakuCount"
:min="10" :min="10"
:max="200" :max="200"
@update:value="val => updateSetting('maxDanmakuCount', val || 50)"
/> />
</NFormItem> </NFormItem>
</NGi> </NGi>
@@ -408,7 +367,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
:min="0" :min="0"
:max="1000" :max="1000"
:step="50" :step="50"
@update:value="val => updateSetting('animationDuration', val || 300)"
> >
<template #suffix> <template #suffix>
ms ms

View File

@@ -1,11 +1,7 @@
import { EventDataTypes, EventModel } from "@/api/api-models"; import { EventDataTypes, EventModel } from "@/api/api-models";
import { CURRENT_HOST } from "@/data/constants";
import { useDanmakuClient } from "@/store/useDanmakuClient"; import { useDanmakuClient } from "@/store/useDanmakuClient";
import { useWebFetcher } from "@/store/useWebFetcher";
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi"; import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
import { Webview } from "@tauri-apps/api/webview"; import { getAllWebviewWindows, WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Window } from "@tauri-apps/api/window";
export type DanmakuWindowSettings = { export type DanmakuWindowSettings = {
width: number; width: number;
@@ -39,10 +35,12 @@ export type DanmakuWindowBCData = {
} | { } | {
type: 'update-setting', type: 'update-setting',
data: DanmakuWindowSettings; data: DanmakuWindowSettings;
} | {
type: 'window-ready';
}; };
export const useDanmakuWindow = defineStore('danmakuWindow', () => { export const useDanmakuWindow = defineStore('danmakuWindow', () => {
const danmakuWindow = ref<Webview>(); const danmakuWindow = ref<WebviewWindow>();
const danmakuWindowSetting = useStorage<DanmakuWindowSettings>('Setting.DanmakuWindow', { const danmakuWindowSetting = useStorage<DanmakuWindowSettings>('Setting.DanmakuWindow', {
width: 400, width: 400,
height: 600, height: 600,
@@ -68,15 +66,19 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
shadowColor: 'rgba(0,0,0,0.5)' shadowColor: 'rgba(0,0,0,0.5)'
}); });
const danmakuClient = useDanmakuClient(); const danmakuClient = useDanmakuClient();
const isWindowOpened = ref(false);
let bc: BroadcastChannel | undefined = undefined; let bc: BroadcastChannel | undefined = undefined;
function hideWindow() {
danmakuWindow.value?.window.hide();
danmakuWindow.value = undefined;
}
function closeWindow() { function closeWindow() {
danmakuWindow.value?.close(); danmakuWindow.value?.hide();
danmakuWindow.value = undefined; isWindowOpened.value = false;
}
function openWindow() {
if (!isInited) {
init();
}
danmakuWindow.value?.show();
isWindowOpened.value = true;
} }
function setDanmakuWindowSize(width: number, height: number) { function setDanmakuWindowSize(width: number, height: number) {
@@ -90,47 +92,58 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
danmakuWindowSetting.value.y = y; danmakuWindowSetting.value.y = y;
danmakuWindow.value?.setPosition(new PhysicalPosition(x, y)); danmakuWindow.value?.setPosition(new PhysicalPosition(x, y));
} }
function updateWindowPosition() {
function updateSetting<K extends keyof DanmakuWindowSettings>(key: K, value: DanmakuWindowSettings[K]) { danmakuWindow.value?.setPosition(new PhysicalPosition(danmakuWindowSetting.value.x, danmakuWindowSetting.value.y));
danmakuWindowSetting.value[key] = value;
// 特定设置需要直接应用到窗口
if (key === 'alwaysOnTop' && danmakuWindow.value) {
danmakuWindow.value.window.setAlwaysOnTop(value as boolean);
}
if (key === 'interactive' && danmakuWindow.value) {
danmakuWindow.value.window.setIgnoreCursorEvents(value as boolean);
}
} }
let isInited = false;
async function createWindow() { async function init() {
const appWindow = new Window('uniqueLabel', { if (isInited) return;
decorations: true, danmakuWindow.value = (await getAllWebviewWindows()).find((win) => win.label === 'danmaku-window');
resizable: true, if (!danmakuWindow.value) {
transparent: true, window.$message.error('弹幕窗口不存在,请先打开弹幕窗口。');
fullscreen: false, return;
alwaysOnTop: danmakuWindowSetting.value.alwaysOnTop, }
title: "VTsuru 弹幕窗口", console.log('打开弹幕窗口', danmakuWindow.value.label, danmakuWindowSetting.value);
}); danmakuWindow.value.onCloseRequested(() => {
// loading embedded asset:
danmakuWindow.value = new Webview(appWindow, 'theUniqueLabel', {
url: `${CURRENT_HOST}/client/danaku-window-manage`,
width: danmakuWindowSetting.value.width,
height: danmakuWindowSetting.value.height,
x: danmakuWindowSetting.value.x,
y: danmakuWindowSetting.value.y,
});
appWindow.onCloseRequested(() => {
danmakuWindow.value = undefined; danmakuWindow.value = undefined;
bc?.close(); bc?.close();
bc = undefined; bc = undefined;
}); });
danmakuWindow.value.once('tauri://window-created', async () => { await danmakuWindow.value.setIgnoreCursorEvents(false);
console.log('弹幕窗口已创建'); await danmakuWindow.value.show();
await danmakuWindow.value?.window.setIgnoreCursorEvents(true); danmakuWindow.value.onCloseRequested(() => {
closeWindow();
console.log('弹幕窗口关闭');
}); });
danmakuWindow.value.onMoved(({
payload: position
}) => {
danmakuWindowSetting.value.x = position.x;
danmakuWindowSetting.value.y = position.y;
});
isWindowOpened.value = true;
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
bc.onmessage = (event: MessageEvent<DanmakuWindowBCData>) => {
if (event.data.type === 'window-ready') {
console.log(`[danmaku-window] 窗口已就绪`);
bc?.postMessage({
type: 'update-setting',
data: toRaw(danmakuWindowSetting.value),
});
}
};
bc.postMessage({
type: 'window-ready',
});
bc.postMessage({
type: 'update-setting',
data: toRaw(danmakuWindowSetting.value),
});
bc?.postMessage({ bc?.postMessage({
type: 'danmaku', type: 'danmaku',
data: { data: {
@@ -139,16 +152,34 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
} as Partial<EventModel>, } as Partial<EventModel>,
}); });
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL); danmakuClient.onEvent('danmaku', (event) => onGetDanmakus(event));
danmakuClient.onEvent('gift', (event) => onGetDanmakus(event));
danmakuClient.onEvent('sc', (event) => onGetDanmakus(event));
danmakuClient.onEvent('guard', (event) => onGetDanmakus(event));
danmakuClient.onEvent('enter', (event) => onGetDanmakus(event));
danmakuClient.onEvent('scDel', (event) => onGetDanmakus(event));
if (danmakuClient.connected) { watch(() => danmakuWindowSetting, async (newValue) => {
danmakuClient.onEvent('danmaku', (event) => onGetDanmakus(event)); if (danmakuWindow.value) {
danmakuClient.onEvent('gift', (event) => onGetDanmakus(event)); bc?.postMessage({
danmakuClient.onEvent('sc', (event) => onGetDanmakus(event)); type: 'update-setting',
danmakuClient.onEvent('guard', (event) => onGetDanmakus(event)); data: toRaw(newValue.value),
danmakuClient.onEvent('enter', (event) => onGetDanmakus(event)); });
danmakuClient.onEvent('scDel', (event) => onGetDanmakus(event)); if (newValue.value.alwaysOnTop) {
} await danmakuWindow.value.setAlwaysOnTop(true);
}
else {
await danmakuWindow.value.setAlwaysOnTop(false);
}
if (newValue.value.interactive) {
await danmakuWindow.value.setIgnoreCursorEvents(true);
} else {
await danmakuWindow.value.setIgnoreCursorEvents(false);
}
}
}, { deep: true });
isInited = true;
} }
function onGetDanmakus(data: EventModel) { function onGetDanmakus(data: EventModel) {
@@ -158,29 +189,17 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
}); });
} }
watch(danmakuWindowSetting, async (newValue) => {
if (danmakuWindow.value) {
if (await danmakuWindow.value.window.isVisible()) {
danmakuWindow.value.setSize(new PhysicalSize(newValue.width, newValue.height));
danmakuWindow.value.setPosition(new PhysicalPosition(newValue.x, newValue.y));
}
bc?.postMessage({
type: 'update-setting',
data: newValue,
});
}
}, { deep: true });
return { return {
danmakuWindow, danmakuWindow,
danmakuWindowSetting, danmakuWindowSetting,
setDanmakuWindowSize, setDanmakuWindowSize,
setDanmakuWindowPosition, setDanmakuWindowPosition,
updateSetting, updateWindowPosition,
isDanmakuWindowOpen: computed(() => !!danmakuWindow.value), isDanmakuWindowOpen: isWindowOpened,
createWindow, openWindow,
closeWindow, closeWindow,
hideWindow
}; };
}); });

View File

@@ -102,7 +102,7 @@ export default abstract class BaseDanmakuClient {
const result = await this.initClient(); const result = await this.initClient();
if (result.success) { if (result.success) {
this.state = 'connected'; this.state = 'connected';
console.log(`[${this.type}] 弹幕客户端启动成功`); console.log(`[${this.type}] 弹幕客户端已完成启动`);
} else { } else {
this.state = 'disconnected'; this.state = 'disconnected';
console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`); console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`);

View File

@@ -34,7 +34,7 @@ export default class DirectClient extends BaseDanmakuClient {
}); });
chatClient.on('live', () => { chatClient.on('live', () => {
console.log('[DIRECT] 已连接房间: ' + this.authInfo.roomId); console.log('[direct] 已连接房间: ' + this.authInfo.roomId);
}); });
chatClient.on('DANMU_MSG', (data) => this.onDanmaku(data)); chatClient.on('DANMU_MSG', (data) => this.onDanmaku(data));
chatClient.on('SEND_GIFT', (data) => this.onGift(data)); chatClient.on('SEND_GIFT', (data) => this.onGift(data));
@@ -45,7 +45,7 @@ export default class DirectClient extends BaseDanmakuClient {
return await super.initClientInner(chatClient); return await super.initClientInner(chatClient);
} else { } else {
console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息'); console.log('[direct] 无法开启场次, 未提供弹幕客户端认证信息');
return { return {
success: false, success: false,
message: '未提供弹幕客户端认证信息' message: '未提供弹幕客户端认证信息'

View File

@@ -34,6 +34,13 @@ export default {
title: '弹幕窗口管理', title: '弹幕窗口管理',
} }
}, },
{
path: 'danmaku-window',
name: 'client-danmaku-window-redirect',
redirect: {
name: 'client-danmaku-window'
}
},
{ {
path: 'test', path: 'test',
name: 'client-test', name: 'client-test',

View File

@@ -17,10 +17,11 @@ export default [
}, },
{ {
path: '/danmaku-window', path: '/danmaku-window',
name: 'client-danmaku-client', name: 'client-danmaku-window',
component: () => import('@/client/ClientDanmakuWindow.vue'), component: () => import('@/client/ClientDanmakuWindow.vue'),
meta: { meta: {
title: '弹幕窗口' title: '弹幕窗口',
ignoreLogin: true
} }
} }
] ]

View File

@@ -20,7 +20,7 @@ type GenericListener = Listener | AllEventListener;
export const useDanmakuClient = defineStore('DanmakuClient', () => { export const useDanmakuClient = defineStore('DanmakuClient', () => {
// 使用 shallowRef 存储 danmakuClient 实例, 性能稍好 // 使用 shallowRef 存储 danmakuClient 实例, 性能稍好
const danmakuClient = shallowRef<BaseDanmakuClient>(); const danmakuClient = shallowRef<BaseDanmakuClient | undefined>(new OpenLiveClient()); // 默认实例化一个 OpenLiveClient
// 连接状态: 'waiting'-等待初始化, 'connecting'-连接中, 'connected'-已连接 // 连接状态: 'waiting'-等待初始化, 'connecting'-连接中, 'connected'-已连接
const state = ref<'waiting' | 'connecting' | 'connected'>('waiting'); const state = ref<'waiting' | 'connecting' | 'connected'>('waiting');
@@ -43,7 +43,7 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
function onEvent(eventName: EventName, listener: Listener): void; function onEvent(eventName: EventName, listener: Listener): void;
// --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 --- // --- 修正点: 实现签名使用联合类型,并在内部进行类型断言 ---
function onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: GenericListener): void { function onEvent(eventName: keyof BaseDanmakuClient['eventsAsModel'], listener: GenericListener): void {
if(!danmakuClient.value) { if (!danmakuClient.value) {
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'onEvent'。"); console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'onEvent'。");
return; return;
} }
@@ -189,7 +189,9 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
// 先停止并清理旧客户端 (如果存在) // 先停止并清理旧客户端 (如果存在)
if (danmakuClient.value) { if (danmakuClient.value) {
console.log('[DanmakuClient] 正在处理旧的客户端实例...'); console.log('[DanmakuClient] 正在处理旧的客户端实例...');
await disposeClientInstance(danmakuClient.value); if (danmakuClient.value.state === 'connected') {
await disposeClientInstance(danmakuClient.value);
}
danmakuClient.value = undefined; // 显式清除旧实例引用 danmakuClient.value = undefined; // 显式清除旧实例引用
} }

View File

@@ -186,6 +186,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
return { success: false, message: '未提供弹幕客户端认证信息' }; return { success: false, message: '未提供弹幕客户端认证信息' };
} }
await client.initDirect(directConnectInfo); await client.initDirect(directConnectInfo);
return { success: true, message: '弹幕客户端已启动' };
} }
// 监听所有事件,用于处理和转发 // 监听所有事件,用于处理和转发
@@ -197,11 +198,14 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址 danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
// 启动事件发送定时器 (如果之前没有启动) // 启动事件发送定时器 (如果之前没有启动)
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件 timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
return { success: true, message: '弹幕客户端已启动' };
} else { } else {
console.error(prefix.value + '弹幕客户端启动失败'); console.error(prefix.value + '弹幕客户端启动失败');
danmakuClientState.value = 'stopped'; danmakuClientState.value = 'stopped';
danmakuServerUrl.value = undefined; danmakuServerUrl.value = undefined;
client.dispose(); // 启动失败,清理实例,下次会重建 client.dispose(); // 启动失败,清理实例,下次会重建
return { success: false, message: '弹幕客户端启动失败' };
} }
} }
@@ -265,11 +269,11 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
// --- 尝试启动连接 --- // --- 尝试启动连接 ---
try { try {
await connection.start(); await connection.start();
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId); // 调试输出连接状态
signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID
signalRId.value = await sendSelfInfo(connection); // 发送客户端信息 signalRId.value = await sendSelfInfo(connection); // 发送客户端信息
await connection.send('Finished'); // 通知服务器已准备好 await connection.send('Finished'); // 通知服务器已准备好
signalRClient.value = connection; // 保存实例 signalRClient.value = connection; // 保存实例
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + signalRId.value); // 调试输出连接状态
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置 // state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -459,7 +459,6 @@
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<Transition <Transition
name="fade-slide" name="fade-slide"
mode="out-in"
:appear="true" :appear="true"
> >
<KeepAlive> <KeepAlive>