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:
2025-04-07 19:14:39 +08:00
parent 277497420c
commit 0195e7b01a
14 changed files with 536 additions and 328 deletions

View File

@@ -357,8 +357,8 @@
const minutes = duration.minutes || 0; const seconds = duration.seconds || 0;
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
async function onSwitchDanmakuClientMode(type: 'openlive' | 'direct') {
if (webfetcher.webfetcherType === type) {
async function onSwitchDanmakuClientMode(type: 'openlive' | 'direct', force: boolean = false) {
if (webfetcher.webfetcherType === type && !force) {
message.info('当前已是该模式'); return;
}
const noticeRef = window.$notification.info({
@@ -494,8 +494,7 @@
embedded
style="width: 100%; max-width: 800px;"
>
<NFlex align="center">
<NText>模式:</NText>
<NFlex vertical>
<NRadioGroup
v-model:value="settings.settings.useDanmakuClientType"
:disabled="webfetcher.state === 'connecting'"
@@ -519,6 +518,27 @@
</NText>
</NRadioButton>
</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>
</NCard>
@@ -789,6 +809,7 @@
bordered
:columns="2"
size="small"
style="overflow-x: auto;"
>
<NDescriptionsItem label="启动时间">
<NIcon :component="TimeOutline" /> {{ formattedStartedAt }}
@@ -800,6 +821,7 @@
<NFlex
align="center"
size="small"
:wrap="false"
>
<NTag
:type="signalRStateType"
@@ -807,8 +829,8 @@
>
{{ signalRStateText }}
</NTag>
<NEllipsis style="max-width: 200px;">
{{ webfetcher.signalRClient?.connectionId ?? 'N/A' }}
<NEllipsis style="max-width: 150px;">
{{ webfetcher.signalRId ?? 'N/A' }}
</NEllipsis>
</NFlex>
</NDescriptionsItem>
@@ -816,6 +838,7 @@
<NFlex
align="center"
size="small"
:wrap="false"
>
<NTag
:type="danmakuClientStateType"
@@ -823,7 +846,7 @@
>
{{ danmakuClientStateText }}
</NTag>
<NEllipsis style="max-width: 200px;">
<NEllipsis style="max-width: 150px;">
{{ webfetcher.danmakuServerUrl ?? 'N/A' }}
</NEllipsis> <!-- Assuming this is exposed -->
</NFlex>
@@ -968,10 +991,6 @@
<NStatistic label="会话事件总数">
<NIcon :component="BarChartOutline" /> {{ webfetcher.sessionEventCount?.toLocaleString() ?? 0 }}
</NStatistic>
<NStatistic label="数据上传">
<NIcon :component="DownloadOutline" /> 成功: {{ webfetcher.successfulUploads ?? 0 }} / 失败:
{{ webfetcher.failedUploads ?? 0 }} <!-- Assuming exposed -->
</NStatistic>
<NStatistic label="已发送">
{{ ((webfetcher.bytesSentSession ?? 0) / 1024).toFixed(2) }} KB <!-- Assuming exposed -->
</NStatistic>

View File

@@ -17,6 +17,7 @@
import WindowBar from './WindowBar.vue';
import { initAll, OnClientUnmounted } from './data/initialize';
import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
import { isTauri } from '@/data/constants';
// --- 响应式状态 ---
@@ -68,7 +69,7 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
window.$message.success('登陆成功');
ACCOUNT.value = result; // 更新全局账户信息
// isLoadingAccount.value = false; // 状态在 finally 中统一处理
initAll(); // 初始化 WebFetcher
//initAll(false); // 初始化 WebFetcher
}
}
} catch (error) {
@@ -116,160 +117,169 @@ import { CloudArchive24Filled, Settings24Filled } from '@vicons/fluent';
</script>
<template>
<WindowBar />
<div
v-if="!isLoggedIn"
class="login-container"
<NAlert
v-if="!isTauri"
type="error"
title="错误"
>
<NCard
v-if="!isLoadingAccount"
:bordered="false"
size="large"
class="login-card"
>
<template #header>
<div class="login-header">
<div class="login-title">
登陆
</div>
<div class="login-subtitle">
输入你的 VTsuru Token
</div>
</div>
</template>
此应用在 Tauri 环境外运行无法使用
</NAlert>
<template v-else>
<WindowBar />
<NSpace
vertical
<div
v-if="!isLoggedIn"
class="login-container"
>
<NCard
v-if="!isLoadingAccount"
:bordered="false"
size="large"
class="login-card"
>
<NSpace vertical>
<div class="token-label-container">
<span class="token-label">Token</span>
<NTooltip placement="top">
<template #header>
<div class="login-header">
<div class="login-title">
登陆
</div>
<div class="login-subtitle">
输入你的 VTsuru Token
</div>
</div>
</template>
<NSpace
vertical
size="large"
>
<NSpace vertical>
<div class="token-label-container">
<span class="token-label">Token</span>
<NTooltip placement="top">
<template #trigger>
<NA
class="token-get-link"
@click="openUrl('https://vtsuru.suki.club/manage')"
>
前往获取
</NA>
</template>
登录后在管理面板主页的个人信息下方
</NTooltip>
</div>
<NInput
v-model:value="token"
type="password"
show-password-on="click"
placeholder="请输入Token"
@keyup.enter="login"
/>
</NSpace>
<NButton
block
type="primary"
:loading="isLoadingAccount"
:disabled="isLoadingAccount"
@click="login"
>
登陆
</NButton>
</NSpace>
</NCard>
<NSpin
v-else
size="large"
/>
</div>
<NLayout
v-else
has-sider
class="main-layout"
@vue:mounted="initAll(true)"
>
<NLayoutSider
width="200"
bordered
class="main-layout-sider"
>
<div class="sider-content">
<div class="sider-header">
<NText
tag="div"
class="app-title"
>
<span>VTsuru.Client</span>
</NText>
<NTooltip trigger="hover">
<template #trigger>
<NA
class="token-get-link"
@click="openUrl('https://vtsuru.suki.club/manage')"
<NButton
quaternary
class="fetcher-status-button"
:type="webfetcher.state === 'connected' ? 'success' : 'error'"
>
前往获取
</NA>
<CheckmarkCircle
v-if="webfetcher.state === 'connected'"
class="fetcher-status-icon connected"
/>
<CloseCircle
v-else
class="fetcher-status-icon disconnected"
/>
</NButton>
</template>
登录后在管理面板主页的个人信息下方
<div>
<div>EventFetcher 状态</div>
<div v-if="webfetcher.state === 'connected'">
运行中
</div>
<div v-else>
未运行 / 连接断开
</div>
</div>
</NTooltip>
</div>
<NInput
v-model:value="token"
type="password"
show-password-on="click"
placeholder="请输入Token"
@keyup.enter="login"
<NMenu
:options="menuOptions"
:default-value="'go-back-home'"
class="sider-menu"
/>
</NSpace>
<NButton
block
type="primary"
:loading="isLoadingAccount"
:disabled="isLoadingAccount"
@click="login"
>
登陆
</NButton>
</NSpace>
</NCard>
<NSpin
v-else
size="large"
/>
</div>
<NLayout
v-else
has-sider
class="main-layout"
@vue:mounted="initAll()"
>
<NLayoutSider
width="200"
bordered
class="main-layout-sider"
>
<div class="sider-content">
<div class="sider-header">
<NText
tag="div"
class="app-title"
>
<span>VTsuru.Client</span>
</NText>
<NTooltip trigger="hover">
<template #trigger>
<NButton
quaternary
class="fetcher-status-button"
:type="webfetcher.state === 'connected' ? 'success' : 'error'"
>
<CheckmarkCircle
v-if="webfetcher.state === 'connected'"
class="fetcher-status-icon connected"
/>
<CloseCircle
v-else
class="fetcher-status-icon disconnected"
/>
</NButton>
</template>
<div>
<div>EventFetcher 状态</div>
<div v-if="webfetcher.state === 'connected'">
运行中
</div>
<div v-else>
未运行 / 连接断开
</div>
</div>
</NTooltip>
</div>
</NLayoutSider>
<NMenu
:options="menuOptions"
:default-value="'go-back-home'"
class="sider-menu"
/>
</div>
</NLayoutSider>
<NLayoutContent
class="main-layout-content"
:native-scrollbar="false"
:scrollbar-props="{
trigger: 'none'
}"
>
<div style="padding: 12px; padding-right: 15px;">
<RouterView v-slot="{ Component }">
<Transition
name="fade-slide"
mode="out-in"
:appear="true"
>
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>
<div class="suspense-fallback">
加载中...
</div>
</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
</div>
</NLayoutContent>
</NLayout>
<NLayoutContent
class="main-layout-content"
:native-scrollbar="false"
:scrollbar-props="{
trigger: 'none'
}"
>
<div style="padding: 12px; padding-right: 15px;">
<RouterView v-slot="{ Component }">
<Transition
name="fade-slide"
mode="out-in"
:appear="true"
>
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>
<div class="suspense-fallback">
加载中...
</div>
</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
</div>
</NLayoutContent>
</NLayout>
</template>
</template>
<style scoped>

View File

@@ -15,11 +15,13 @@ import {
NFormItem, // Added NFormItem
NAlert,
NCheckboxGroup,
NCheckbox, // Added NAlert for error messages
NCheckbox,
NDivider, // Added NAlert for error messages
} from 'naive-ui';
import type { MenuOption } from 'naive-ui'; // Import MenuOption type
import { ThemeType } from '@/api/api-models';
import { NotificationType, useSettings } from './store/useSettings';
import { getVersion } from '@tauri-apps/api/app';
// --- State ---
@@ -28,6 +30,7 @@ const isLoading = ref(true); // Loading state for initial fetch
const errorMsg = ref<string | null>(null); // Error message state
const setting = useSettings();
const currentVersion = await getVersion(); // Fetch current version on mount
// Navigation
const navOptions: MenuOption[] = [ // Explicitly typed
@@ -73,10 +76,10 @@ watch(isStartOnBoot, async (newValue, oldValue) => {
try {
if (newValue) {
await enable();
window.$message.success('已启用开机启动');
//window.$message.success('已启用开机启动');
} else {
await disable();
window.$message.success('已禁用开机启动'); // Provide feedback for disabling too
//window.$message.success('已禁用开机启动'); // Provide feedback for disabling too
}
} catch (err) {
console.error("Failed to update autostart status:", err);
@@ -170,14 +173,16 @@ watch(minimizeOnStart, (newValue) => {
<template v-if="currentTab === 'general'">
<NSpace
vertical
size="large"
>
<NCard
title="启动"
:bordered="false"
>
<NSpace vertical>
<NFormItem
<NFlex
vertical
align="start"
>
<LabelItem
label="开机时启动应用"
label-placement="left"
>
@@ -185,15 +190,19 @@ watch(minimizeOnStart, (newValue) => {
v-model:value="isStartOnBoot"
:disabled="isLoading"
/>
</NFormItem>
<NFormItem
</LabelItem>
<LabelItem
v-if="isStartOnBoot"
label="启动后最小化到托盘"
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 -->
</NFormItem>
</NSpace>
</LabelItem>
</NFlex>
</NCard>
<NCard
@@ -230,6 +239,10 @@ watch(minimizeOnStart, (newValue) => {
title="通知设置"
:bordered="false"
>
<NAlert type="warning">
暂未完成
</NAlert>
<NDivider />
<NSpace vertical>
<NCheckbox
v-model:checked="setting.settings.enableNotification"
@@ -284,8 +297,42 @@ watch(minimizeOnStart, (newValue) => {
@click="$router.push({name: 'client-test'})"
/>
</template>
<p>应用名称: Your App Name</p>
<p>版本: 1.0.0</p>
<p>VTsuruEventFetcher Tauri</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 -->
</NCard>
</template>
@@ -312,7 +359,9 @@ watch(minimizeOnStart, (newValue) => {
.fade-leave-to {
opacity: 0;
}
.label-item {
height: 20px;
}
/* Optional: Adjust NFormItem label alignment if needed */
/* :deep(.n-form-item-label) { */
/* Add custom styles */

View File

@@ -7,7 +7,7 @@ import { getBuvid, getRoomKey } from "./utils";
import { initInfo } from "./info";
import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray';
import { Menu } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { getCurrentWindow, PhysicalSize } from "@tauri-apps/api/window";
import {
isPermissionGranted,
onAction,
@@ -15,7 +15,7 @@ import {
sendNotification,
} from '@tauri-apps/plugin-notification';
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 { check } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
@@ -24,10 +24,17 @@ const accountInfo = useAccount();
export const clientInited = ref(false);
let tray: TrayIcon;
export async function initAll() {
export async function initAll(isOnBoot: boolean) {
const setting = useSettings();
if (clientInited.value) {
return;
}
if (isOnBoot) {
if (setting.settings.bootAsMinimized && !isDev) {
const appWindow = getCurrentWindow();
appWindow.hide();
}
}
let permissionGranted = await isPermissionGranted();
checkUpdate();
@@ -96,16 +103,16 @@ export async function initAll() {
case 'DoubleClick':
appWindow.show();
break;
case 'Click':
case 'Click':
appWindow.show();
break;
}
}
}
};
tray = await TrayIcon.new(options);
appWindow.setMinSize(new PhysicalSize(720, 480));
clientInited.value = true;
}
export function OnClientUnmounted() {
@@ -209,7 +216,7 @@ export async function initOpenLive() {
}
return reuslt;
}
function initNotificationHandler(){
function initNotificationHandler() {
onAction((event) => {
if (event.extra?.type === 'question-box') {
openUrl(CN_HOST + '/manage/question-box');

View File

@@ -4,7 +4,7 @@ import { QueryPostAPI } from '@/api/query';
import { OPEN_LIVE_API_URL } from '@/data/constants';
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);
return fetch(url, {
method: method,
@@ -12,8 +12,7 @@ export async function QueryBiliAPI(url: string, method: string = 'GET', cookie:
'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',
Origin: '',
Cookie: cookie || (await useBiliCookie().getBiliCookie()) || '',
'Upgrade-Insecure-Requests': '1',
Cookie: useCookie ? (cookie || (await useBiliCookie().getBiliCookie()) || '') : ''
},
});
}

View File

@@ -7,6 +7,7 @@ export type NotificationSettings = {
export type VTsuruClientSettings = {
useDanmakuClientType: 'openlive' | 'direct';
fallbackToOpenLive: boolean;
bootAsMinimized: boolean;
danmakuHistorySize: number;
loginType: 'qrcode' | 'cookiecloud'
@@ -20,6 +21,7 @@ export const useSettings = defineStore('settings', () => {
const defaultSettings: VTsuruClientSettings = {
useDanmakuClientType: 'openlive',
fallbackToOpenLive: true,
bootAsMinimized: true,
danmakuHistorySize: 100,
loginType: 'qrcode',