mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 添加弹幕窗口管理功能;优化弹幕客户端连接逻辑;实现自动滚动和设置更新; 修复浏览页页面切换的问题
This commit is contained in:
@@ -3,7 +3,8 @@ import { useDanmakuClient } from '@/store/useDanmakuClient';
|
||||
import { DANMAKU_WINDOW_BROADCAST_CHANNEL, DanmakuWindowBCData, DanmakuWindowSettings } from './store/useDanmakuWindow';
|
||||
import { NSpin, NEmpty, NIcon } from 'naive-ui';
|
||||
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 { Money24Regular, VehicleShip24Filled } from '@vicons/fluent';
|
||||
|
||||
@@ -11,6 +12,8 @@ let bc: BroadcastChannel | undefined = undefined;
|
||||
const setting = ref<DanmakuWindowSettings>();
|
||||
const danmakuList = ref<EventModel[]>([]);
|
||||
const maxItems = computed(() => setting.value?.maxDanmakuCount || 50);
|
||||
// Ref for the scroll container
|
||||
const scrollContainerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const isConnected = computed(() => {
|
||||
return setting.value !== undefined;
|
||||
@@ -49,8 +52,8 @@ function formatUsername(item: EventModel): string {
|
||||
function addDanmaku(data: EventModel) {
|
||||
if (!setting.value) return;
|
||||
|
||||
// 检查是否是需要过滤的消息类型
|
||||
const typeMap: Record<number, string> = {
|
||||
// Map EventDataTypes enum values to the string values used in filterTypes
|
||||
const typeToStringMap: { [key in EventDataTypes]?: string } = {
|
||||
[EventDataTypes.Message]: "Message",
|
||||
[EventDataTypes.Gift]: "Gift",
|
||||
[EventDataTypes.SC]: "SC",
|
||||
@@ -58,12 +61,30 @@ function addDanmaku(data: EventModel) {
|
||||
[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)) {
|
||||
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) {
|
||||
danmakuList.value.unshift(data);
|
||||
if (danmakuList.value.length > maxItems.value) {
|
||||
@@ -75,18 +96,47 @@ function addDanmaku(data: EventModel) {
|
||||
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(() => {
|
||||
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
|
||||
console.log(`[DanmakuWindow] BroadcastChannel 已创建: ${DANMAKU_WINDOW_BROADCAST_CHANNEL}`);
|
||||
bc.postMessage({
|
||||
type: 'window-ready',
|
||||
})
|
||||
bc.onmessage = (event) => {
|
||||
const data = event.data as DanmakuWindowBCData;
|
||||
switch (data.type) {
|
||||
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;
|
||||
case 'update-setting':
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -143,6 +193,7 @@ function formatMessage(item: EventModel): string {
|
||||
}"
|
||||
>
|
||||
<TransitionGroup
|
||||
ref="scrollContainerRef"
|
||||
:class="['danmaku-list', {'reverse': setting?.reverseOrder}]"
|
||||
name="danmaku-list"
|
||||
tag="div"
|
||||
@@ -158,6 +209,7 @@ function formatMessage(item: EventModel): string {
|
||||
<div
|
||||
v-for="(item, index) in danmakuList"
|
||||
:key="`${item.time}-${index}`"
|
||||
:data-type="item.type"
|
||||
class="danmaku-item"
|
||||
:style="{
|
||||
marginBottom: `${setting?.itemSpacing || 5}px`,
|
||||
@@ -235,7 +287,11 @@ function formatMessage(item: EventModel): string {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
html, body{
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.danmaku-list-enter-active,
|
||||
.danmaku-list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
@@ -283,6 +339,10 @@ function formatMessage(item: EventModel): string {
|
||||
color: #9d78c1;
|
||||
}
|
||||
|
||||
.danmaku-item[data-type="3"] { /* Guard */
|
||||
color: #9d78c1;
|
||||
}
|
||||
|
||||
.danmaku-item[data-type="4"] { /* Enter */
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ async function testNotification() {
|
||||
>
|
||||
测试通知
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'client-danmaku-window-manage'})"
|
||||
>
|
||||
弹幕机
|
||||
</NButton>
|
||||
<LabelItem label="关闭弹幕客户端">
|
||||
<NSwitch
|
||||
v-model:value="setting.settings.dev_disableDanmakuClient"
|
||||
|
||||
@@ -58,33 +58,17 @@ const presets = {
|
||||
// 应用预设
|
||||
function applyPreset(preset: 'dark' | 'light' | 'transparent') {
|
||||
const presetData = presets[preset];
|
||||
danmakuWindow.updateSetting('backgroundColor', presetData.backgroundColor);
|
||||
danmakuWindow.updateSetting('textColor', presetData.textColor);
|
||||
danmakuWindow.updateSetting('shadowColor', presetData.shadowColor);
|
||||
danmakuWindow.danmakuWindowSetting.backgroundColor = presetData.backgroundColor;
|
||||
danmakuWindow.danmakuWindowSetting.textColor = presetData.textColor;
|
||||
danmakuWindow.danmakuWindowSetting.shadowColor = presetData.shadowColor;
|
||||
message.success(`已应用${preset === 'dark' ? '暗黑' : preset === 'light' ? '明亮' : '透明'}主题预设`);
|
||||
}
|
||||
|
||||
// 重置位置到屏幕中央
|
||||
async function resetPosition() {
|
||||
// 假设屏幕尺寸为 1920x1080,将窗口居中
|
||||
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);
|
||||
danmakuWindow.setDanmakuWindowPosition(0, 0);
|
||||
message.success('窗口位置已重置');
|
||||
}
|
||||
|
||||
// 更新设置,包装了updateSetting方法
|
||||
function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting>(
|
||||
key: K,
|
||||
value: typeof danmakuWindow.danmakuWindowSetting[K]
|
||||
) {
|
||||
danmakuWindow.updateSetting(key, value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -95,7 +79,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<template #header-extra>
|
||||
<NButton
|
||||
:type="danmakuWindow.isDanmakuWindowOpen ? 'warning' : 'primary'"
|
||||
@click="danmakuWindow.isDanmakuWindowOpen ? danmakuWindow.closeWindow() : danmakuWindow.createWindow()"
|
||||
@click="danmakuWindow.isDanmakuWindowOpen ? danmakuWindow.closeWindow() : danmakuWindow.openWindow()"
|
||||
>
|
||||
{{ danmakuWindow.isDanmakuWindowOpen ? '关闭弹幕窗口' : '打开弹幕窗口' }}
|
||||
</NButton>
|
||||
@@ -136,7 +120,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.width"
|
||||
:min="200"
|
||||
:max="2000"
|
||||
@update:value="val => updateSetting('width', val || 400)"
|
||||
@update:value="(value) => danmakuWindow.setDanmakuWindowSize(value as number, danmakuWindow.danmakuWindowSetting.height)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -146,7 +130,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.height"
|
||||
:min="200"
|
||||
:max="2000"
|
||||
@update:value="val => updateSetting('height', val || 600)"
|
||||
@update:value="(value) => danmakuWindow.setDanmakuWindowSize(danmakuWindow.danmakuWindowSetting.width, value as number)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -155,7 +139,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.x"
|
||||
:min="0"
|
||||
@update:value="val => updateSetting('x', val || 0)"
|
||||
@update:value="() => danmakuWindow.updateWindowPosition()"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -164,7 +148,7 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.y"
|
||||
:min="0"
|
||||
@update:value="val => updateSetting('y', val || 0)"
|
||||
@update:value="() => danmakuWindow.updateWindowPosition()"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -188,13 +172,11 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NFormItem label="总是置顶">
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.alwaysOnTop"
|
||||
@update:value="val => updateSetting('alwaysOnTop', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="鼠标穿透">
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.interactive"
|
||||
@update:value="val => updateSetting('interactive', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
@@ -217,7 +199,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.backgroundColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('backgroundColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -226,7 +207,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.textColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('textColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -237,7 +217,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:min="0.1"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
@update:value="val => updateSetting('opacity', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -248,7 +227,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:min="10"
|
||||
:max="24"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('fontSize', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -259,7 +237,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:min="0"
|
||||
:max="20"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('borderRadius', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -270,7 +247,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:min="0"
|
||||
:max="20"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('itemSpacing', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -284,7 +260,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NFormItem label="启用阴影">
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.enableShadow"
|
||||
@update:value="val => updateSetting('enableShadow', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
@@ -294,7 +269,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.shadowColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('shadowColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
@@ -331,15 +305,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
: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>
|
||||
</NCheckboxGroup>
|
||||
@@ -351,25 +316,21 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NSpace>
|
||||
<NCheckbox
|
||||
:checked="danmakuWindow.danmakuWindowSetting.showAvatar"
|
||||
@update:checked="val => updateSetting('showAvatar', val)"
|
||||
>
|
||||
显示头像
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
:checked="danmakuWindow.danmakuWindowSetting.showUsername"
|
||||
@update:checked="val => updateSetting('showUsername', val)"
|
||||
>
|
||||
显示用户名
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
:checked="danmakuWindow.danmakuWindowSetting.showFansMedal"
|
||||
@update:checked="val => updateSetting('showFansMedal', val)"
|
||||
>
|
||||
显示粉丝牌
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
:checked="danmakuWindow.danmakuWindowSetting.showGuardIcon"
|
||||
@update:checked="val => updateSetting('showGuardIcon', val)"
|
||||
>
|
||||
显示舰长图标
|
||||
</NCheckbox>
|
||||
@@ -383,7 +344,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
<NText>从上往下</NText>
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.reverseOrder"
|
||||
@update:value="val => updateSetting('reverseOrder', val)"
|
||||
/>
|
||||
<NText>从下往上</NText>
|
||||
</NSpace>
|
||||
@@ -396,7 +356,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.maxDanmakuCount"
|
||||
:min="10"
|
||||
:max="200"
|
||||
@update:value="val => updateSetting('maxDanmakuCount', val || 50)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
@@ -408,7 +367,6 @@ function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting
|
||||
:min="0"
|
||||
:max="1000"
|
||||
:step="50"
|
||||
@update:value="val => updateSetting('animationDuration', val || 300)"
|
||||
>
|
||||
<template #suffix>
|
||||
ms
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { EventDataTypes, EventModel } from "@/api/api-models";
|
||||
import { CURRENT_HOST } from "@/data/constants";
|
||||
import { useDanmakuClient } from "@/store/useDanmakuClient";
|
||||
import { useWebFetcher } from "@/store/useWebFetcher";
|
||||
import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
|
||||
import { Webview } from "@tauri-apps/api/webview";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { Window } from "@tauri-apps/api/window";
|
||||
import { getAllWebviewWindows, WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
export type DanmakuWindowSettings = {
|
||||
width: number;
|
||||
@@ -39,10 +35,12 @@ export type DanmakuWindowBCData = {
|
||||
} | {
|
||||
type: 'update-setting',
|
||||
data: DanmakuWindowSettings;
|
||||
} | {
|
||||
type: 'window-ready';
|
||||
};
|
||||
|
||||
export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
const danmakuWindow = ref<Webview>();
|
||||
const danmakuWindow = ref<WebviewWindow>();
|
||||
const danmakuWindowSetting = useStorage<DanmakuWindowSettings>('Setting.DanmakuWindow', {
|
||||
width: 400,
|
||||
height: 600,
|
||||
@@ -68,15 +66,19 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
shadowColor: 'rgba(0,0,0,0.5)'
|
||||
});
|
||||
const danmakuClient = useDanmakuClient();
|
||||
const isWindowOpened = ref(false);
|
||||
let bc: BroadcastChannel | undefined = undefined;
|
||||
|
||||
function hideWindow() {
|
||||
danmakuWindow.value?.window.hide();
|
||||
danmakuWindow.value = undefined;
|
||||
}
|
||||
function closeWindow() {
|
||||
danmakuWindow.value?.close();
|
||||
danmakuWindow.value = undefined;
|
||||
danmakuWindow.value?.hide();
|
||||
isWindowOpened.value = false;
|
||||
}
|
||||
function openWindow() {
|
||||
if (!isInited) {
|
||||
init();
|
||||
}
|
||||
danmakuWindow.value?.show();
|
||||
isWindowOpened.value = true;
|
||||
}
|
||||
|
||||
function setDanmakuWindowSize(width: number, height: number) {
|
||||
@@ -90,47 +92,58 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
danmakuWindowSetting.value.y = y;
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(x, y));
|
||||
}
|
||||
|
||||
function updateSetting<K extends keyof DanmakuWindowSettings>(key: K, value: DanmakuWindowSettings[K]) {
|
||||
danmakuWindowSetting.value[key] = value;
|
||||
// 特定设置需要直接应用到窗口
|
||||
if (key === 'alwaysOnTop' && danmakuWindow.value) {
|
||||
danmakuWindow.value.window.setAlwaysOnTop(value as boolean);
|
||||
function updateWindowPosition() {
|
||||
danmakuWindow.value?.setPosition(new PhysicalPosition(danmakuWindowSetting.value.x, danmakuWindowSetting.value.y));
|
||||
}
|
||||
if (key === 'interactive' && danmakuWindow.value) {
|
||||
danmakuWindow.value.window.setIgnoreCursorEvents(value as boolean);
|
||||
let isInited = false;
|
||||
|
||||
async function init() {
|
||||
if (isInited) return;
|
||||
danmakuWindow.value = (await getAllWebviewWindows()).find((win) => win.label === 'danmaku-window');
|
||||
if (!danmakuWindow.value) {
|
||||
window.$message.error('弹幕窗口不存在,请先打开弹幕窗口。');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
const appWindow = new Window('uniqueLabel', {
|
||||
decorations: true,
|
||||
resizable: true,
|
||||
transparent: true,
|
||||
fullscreen: false,
|
||||
alwaysOnTop: danmakuWindowSetting.value.alwaysOnTop,
|
||||
title: "VTsuru 弹幕窗口",
|
||||
});
|
||||
|
||||
// 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(() => {
|
||||
console.log('打开弹幕窗口', danmakuWindow.value.label, danmakuWindowSetting.value);
|
||||
danmakuWindow.value.onCloseRequested(() => {
|
||||
danmakuWindow.value = undefined;
|
||||
bc?.close();
|
||||
bc = undefined;
|
||||
});
|
||||
|
||||
danmakuWindow.value.once('tauri://window-created', async () => {
|
||||
console.log('弹幕窗口已创建');
|
||||
await danmakuWindow.value?.window.setIgnoreCursorEvents(true);
|
||||
await danmakuWindow.value.setIgnoreCursorEvents(false);
|
||||
await danmakuWindow.value.show();
|
||||
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({
|
||||
type: 'danmaku',
|
||||
data: {
|
||||
@@ -139,16 +152,34 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
} as Partial<EventModel>,
|
||||
});
|
||||
|
||||
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
|
||||
|
||||
if (danmakuClient.connected) {
|
||||
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));
|
||||
|
||||
watch(() => danmakuWindowSetting, async (newValue) => {
|
||||
if (danmakuWindow.value) {
|
||||
bc?.postMessage({
|
||||
type: 'update-setting',
|
||||
data: toRaw(newValue.value),
|
||||
});
|
||||
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) {
|
||||
@@ -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 {
|
||||
danmakuWindow,
|
||||
danmakuWindowSetting,
|
||||
setDanmakuWindowSize,
|
||||
setDanmakuWindowPosition,
|
||||
updateSetting,
|
||||
isDanmakuWindowOpen: computed(() => !!danmakuWindow.value),
|
||||
createWindow,
|
||||
updateWindowPosition,
|
||||
isDanmakuWindowOpen: isWindowOpened,
|
||||
openWindow,
|
||||
closeWindow,
|
||||
hideWindow
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export default abstract class BaseDanmakuClient {
|
||||
const result = await this.initClient();
|
||||
if (result.success) {
|
||||
this.state = 'connected';
|
||||
console.log(`[${this.type}] 弹幕客户端启动成功`);
|
||||
console.log(`[${this.type}] 弹幕客户端已完成启动`);
|
||||
} else {
|
||||
this.state = 'disconnected';
|
||||
console.error(`[${this.type}] 弹幕客户端启动失败: ${result.message}`);
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
});
|
||||
|
||||
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('SEND_GIFT', (data) => this.onGift(data));
|
||||
@@ -45,7 +45,7 @@ export default class DirectClient extends BaseDanmakuClient {
|
||||
|
||||
return await super.initClientInner(chatClient);
|
||||
} else {
|
||||
console.log('[DIRECT] 无法开启场次, 未提供弹幕客户端认证信息');
|
||||
console.log('[direct] 无法开启场次, 未提供弹幕客户端认证信息');
|
||||
return {
|
||||
success: false,
|
||||
message: '未提供弹幕客户端认证信息'
|
||||
|
||||
@@ -34,6 +34,13 @@ export default {
|
||||
title: '弹幕窗口管理',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'danmaku-window',
|
||||
name: 'client-danmaku-window-redirect',
|
||||
redirect: {
|
||||
name: 'client-danmaku-window'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'test',
|
||||
name: 'client-test',
|
||||
|
||||
@@ -17,10 +17,11 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/danmaku-window',
|
||||
name: 'client-danmaku-client',
|
||||
name: 'client-danmaku-window',
|
||||
component: () => import('@/client/ClientDanmakuWindow.vue'),
|
||||
meta: {
|
||||
title: '弹幕窗口'
|
||||
title: '弹幕窗口',
|
||||
ignoreLogin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ type GenericListener = Listener | AllEventListener;
|
||||
|
||||
export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
// 使用 shallowRef 存储 danmakuClient 实例, 性能稍好
|
||||
const danmakuClient = shallowRef<BaseDanmakuClient>();
|
||||
const danmakuClient = shallowRef<BaseDanmakuClient | undefined>(new OpenLiveClient()); // 默认实例化一个 OpenLiveClient
|
||||
|
||||
// 连接状态: 'waiting'-等待初始化, 'connecting'-连接中, 'connected'-已连接
|
||||
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: keyof BaseDanmakuClient['eventsAsModel'], listener: GenericListener): void {
|
||||
if(!danmakuClient.value) {
|
||||
if (!danmakuClient.value) {
|
||||
console.warn("[DanmakuClient] 尝试在客户端初始化之前调用 'onEvent'。");
|
||||
return;
|
||||
}
|
||||
@@ -189,7 +189,9 @@ export const useDanmakuClient = defineStore('DanmakuClient', () => {
|
||||
// 先停止并清理旧客户端 (如果存在)
|
||||
if (danmakuClient.value) {
|
||||
console.log('[DanmakuClient] 正在处理旧的客户端实例...');
|
||||
if (danmakuClient.value.state === 'connected') {
|
||||
await disposeClientInstance(danmakuClient.value);
|
||||
}
|
||||
danmakuClient.value = undefined; // 显式清除旧实例引用
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
return { success: false, message: '未提供弹幕客户端认证信息' };
|
||||
}
|
||||
await client.initDirect(directConnectInfo);
|
||||
return { success: true, message: '弹幕客户端已启动' };
|
||||
}
|
||||
|
||||
// 监听所有事件,用于处理和转发
|
||||
@@ -197,11 +198,14 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
danmakuServerUrl.value = client.danmakuClient!.serverUrl; // 获取服务器地址
|
||||
// 启动事件发送定时器 (如果之前没有启动)
|
||||
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
|
||||
return { success: true, message: '弹幕客户端已启动' };
|
||||
} else {
|
||||
console.error(prefix.value + '弹幕客户端启动失败');
|
||||
danmakuClientState.value = 'stopped';
|
||||
danmakuServerUrl.value = undefined;
|
||||
client.dispose(); // 启动失败,清理实例,下次会重建
|
||||
return { success: false, message: '弹幕客户端启动失败' };
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,11 +269,11 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||
// --- 尝试启动连接 ---
|
||||
try {
|
||||
await connection.start();
|
||||
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId); // 调试输出连接状态
|
||||
signalRConnectionId.value = connection.connectionId ?? undefined; // 保存连接ID
|
||||
signalRId.value = await sendSelfInfo(connection); // 发送客户端信息
|
||||
await connection.send('Finished'); // 通知服务器已准备好
|
||||
signalRClient.value = connection; // 保存实例
|
||||
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + signalRId.value); // 调试输出连接状态
|
||||
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -459,7 +459,6 @@
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition
|
||||
name="fade-slide"
|
||||
mode="out-in"
|
||||
:appear="true"
|
||||
>
|
||||
<KeepAlive>
|
||||
|
||||
Reference in New Issue
Block a user