diff --git a/bun.lockb b/bun.lockb index c245fb7..9f01f96 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5ad9497..a7f0384 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "md5": "^2.3.0", "mitt": "^3.0.1", "monaco-editor": "^0.54.0", - "naive-ui": "2.43.2", + "naive-ui": "^2.43.2", "nanoid": "^5.1.6", "obs-websocket-js": "^5.0.7", "peerjs": "^1.5.5", diff --git a/src/client/ClientDanmakuWindow.vue b/src/client/ClientDanmakuWindow.vue index 085c07a..f8a064c 100644 --- a/src/client/ClientDanmakuWindow.vue +++ b/src/client/ClientDanmakuWindow.vue @@ -15,6 +15,70 @@ type TempDanmakuType = EventModel & { timestamp?: number // 添加:记录插入时间戳 } +type ParsedColor = { + rgb: string + alpha: number +} + +function parseColorToRgba(color?: string): ParsedColor | null { + if (!color) return null + const value = color.trim() + + const rgbaMatch = value.match(/^rgba?\(([^)]+)\)$/i) + if (rgbaMatch) { + const parts = rgbaMatch[1].split(',').map(part => part.trim()) + if (parts.length >= 3) { + const r = Number(parts[0]) + const g = Number(parts[1]) + const b = Number(parts[2]) + const a = parts[3] !== undefined ? Number(parts[3]) : 1 + return { + rgb: `${Number.isFinite(r) ? r : 0}, ${Number.isFinite(g) ? g : 0}, ${Number.isFinite(b) ? b : 0}`, + alpha: Number.isFinite(a) ? Math.max(0, Math.min(1, a)) : 1, + } + } + return null + } + + if (value.startsWith('#')) { + const hex = value.slice(1) + const normalizeHex = (segment: string) => segment.length === 1 ? segment + segment : segment + let r = 0 + let g = 0 + let b = 0 + let a = 255 + + if (hex.length === 3 || hex.length === 4) { + r = parseInt(normalizeHex(hex[0]), 16) + g = parseInt(normalizeHex(hex[1]), 16) + b = parseInt(normalizeHex(hex[2]), 16) + if (hex.length === 4) { + a = parseInt(normalizeHex(hex[3]), 16) + } + } else if (hex.length === 6 || hex.length === 8) { + r = parseInt(hex.slice(0, 2), 16) + g = parseInt(hex.slice(2, 4), 16) + b = parseInt(hex.slice(4, 6), 16) + if (hex.length === 8) { + a = parseInt(hex.slice(6, 8), 16) + } + } else { + return null + } + + if ([r, g, b, a].some(value => Number.isNaN(value))) { + return null + } + + return { + rgb: `${r}, ${g}, ${b}`, + alpha: Math.max(0, Math.min(1, a / 255)), + } + } + + return null +} + let bc: BroadcastChannel | undefined const setting = ref() const danmakuList = ref([]) @@ -33,7 +97,17 @@ function updateCssVariables() { root.style.setProperty('--dw-direction', setting.value.reverseOrder ? 'column-reverse' : 'column') // 背景和文字颜色 - root.style.setProperty('--dw-bg-color', setting.value.backgroundColor || 'rgba(0,0,0,0.6)') + const bgColor = setting.value.backgroundColor || 'rgba(0,0,0,0.6)' + root.style.setProperty('--dw-bg-color', bgColor) + const parsedColor = parseColorToRgba(bgColor) + if (parsedColor) { + root.style.setProperty('--dw-bg-color-rgb', parsedColor.rgb) + root.style.setProperty('--dw-bg-alpha', `${parsedColor.alpha}`) + } else { + root.style.setProperty('--dw-bg-color-rgb', '0, 0, 0') + root.style.setProperty('--dw-bg-alpha', '0.6') + } + root.style.setProperty('--dw-window-bg-color', setting.value.windowBackgroundColor || 'transparent') root.style.setProperty('--dw-text-color', setting.value.textColor || '#ffffff') // 尺寸相关 @@ -224,6 +298,7 @@ watch(() => setting.value, () => { class="danmaku-window" :class="{ 'has-items': hasItems, 'batch-update': isInBatchUpdate }" > +
+ { + if (!biliCookie.hasBiliCookie) { + return 'warning' + } + return biliCookie.isCookieValid ? 'success' : 'error' +}) + +const cookieStatusText = computed(() => { + if (!biliCookie.hasBiliCookie) { + return '未同步' + } + return biliCookie.isCookieValid ? '正常' : '已失效' +}) + +function goCookieManagement() { + router.push({ name: 'client-fetcher' }) +} + // --- 计算属性 --- // (这里没有显式的计算属性,但 isLoggedIn 本身可能是一个来自 account 模块的计算属性) @@ -279,6 +300,33 @@ onMounted(() => { default-value="go-back-home" class="sider-menu" /> +
@@ -452,6 +500,28 @@ onMounted(() => { /* 菜单与顶部的间距 */ } + .cookie-status-card { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--n-border-color); + border-radius: 8px; + background-color: var(--n-card-color); + display: flex; + flex-direction: column; + gap: 8px; + } + + .cookie-status-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + .cookie-status-button { + margin-top: 4px; + } + /* Suspense 后备内容样式 */ .suspense-fallback { display: flex; diff --git a/src/client/DanmakuWindowManager.vue b/src/client/DanmakuWindowManager.vue index d6ca979..591418f 100644 --- a/src/client/DanmakuWindowManager.vue +++ b/src/client/DanmakuWindowManager.vue @@ -210,8 +210,19 @@ const separatorOptions = [ :x-gap="12" > - - + + + + + + + @@ -226,7 +237,7 @@ const separatorOptions = [ diff --git a/src/client/components/CookieInvalidAlert.vue b/src/client/components/CookieInvalidAlert.vue new file mode 100644 index 0000000..557af48 --- /dev/null +++ b/src/client/components/CookieInvalidAlert.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/client/data/initialize.ts b/src/client/data/initialize.ts index c894d75..3cd4d4e 100644 --- a/src/client/data/initialize.ts +++ b/src/client/data/initialize.ts @@ -36,6 +36,50 @@ let heartbeatTimer: number | null = null let updateCheckTimer: number | null = null let updateNotificationRef: any = null +function setInitStageSafely(stage: string) { + if (clientInitStage.value !== '启动完成') { + clientInitStage.value = stage + } +} + +function startDanmakuClientInitFlow() { + const danmakuInitNoticeRef = window.$notification.info({ + title: '正在初始化弹幕客户端...', + closable: false, + }) + setInitStageSafely('初始化弹幕客户端...') + + void initDanmakuClient() + .then((result) => { + if (result.success) { + info('[init] 弹幕客户端初始化完成') + window.$notification.success({ + title: '弹幕客户端初始化完成', + duration: 3000, + }) + setInitStageSafely('弹幕客户端初始化完成') + } else { + warn(`[init] 弹幕客户端初始化失败: ${result.message}`) + window.$notification.error({ + title: '弹幕客户端初始化失败', + content: result.message || '请稍后重试', + }) + setInitStageSafely('弹幕客户端初始化失败') + } + }) + .catch((error) => { + warn(`[init] 弹幕客户端初始化异常: ${error}`) + window.$notification.error({ + title: '弹幕客户端初始化异常', + content: `${error}`, + }) + setInitStageSafely('弹幕客户端初始化失败') + }) + .finally(() => { + danmakuInitNoticeRef?.destroy() + }) +} + // interface RtmpRelayState { // roomId: number // targetRtmpUrl: string @@ -334,29 +378,6 @@ export async function initAll(isOnBoot: boolean) { initInfo() info('[init] 开始更新数据') - if (isLoggedIn.value && accountInfo.value.isBiliVerified && !setting.settings.dev_disableDanmakuClient) { - const danmakuInitNoticeRef = window.$notification.info({ - title: '正在初始化弹幕客户端...', - closable: false, - }) - clientInitStage.value = '初始化弹幕客户端...' - const result = await initDanmakuClient() - danmakuInitNoticeRef.destroy() - if (result.success) { - window.$notification.success({ - title: '弹幕客户端初始化完成', - duration: 3000, - }) - clientInitStage.value = '弹幕客户端初始化完成' - } else { - window.$notification.error({ - title: `弹幕客户端初始化失败: ${result.message}`, - }) - clientInitStage.value = '弹幕客户端初始化失败' - } - } - info('[init] 已加载弹幕客户端') - // 初始化系统托盘图标和菜单 clientInitStage.value = '创建系统托盘...' const menu = await Menu.new({ items: [ @@ -378,7 +399,6 @@ export async function initAll(isOnBoot: boolean) { }) const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer() const options: TrayIconOptions = { - // here you can add a tray menu, title, tooltip, event handler, etc menu, title: 'VTsuru.Client', tooltip: 'VTsuru 事件收集器', @@ -393,6 +413,15 @@ export async function initAll(isOnBoot: boolean) { tray = await TrayIcon.new(options) clientInitStage.value = '系统托盘就绪' + const shouldInitDanmakuClient = isLoggedIn.value + && accountInfo.value.isBiliVerified + && !setting.settings.dev_disableDanmakuClient + if (shouldInitDanmakuClient) { + startDanmakuClientInitFlow() + } else { + info('[init] 跳过弹幕客户端初始化') + } + appWindow.setMinSize(new PhysicalSize(720, 480)) getAllWebviewWindows().then(async (windows) => { diff --git a/src/client/store/useDanmakuWindow.ts b/src/client/store/useDanmakuWindow.ts index 16ea4b1..3ef82ec 100644 --- a/src/client/store/useDanmakuWindow.ts +++ b/src/client/store/useDanmakuWindow.ts @@ -24,6 +24,7 @@ export interface DanmakuWindowSettings { animationDuration: number // 动画持续时间 enableAnimation: boolean // 是否启用动画效果 backgroundColor: string // 背景色 + windowBackgroundColor: string // 窗口背景色 textColor: string // 文字颜色 alwaysOnTop: boolean // 是否总在最前 interactive: boolean // 是否可交互(穿透鼠标点击) @@ -173,6 +174,7 @@ export const useDanmakuWindow = defineStore('danmakuWindow', () => { filterTypes: ['Message', 'Gift', 'SC', 'Guard'], animationDuration: 300, backgroundColor: 'rgba(0,0,0,0.6)', + windowBackgroundColor: 'rgba(0,0,0,0)', textColor: '#ffffff', alwaysOnTop: true, interactive: false,