Compare commits

..

2 Commits

6 changed files with 250 additions and 175 deletions

View File

@@ -4,36 +4,39 @@ import { disable, enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
import { import {
NGrid, NGrid,
NGridItem, // Corrected import NGridItem NGridItem,
NMenu, NMenu,
NRadio, NRadio,
NRadioGroup, // Added NRadioGroup NRadioGroup,
NSwitch, NSwitch,
NSpace, NSpace,
NCard, NCard,
NSpin, // Added NSpin for loading state NSpin,
NFormItem, // Added NFormItem NFormItem,
NAlert, NAlert,
NCheckboxGroup, NCheckboxGroup,
NCheckbox, NCheckbox,
NDivider, // Added NAlert for error messages NDivider,
} from 'naive-ui'; } from 'naive-ui';
import type { MenuOption } from 'naive-ui'; // Import MenuOption type import type { MenuOption } from 'naive-ui';
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'; import { getVersion } from '@tauri-apps/api/app';
import { invoke } from '@tauri-apps/api/core';
// --- State --- // --- State ---
const currentTab = ref('general'); const currentTab = ref('general');
const isLoading = ref(true); // Loading state for initial fetch const isLoading = ref(true);
const errorMsg = ref<string | null>(null); // Error message state const errorMsg = ref<string | null>(null);
const titleClickCount = ref(0); // 添加计数器状态变量
let resetTimeout: number | null = null; // 用于重置计数器的超时ID
const setting = useSettings(); const setting = useSettings();
const currentVersion = await getVersion(); // Fetch current version on mount const currentVersion = await getVersion();
// Navigation // Navigation
const navOptions: MenuOption[] = [ // Explicitly typed const navOptions: MenuOption[] = [
{ label: '常规', key: 'general' }, { label: '常规', key: 'general' },
{ label: '通知', key: 'notification' }, { label: '通知', key: 'notification' },
{ label: '其他', key: 'other' }, { label: '其他', key: 'other' },
@@ -45,8 +48,8 @@ const navOptions: MenuOption[] = [ // Explicitly typed
const themeType = useStorage('Settings.Theme', ThemeType.Auto); const themeType = useStorage('Settings.Theme', ThemeType.Auto);
// Autostart Settings // Autostart Settings
const isStartOnBoot = ref(false); // Initialize with default, fetch in onMounted const isStartOnBoot = ref(false);
const minimizeOnStart = ref(false); // Placeholder state for minimize setting const minimizeOnStart = ref(false);
// --- Lifecycle Hooks --- // --- Lifecycle Hooks ---
@@ -55,11 +58,9 @@ onMounted(async () => {
errorMsg.value = null; errorMsg.value = null;
try { try {
isStartOnBoot.value = await isEnabled(); isStartOnBoot.value = await isEnabled();
// TODO: Fetch initial state for minimizeOnStart if applicable
} catch (err) { } catch (err) {
console.error("Failed to fetch autostart status:", err); console.error("Failed to fetch autostart status:", err);
errorMsg.value = "无法获取开机启动状态,请稍后重试。"; errorMsg.value = "无法获取开机启动状态,请稍后重试。";
// Keep default isStartOnBoot value (false)
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -68,27 +69,23 @@ onMounted(async () => {
// --- Watchers for Side Effects --- // --- Watchers for Side Effects ---
watch(isStartOnBoot, async (newValue, oldValue) => { watch(isStartOnBoot, async (newValue, oldValue) => {
// Prevent running on initial load if oldValue is the initial default if (isLoading.value || newValue === oldValue) return;
// or during the initial fetch if needed (though onMounted handles initial state)
if (isLoading.value || newValue === oldValue) return; // Avoid unnecessary calls
errorMsg.value = null; // Clear previous errors errorMsg.value = null;
try { try {
if (newValue) { if (newValue) {
await enable(); await enable();
//window.$message.success('已启用开机启动');
} else { } else {
await disable(); await disable();
//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);
errorMsg.value = `设置开机启动失败: ${err instanceof Error ? err.message : '未知错误'}`; errorMsg.value = `设置开机启动失败: ${err instanceof Error ? err.message : '未知错误'}`;
// Revert UI state on failure
isStartOnBoot.value = oldValue; isStartOnBoot.value = oldValue;
window.$message.error('设置开机启动失败'); window.$message.error('设置开机启动失败');
} }
}); });
const renderNotifidactionEnable = (name: NotificationType) => h(NCheckbox, { const renderNotifidactionEnable = (name: NotificationType) => h(NCheckbox, {
checked: setting.settings.notificationSettings?.enableTypes.includes(name), checked: setting.settings.notificationSettings?.enableTypes.includes(name),
onUpdateChecked: (value) => { onUpdateChecked: (value) => {
@@ -101,15 +98,25 @@ const renderNotifidactionEnable = (name: NotificationType) => h(NCheckbox, {
}, },
}, () => '启用'); }, () => '启用');
watch(minimizeOnStart, (newValue) => { // --- 隐藏功能处理函数 ---
// TODO: Implement logic to save/apply minimizeOnStart setting const handleTitleClick = () => {
// Example: saveToConfig('minimizeOnStart', newValue); titleClickCount.value++;
console.log("Minimize on start:", newValue);
if (newValue) {
window.$message.info('启动后最小化功能待实现'); // Placeholder feedback
}
});
if (resetTimeout !== null) {
clearTimeout(resetTimeout);
}
resetTimeout = setTimeout(() => {
titleClickCount.value = 0;
}, 3000) as unknown as number;
if (titleClickCount.value === 10) {
invoke('open_dev_tools')
.then(() => {
window.$message.success('已打开 Dev Tools');
})
}
};
</script> </script>
<template> <template>
@@ -121,7 +128,10 @@ watch(minimizeOnStart, (newValue) => {
<!-- 标题区域 --> <!-- 标题区域 -->
<div style="max-width: 72rem; margin: 0 auto; padding: 0 1rem;"> <div style="max-width: 72rem; margin: 0 auto; padding: 0 1rem;">
<!-- Added padding --> <!-- Added padding -->
<h1 style="font-size: 1.875rem; font-weight: 600; margin-bottom: 1rem;"> <h1
style="font-size: 1.875rem; font-weight: 600; margin-bottom: 1rem;"
@click="handleTitleClick"
>
<!-- Added margin --> <!-- Added margin -->
设置 设置
</h1> </h1>
@@ -135,7 +145,6 @@ watch(minimizeOnStart, (newValue) => {
> >
<!-- Left Navigation --> <!-- Left Navigation -->
<NGridItem span="6"> <NGridItem span="6">
<!-- Responsive spans -->
<NMenu <NMenu
v-model:value="currentTab" v-model:value="currentTab"
:options="navOptions" :options="navOptions"
@@ -145,13 +154,11 @@ watch(minimizeOnStart, (newValue) => {
<!-- Right Content Area --> <!-- Right Content Area -->
<NGridItem span="18"> <NGridItem span="18">
<!-- Responsive spans -->
<NSpin :show="isLoading"> <NSpin :show="isLoading">
<NSpace <NSpace
vertical vertical
size="large" size="large"
> >
<!-- Global Error Display -->
<NAlert <NAlert
v-if="errorMsg" v-if="errorMsg"
title="操作错误" title="操作错误"
@@ -162,18 +169,13 @@ watch(minimizeOnStart, (newValue) => {
{{ errorMsg }} {{ errorMsg }}
</NAlert> </NAlert>
<!-- Content Transition -->
<Transition <Transition
name="fade" name="fade"
mode="out-in" mode="out-in"
> >
<div :key="currentTab"> <div :key="currentTab">
<!-- Key needed for transition on content change -->
<!-- General Settings -->
<template v-if="currentTab === 'general'"> <template v-if="currentTab === 'general'">
<NSpace <NSpace vertical>
vertical
>
<NCard <NCard
title="启动" title="启动"
:bordered="false" :bordered="false"
@@ -200,7 +202,6 @@ watch(minimizeOnStart, (newValue) => {
v-model:value="setting.settings.bootAsMinimized" v-model:value="setting.settings.bootAsMinimized"
@update:value="setting.save()" @update:value="setting.save()"
/> />
<!-- Add appropriate logic/state for this -->
</LabelItem> </LabelItem>
</NFlex> </NFlex>
</NCard> </NCard>
@@ -233,21 +234,20 @@ watch(minimizeOnStart, (newValue) => {
</NSpace> </NSpace>
</template> </template>
<!-- Notification Settings -->
<template v-else-if="currentTab === 'notification'"> <template v-else-if="currentTab === 'notification'">
<NCard <NCard
title="通知设置" title="通知设置"
:bordered="false" :bordered="false"
> >
<NAlert type="warning"> <NAlert type="warning">
未完成 完全完成
</NAlert> </NAlert>
<NDivider /> <NDivider />
<NSpace vertical> <NSpace vertical>
<NCheckbox <NCheckbox
v-model:checked="setting.settings.enableNotification" v-model:checked="setting.settings.enableNotification"
@update:checked="(value) => { @update:checked="(value) => {
setting.save() setting.save();
}" }"
> >
启用通知 启用通知
@@ -262,6 +262,14 @@ watch(minimizeOnStart, (newValue) => {
<component :is="renderNotifidactionEnable('question-box')" /> <component :is="renderNotifidactionEnable('question-box')" />
</template> </template>
</NCard> </NCard>
<NCard
size="small"
title="积分兑换通知"
>
<template #header-extra>
<component :is="renderNotifidactionEnable('goods-buy')" />
</template>
</NCard>
<NCard <NCard
size="small" size="small"
title="弹幕相关" title="弹幕相关"
@@ -275,7 +283,6 @@ watch(minimizeOnStart, (newValue) => {
</NCard> </NCard>
</template> </template>
<!-- Other Settings -->
<template v-else-if="currentTab === 'other'"> <template v-else-if="currentTab === 'other'">
<NCard <NCard
title="其他设置" title="其他设置"
@@ -285,7 +292,6 @@ watch(minimizeOnStart, (newValue) => {
</NCard> </NCard>
</template> </template>
<!-- About Section -->
<template v-else-if="currentTab === 'about'"> <template v-else-if="currentTab === 'about'">
<NCard <NCard
title="关于" title="关于"
@@ -336,7 +342,6 @@ watch(minimizeOnStart, (newValue) => {
<p> <p>
反馈: 🐧 873260337 反馈: 🐧 873260337
</p> </p>
<!-- Add more about info -->
</NCard> </NCard>
</template> </template>
</div> </div>
@@ -352,7 +357,7 @@ watch(minimizeOnStart, (newValue) => {
</template> </template>
<style scoped> <style scoped>
/* Scoped styles if needed, e.g., for the transition */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -362,11 +367,8 @@ watch(minimizeOnStart, (newValue) => {
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
.label-item { .label-item {
height: 20px; height: 20px;
} }
/* Optional: Adjust NFormItem label alignment if needed */
/* :deep(.n-form-item-label) { */
/* Add custom styles */
/* } */
</style> </style>

View File

@@ -1,19 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { isPermissionGranted, onAction, sendNotification } from '@tauri-apps/plugin-notification'; import { isPermissionGranted, onAction, sendNotification } from '@tauri-apps/plugin-notification';
import { NSwitch } from 'naive-ui';
import { useSettings } from './store/useSettings';
import { onReceivedQuestion } from './data/notification';
import { QAInfo } from '@/api/api-models';
const setting = useSettings()
async function testNotification() { async function testNotification() {
let permissionGranted = await isPermissionGranted(); onReceivedQuestion({
if (permissionGranted) { id: 1,
sendNotification({ question: {
title: "测试通知", message: '这是一条测试问题',
body: "这是一个测试通知", },
silent: false, tag: '测试标签',
extra: { type: 'test' }, sender: { name: '测试用户', id: 1, isBiliAuthed: false },
}); isPublic: true,
onAction((event) => { } as QAInfo);
console.log('Notification clicked:', event);
});
}
} }
</script> </script>
@@ -26,6 +28,12 @@ async function testNotification() {
> >
测试通知 测试通知
</NButton> </NButton>
<LabelItem label="关闭弹幕客户端">
<NSwitch
v-model:value="setting.settings.dev_disableDanmakuClient"
@update:value="setting.save()"
/>
</LabelItem>
</NFlex> </NFlex>
</div> </div>
</template> </template>

View File

@@ -29,14 +29,9 @@ export async function initAll(isOnBoot: boolean) {
if (clientInited.value) { if (clientInited.value) {
return; return;
} }
if (isOnBoot) {
if (setting.settings.bootAsMinimized && !isDev) {
const appWindow = getCurrentWindow();
appWindow.hide();
}
}
let permissionGranted = await isPermissionGranted();
checkUpdate(); checkUpdate();
const appWindow = getCurrentWindow();
let permissionGranted = await isPermissionGranted();
// If not we need to request it // If not we need to request it
if (!permissionGranted) { if (!permissionGranted) {
@@ -45,7 +40,16 @@ export async function initAll(isOnBoot: boolean) {
if (permissionGranted) { if (permissionGranted) {
info('Notification permission granted'); info('Notification permission granted');
} }
}
if (isOnBoot) {
if (setting.settings.bootAsMinimized && !isDev && await appWindow.isVisible()) {
appWindow.hide();
sendNotification({
title: "VTsuru.Client",
body: '已启动并最小化到托盘'
});
}
} }
initNotificationHandler(); initNotificationHandler();
const detach = await attachConsole(); const detach = await attachConsole();
@@ -58,7 +62,7 @@ export async function initAll(isOnBoot: boolean) {
initInfo(); initInfo();
info('[init] 开始更新数据'); info('[init] 开始更新数据');
if (isLoggedIn && accountInfo.value.isBiliVerified) { if (isLoggedIn && accountInfo.value.isBiliVerified && !setting.settings.dev_disableDanmakuClient) {
const danmakuInitNoticeRef = window.$notification.info({ const danmakuInitNoticeRef = window.$notification.info({
title: '正在初始化弹幕客户端...', title: '正在初始化弹幕客户端...',
closable: false closable: false
@@ -90,7 +94,6 @@ export async function initAll(isOnBoot: boolean) {
], ],
}); });
const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer(); const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer();
const appWindow = getCurrentWindow();
const options: TrayIconOptions = { const options: TrayIconOptions = {
// here you can add a tray menu, title, tooltip, event handler, etc // here you can add a tray menu, title, tooltip, event handler, etc
menu: menu, menu: menu,
@@ -113,6 +116,16 @@ export async function initAll(isOnBoot: boolean) {
appWindow.setMinSize(new PhysicalSize(720, 480)); appWindow.setMinSize(new PhysicalSize(720, 480));
// 监听f12事件
if (!isDev) {
window.addEventListener('keydown', (event) => {
if (event.key === 'F12') {
event.preventDefault();
event.stopPropagation();
}
});
}
clientInited.value = true; clientInited.value = true;
} }
export function OnClientUnmounted() { export function OnClientUnmounted() {

View File

@@ -1,16 +1,28 @@
import { QAInfo } from "@/api/api-models"; import { QAInfo, ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from "@/api/api-models";
import { useSettings } from "../store/useSettings"; import { useSettings } from "../store/useSettings";
import { isPermissionGranted, onAction, sendNotification } from "@tauri-apps/plugin-notification"; import { isPermissionGranted, onAction, Options, sendNotification } 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 } from "@/data/constants";
import { NButton, NFlex } from "naive-ui";
import QuestionItem from "@/components/QuestionItem.vue";
export function onReceivedNotification(type: string, data: any) { export async function trySendNotification(option: Options) {
let permissionGranted = await isPermissionGranted();
if (permissionGranted) {
sendNotification(option);
}
}
export function onReceivedNotification(type: string, json: string) {
console.log(`接收到通知: ${type}`, json);
const data = JSON.parse(json);
switch (type) { switch (type) {
case 'question-box': case 'question-box':
onReceivedQuestion(data); onReceivedQuestion(data);
break; break;
case 'goods-buy':
onGoodsBuy(data);
break;
default: default:
console.warn(`Unhandled notification type: ${type}`); console.warn(`Unhandled notification type: ${type}`);
} }
@@ -20,20 +32,57 @@ export async function onReceivedQuestion(question: QAInfo) {
const setting = useSettings(); const setting = useSettings();
if (setting.settings.notificationSettings.enableTypes.includes("question-box")) { if (setting.settings.notificationSettings.enableTypes.includes("question-box")) {
window.$notification.info({ window.$notification.info({
title: "收到提问", title: "提问",
description: '收到来自 [' + question.sender.name || '匿名用户' + '] 的提问', description: '收到来自 [' + (question.sender.name || '匿名用户') + '] 的提问',
duration: 5, duration: 0,
action: () => h(NFlex, {}, () => [
h(NButton, {
text: true, type: 'info', onClick: () => {
window.$modal.create({
title: '快速查看',
preset: 'card',
style: { maxWidth: '80vw' },
content: () => h(QuestionItem, { item: question }),
}); });
let permissionGranted = await isPermissionGranted(); }
if (permissionGranted) { }, () => '快速查看'),
sendNotification({ h(NButton, {
title: "收到提问", text: true, type: 'primary', onClick: () => {
body: '来自 [' + question.sender.name || '匿名用户' + '] 的提问', openUrl(`${CN_HOST}manage/question-box`);
silent: false, }
}, () => '查看详情'),
])
});
trySendNotification({
title: "提问箱",
body: '收到来自 [' + (question.sender.name || '匿名用户') + '] 的提问',
extra: { type: 'question-box' }, extra: { type: 'question-box' },
}); });
} }
} }
export function onGoodsBuy(info: {
data: ResponsePointOrder2OwnerModel,
goods: ResponsePointGoodModel
}) {
const setting = useSettings();
const order = info.data;
const goods = info.goods;
if (setting.settings.notificationSettings.enableTypes.includes("goods-buy")) {
window.$notification.info({
title: "礼物兑换",
description: `${order.customer.name} 兑换了你的 [${goods.name}],数量: ${order.count},总价: ${order.point}`,
duration: 0,
action: () => h(NButton, {
text: true, type: 'primary', onClick: () => {
openUrl(`${CN_HOST}manage/goods-buy`);
}
}, () => '查看详情'),
});
trySendNotification({
title: "礼物兑换",
body: `${order.customer.name} 兑换了你的 [${goods.name}],数量: ${order.count},总价: ${order.point}`,
extra: { type: 'goods-buy' },
});
}
} }

View File

@@ -1,6 +1,6 @@
import { useTauriStore } from './useTauriStore'; import { useTauriStore } from './useTauriStore';
export type NotificationType = 'question-box' | 'danmaku'; export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy';
export type NotificationSettings = { export type NotificationSettings = {
enableTypes: NotificationType[]; enableTypes: NotificationType[];
}; };
@@ -14,6 +14,8 @@ export type VTsuruClientSettings = {
enableNotification: boolean; enableNotification: boolean;
notificationSettings: NotificationSettings; notificationSettings: NotificationSettings;
dev_disableDanmakuClient: boolean;
}; };
export const useSettings = defineStore('settings', () => { export const useSettings = defineStore('settings', () => {
@@ -29,6 +31,8 @@ export const useSettings = defineStore('settings', () => {
notificationSettings: { notificationSettings: {
enableTypes: ['question-box', 'danmaku'], enableTypes: ['question-box', 'danmaku'],
}, },
dev_disableDanmakuClient: false,
}; };
const settings = ref<VTsuruClientSettings>(Object.assign({}, defaultSettings)); const settings = ref<VTsuruClientSettings>(Object.assign({}, defaultSettings));

View File

@@ -14,11 +14,10 @@ export const updateNotes: updateNoteType[] = [
content: [ content: [
['比当前所有 EventFetcher 部署方法都更要简单且支持扫码登录的客户端开始测试力, 支持Windows, Linux, MacOS (后两个没测试过'], ['比当前所有 EventFetcher 部署方法都更要简单且支持扫码登录的客户端开始测试力, 支持Windows, Linux, MacOS (后两个没测试过'],
[ [
'如果对此感兴趣的话可以使用 ', '安装方式: ',
() => h(NButton, { () => h(NButton, {
text: true, tag: 'a', href: FETCH_API + 'https://github.com/Megghy/vtsuru-fetcher-client/releases/download/app-v0.1.0/vtsuru-fetcher-client_0.1.0_x64-setup.exe', target: '_blank', type: 'info' text: true, tag: 'a', href: 'https://www.wolai.com/carN6qvUm3FErze9Xo53ii', target: '_blank', type: 'info'
}, () => '这个链接'), }, () => '查看介绍'),
' 下载Windows客户端, 其他平台请在下面的客户端 Repo 中的 Release 下载',
], ],
[ [
'当前可能存在一些问题, 可以加入秋秋群 873260337 进行反馈, 有功能需求也可以提出' '当前可能存在一些问题, 可以加入秋秋群 873260337 进行反馈, 有功能需求也可以提出'