mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: Add Tauri support and enhance client functionality
- Introduced Tauri as a new EventFetcherType in api-models. - Enhanced ClientFetcher.vue to support forced mode switching for Danmaku client. - Updated ClientLayout.vue to restrict usage outside Tauri environment with appropriate alerts. - Improved ClientSettings.vue to fetch and display the current version of the application. - Modified initialization logic in initialize.ts to handle minimized startup for Tauri. - Updated QueryBiliAPI function to conditionally use cookies based on a new parameter. - Added bootAsMinimized setting to useSettings store for better user experience. - Refactored logging in useWebFetcher to use console instead of logError/logInfo for clarity. - Created a new LabelItem component for better label handling in forms. - Enhanced EventFetcherStatusCard.vue to display version information based on EventFetcherType.
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"@microsoft/signalr": "^8.0.7",
|
"@microsoft/signalr": "^8.0.7",
|
||||||
"@microsoft/signalr-protocol-msgpack": "^8.0.7",
|
"@microsoft/signalr-protocol-msgpack": "^8.0.7",
|
||||||
"@mixer/postmessage-rpc": "^1.1.4",
|
"@mixer/postmessage-rpc": "^1.1.4",
|
||||||
|
"@oneidentity/zstd-js": "^1.0.3",
|
||||||
"@tauri-apps/api": "^2.4.0",
|
"@tauri-apps/api": "^2.4.0",
|
||||||
"@tauri-apps/plugin-autostart": "^2.3.0",
|
"@tauri-apps/plugin-autostart": "^2.3.0",
|
||||||
"@tauri-apps/plugin-http": "^2.4.2",
|
"@tauri-apps/plugin-http": "^2.4.2",
|
||||||
|
|||||||
215
src/Utils.ts
215
src/Utils.ts
@@ -1,4 +1,4 @@
|
|||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core';
|
||||||
import {
|
import {
|
||||||
ConfigProviderProps,
|
ConfigProviderProps,
|
||||||
NButton,
|
NButton,
|
||||||
@@ -10,114 +10,114 @@ import {
|
|||||||
dateZhCN,
|
dateZhCN,
|
||||||
useOsTheme,
|
useOsTheme,
|
||||||
zhCN
|
zhCN
|
||||||
} from 'naive-ui'
|
} from 'naive-ui';
|
||||||
import { SongFrom, SongsInfo, ThemeType } from './api/api-models'
|
import { SongFrom, SongsInfo, ThemeType } from './api/api-models';
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue';
|
||||||
import { VTSURU_API_URL } from './data/constants'
|
import { VTSURU_API_URL } from './data/constants';
|
||||||
import { DiscreteApiType } from 'naive-ui/es/discrete/src/interface'
|
import { DiscreteApiType } from 'naive-ui/es/discrete/src/interface';
|
||||||
import { SquareArrowForward24Filled } from '@vicons/fluent';
|
import { SquareArrowForward24Filled } from '@vicons/fluent';
|
||||||
import FiveSingIcon from '@/svgs/fivesing.svg'
|
import FiveSingIcon from '@/svgs/fivesing.svg';
|
||||||
import NeteaseIcon from '@/svgs/netease.svg'
|
import NeteaseIcon from '@/svgs/netease.svg';
|
||||||
|
|
||||||
const { message } = createDiscreteApi(['message'])
|
const { message } = createDiscreteApi(['message']);
|
||||||
|
|
||||||
const osThemeRef = useOsTheme() //获取当前系统主题
|
const osThemeRef = useOsTheme(); //获取当前系统主题
|
||||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
const themeType = useStorage('Settings.Theme', ThemeType.Auto);
|
||||||
export const theme = computed(() => {
|
export const theme = computed(() => {
|
||||||
if (themeType.value == ThemeType.Auto) {
|
if (themeType.value == ThemeType.Auto) {
|
||||||
var osThemeRef = useOsTheme() //获取当前系统主题
|
var osThemeRef = useOsTheme(); //获取当前系统主题
|
||||||
return osThemeRef.value === 'dark' ? darkTheme : null
|
return osThemeRef.value === 'dark' ? darkTheme : null;
|
||||||
} else {
|
} else {
|
||||||
return themeType.value == ThemeType.Dark ? darkTheme : null
|
return themeType.value == ThemeType.Dark ? darkTheme : null;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
export const configProviderPropsRef = computed<ConfigProviderProps>(() => ({
|
export const configProviderPropsRef = computed<ConfigProviderProps>(() => ({
|
||||||
theme: theme.value,
|
theme: theme.value,
|
||||||
locale: zhCN,
|
locale: zhCN,
|
||||||
dateLocale: dateZhCN,
|
dateLocale: dateZhCN,
|
||||||
|
|
||||||
}))
|
}));
|
||||||
export function createNaiveUIApi(types: DiscreteApiType[]) {
|
export function createNaiveUIApi(types: DiscreteApiType[]) {
|
||||||
return createDiscreteApi(types, {
|
return createDiscreteApi(types, {
|
||||||
configProviderProps: configProviderPropsRef
|
configProviderProps: configProviderPropsRef
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
export function NavigateToNewTab(url: string) {
|
export function NavigateToNewTab(url: string) {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
export const isDarkMode = computed(() => {
|
export const isDarkMode = computed(() => {
|
||||||
if (themeType.value == ThemeType.Auto) return osThemeRef.value === 'dark'
|
if (themeType.value == ThemeType.Auto) return osThemeRef.value === 'dark';
|
||||||
else return themeType.value == ThemeType.Dark
|
else return themeType.value == ThemeType.Dark;
|
||||||
})
|
});
|
||||||
export function copyToClipboard(text: string) {
|
export function copyToClipboard(text: string) {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text);
|
||||||
message.success('已复制到剪切板')
|
message.success('已复制到剪切板');
|
||||||
} else {
|
} else {
|
||||||
message.warning('当前环境不支持自动复制, 请手动选择并复制')
|
message.warning('当前环境不支持自动复制, 请手动选择并复制');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function objectsToCSV(arr: any[]) {
|
export function objectsToCSV(arr: any[]) {
|
||||||
const array = [Object.keys(arr[0])].concat(arr)
|
const array = [Object.keys(arr[0])].concat(arr);
|
||||||
return array
|
return array
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
return Object.values(row)
|
return Object.values(row)
|
||||||
.map((value) => {
|
.map((value) => {
|
||||||
return typeof value === 'string' ? JSON.stringify(value) : value
|
return typeof value === 'string' ? JSON.stringify(value) : value;
|
||||||
})
|
})
|
||||||
.toString()
|
.toString();
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n');
|
||||||
}
|
}
|
||||||
export function GetGuardColor(level: number | null | undefined): string {
|
export function GetGuardColor(level: number | null | undefined): string {
|
||||||
if (level) {
|
if (level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 1: {
|
case 1: {
|
||||||
return 'rgb(122, 4, 35)'
|
return 'rgb(122, 4, 35)';
|
||||||
}
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
return 'rgb(157, 155, 255)'
|
return 'rgb(157, 155, 255)';
|
||||||
}
|
}
|
||||||
case 3: {
|
case 3: {
|
||||||
return 'rgb(104, 136, 241)'
|
return 'rgb(104, 136, 241)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return '';
|
||||||
}
|
}
|
||||||
export function downloadImage(imageSrc: string, filename: string) {
|
export function downloadImage(imageSrc: string, filename: string) {
|
||||||
const image = new Image()
|
const image = new Image();
|
||||||
image.crossOrigin = 'Anonymous' // This might be needed depending on the image's server
|
image.crossOrigin = 'Anonymous'; // This might be needed depending on the image's server
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = image.width
|
canvas.width = image.width;
|
||||||
canvas.height = image.height
|
canvas.height = image.height;
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d');
|
||||||
ctx!.drawImage(image, 0, 0)
|
ctx!.drawImage(image, 0, 0);
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob)
|
link.href = URL.createObjectURL(blob);
|
||||||
link.download = filename
|
link.download = filename;
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link);
|
||||||
link.click()
|
link.click();
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link);
|
||||||
}
|
}
|
||||||
}) // Omitted the 'image/jpeg' to use the original image format
|
}); // Omitted the 'image/jpeg' to use the original image format
|
||||||
}
|
};
|
||||||
image.src = imageSrc
|
image.src = imageSrc;
|
||||||
}
|
}
|
||||||
export function getBase64(
|
export function getBase64(
|
||||||
file: File | undefined | null
|
file: File | undefined | null
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (!file) return new Promise((resolve) => resolve(undefined))
|
if (!file) return new Promise((resolve) => resolve(undefined));
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file);
|
||||||
reader.onload = () =>
|
reader.onload = () =>
|
||||||
resolve(reader.result?.toString().split(',')[1] || undefined)
|
resolve(reader.result?.toString().split(',')[1] || undefined);
|
||||||
reader.onerror = (error) => reject(error)
|
reader.onerror = (error) => reject(error);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
export async function getImageUploadModel(
|
export async function getImageUploadModel(
|
||||||
files: UploadFileInfo[] | undefined | null,
|
files: UploadFileInfo[] | undefined | null,
|
||||||
@@ -126,83 +126,83 @@ export async function getImageUploadModel(
|
|||||||
const result = {
|
const result = {
|
||||||
existImages: [],
|
existImages: [],
|
||||||
newImagesBase64: []
|
newImagesBase64: []
|
||||||
} as { existImages: string[]; newImagesBase64: string[] }
|
} as { existImages: string[]; newImagesBase64: string[]; };
|
||||||
if (!files) return result
|
if (!files) return result;
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i]
|
const file = files[i];
|
||||||
if ((file.file?.size ?? 0) > maxSize) {
|
if ((file.file?.size ?? 0) > maxSize) {
|
||||||
message.error('文件大小不能超过 ' + maxSize / 1024 / 1024 + 'MB')
|
message.error('文件大小不能超过 ' + maxSize / 1024 / 1024 + 'MB');
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
if (!file.file) {
|
if (!file.file) {
|
||||||
result.existImages.push(file.id) //用id绝对路径当的文件名
|
result.existImages.push(file.id); //用id绝对路径当的文件名
|
||||||
} else {
|
} else {
|
||||||
const base64 = await getBase64(file.file)
|
const base64 = await getBase64(file.file);
|
||||||
if (base64) {
|
if (base64) {
|
||||||
result.newImagesBase64.push(base64)
|
result.newImagesBase64.push(base64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
export function getUserAvatarUrl(userId: number | undefined | null) {
|
export function getUserAvatarUrl(userId: number | undefined | null) {
|
||||||
if (!userId) return ''
|
if (!userId) return '';
|
||||||
return VTSURU_API_URL + 'user-face/' + userId
|
return VTSURU_API_URL + 'user-face/' + userId;
|
||||||
}
|
}
|
||||||
export function getOUIdAvatarUrl(ouid: string) {
|
export function getOUIdAvatarUrl(ouid: string) {
|
||||||
return VTSURU_API_URL + 'face/' + ouid
|
return VTSURU_API_URL + 'face/' + ouid;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GuidUtils {
|
export class GuidUtils {
|
||||||
// 将数字转换为GUID
|
// 将数字转换为GUID
|
||||||
public static numToGuid(value: number): string {
|
public static numToGuid(value: number): string {
|
||||||
const buffer = new ArrayBuffer(16)
|
const buffer = new ArrayBuffer(16);
|
||||||
const view = new DataView(buffer)
|
const view = new DataView(buffer);
|
||||||
view.setBigUint64(8, BigInt(value)) // 将数字写入后8个字节
|
view.setBigUint64(8, BigInt(value)); // 将数字写入后8个字节
|
||||||
return GuidUtils.bufferToGuid(buffer)
|
return GuidUtils.bufferToGuid(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查GUID是否由数字生成
|
// 检查GUID是否由数字生成
|
||||||
public static isGuidFromUserId(guid: string): boolean {
|
public static isGuidFromUserId(guid: string): boolean {
|
||||||
const buffer = GuidUtils.guidToBuffer(guid)
|
const buffer = GuidUtils.guidToBuffer(guid);
|
||||||
const view = new DataView(buffer)
|
const view = new DataView(buffer);
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
if (view.getUint8(i) !== 0) return false // 检查前8个字节是否为0
|
if (view.getUint8(i) !== 0) return false; // 检查前8个字节是否为0
|
||||||
}
|
}
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将GUID转换为数字
|
// 将GUID转换为数字
|
||||||
public static guidToLong(guid: string): number {
|
public static guidToLong(guid: string): number {
|
||||||
const buffer = GuidUtils.guidToBuffer(guid)
|
const buffer = GuidUtils.guidToBuffer(guid);
|
||||||
const view = new DataView(buffer)
|
const view = new DataView(buffer);
|
||||||
return Number(view.getBigUint64(8))
|
return Number(view.getBigUint64(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:将ArrayBuffer转换为GUID字符串
|
// 辅助方法:将ArrayBuffer转换为GUID字符串
|
||||||
private static bufferToGuid(buffer: ArrayBuffer): string {
|
private static bufferToGuid(buffer: ArrayBuffer): string {
|
||||||
const bytes = new Uint8Array(buffer)
|
const bytes = new Uint8Array(buffer);
|
||||||
const guid = bytes.reduce((str, byte, idx) => {
|
const guid = bytes.reduce((str, byte, idx) => {
|
||||||
const pair = byte.toString(16).padStart(2, '0')
|
const pair = byte.toString(16).padStart(2, '0');
|
||||||
return (
|
return (
|
||||||
str +
|
str +
|
||||||
pair +
|
pair +
|
||||||
(idx === 3 || idx === 5 || idx === 7 || idx === 9 ? '-' : '')
|
(idx === 3 || idx === 5 || idx === 7 || idx === 9 ? '-' : '')
|
||||||
)
|
);
|
||||||
}, '')
|
}, '');
|
||||||
return guid
|
return guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:将GUID字符串转换为ArrayBuffer
|
// 辅助方法:将GUID字符串转换为ArrayBuffer
|
||||||
private static guidToBuffer(guid: string): ArrayBuffer {
|
private static guidToBuffer(guid: string): ArrayBuffer {
|
||||||
const hex = guid.replace(/-/g, '')
|
const hex = guid.replace(/-/g, '');
|
||||||
if (hex.length !== 32) throw new Error('Invalid GUID format.')
|
if (hex.length !== 32) throw new Error('Invalid GUID format.');
|
||||||
const buffer = new ArrayBuffer(16)
|
const buffer = new ArrayBuffer(16);
|
||||||
const view = new DataView(buffer)
|
const view = new DataView(buffer);
|
||||||
for (let i = 0; i < 16; i++) {
|
for (let i = 0; i < 16; i++) {
|
||||||
view.setUint8(i, parseInt(hex.substr(i * 2, 2), 16))
|
view.setUint8(i, parseInt(hex.substr(i * 2, 2), 16));
|
||||||
}
|
}
|
||||||
return buffer
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function GetPlayButton(song: SongsInfo) {
|
export function GetPlayButton(song: SongsInfo) {
|
||||||
@@ -218,7 +218,7 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
color: '#00BBB3',
|
color: '#00BBB3',
|
||||||
ghost: true,
|
ghost: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
window.open(`http://5sing.kugou.com/bz/${song.id}.html`)
|
window.open(`http://5sing.kugou.com/bz/${song.id}.html`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -227,7 +227,7 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
default: () => '在5sing打开',
|
default: () => '在5sing打开',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
case SongFrom.Netease:
|
case SongFrom.Netease:
|
||||||
return h(NTooltip, null, {
|
return h(NTooltip, null, {
|
||||||
@@ -239,7 +239,7 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
color: '#C20C0C',
|
color: '#C20C0C',
|
||||||
ghost: true,
|
ghost: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
window.open(`https://music.163.com/#/song?id=${song.id}`)
|
window.open(`https://music.163.com/#/song?id=${song.id}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -247,7 +247,7 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
default: () => '在网易云打开',
|
default: () => '在网易云打开',
|
||||||
})
|
});
|
||||||
case SongFrom.Custom:
|
case SongFrom.Custom:
|
||||||
return song.url
|
return song.url
|
||||||
? h(NTooltip, null, {
|
? h(NTooltip, null, {
|
||||||
@@ -259,7 +259,7 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
color: '#6b95bd',
|
color: '#6b95bd',
|
||||||
ghost: true,
|
ghost: true,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
window.open(song.url)
|
window.open(song.url);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -268,6 +268,33 @@ export function GetPlayButton(song: SongsInfo) {
|
|||||||
),
|
),
|
||||||
default: () => '打开链接',
|
default: () => '打开链接',
|
||||||
})
|
})
|
||||||
: null
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function getBrowserName() {
|
||||||
|
var userAgent = navigator.userAgent;
|
||||||
|
if (userAgent.indexOf("Opera") > -1 || userAgent.indexOf("OPR") > -1) {
|
||||||
|
return 'Opera';
|
||||||
|
}
|
||||||
|
else if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1) {
|
||||||
|
return 'IE';
|
||||||
|
}
|
||||||
|
else if (userAgent.indexOf("Edge") > -1) {
|
||||||
|
return 'Edge';
|
||||||
|
}
|
||||||
|
else if (userAgent.indexOf("Firefox") > -1) {
|
||||||
|
return 'Firefox';
|
||||||
|
}
|
||||||
|
else if (userAgent.indexOf("Safari") > -1 && userAgent.indexOf("Chrome") == -1) {
|
||||||
|
return 'Safari';
|
||||||
|
}
|
||||||
|
else if (userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1) {
|
||||||
|
return 'Chrome';
|
||||||
|
}
|
||||||
|
else if (!!window.ActiveXObject || "ActiveXObject" in window) {
|
||||||
|
return 'IE>=11';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'Unkonwn';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,6 +54,7 @@ export interface EventFetcherStateModel {
|
|||||||
export enum EventFetcherType {
|
export enum EventFetcherType {
|
||||||
Application,
|
Application,
|
||||||
OBS,
|
OBS,
|
||||||
|
Tauri,
|
||||||
Server
|
Server
|
||||||
}
|
}
|
||||||
export interface AccountInfo extends UserInfo {
|
export interface AccountInfo extends UserInfo {
|
||||||
|
|||||||
@@ -357,8 +357,8 @@
|
|||||||
const minutes = duration.minutes || 0; const seconds = duration.seconds || 0;
|
const minutes = duration.minutes || 0; const seconds = duration.seconds || 0;
|
||||||
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
async function onSwitchDanmakuClientMode(type: 'openlive' | 'direct') {
|
async function onSwitchDanmakuClientMode(type: 'openlive' | 'direct', force: boolean = false) {
|
||||||
if (webfetcher.webfetcherType === type) {
|
if (webfetcher.webfetcherType === type && !force) {
|
||||||
message.info('当前已是该模式'); return;
|
message.info('当前已是该模式'); return;
|
||||||
}
|
}
|
||||||
const noticeRef = window.$notification.info({
|
const noticeRef = window.$notification.info({
|
||||||
@@ -494,8 +494,7 @@
|
|||||||
embedded
|
embedded
|
||||||
style="width: 100%; max-width: 800px;"
|
style="width: 100%; max-width: 800px;"
|
||||||
>
|
>
|
||||||
<NFlex align="center">
|
<NFlex vertical>
|
||||||
<NText>模式:</NText>
|
|
||||||
<NRadioGroup
|
<NRadioGroup
|
||||||
v-model:value="settings.settings.useDanmakuClientType"
|
v-model:value="settings.settings.useDanmakuClientType"
|
||||||
:disabled="webfetcher.state === 'connecting'"
|
:disabled="webfetcher.state === 'connecting'"
|
||||||
@@ -519,6 +518,27 @@
|
|||||||
</NText>
|
</NText>
|
||||||
</NRadioButton>
|
</NRadioButton>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
|
<NPopconfirm
|
||||||
|
type="info"
|
||||||
|
:disabled="webfetcher.state === 'connecting'"
|
||||||
|
@positive-click="async () => {
|
||||||
|
await onSwitchDanmakuClientMode(settings.settings.useDanmakuClientType, true);
|
||||||
|
message.success('已重启弹幕服务器');
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<NButton
|
||||||
|
type="error"
|
||||||
|
style="max-width: 150px;"
|
||||||
|
:disabled="webfetcher.state === 'connecting'"
|
||||||
|
>
|
||||||
|
强制重启弹幕客户端
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
确定要强制重启弹幕服务器吗?
|
||||||
|
</template>
|
||||||
|
</NPopconfirm>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
@@ -789,6 +809,7 @@
|
|||||||
bordered
|
bordered
|
||||||
:columns="2"
|
:columns="2"
|
||||||
size="small"
|
size="small"
|
||||||
|
style="overflow-x: auto;"
|
||||||
>
|
>
|
||||||
<NDescriptionsItem label="启动时间">
|
<NDescriptionsItem label="启动时间">
|
||||||
<NIcon :component="TimeOutline" /> {{ formattedStartedAt }}
|
<NIcon :component="TimeOutline" /> {{ formattedStartedAt }}
|
||||||
@@ -800,6 +821,7 @@
|
|||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
size="small"
|
size="small"
|
||||||
|
:wrap="false"
|
||||||
>
|
>
|
||||||
<NTag
|
<NTag
|
||||||
:type="signalRStateType"
|
:type="signalRStateType"
|
||||||
@@ -807,8 +829,8 @@
|
|||||||
>
|
>
|
||||||
{{ signalRStateText }}
|
{{ signalRStateText }}
|
||||||
</NTag>
|
</NTag>
|
||||||
<NEllipsis style="max-width: 200px;">
|
<NEllipsis style="max-width: 150px;">
|
||||||
{{ webfetcher.signalRClient?.connectionId ?? 'N/A' }}
|
{{ webfetcher.signalRId ?? 'N/A' }}
|
||||||
</NEllipsis>
|
</NEllipsis>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NDescriptionsItem>
|
</NDescriptionsItem>
|
||||||
@@ -816,6 +838,7 @@
|
|||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
size="small"
|
size="small"
|
||||||
|
:wrap="false"
|
||||||
>
|
>
|
||||||
<NTag
|
<NTag
|
||||||
:type="danmakuClientStateType"
|
:type="danmakuClientStateType"
|
||||||
@@ -823,7 +846,7 @@
|
|||||||
>
|
>
|
||||||
{{ danmakuClientStateText }}
|
{{ danmakuClientStateText }}
|
||||||
</NTag>
|
</NTag>
|
||||||
<NEllipsis style="max-width: 200px;">
|
<NEllipsis style="max-width: 150px;">
|
||||||
{{ webfetcher.danmakuServerUrl ?? 'N/A' }}
|
{{ webfetcher.danmakuServerUrl ?? 'N/A' }}
|
||||||
</NEllipsis> <!-- Assuming this is exposed -->
|
</NEllipsis> <!-- Assuming this is exposed -->
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -968,10 +991,6 @@
|
|||||||
<NStatistic label="会话事件总数">
|
<NStatistic label="会话事件总数">
|
||||||
<NIcon :component="BarChartOutline" /> {{ webfetcher.sessionEventCount?.toLocaleString() ?? 0 }}
|
<NIcon :component="BarChartOutline" /> {{ webfetcher.sessionEventCount?.toLocaleString() ?? 0 }}
|
||||||
</NStatistic>
|
</NStatistic>
|
||||||
<NStatistic label="数据上传">
|
|
||||||
<NIcon :component="DownloadOutline" /> 成功: {{ webfetcher.successfulUploads ?? 0 }} / 失败:
|
|
||||||
{{ webfetcher.failedUploads ?? 0 }} <!-- Assuming exposed -->
|
|
||||||
</NStatistic>
|
|
||||||
<NStatistic label="已发送">
|
<NStatistic label="已发送">
|
||||||
{{ ((webfetcher.bytesSentSession ?? 0) / 1024).toFixed(2) }} KB <!-- Assuming exposed -->
|
{{ ((webfetcher.bytesSentSession ?? 0) / 1024).toFixed(2) }} KB <!-- Assuming exposed -->
|
||||||
</NStatistic>
|
</NStatistic>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import WindowBar from './WindowBar.vue';
|
import WindowBar from './WindowBar.vue';
|
||||||
import { initAll, OnClientUnmounted } from './data/initialize';
|
import { initAll, OnClientUnmounted } from './data/initialize';
|
||||||
import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
||||||
|
import { isTauri } from '@/data/constants';
|
||||||
|
|
||||||
// --- 响应式状态 ---
|
// --- 响应式状态 ---
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
|||||||
window.$message.success('登陆成功');
|
window.$message.success('登陆成功');
|
||||||
ACCOUNT.value = result; // 更新全局账户信息
|
ACCOUNT.value = result; // 更新全局账户信息
|
||||||
// isLoadingAccount.value = false; // 状态在 finally 中统一处理
|
// isLoadingAccount.value = false; // 状态在 finally 中统一处理
|
||||||
initAll(); // 初始化 WebFetcher
|
//initAll(false); // 初始化 WebFetcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -116,6 +117,14 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<NAlert
|
||||||
|
v-if="!isTauri"
|
||||||
|
type="error"
|
||||||
|
title="错误"
|
||||||
|
>
|
||||||
|
此应用在 Tauri 环境外运行,无法使用
|
||||||
|
</NAlert>
|
||||||
|
<template v-else>
|
||||||
<WindowBar />
|
<WindowBar />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -189,7 +198,7 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
|||||||
v-else
|
v-else
|
||||||
has-sider
|
has-sider
|
||||||
class="main-layout"
|
class="main-layout"
|
||||||
@vue:mounted="initAll()"
|
@vue:mounted="initAll(true)"
|
||||||
>
|
>
|
||||||
<NLayoutSider
|
<NLayoutSider
|
||||||
width="200"
|
width="200"
|
||||||
@@ -270,6 +279,7 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
|
|||||||
</div>
|
</div>
|
||||||
</NLayoutContent>
|
</NLayoutContent>
|
||||||
</NLayout>
|
</NLayout>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
NFormItem, // Added NFormItem
|
NFormItem, // Added NFormItem
|
||||||
NAlert,
|
NAlert,
|
||||||
NCheckboxGroup,
|
NCheckboxGroup,
|
||||||
NCheckbox, // Added NAlert for error messages
|
NCheckbox,
|
||||||
|
NDivider, // Added NAlert for error messages
|
||||||
} from 'naive-ui';
|
} from 'naive-ui';
|
||||||
import type { MenuOption } from 'naive-ui'; // Import MenuOption type
|
import type { MenuOption } from 'naive-ui'; // Import MenuOption type
|
||||||
import { ThemeType } from '@/api/api-models';
|
import { ThemeType } from '@/api/api-models';
|
||||||
import { NotificationType, useSettings } from './store/useSettings';
|
import { NotificationType, useSettings } from './store/useSettings';
|
||||||
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ const isLoading = ref(true); // Loading state for initial fetch
|
|||||||
const errorMsg = ref<string | null>(null); // Error message state
|
const errorMsg = ref<string | null>(null); // Error message state
|
||||||
|
|
||||||
const setting = useSettings();
|
const setting = useSettings();
|
||||||
|
const currentVersion = await getVersion(); // Fetch current version on mount
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
const navOptions: MenuOption[] = [ // Explicitly typed
|
const navOptions: MenuOption[] = [ // Explicitly typed
|
||||||
@@ -73,10 +76,10 @@ watch(isStartOnBoot, async (newValue, oldValue) => {
|
|||||||
try {
|
try {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
await enable();
|
await enable();
|
||||||
window.$message.success('已启用开机启动');
|
//window.$message.success('已启用开机启动');
|
||||||
} else {
|
} else {
|
||||||
await disable();
|
await disable();
|
||||||
window.$message.success('已禁用开机启动'); // Provide feedback for disabling too
|
//window.$message.success('已禁用开机启动'); // Provide feedback for disabling too
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to update autostart status:", err);
|
console.error("Failed to update autostart status:", err);
|
||||||
@@ -170,14 +173,16 @@ watch(minimizeOnStart, (newValue) => {
|
|||||||
<template v-if="currentTab === 'general'">
|
<template v-if="currentTab === 'general'">
|
||||||
<NSpace
|
<NSpace
|
||||||
vertical
|
vertical
|
||||||
size="large"
|
|
||||||
>
|
>
|
||||||
<NCard
|
<NCard
|
||||||
title="启动"
|
title="启动"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
>
|
>
|
||||||
<NSpace vertical>
|
<NFlex
|
||||||
<NFormItem
|
vertical
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<LabelItem
|
||||||
label="开机时启动应用"
|
label="开机时启动应用"
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
>
|
>
|
||||||
@@ -185,15 +190,19 @@ watch(minimizeOnStart, (newValue) => {
|
|||||||
v-model:value="isStartOnBoot"
|
v-model:value="isStartOnBoot"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</LabelItem>
|
||||||
<NFormItem
|
<LabelItem
|
||||||
|
v-if="isStartOnBoot"
|
||||||
label="启动后最小化到托盘"
|
label="启动后最小化到托盘"
|
||||||
label-placement="left"
|
label-placement="left"
|
||||||
>
|
>
|
||||||
<NSwitch v-model:value="minimizeOnStart" />
|
<NSwitch
|
||||||
|
v-model:value="setting.settings.bootAsMinimized"
|
||||||
|
@update:value="setting.save()"
|
||||||
|
/>
|
||||||
<!-- Add appropriate logic/state for this -->
|
<!-- Add appropriate logic/state for this -->
|
||||||
</NFormItem>
|
</LabelItem>
|
||||||
</NSpace>
|
</NFlex>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<NCard
|
<NCard
|
||||||
@@ -230,6 +239,10 @@ watch(minimizeOnStart, (newValue) => {
|
|||||||
title="通知设置"
|
title="通知设置"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
>
|
>
|
||||||
|
<NAlert type="warning">
|
||||||
|
暂未完成
|
||||||
|
</NAlert>
|
||||||
|
<NDivider />
|
||||||
<NSpace vertical>
|
<NSpace vertical>
|
||||||
<NCheckbox
|
<NCheckbox
|
||||||
v-model:checked="setting.settings.enableNotification"
|
v-model:checked="setting.settings.enableNotification"
|
||||||
@@ -284,8 +297,42 @@ watch(minimizeOnStart, (newValue) => {
|
|||||||
@click="$router.push({name: 'client-test'})"
|
@click="$router.push({name: 'client-test'})"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<p>应用名称: Your App Name</p>
|
<p>VTsuruEventFetcher Tauri</p>
|
||||||
<p>版本: 1.0.0</p>
|
<p>版本: {{ currentVersion }}</p>
|
||||||
|
<p>
|
||||||
|
作者:
|
||||||
|
<NButton
|
||||||
|
tag="a"
|
||||||
|
href="https://space.bilibili.com/10021741"
|
||||||
|
target="_blank"
|
||||||
|
type="info"
|
||||||
|
text
|
||||||
|
>
|
||||||
|
Megghy
|
||||||
|
</NButton>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
储存库:
|
||||||
|
<NButton
|
||||||
|
tag="a"
|
||||||
|
href="https://github.com/Megghy/vtsuru.live/tree/master/src/client"
|
||||||
|
target="_blank"
|
||||||
|
type="info"
|
||||||
|
text
|
||||||
|
>
|
||||||
|
界面/逻辑
|
||||||
|
</NButton>
|
||||||
|
<NDivider vertical />
|
||||||
|
<NButton
|
||||||
|
tag="a"
|
||||||
|
href="https://github.com/Megghy/vtsuru-fetcher-client"
|
||||||
|
target="_blank"
|
||||||
|
type="info"
|
||||||
|
text
|
||||||
|
>
|
||||||
|
Tauri 客户端
|
||||||
|
</NButton>
|
||||||
|
</p>
|
||||||
<!-- Add more about info -->
|
<!-- Add more about info -->
|
||||||
</NCard>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
@@ -312,7 +359,9 @@ watch(minimizeOnStart, (newValue) => {
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
.label-item {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
/* Optional: Adjust NFormItem label alignment if needed */
|
/* Optional: Adjust NFormItem label alignment if needed */
|
||||||
/* :deep(.n-form-item-label) { */
|
/* :deep(.n-form-item-label) { */
|
||||||
/* Add custom styles */
|
/* Add custom styles */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getBuvid, getRoomKey } from "./utils";
|
|||||||
import { initInfo } from "./info";
|
import { initInfo } from "./info";
|
||||||
import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray';
|
import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray';
|
||||||
import { Menu } from "@tauri-apps/api/menu";
|
import { Menu } from "@tauri-apps/api/menu";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow, PhysicalSize } from "@tauri-apps/api/window";
|
||||||
import {
|
import {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
onAction,
|
onAction,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
sendNotification,
|
sendNotification,
|
||||||
} from '@tauri-apps/plugin-notification';
|
} from '@tauri-apps/plugin-notification';
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { CN_HOST } from "@/data/constants";
|
import { CN_HOST, isDev } from "@/data/constants";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { check } from '@tauri-apps/plugin-updater';
|
import { check } from '@tauri-apps/plugin-updater';
|
||||||
import { relaunch } from '@tauri-apps/plugin-process';
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
@@ -24,10 +24,17 @@ const accountInfo = useAccount();
|
|||||||
|
|
||||||
export const clientInited = ref(false);
|
export const clientInited = ref(false);
|
||||||
let tray: TrayIcon;
|
let tray: TrayIcon;
|
||||||
export async function initAll() {
|
export async function initAll(isOnBoot: boolean) {
|
||||||
|
const setting = useSettings();
|
||||||
if (clientInited.value) {
|
if (clientInited.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isOnBoot) {
|
||||||
|
if (setting.settings.bootAsMinimized && !isDev) {
|
||||||
|
const appWindow = getCurrentWindow();
|
||||||
|
appWindow.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
let permissionGranted = await isPermissionGranted();
|
let permissionGranted = await isPermissionGranted();
|
||||||
checkUpdate();
|
checkUpdate();
|
||||||
|
|
||||||
@@ -102,10 +109,10 @@ export async function initAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
tray = await TrayIcon.new(options);
|
tray = await TrayIcon.new(options);
|
||||||
|
|
||||||
|
appWindow.setMinSize(new PhysicalSize(720, 480));
|
||||||
|
|
||||||
clientInited.value = true;
|
clientInited.value = true;
|
||||||
}
|
}
|
||||||
export function OnClientUnmounted() {
|
export function OnClientUnmounted() {
|
||||||
@@ -209,7 +216,7 @@ export async function initOpenLive() {
|
|||||||
}
|
}
|
||||||
return reuslt;
|
return reuslt;
|
||||||
}
|
}
|
||||||
function initNotificationHandler(){
|
function initNotificationHandler() {
|
||||||
onAction((event) => {
|
onAction((event) => {
|
||||||
if (event.extra?.type === 'question-box') {
|
if (event.extra?.type === 'question-box') {
|
||||||
openUrl(CN_HOST + '/manage/question-box');
|
openUrl(CN_HOST + '/manage/question-box');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { QueryPostAPI } from '@/api/query';
|
|||||||
import { OPEN_LIVE_API_URL } from '@/data/constants';
|
import { OPEN_LIVE_API_URL } from '@/data/constants';
|
||||||
import { error } from '@tauri-apps/plugin-log';
|
import { error } from '@tauri-apps/plugin-log';
|
||||||
|
|
||||||
export async function QueryBiliAPI(url: string, method: string = 'GET', cookie: string = '') {
|
export async function QueryBiliAPI(url: string, method: string = 'GET', cookie: string = '', useCookie: boolean = true) {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
@@ -12,8 +12,7 @@ export async function QueryBiliAPI(url: string, method: string = 'GET', cookie:
|
|||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
|
||||||
Origin: '',
|
Origin: '',
|
||||||
Cookie: cookie || (await useBiliCookie().getBiliCookie()) || '',
|
Cookie: useCookie ? (cookie || (await useBiliCookie().getBiliCookie()) || '') : ''
|
||||||
'Upgrade-Insecure-Requests': '1',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type NotificationSettings = {
|
|||||||
export type VTsuruClientSettings = {
|
export type VTsuruClientSettings = {
|
||||||
useDanmakuClientType: 'openlive' | 'direct';
|
useDanmakuClientType: 'openlive' | 'direct';
|
||||||
fallbackToOpenLive: boolean;
|
fallbackToOpenLive: boolean;
|
||||||
|
bootAsMinimized: boolean;
|
||||||
|
|
||||||
danmakuHistorySize: number;
|
danmakuHistorySize: number;
|
||||||
loginType: 'qrcode' | 'cookiecloud'
|
loginType: 'qrcode' | 'cookiecloud'
|
||||||
@@ -20,6 +21,7 @@ export const useSettings = defineStore('settings', () => {
|
|||||||
const defaultSettings: VTsuruClientSettings = {
|
const defaultSettings: VTsuruClientSettings = {
|
||||||
useDanmakuClientType: 'openlive',
|
useDanmakuClientType: 'openlive',
|
||||||
fallbackToOpenLive: true,
|
fallbackToOpenLive: true,
|
||||||
|
bootAsMinimized: true,
|
||||||
|
|
||||||
danmakuHistorySize: 100,
|
danmakuHistorySize: 100,
|
||||||
loginType: 'qrcode',
|
loginType: 'qrcode',
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -15,6 +15,7 @@ declare module 'vue' {
|
|||||||
EventFetcherAlert: typeof import('./components/EventFetcherAlert.vue')['default']
|
EventFetcherAlert: typeof import('./components/EventFetcherAlert.vue')['default']
|
||||||
EventFetcherStatusCard: typeof import('./components/EventFetcherStatusCard.vue')['default']
|
EventFetcherStatusCard: typeof import('./components/EventFetcherStatusCard.vue')['default']
|
||||||
FeedbackItem: typeof import('./components/FeedbackItem.vue')['default']
|
FeedbackItem: typeof import('./components/FeedbackItem.vue')['default']
|
||||||
|
LabelItem: typeof import('./components/LabelItem.vue')['default']
|
||||||
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
||||||
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
|
|||||||
@@ -6,7 +6,21 @@ import { NAlert, NButton, NDivider, NIcon, NTag, NText, NTooltip } from 'naive-u
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const state = accountInfo.value?.eventFetcherState
|
const state = accountInfo.value?.eventFetcherState
|
||||||
|
|
||||||
|
const eventFetcherVersionName = computed(() => {
|
||||||
|
if (state?.type == EventFetcherType.OBS) {
|
||||||
|
return 'OBS/网页端'
|
||||||
|
} else if (state?.type == EventFetcherType.Application) {
|
||||||
|
return '控制台应用'
|
||||||
|
} else if (state?.type == EventFetcherType.Server) {
|
||||||
|
return '本站监听 (已删除)'
|
||||||
|
} else if (state?.type == EventFetcherType.Tauri) {
|
||||||
|
return 'Tauri 应用'
|
||||||
|
} else {
|
||||||
|
return state?.version ?? '未知'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const status = computed(() => {
|
const status = computed(() => {
|
||||||
if (state.online == true) {
|
if (state.online == true) {
|
||||||
@@ -57,7 +71,7 @@ const status = computed(() => {
|
|||||||
:color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }"
|
:color="{ borderColor: 'white', textColor: 'white', color: '#4b6159' }"
|
||||||
>
|
>
|
||||||
<NIcon :component="FlashCheckmark16Filled" />
|
<NIcon :component="FlashCheckmark16Filled" />
|
||||||
{{ state.type == EventFetcherType.OBS ? 'OBS/网页端' : state.version ?? '未知' }}
|
{{ eventFetcherVersionName }}
|
||||||
</NTag>
|
</NTag>
|
||||||
</template>
|
</template>
|
||||||
你所使用的版本
|
你所使用的版本
|
||||||
|
|||||||
24
src/components/LabelItem.vue
Normal file
24
src/components/LabelItem.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<span class="label-item">
|
||||||
|
<p>
|
||||||
|
{{ label }}
|
||||||
|
</p>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
label: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.label-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { cookie, useAccount } from '@/api/account';
|
||||||
import { ref, computed, shallowRef } from 'vue'; // shallowRef 用于非深度响应对象
|
import { getEventType, recordEvent, streamingInfo } from '@/client/data/info';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { BASE_HUB_URL, isDev, isTauri } from '@/data/constants';
|
||||||
import { useRoute } from 'vue-router';
|
import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient';
|
||||||
import { compress } from 'brotli-compress';
|
import DirectClient, { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient';
|
||||||
import { format } from 'date-fns';
|
import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient';
|
||||||
import * as signalR from '@microsoft/signalr';
|
import * as signalR from '@microsoft/signalr';
|
||||||
import * as msgpack from '@microsoft/signalr-protocol-msgpack';
|
import * as msgpack from '@microsoft/signalr-protocol-msgpack';
|
||||||
import { cookie, useAccount } from '@/api/account'; // 假设账户信息路径
|
import { defineStore } from 'pinia';
|
||||||
import { BASE_HUB_URL, isDev } from '@/data/constants'; // 假设常量路径
|
import { computed, ref, shallowRef } from 'vue'; // shallowRef 用于非深度响应对象
|
||||||
import BaseDanmakuClient from '@/data/DanmakuClients/BaseDanmakuClient'; // 假设弹幕客户端基类路径
|
import { useRoute } from 'vue-router';
|
||||||
import DirectClient, { DirectClientAuthInfo } from '@/data/DanmakuClients/DirectClient'; // 假设直连客户端路径
|
|
||||||
import OpenLiveClient from '@/data/DanmakuClients/OpenLiveClient'; // 假设开放平台客户端路径
|
|
||||||
import { error as logError, info as logInfo } from '@tauri-apps/plugin-log'; // 使用日志插件
|
|
||||||
import { getEventType, recordEvent, streamingInfo } from '@/client/data/info';
|
|
||||||
import { useWebRTC } from './useRTC';
|
import { useWebRTC } from './useRTC';
|
||||||
|
import { QueryBiliAPI } from '@/client/data/utils';
|
||||||
|
import { platform, type, version } from '@tauri-apps/plugin-os';
|
||||||
|
import { ZstdCodec, ZstdInit } from '@oneidentity/zstd-js/wasm';
|
||||||
|
|
||||||
|
import { encode } from "@msgpack/msgpack";
|
||||||
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
|
|
||||||
export const useWebFetcher = defineStore('WebFetcher', () => {
|
export const useWebFetcher = defineStore('WebFetcher', () => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -24,11 +26,14 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
const state = ref<'disconnected' | 'connecting' | 'connected'>('disconnected'); // SignalR 连接状态
|
const state = ref<'disconnected' | 'connecting' | 'connected'>('disconnected'); // SignalR 连接状态
|
||||||
const startedAt = ref<Date>(); // 本次启动时间
|
const startedAt = ref<Date>(); // 本次启动时间
|
||||||
const signalRClient = shallowRef<signalR.HubConnection>(); // SignalR 客户端实例 (浅响应)
|
const signalRClient = shallowRef<signalR.HubConnection>(); // SignalR 客户端实例 (浅响应)
|
||||||
|
const signalRId = ref<string>(); // SignalR 连接 ID
|
||||||
const client = shallowRef<BaseDanmakuClient>(); // 弹幕客户端实例 (浅响应)
|
const client = shallowRef<BaseDanmakuClient>(); // 弹幕客户端实例 (浅响应)
|
||||||
let timer: any; // 事件发送定时器
|
let timer: any; // 事件发送定时器
|
||||||
let disconnectedByServer = false;
|
let disconnectedByServer = false;
|
||||||
let isFromClient = false; // 是否由Tauri客户端启动
|
let isFromClient = false; // 是否由Tauri客户端启动
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- 新增: 详细状态与信息 ---
|
// --- 新增: 详细状态与信息 ---
|
||||||
/** 弹幕客户端内部状态 */
|
/** 弹幕客户端内部状态 */
|
||||||
const danmakuClientState = ref<'stopped' | 'connecting' | 'connected'>('stopped'); // 更详细的弹幕客户端状态
|
const danmakuClientState = ref<'stopped' | 'connecting' | 'connected'>('stopped'); // 更详细的弹幕客户端状态
|
||||||
@@ -52,6 +57,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
const failedUploads = ref(0);
|
const failedUploads = ref(0);
|
||||||
/** 本次会话发送的总字节数 (压缩后) */
|
/** 本次会话发送的总字节数 (压缩后) */
|
||||||
const bytesSentSession = ref(0);
|
const bytesSentSession = ref(0);
|
||||||
|
let zstd: ZstdCodec | undefined = undefined; // Zstd 编码器实例 (如果需要压缩)
|
||||||
|
|
||||||
const prefix = computed(() => isFromClient ? '[web-fetcher-iframe] ' : '[web-fetcher] ');
|
const prefix = computed(() => isFromClient ? '[web-fetcher-iframe] ' : '[web-fetcher] ');
|
||||||
|
|
||||||
@@ -64,9 +70,15 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
_isFromClient: boolean = false
|
_isFromClient: boolean = false
|
||||||
): Promise<{ success: boolean; message: string; }> {
|
): Promise<{ success: boolean; message: string; }> {
|
||||||
if (state.value === 'connected' || state.value === 'connecting') {
|
if (state.value === 'connected' || state.value === 'connecting') {
|
||||||
logInfo(prefix.value + '已经启动,无需重复启动');
|
console.log(prefix.value + '已经启动,无需重复启动');
|
||||||
return { success: true, message: '已启动' };
|
return { success: true, message: '已启动' };
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
zstd ??= await ZstdInit();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(prefix.value + '当前浏览器不支持zstd压缩, 回退到原始数据传输');
|
||||||
|
}
|
||||||
webfetcherType.value = type; // 设置弹幕客户端类型
|
webfetcherType.value = type; // 设置弹幕客户端类型
|
||||||
// 重置会话统计数据
|
// 重置会话统计数据
|
||||||
resetSessionStats();
|
resetSessionStats();
|
||||||
@@ -76,9 +88,9 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
|
|
||||||
// 使用 navigator.locks 确保同一时间只有一个 Start 操作执行
|
// 使用 navigator.locks 确保同一时间只有一个 Start 操作执行
|
||||||
const result = await navigator.locks.request('webFetcherStartLock', async () => {
|
const result = await navigator.locks.request('webFetcherStartLock', async () => {
|
||||||
logInfo(prefix.value + '开始启动...');
|
console.log(prefix.value + '开始启动...');
|
||||||
while (!(await connectSignalR())) {
|
while (!(await connectSignalR())) {
|
||||||
logInfo(prefix.value + '连接 SignalR 失败, 5秒后重试');
|
console.log(prefix.value + '连接 SignalR 失败, 5秒后重试');
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
// 如果用户手动停止,则退出重试循环
|
// 如果用户手动停止,则退出重试循环
|
||||||
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
|
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
|
||||||
@@ -86,7 +98,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
|
|
||||||
let danmakuResult = await connectDanmakuClient(type, directAuthInfo);
|
let danmakuResult = await connectDanmakuClient(type, directAuthInfo);
|
||||||
while (!danmakuResult?.success) {
|
while (!danmakuResult?.success) {
|
||||||
logInfo(prefix.value + '弹幕客户端启动失败, 5秒后重试');
|
console.log(prefix.value + '弹幕客户端启动失败, 5秒后重试');
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
// 如果用户手动停止,则退出重试循环
|
// 如果用户手动停止,则退出重试循环
|
||||||
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
|
if (state.value === 'disconnected') return { success: false, message: '用户手动停止' };
|
||||||
@@ -96,7 +108,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
// 只有在两个连接都成功后才设置为 connected
|
// 只有在两个连接都成功后才设置为 connected
|
||||||
state.value = 'connected';
|
state.value = 'connected';
|
||||||
disconnectedByServer = false;
|
disconnectedByServer = false;
|
||||||
logInfo(prefix.value + '启动成功');
|
console.log(prefix.value + '启动成功');
|
||||||
return { success: true, message: '启动成功' };
|
return { success: true, message: '启动成功' };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,7 +127,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
function Stop() {
|
function Stop() {
|
||||||
if (state.value === 'disconnected') return;
|
if (state.value === 'disconnected') return;
|
||||||
|
|
||||||
logInfo(prefix.value + '正在停止...');
|
console.log(prefix.value + '正在停止...');
|
||||||
state.value = 'disconnected'; // 立即设置状态,防止重连逻辑触发
|
state.value = 'disconnected'; // 立即设置状态,防止重连逻辑触发
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
@@ -137,7 +149,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
events.length = 0; // 清空事件队列
|
events.length = 0; // 清空事件队列
|
||||||
// resetSessionStats(); // 会话统计在下次 Start 时重置
|
// resetSessionStats(); // 会话统计在下次 Start 时重置
|
||||||
|
|
||||||
logInfo(prefix.value + '已停止');
|
console.log(prefix.value + '已停止');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置会话统计数据 */
|
/** 重置会话统计数据 */
|
||||||
@@ -157,11 +169,11 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
directConnectInfo?: DirectClientAuthInfo
|
directConnectInfo?: DirectClientAuthInfo
|
||||||
) {
|
) {
|
||||||
if (client.value?.state === 'connected' || client.value?.state === 'connecting') {
|
if (client.value?.state === 'connected' || client.value?.state === 'connecting') {
|
||||||
logInfo(prefix.value + '弹幕客户端已连接或正在连接');
|
console.log(prefix.value + '弹幕客户端已连接或正在连接');
|
||||||
return { success: true, message: '弹幕客户端已启动' };
|
return { success: true, message: '弹幕客户端已启动' };
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(prefix.value + '正在连接弹幕客户端...');
|
console.log(prefix.value + '正在连接弹幕客户端...');
|
||||||
danmakuClientState.value = 'connecting';
|
danmakuClientState.value = 'connecting';
|
||||||
|
|
||||||
// 如果实例存在但已停止,先清理
|
// 如果实例存在但已停止,先清理
|
||||||
@@ -176,7 +188,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
} else {
|
} else {
|
||||||
if (!directConnectInfo) {
|
if (!directConnectInfo) {
|
||||||
danmakuClientState.value = 'stopped';
|
danmakuClientState.value = 'stopped';
|
||||||
logError(prefix.value + '未提供直连弹幕客户端认证信息');
|
console.error(prefix.value + '未提供直连弹幕客户端认证信息');
|
||||||
return { success: false, message: '未提供弹幕客户端认证信息' };
|
return { success: false, message: '未提供弹幕客户端认证信息' };
|
||||||
}
|
}
|
||||||
client.value = new DirectClient(directConnectInfo);
|
client.value = new DirectClient(directConnectInfo);
|
||||||
@@ -193,13 +205,13 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
const result = await client.value?.Start();
|
const result = await client.value?.Start();
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
logInfo(prefix.value + '弹幕客户端连接成功, 开始监听弹幕');
|
console.log(prefix.value + '弹幕客户端连接成功, 开始监听弹幕');
|
||||||
danmakuClientState.value = 'connected'; // 明确设置状态
|
danmakuClientState.value = 'connected'; // 明确设置状态
|
||||||
danmakuServerUrl.value = client.value.serverUrl; // 获取服务器地址
|
danmakuServerUrl.value = client.value.serverUrl; // 获取服务器地址
|
||||||
// 启动事件发送定时器 (如果之前没有启动)
|
// 启动事件发送定时器 (如果之前没有启动)
|
||||||
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
|
timer ??= setInterval(sendEvents, 1500); // 每 1.5 秒尝试发送一次事件
|
||||||
} else {
|
} else {
|
||||||
logError(prefix.value + '弹幕客户端启动失败: ' + result?.message);
|
console.error(prefix.value + '弹幕客户端启动失败: ' + result?.message);
|
||||||
danmakuClientState.value = 'stopped';
|
danmakuClientState.value = 'stopped';
|
||||||
danmakuServerUrl.value = undefined;
|
danmakuServerUrl.value = undefined;
|
||||||
client.value = undefined; // 启动失败,清理实例,下次会重建
|
client.value = undefined; // 启动失败,清理实例,下次会重建
|
||||||
@@ -212,11 +224,11 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
*/
|
*/
|
||||||
async function connectSignalR() {
|
async function connectSignalR() {
|
||||||
if (signalRClient.value && signalRClient.value.state !== signalR.HubConnectionState.Disconnected) {
|
if (signalRClient.value && signalRClient.value.state !== signalR.HubConnectionState.Disconnected) {
|
||||||
logInfo(prefix.value + "SignalR 已连接或正在连接");
|
console.log(prefix.value + "SignalR 已连接或正在连接");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(prefix.value + '正在连接到 vtsuru 服务器...');
|
console.log(prefix.value + '正在连接到 vtsuru 服务器...');
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + (route.query.token ?? account.value.token), { // 使用 account.token
|
.withUrl(BASE_HUB_URL + 'web-fetcher?token=' + (route.query.token ?? account.value.token), { // 使用 account.token
|
||||||
headers: { Authorization: `Bearer ${cookie.value?.cookie}` },
|
headers: { Authorization: `Bearer ${cookie.value?.cookie}` },
|
||||||
@@ -229,71 +241,98 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
|
|
||||||
// --- SignalR 事件监听 ---
|
// --- SignalR 事件监听 ---
|
||||||
connection.onreconnecting(error => {
|
connection.onreconnecting(error => {
|
||||||
logInfo(prefix.value + `与服务器断开,正在尝试重连... ${error?.message || ''}`);
|
console.log(prefix.value + `与服务器断开,正在尝试重连... ${error?.message || ''}`);
|
||||||
state.value = 'connecting'; // 更新状态为连接中
|
state.value = 'connecting'; // 更新状态为连接中
|
||||||
signalRConnectionId.value = undefined; // 连接断开,ID失效
|
signalRConnectionId.value = undefined; // 连接断开,ID失效
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onreconnected(connectionId => {
|
connection.onreconnected(async connectionId => {
|
||||||
logInfo(prefix.value + `与服务器重新连接成功! ConnectionId: ${connectionId}`);
|
console.log(prefix.value + `与服务器重新连接成功! ConnectionId: ${connectionId}`);
|
||||||
signalRConnectionId.value = connectionId ?? undefined;
|
signalRConnectionId.value = connectionId ?? undefined;
|
||||||
state.value = 'connected'; // 更新状态为已连接
|
state.value = 'connected'; // 更新状态为已连接
|
||||||
// 重连成功后可能需要重新发送标识
|
signalRId.value = connectionId ?? await sendSelfInfo(connection); // 更新连接ID
|
||||||
if (isFromClient) {
|
connection.send('Reconnected').catch(err => console.error(prefix.value + "Send Reconnected failed: " + err));
|
||||||
connection.send('SetAsVTsuruClient').catch(err => logError(prefix.value + "Send SetAsVTsuruClient failed: " + err));
|
|
||||||
}
|
|
||||||
connection.send('Reconnected').catch(err => logError(prefix.value + "Send Reconnected failed: " + err));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onclose(async (error) => {
|
connection.onclose(async (error) => {
|
||||||
// 只有在不是由 Stop() 或服务器明确要求断开时才记录错误并尝试独立重连(虽然 withAutomaticReconnect 应该处理)
|
// 只有在不是由 Stop() 或服务器明确要求断开时才记录错误并尝试独立重连(虽然 withAutomaticReconnect 应该处理)
|
||||||
if (state.value !== 'disconnected' && !disconnectedByServer) {
|
if (state.value !== 'disconnected' && !disconnectedByServer) {
|
||||||
logError(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`);
|
console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`);
|
||||||
state.value = 'connecting'; // 标记为连接中,等待自动重连
|
state.value = 'connecting'; // 标记为连接中,等待自动重连
|
||||||
signalRConnectionId.value = undefined;
|
signalRConnectionId.value = undefined;
|
||||||
// withAutomaticReconnect 会处理重连,这里不需要手动调用 reconnect
|
// withAutomaticReconnect 会处理重连,这里不需要手动调用 reconnect
|
||||||
} else if (disconnectedByServer) {
|
} else if (disconnectedByServer) {
|
||||||
logInfo(prefix.value + `连接已被服务器关闭.`);
|
console.log(prefix.value + `连接已被服务器关闭.`);
|
||||||
Stop(); // 服务器要求断开,则彻底停止
|
Stop(); // 服务器要求断开,则彻底停止
|
||||||
} else {
|
} else {
|
||||||
logInfo(prefix.value + `连接已手动关闭.`);
|
console.log(prefix.value + `连接已手动关闭.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.on('Disconnect', (reason: unknown) => {
|
connection.on('Disconnect', (reason: unknown) => {
|
||||||
logInfo(prefix.value + '被服务器断开连接: ' + reason);
|
console.log(prefix.value + '被服务器断开连接: ' + reason);
|
||||||
disconnectedByServer = true; // 标记是服务器主动断开
|
disconnectedByServer = true; // 标记是服务器主动断开
|
||||||
Stop(); // 服务器要求断开,调用 Stop 清理所有资源
|
Stop(); // 服务器要求断开,调用 Stop 清理所有资源
|
||||||
});
|
});
|
||||||
|
connection.on('Request', async (url: string, method: string, body: string, useCookie: boolean) => onRequest(url, method, body, useCookie));
|
||||||
|
|
||||||
// --- 尝试启动连接 ---
|
// --- 尝试启动连接 ---
|
||||||
try {
|
try {
|
||||||
await connection.start();
|
await connection.start();
|
||||||
logInfo(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId);
|
|
||||||
console.log(prefix.value + '已连接到 vtsuru 服务器, ConnectionId: ' + connection.connectionId); // 调试输出连接状态
|
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); // 发送客户端信息
|
||||||
await connection.send('Finished'); // 通知服务器已准备好
|
await connection.send('Finished'); // 通知服务器已准备好
|
||||||
if (isFromClient) {
|
|
||||||
await connection.send('SetAsVTsuruClient'); // 如果是客户端,发送标识
|
|
||||||
}
|
|
||||||
signalRClient.value = connection; // 保存实例
|
signalRClient.value = connection; // 保存实例
|
||||||
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
|
// state.value = 'connected'; // 状态将在 Start 函数末尾统一设置
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(prefix.value + '无法连接到 vtsuru 服务器: ' + e);
|
console.error(prefix.value + '无法连接到 vtsuru 服务器: ' + e);
|
||||||
signalRConnectionId.value = undefined;
|
signalRConnectionId.value = undefined;
|
||||||
signalRClient.value = undefined;
|
signalRClient.value = undefined;
|
||||||
// state.value = 'disconnected'; // 保持 connecting 或由 Start 控制
|
// state.value = 'disconnected'; // 保持 connecting 或由 Start 控制
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function sendSelfInfo(client: signalR.HubConnection) {
|
||||||
|
return client.invoke('SetSelfInfo',
|
||||||
|
isTauri ? `tauri ${platform()} ${version()}` : navigator.userAgent,
|
||||||
|
isTauri ? 'tauri' : 'web',
|
||||||
|
isTauri ? await getVersion() : '1.0.0',
|
||||||
|
webfetcherType.value === 'direct');
|
||||||
|
}
|
||||||
|
type ResponseFetchRequestData = {
|
||||||
|
Message: string;
|
||||||
|
Success: boolean;
|
||||||
|
Data: string;
|
||||||
|
};
|
||||||
|
async function onRequest(url: string, method: string, body: string, useCookie: boolean) {
|
||||||
|
if (!isTauri) {
|
||||||
|
console.error(prefix.value + '非Tauri环境下无法处理请求: ' + url);
|
||||||
|
return {
|
||||||
|
Message: '非Tauri环境',
|
||||||
|
Success: false,
|
||||||
|
Data: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const result = await QueryBiliAPI(url, method, body, useCookie);
|
||||||
|
console.log(`${prefix.value}请求 (${method})${url}: `, result.statusText);
|
||||||
|
if (result.ok) {
|
||||||
|
const data = await result.text();
|
||||||
|
return {
|
||||||
|
Message: '请求成功',
|
||||||
|
Success: true,
|
||||||
|
Data: data
|
||||||
|
} as ResponseFetchRequestData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// async function reconnect() { // withAutomaticReconnect 存在时,此函数通常不需要手动调用
|
// async function reconnect() { // withAutomaticReconnect 存在时,此函数通常不需要手动调用
|
||||||
// if (disconnectedByServer || state.value === 'disconnected') return;
|
// if (disconnectedByServer || state.value === 'disconnected') return;
|
||||||
// logInfo(prefix.value + '尝试手动重连...');
|
// console.log(prefix.value + '尝试手动重连...');
|
||||||
// try {
|
// try {
|
||||||
// await signalRClient.value?.start();
|
// await signalRClient.value?.start();
|
||||||
// logInfo(prefix.value + '手动重连成功');
|
// console.log(prefix.value + '手动重连成功');
|
||||||
// signalRConnectionId.value = signalRClient.value?.connectionId ?? null;
|
// signalRConnectionId.value = signalRClient.value?.connectionId ?? null;
|
||||||
// state.value = 'connected';
|
// state.value = 'connected';
|
||||||
// if (isFromClient) {
|
// if (isFromClient) {
|
||||||
@@ -301,7 +340,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
// }
|
// }
|
||||||
// await signalRClient.value?.send('Reconnected');
|
// await signalRClient.value?.send('Reconnected');
|
||||||
// } catch (err) {
|
// } catch (err) {
|
||||||
// logError(prefix.value + '手动重连失败: ' + err);
|
// console.error(prefix.value + '手动重连失败: ' + err);
|
||||||
// setTimeout(reconnect, 10000); // 失败后10秒再次尝试
|
// setTimeout(reconnect, 10000); // 失败后10秒再次尝试
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
@@ -351,20 +390,34 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>(
|
let result: { Success: boolean; Message: string; } = { Success: false, Message: '' };
|
||||||
'UploadEvents', batch, webfetcherType.value === 'direct'? true : false
|
let length = 0;
|
||||||
|
let eventCharLength = batch.map(event => event.length).reduce((a, b) => a + b, 0); // 计算字符长度
|
||||||
|
if (zstd && eventCharLength > 100) {
|
||||||
|
const data = zstd.ZstdSimple.compress(encode(batch), 11);
|
||||||
|
length = data.length;
|
||||||
|
result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>(
|
||||||
|
'UploadEventsCompressedV2', data
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
length = new TextEncoder().encode(batch.join()).length;
|
||||||
|
result = await signalRClient.value.invoke<{ Success: boolean; Message: string; }>(
|
||||||
|
'UploadEvents', batch, webfetcherType.value === 'direct' ? true : false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (result?.Success) {
|
if (result?.Success) {
|
||||||
events.splice(0, batch.length); // 从队列中移除已成功发送的事件
|
events.splice(0, batch.length); // 从队列中移除已成功发送的事件
|
||||||
successfulUploads.value++;
|
successfulUploads.value++;
|
||||||
bytesSentSession.value += new TextEncoder().encode(batch.join()).length;
|
bytesSentSession.value += length;
|
||||||
} else {
|
} else {
|
||||||
failedUploads.value++;
|
failedUploads.value++;
|
||||||
logError(prefix.value + '上传弹幕失败: ' + result?.Message);
|
console.error(prefix.value + '上传弹幕失败: ' + result?.Message);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failedUploads.value++;
|
failedUploads.value++;
|
||||||
logError(prefix.value + '发送事件时出错: ' + err);
|
console.error(prefix.value + '发送事件时出错: ' + err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +432,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
|
|||||||
startedAt,
|
startedAt,
|
||||||
isStreaming: computed(() => streamingInfo.value?.status === 'streaming'), // 从 statistics 模块获取
|
isStreaming: computed(() => streamingInfo.value?.status === 'streaming'), // 从 statistics 模块获取
|
||||||
webfetcherType,
|
webfetcherType,
|
||||||
|
signalRId,
|
||||||
|
|
||||||
// 连接详情
|
// 连接详情
|
||||||
danmakuClientState,
|
danmakuClientState,
|
||||||
|
|||||||
Reference in New Issue
Block a user