Compare commits

...

2 Commits

6 changed files with 250 additions and 175 deletions

View File

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

View File

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

View File

@@ -29,14 +29,9 @@ export async function initAll(isOnBoot: boolean) {
if (clientInited.value) {
return;
}
if (isOnBoot) {
if (setting.settings.bootAsMinimized && !isDev) {
const appWindow = getCurrentWindow();
appWindow.hide();
}
}
let permissionGranted = await isPermissionGranted();
checkUpdate();
const appWindow = getCurrentWindow();
let permissionGranted = await isPermissionGranted();
// If not we need to request it
if (!permissionGranted) {
@@ -45,7 +40,16 @@ export async function initAll(isOnBoot: boolean) {
if (permissionGranted) {
info('Notification permission granted');
}
}
if (isOnBoot) {
if (setting.settings.bootAsMinimized && !isDev && await appWindow.isVisible()) {
appWindow.hide();
sendNotification({
title: "VTsuru.Client",
body: '已启动并最小化到托盘'
});
}
}
initNotificationHandler();
const detach = await attachConsole();
@@ -58,7 +62,7 @@ export async function initAll(isOnBoot: boolean) {
initInfo();
info('[init] 开始更新数据');
if (isLoggedIn && accountInfo.value.isBiliVerified) {
if (isLoggedIn && accountInfo.value.isBiliVerified && !setting.settings.dev_disableDanmakuClient) {
const danmakuInitNoticeRef = window.$notification.info({
title: '正在初始化弹幕客户端...',
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 appWindow = getCurrentWindow();
const options: TrayIconOptions = {
// here you can add a tray menu, title, tooltip, event handler, etc
menu: menu,
@@ -113,6 +116,16 @@ export async function initAll(isOnBoot: boolean) {
appWindow.setMinSize(new PhysicalSize(720, 480));
// 监听f12事件
if (!isDev) {
window.addEventListener('keydown', (event) => {
if (event.key === 'F12') {
event.preventDefault();
event.stopPropagation();
}
});
}
clientInited.value = true;
}
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 { 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 { 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) {
case 'question-box':
onReceivedQuestion(data);
break;
case 'goods-buy':
onGoodsBuy(data);
break;
default:
console.warn(`Unhandled notification type: ${type}`);
}
@@ -20,20 +32,57 @@ export async function onReceivedQuestion(question: QAInfo) {
const setting = useSettings();
if (setting.settings.notificationSettings.enableTypes.includes("question-box")) {
window.$notification.info({
title: "收到提问",
description: '收到来自 [' + question.sender.name || '匿名用户' + '] 的提问',
duration: 5,
title: "提问",
description: '收到来自 [' + (question.sender.name || '匿名用户') + '] 的提问',
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({
title: "收到提问",
body: '来自 [' + question.sender.name || '匿名用户' + '] 的提问',
silent: false,
}
}, () => '快速查看'),
h(NButton, {
text: true, type: 'primary', onClick: () => {
openUrl(`${CN_HOST}manage/question-box`);
}
}, () => '查看详情'),
])
});
trySendNotification({
title: "提问箱",
body: '收到来自 [' + (question.sender.name || '匿名用户') + '] 的提问',
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';
export type NotificationType = 'question-box' | 'danmaku';
export type NotificationType = 'question-box' | 'danmaku' | 'goods-buy';
export type NotificationSettings = {
enableTypes: NotificationType[];
};
@@ -14,6 +14,8 @@ export type VTsuruClientSettings = {
enableNotification: boolean;
notificationSettings: NotificationSettings;
dev_disableDanmakuClient: boolean;
};
export const useSettings = defineStore('settings', () => {
@@ -29,6 +31,8 @@ export const useSettings = defineStore('settings', () => {
notificationSettings: {
enableTypes: ['question-box', 'danmaku'],
},
dev_disableDanmakuClient: false,
};
const settings = ref<VTsuruClientSettings>(Object.assign({}, defaultSettings));

View File

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