mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-10 20:36:55 +08:00
feat: 修复图片url, 开始弹幕机编写
This commit is contained in:
289
src/client/ClientDanmakuWindow.vue
Normal file
289
src/client/ClientDanmakuWindow.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script setup lang="ts">
|
||||
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 { TransitionGroup } from 'vue';
|
||||
import { Money24Regular, VehicleShip24Filled } from '@vicons/fluent';
|
||||
|
||||
let bc: BroadcastChannel | undefined = undefined;
|
||||
const setting = ref<DanmakuWindowSettings>();
|
||||
const danmakuList = ref<EventModel[]>([]);
|
||||
const maxItems = computed(() => setting.value?.maxDanmakuCount || 50);
|
||||
|
||||
const isConnected = computed(() => {
|
||||
return setting.value !== undefined;
|
||||
});
|
||||
|
||||
function GetSCColor(price: number): string {
|
||||
if (price === 0) return `#2a60b2`;
|
||||
if (price > 0 && price < 50) return `#2a60b2`;
|
||||
if (price >= 50 && price < 100) return `#427d9e`;
|
||||
if (price >= 100 && price < 500) return `#c99801`;
|
||||
if (price >= 500 && price < 1000) return `#e09443`;
|
||||
if (price >= 1000 && price < 2000) return `#e54d4d`;
|
||||
if (price >= 2000) return `#ab1a32`;
|
||||
return '';
|
||||
}
|
||||
|
||||
function GetGuardColor(level: number | null | undefined): string {
|
||||
if (level) {
|
||||
switch (level) {
|
||||
case 1: return 'rgb(122, 4, 35)';
|
||||
case 2: return 'rgb(157, 155, 255)';
|
||||
case 3: return 'rgb(104, 136, 241)';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function formatUsername(item: EventModel): string {
|
||||
let result = item.name;
|
||||
if (setting.value?.showFansMedal && item.fans_medal_wearing_status) {
|
||||
result = `[${item.fans_medal_name} ${item.fans_medal_level}] ${result}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addDanmaku(data: EventModel) {
|
||||
if (!setting.value) return;
|
||||
|
||||
// 检查是否是需要过滤的消息类型
|
||||
const typeMap: Record<number, string> = {
|
||||
[EventDataTypes.Message]: "Message",
|
||||
[EventDataTypes.Gift]: "Gift",
|
||||
[EventDataTypes.SC]: "SC",
|
||||
[EventDataTypes.Guard]: "Guard",
|
||||
[EventDataTypes.Enter]: "Enter"
|
||||
};
|
||||
|
||||
const typeStr = typeMap[data.type];
|
||||
if (!typeStr || !setting.value.filterTypes.includes(typeStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 维护最大消息数量
|
||||
if (setting.value.reverseOrder) {
|
||||
danmakuList.value.unshift(data);
|
||||
if (danmakuList.value.length > maxItems.value) {
|
||||
danmakuList.value.pop();
|
||||
}
|
||||
} else {
|
||||
danmakuList.value.push(data);
|
||||
if (danmakuList.value.length > maxItems.value) {
|
||||
danmakuList.value.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bc = new BroadcastChannel(DANMAKU_WINDOW_BROADCAST_CHANNEL);
|
||||
bc.onmessage = (event) => {
|
||||
const data = event.data as DanmakuWindowBCData;
|
||||
switch (data.type) {
|
||||
case 'danmaku':
|
||||
addDanmaku(data.data);
|
||||
break;
|
||||
case 'update-setting':
|
||||
setting.value = data.data;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatch a request for settings
|
||||
bc.postMessage({ type: 'request-settings' });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (bc) {
|
||||
bc.close();
|
||||
bc = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// 格式化弹幕消息
|
||||
function formatMessage(item: EventModel): string {
|
||||
switch(item.type) {
|
||||
case EventDataTypes.Message:
|
||||
return item.msg;
|
||||
case EventDataTypes.Gift:
|
||||
return `${item.msg} ${item.num > 1 ? 'x'+item.num : ''}`;
|
||||
case EventDataTypes.SC:
|
||||
return item.msg;
|
||||
case EventDataTypes.Guard:
|
||||
return `开通了${item.guard_level === 1 ? '总督' : item.guard_level === 2 ? '提督' : '舰长'}`;
|
||||
case EventDataTypes.Enter:
|
||||
return '进入直播间';
|
||||
default:
|
||||
return item.msg;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin
|
||||
v-if="!isConnected"
|
||||
show
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="danmaku-window"
|
||||
:style="{
|
||||
backgroundColor: setting?.backgroundColor || 'rgba(0,0,0,0.6)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: `${setting?.borderRadius || 0}px`,
|
||||
opacity: setting?.opacity || 1,
|
||||
color: setting?.textColor || '#ffffff',
|
||||
fontSize: `${setting?.fontSize || 14}px`,
|
||||
overflow: 'hidden',
|
||||
boxShadow: setting?.enableShadow ? `0 0 10px ${setting?.shadowColor}` : 'none'
|
||||
}"
|
||||
>
|
||||
<TransitionGroup
|
||||
:class="['danmaku-list', {'reverse': setting?.reverseOrder}]"
|
||||
name="danmaku-list"
|
||||
tag="div"
|
||||
:style="{
|
||||
padding: '8px',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: setting?.reverseOrder ? 'column-reverse' : 'column'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in danmakuList"
|
||||
:key="`${item.time}-${index}`"
|
||||
class="danmaku-item"
|
||||
:style="{
|
||||
marginBottom: `${setting?.itemSpacing || 5}px`,
|
||||
padding: '6px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: item.type === EventDataTypes.SC ? GetSCColor(item.price) : 'transparent',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: item.type === EventDataTypes.SC ? '#ffffff' : undefined,
|
||||
transition: `all ${setting?.animationDuration || 300}ms ease`
|
||||
}"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<img
|
||||
v-if="setting?.showAvatar && item.uface"
|
||||
:src="item.uface"
|
||||
class="avatar"
|
||||
:style="{
|
||||
width: `${setting?.fontSize + 6 || 20}px`,
|
||||
height: `${setting?.fontSize + 6 || 20}px`,
|
||||
borderRadius: '50%',
|
||||
marginRight: '6px'
|
||||
}"
|
||||
referrerpolicy="no-referrer"
|
||||
>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<div
|
||||
v-if="setting?.showUsername"
|
||||
class="username"
|
||||
:style="{
|
||||
fontWeight: 'bold',
|
||||
marginRight: '6px',
|
||||
}"
|
||||
>
|
||||
<!-- 舰长图标 -->
|
||||
<NIcon
|
||||
v-if="setting?.showGuardIcon && item.guard_level > 0"
|
||||
:component="VehicleShip24Filled"
|
||||
:color="GetGuardColor(item.guard_level)"
|
||||
:size="setting?.fontSize"
|
||||
style="margin-right: 2px; vertical-align: middle;"
|
||||
/>
|
||||
{{ formatUsername(item) }}:
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div class="message">
|
||||
<!-- SC/礼物金额 -->
|
||||
<NIcon
|
||||
v-if="(item.type === EventDataTypes.Gift || item.type === EventDataTypes.SC) && item.price > 0"
|
||||
:component="Money24Regular"
|
||||
:color="item.type === EventDataTypes.SC ? '#ffffff' : '#dd2f2f'"
|
||||
:size="setting?.fontSize"
|
||||
style="margin-right: 4px; vertical-align: middle;"
|
||||
/>
|
||||
<span v-if="(item.type === EventDataTypes.Gift || item.type === EventDataTypes.SC) && item.price > 0">
|
||||
{{ item.price }}¥
|
||||
</span>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
{{ formatMessage(item) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="danmakuList.length === 0"
|
||||
key="empty"
|
||||
style="display: flex; align-items: center; justify-content: center; height: 100%;"
|
||||
>
|
||||
<NEmpty description="暂无弹幕" />
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.danmaku-list-enter-active,
|
||||
.danmaku-list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.danmaku-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.danmaku-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.danmaku-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.danmaku-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.danmaku-list::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 拖动窗口时用于指示 */
|
||||
.danmaku-window {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.danmaku-item {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* 根据消息类型添加特殊样式 */
|
||||
.danmaku-item[data-type="2"] { /* Gift */
|
||||
color: #dd2f2f;
|
||||
}
|
||||
|
||||
.danmaku-item[data-type="0"] { /* Guard */
|
||||
color: #9d78c1;
|
||||
}
|
||||
|
||||
.danmaku-item[data-type="4"] { /* Enter */
|
||||
color: #4caf50;
|
||||
}
|
||||
</style>
|
||||
@@ -101,6 +101,12 @@ import { isTauri } from '@/data/constants';
|
||||
key: 'fetcher',
|
||||
icon: () => h(CloudArchive24Filled)
|
||||
},
|
||||
/*{
|
||||
label: () =>
|
||||
h(RouterLink, { to: { name: 'client-danmaku-window-manage' } }, () => '弹幕机管理'),
|
||||
key: 'danmaku-window-manage',
|
||||
icon: () => h(Settings24Filled)
|
||||
},*/
|
||||
{
|
||||
label: () =>
|
||||
h(RouterLink, { to: { name: 'client-settings' } }, () => '设置'),
|
||||
|
||||
438
src/client/DanmakuWindowManager.vue
Normal file
438
src/client/DanmakuWindowManager.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
import { useDanmakuWindow } from './store/useDanmakuWindow';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { EventDataTypes } from '@/api/api-models';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
NButton, NCard, NSpace, NSlider, NSwitch, NSelect, NInputNumber,
|
||||
NColorPicker, NDivider, NGrid, NGi, NFlex, NCheckbox, NCheckboxGroup,
|
||||
NIcon, NTooltip, NSpin, NText, NAlert, NTabs, NTabPane, NForm, NFormItem
|
||||
} from 'naive-ui';
|
||||
|
||||
import {
|
||||
DesktopMac24Regular,
|
||||
TextFont24Regular,
|
||||
ColorFill24Regular,
|
||||
AppsList24Regular,
|
||||
DesignIdeas24Regular,
|
||||
CheckmarkCircle24Regular,
|
||||
ResizeTable24Filled,
|
||||
} from '@vicons/fluent';
|
||||
|
||||
const danmakuWindow = useDanmakuWindow();
|
||||
const message = useMessage();
|
||||
const route = useRoute();
|
||||
|
||||
const isSettingPositionMode = ref(false);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 计算属性,类型映射
|
||||
const filterTypeOptions = [
|
||||
{ label: '弹幕消息', value: 'Message' },
|
||||
{ label: '礼物', value: 'Gift' },
|
||||
{ label: 'SC', value: 'SC' },
|
||||
{ label: '舰长', value: 'Guard' },
|
||||
{ label: '进场', value: 'Enter' }
|
||||
];
|
||||
|
||||
// 分组预设
|
||||
const presets = {
|
||||
dark: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
textColor: '#ffffff',
|
||||
shadowColor: 'rgba(0,0,0,0.7)'
|
||||
},
|
||||
light: {
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
textColor: '#333333',
|
||||
shadowColor: 'rgba(0,0,0,0.2)'
|
||||
},
|
||||
transparent: {
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
textColor: '#ffffff',
|
||||
shadowColor: 'rgba(0,0,0,0.0)'
|
||||
}
|
||||
};
|
||||
|
||||
// 应用预设
|
||||
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);
|
||||
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);
|
||||
message.success('窗口位置已重置');
|
||||
}
|
||||
|
||||
// 更新设置,包装了updateSetting方法
|
||||
function updateSetting<K extends keyof typeof danmakuWindow.danmakuWindowSetting>(
|
||||
key: K,
|
||||
value: typeof danmakuWindow.danmakuWindowSetting[K]
|
||||
) {
|
||||
danmakuWindow.updateSetting(key, value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard
|
||||
title="弹幕窗口管理"
|
||||
bordered
|
||||
>
|
||||
<template #header-extra>
|
||||
<NButton
|
||||
:type="danmakuWindow.isDanmakuWindowOpen ? 'warning' : 'primary'"
|
||||
@click="danmakuWindow.isDanmakuWindowOpen ? danmakuWindow.closeWindow() : danmakuWindow.createWindow()"
|
||||
>
|
||||
{{ danmakuWindow.isDanmakuWindowOpen ? '关闭弹幕窗口' : '打开弹幕窗口' }}
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<NFlex
|
||||
vertical
|
||||
:size="20"
|
||||
>
|
||||
<NAlert
|
||||
v-if="!danmakuWindow.isDanmakuWindowOpen"
|
||||
title="弹幕窗口未打开"
|
||||
type="info"
|
||||
>
|
||||
请先打开弹幕窗口后再进行设置
|
||||
</NAlert>
|
||||
|
||||
<NTabs
|
||||
type="line"
|
||||
animated
|
||||
>
|
||||
<NTabPane
|
||||
name="position"
|
||||
tab="布局与位置"
|
||||
>
|
||||
<NFlex vertical>
|
||||
<NForm
|
||||
label-placement="left"
|
||||
label-width="100"
|
||||
>
|
||||
<NGrid
|
||||
:cols="2"
|
||||
:x-gap="12"
|
||||
>
|
||||
<NGi>
|
||||
<NFormItem label="窗口宽度">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.width"
|
||||
:min="200"
|
||||
:max="2000"
|
||||
@update:value="val => updateSetting('width', val || 400)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="窗口高度">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.height"
|
||||
:min="200"
|
||||
:max="2000"
|
||||
@update:value="val => updateSetting('height', val || 600)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="X位置">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.x"
|
||||
:min="0"
|
||||
@update:value="val => updateSetting('x', val || 0)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="Y位置">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.y"
|
||||
:min="0"
|
||||
@update:value="val => updateSetting('y', val || 0)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
<NDivider />
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<NButton
|
||||
secondary
|
||||
@click="resetPosition"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ResizeTable24Filled" />
|
||||
</template>
|
||||
重置位置
|
||||
</NButton>
|
||||
<NSpace>
|
||||
<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>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane
|
||||
name="appearance"
|
||||
tab="外观"
|
||||
>
|
||||
<NFlex vertical>
|
||||
<NGrid
|
||||
:cols="2"
|
||||
:x-gap="12"
|
||||
:y-gap="12"
|
||||
>
|
||||
<NGi>
|
||||
<NFormItem label="背景颜色">
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.backgroundColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('backgroundColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="文字颜色">
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.textColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('textColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="透明度">
|
||||
<NSlider
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.opacity"
|
||||
:min="0.1"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
@update:value="val => updateSetting('opacity', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="字体大小">
|
||||
<NSlider
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.fontSize"
|
||||
:min="10"
|
||||
:max="24"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('fontSize', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="圆角大小">
|
||||
<NSlider
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.borderRadius"
|
||||
:min="0"
|
||||
:max="20"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('borderRadius', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NFormItem label="项目间距">
|
||||
<NSlider
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.itemSpacing"
|
||||
:min="0"
|
||||
:max="20"
|
||||
:step="1"
|
||||
@update:value="val => updateSetting('itemSpacing', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<NSpace>
|
||||
<NFormItem label="启用阴影">
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.enableShadow"
|
||||
@update:value="val => updateSetting('enableShadow', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="danmakuWindow.danmakuWindowSetting.enableShadow"
|
||||
label="阴影颜色"
|
||||
>
|
||||
<NColorPicker
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.shadowColor"
|
||||
:show-alpha="true"
|
||||
@update:value="val => updateSetting('shadowColor', val)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
|
||||
<NSpace>
|
||||
<NButton @click="applyPreset('dark')">
|
||||
暗色主题
|
||||
</NButton>
|
||||
<NButton @click="applyPreset('light')">
|
||||
亮色主题
|
||||
</NButton>
|
||||
<NButton @click="applyPreset('transparent')">
|
||||
透明主题
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane
|
||||
name="content"
|
||||
tab="内容设置"
|
||||
>
|
||||
<NGrid
|
||||
:cols="1"
|
||||
:y-gap="12"
|
||||
>
|
||||
<NGi>
|
||||
<NFormItem label="信息显示">
|
||||
<NCheckboxGroup v-model:value="danmakuWindow.danmakuWindowSetting.filterTypes">
|
||||
<NSpace>
|
||||
<NCheckbox
|
||||
v-for="option in filterTypeOptions"
|
||||
: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>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFormItem label="显示选项">
|
||||
<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>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFormItem label="弹幕方向">
|
||||
<NSpace align="center">
|
||||
<NText>从上往下</NText>
|
||||
<NSwitch
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.reverseOrder"
|
||||
@update:value="val => updateSetting('reverseOrder', val)"
|
||||
/>
|
||||
<NText>从下往上</NText>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFormItem label="最大弹幕数量">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.maxDanmakuCount"
|
||||
:min="10"
|
||||
:max="200"
|
||||
@update:value="val => updateSetting('maxDanmakuCount', val || 50)"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
|
||||
<NGi>
|
||||
<NFormItem label="动画持续时间">
|
||||
<NInputNumber
|
||||
v-model:value="danmakuWindow.danmakuWindowSetting.animationDuration"
|
||||
:min="0"
|
||||
:max="1000"
|
||||
:step="50"
|
||||
@update:value="val => updateSetting('animationDuration', val || 300)"
|
||||
>
|
||||
<template #suffix>
|
||||
ms
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</NFormItem>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.position-indicator {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
border: 2px dashed #f56c6c;
|
||||
z-index: 9999;
|
||||
background-color: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -19,6 +19,7 @@ import { CN_HOST, isDev } from "@/data/constants";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { useDanmakuWindow } from "../store/useDanmakuWindow";
|
||||
|
||||
const accountInfo = useAccount();
|
||||
|
||||
@@ -134,6 +135,7 @@ export function OnClientUnmounted() {
|
||||
}
|
||||
|
||||
tray.close();
|
||||
useDanmakuWindow().closeWindow()
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
|
||||
189
src/client/store/useDanmakuWindow.ts
Normal file
189
src/client/store/useDanmakuWindow.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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";
|
||||
|
||||
export type DanmakuWindowSettings = {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
opacity: number; // 窗口透明度
|
||||
showAvatar: boolean; // 是否显示头像
|
||||
showUsername: boolean; // 是否显示用户名
|
||||
showFansMedal: boolean; // 是否显示粉丝牌
|
||||
showGuardIcon: boolean; // 是否显示舰长图标
|
||||
fontSize: number; // 弹幕字体大小
|
||||
maxDanmakuCount: number; // 最大显示的弹幕数量
|
||||
reverseOrder: boolean; // 是否倒序显示(从下往上)
|
||||
filterTypes: string[]; // 要显示的弹幕类型
|
||||
animationDuration: number; // 动画持续时间
|
||||
backgroundColor: string; // 背景色
|
||||
textColor: string; // 文字颜色
|
||||
alwaysOnTop: boolean; // 是否总在最前
|
||||
interactive: boolean; // 是否可交互(穿透鼠标点击)
|
||||
borderRadius: number; // 边框圆角
|
||||
itemSpacing: number; // 项目间距
|
||||
enableShadow: boolean; // 是否启用阴影
|
||||
shadowColor: string; // 阴影颜色
|
||||
};
|
||||
|
||||
export const DANMAKU_WINDOW_BROADCAST_CHANNEL = 'channel.danmaku.window';
|
||||
export type DanmakuWindowBCData = {
|
||||
type: 'danmaku',
|
||||
data: EventModel;
|
||||
} | {
|
||||
type: 'update-setting',
|
||||
data: DanmakuWindowSettings;
|
||||
};
|
||||
|
||||
export const useDanmakuWindow = defineStore('danmakuWindow', () => {
|
||||
const danmakuWindow = ref<Webview>();
|
||||
const danmakuWindowSetting = useStorage<DanmakuWindowSettings>('Setting.DanmakuWindow', {
|
||||
width: 400,
|
||||
height: 600,
|
||||
x: 100,
|
||||
y: 100,
|
||||
opacity: 0.9,
|
||||
showAvatar: true,
|
||||
showUsername: true,
|
||||
showFansMedal: true,
|
||||
showGuardIcon: true,
|
||||
fontSize: 14,
|
||||
maxDanmakuCount: 50,
|
||||
reverseOrder: false,
|
||||
filterTypes: ["Message", "Gift", "SC", "Guard"],
|
||||
animationDuration: 300,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
textColor: '#ffffff',
|
||||
alwaysOnTop: true,
|
||||
interactive: false,
|
||||
borderRadius: 8,
|
||||
itemSpacing: 5,
|
||||
enableShadow: true,
|
||||
shadowColor: 'rgba(0,0,0,0.5)'
|
||||
});
|
||||
const danmakuClient = useDanmakuClient();
|
||||
let bc: BroadcastChannel | undefined = undefined;
|
||||
|
||||
function hideWindow() {
|
||||
danmakuWindow.value?.window.hide();
|
||||
danmakuWindow.value = undefined;
|
||||
}
|
||||
function closeWindow() {
|
||||
danmakuWindow.value?.close();
|
||||
danmakuWindow.value = undefined;
|
||||
}
|
||||
|
||||
function setDanmakuWindowSize(width: number, height: number) {
|
||||
danmakuWindowSetting.value.width = width;
|
||||
danmakuWindowSetting.value.height = height;
|
||||
danmakuWindow.value?.setSize(new PhysicalSize(width, height));
|
||||
}
|
||||
|
||||
function setDanmakuWindowPosition(x: number, y: number) {
|
||||
danmakuWindowSetting.value.x = x;
|
||||
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);
|
||||
}
|
||||
if (key === 'interactive' && danmakuWindow.value) {
|
||||
danmakuWindow.value.window.setIgnoreCursorEvents(value as boolean);
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
danmakuWindow.value = undefined;
|
||||
bc?.close();
|
||||
bc = undefined;
|
||||
});
|
||||
|
||||
danmakuWindow.value.once('tauri://window-created', async () => {
|
||||
console.log('弹幕窗口已创建');
|
||||
await danmakuWindow.value?.window.setIgnoreCursorEvents(true);
|
||||
});
|
||||
bc?.postMessage({
|
||||
type: 'danmaku',
|
||||
data: {
|
||||
type: EventDataTypes.Message,
|
||||
msg: '弹幕窗口已打开',
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
|
||||
function onGetDanmakus(data: EventModel) {
|
||||
bc?.postMessage({
|
||||
type: 'danmaku',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
closeWindow,
|
||||
hideWindow
|
||||
};
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useDanmakuWindow, import.meta.hot));
|
||||
}
|
||||
Reference in New Issue
Block a user