feat: 完善托盘功能、完全退出和主题切换
托盘功能: - 添加主题子菜单(浅色/深色/跟随系统) - 添加完全退出菜单项 - 托盘图标使用应用默认图标 完全退出功能: - 新增 AppState.allow_exit 状态控制 - 点击'完全退出'时允许应用真正退出 - 关闭窗口隐藏到托盘的默认行为 主题切换功能: - 后端:添加 AppTheme 枚举和 set_theme/get_theme 命令 - 前端:实现主题切换逻辑,支持浅色/深色/跟随系统 - 前端:添加主题选择器 UI 组件和样式 - 通过 CSS 变量实现深色/浅色主题切换 - 支持 Tauri 事件监听实现后端主题同步 修改文件: - src/app/state.rs: 添加 AppTheme 枚举和状态管理 - src/app/mod.rs: 完善托盘菜单和退出逻辑 - src/app/commands.rs: 添加主题相关 Tauri 命令 - web/src/App.tsx: 实现主题切换逻辑 - web/src/App.css: 添加主题 CSS 变量和选择器样式 - web/src/pages/SettingsPage.tsx: 添加主题选择器 UI
This commit is contained in:
parent
ceb2df18c4
commit
7ad06cc54a
@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use super::state::AppState;
|
use super::state::{AppState, AppTheme};
|
||||||
|
|
||||||
/// 录音响应
|
/// 录音响应
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@ -147,3 +147,16 @@ pub fn get_history(
|
|||||||
pub fn clear_history(state: State<'_, AppState>) {
|
pub fn clear_history(state: State<'_, AppState>) {
|
||||||
state.clear_history();
|
state.clear_history();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取当前主题
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_theme(state: State<'_, AppState>) -> String {
|
||||||
|
state.get_theme().as_str().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置主题
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_theme(theme: String, state: State<'_, AppState>) {
|
||||||
|
let app_theme = AppTheme::from_str(&theme);
|
||||||
|
state.set_theme(app_theme);
|
||||||
|
}
|
||||||
|
|||||||
112
src/app/mod.rs
112
src/app/mod.rs
@ -15,6 +15,7 @@ pub mod state;
|
|||||||
|
|
||||||
/// 应用状态
|
/// 应用状态
|
||||||
pub use state::AppState;
|
pub use state::AppState;
|
||||||
|
pub use state::AppTheme;
|
||||||
|
|
||||||
/// 运行 Tauri 应用
|
/// 运行 Tauri 应用
|
||||||
pub fn run() -> Result<()> {
|
pub fn run() -> Result<()> {
|
||||||
@ -42,6 +43,8 @@ pub fn run() -> Result<()> {
|
|||||||
commands::save_config,
|
commands::save_config,
|
||||||
commands::get_history,
|
commands::get_history,
|
||||||
commands::clear_history,
|
commands::clear_history,
|
||||||
|
commands::get_theme,
|
||||||
|
commands::set_theme,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
info!("[设置] 初始化应用插件...");
|
info!("[设置] 初始化应用插件...");
|
||||||
@ -145,16 +148,16 @@ pub fn run() -> Result<()> {
|
|||||||
// 处理窗口事件
|
// 处理窗口事件
|
||||||
match event {
|
match event {
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||||
eprintln!(" [窗口] 关闭请求 - 隐藏窗口到托盘");
|
info!(" [窗口] 关闭请求 - 隐藏窗口到托盘");
|
||||||
// 隐藏窗口而不是关闭
|
// 隐藏窗口而不是关闭
|
||||||
window.hide().unwrap();
|
window.hide().unwrap();
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
}
|
}
|
||||||
tauri::WindowEvent::Focused(focused) => {
|
tauri::WindowEvent::Focused(focused) => {
|
||||||
if *focused {
|
if *focused {
|
||||||
eprintln!(" [窗口] 获得焦点");
|
info!(" [窗口] 获得焦点");
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" [窗口] 失去焦点");
|
info!(" [窗口] 失去焦点");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -219,15 +222,24 @@ pub fn run() -> Result<()> {
|
|||||||
eprintln!(" [运行] 进入事件循环...");
|
eprintln!(" [运行] 进入事件循环...");
|
||||||
info!("进入应用事件循环");
|
info!("进入应用事件循环");
|
||||||
|
|
||||||
app.run(|_app_handle, event| {
|
app.run(|app_handle, event| {
|
||||||
// 处理全局事件
|
// 处理全局事件
|
||||||
match event {
|
match event {
|
||||||
|
tauri::RunEvent::Exit => {
|
||||||
|
info!(" [事件] 应用退出");
|
||||||
|
}
|
||||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
eprintln!(" [事件] 退出请求 - 阻止退出");
|
// 检查是否允许完全退出
|
||||||
api.prevent_exit();
|
let state = app_handle.state::<AppState>();
|
||||||
|
if state.is_exit_allowed() {
|
||||||
|
info!(" [事件] 退出请求 - 允许完全退出");
|
||||||
|
} else {
|
||||||
|
info!(" [事件] 退出请求 - 阻止退出(隐藏窗口到托盘)");
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tauri::RunEvent::Ready => {
|
tauri::RunEvent::Ready => {
|
||||||
eprintln!(" [事件] 应用已就绪");
|
info!(" [事件] 应用已就绪");
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -238,35 +250,43 @@ pub fn run() -> Result<()> {
|
|||||||
|
|
||||||
/// 设置系统托盘
|
/// 设置系统托盘
|
||||||
fn setup_tray(app: &tauri::App) -> Result<()> {
|
fn setup_tray(app: &tauri::App) -> Result<()> {
|
||||||
eprintln!(" [托盘] 创建菜单项...");
|
info!(" [托盘] 创建菜单项...");
|
||||||
|
|
||||||
let show = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
|
let show = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
|
||||||
eprintln!(" - '显示窗口' 菜单项已创建");
|
info!(" - '显示窗口' 菜单项已创建");
|
||||||
|
|
||||||
let record = MenuItem::with_id(app, "record", "开始录音", true, None::<&str>)?;
|
let record = MenuItem::with_id(app, "record", "开始录音", true, None::<&str>)?;
|
||||||
eprintln!(" - '开始录音' 菜单项已创建");
|
info!(" - '开始录音' 菜单项已创建");
|
||||||
|
|
||||||
let settings = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?;
|
let settings = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?;
|
||||||
eprintln!(" - '设置' 菜单项已创建");
|
info!(" - '设置' 菜单项已创建");
|
||||||
|
|
||||||
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
// 主题子菜单
|
||||||
eprintln!(" - '退出' 菜单项已创建");
|
let theme_light = MenuItem::with_id(app, "theme_light", "浅色主题", true, None::<&str>)?;
|
||||||
|
let theme_dark = MenuItem::with_id(app, "theme_dark", "深色主题", true, None::<&str>)?;
|
||||||
|
let theme_system = MenuItem::with_id(app, "theme_system", "跟随系统", true, None::<&str>)?;
|
||||||
|
let theme_menu = Menu::with_items(app, &[&theme_light, &theme_dark, &theme_system])?;
|
||||||
|
info!(" - '主题' 子菜单已创建(浅色/深色/跟随系统)");
|
||||||
|
|
||||||
eprintln!(" [托盘] 组合菜单...");
|
// 完全退出选项
|
||||||
let menu = Menu::with_items(app, &[&show, &record, &settings, &quit])?;
|
let quit_now = MenuItem::with_id(app, "quit_now", "完全退出", true, None::<&str>)?;
|
||||||
eprintln!(" ✓ 菜单创建成功 (4 项)");
|
info!(" - '完全退出' 菜单项已创建");
|
||||||
|
|
||||||
eprintln!(" [托盘] 加载图标...");
|
info!(" [托盘] 组合菜单...");
|
||||||
|
let menu = Menu::with_items(app, &[&show, &record, &settings, &theme_menu, &quit_now])?;
|
||||||
|
info!(" ✓ 菜单创建成功 (5 项 + 主题子菜单)");
|
||||||
|
|
||||||
|
info!(" [托盘] 加载图标...");
|
||||||
let icon = app.default_window_icon().cloned();
|
let icon = app.default_window_icon().cloned();
|
||||||
match icon {
|
match icon {
|
||||||
Some(_) => eprintln!(" ✓ 窗口图标加载成功"),
|
Some(_) => info!(" ✓ 窗口图标加载成功"),
|
||||||
None => {
|
None => {
|
||||||
eprintln!(" ⚠ 窗口图标未找到,使用默认图标");
|
info!(" ⚠ 窗口图标未找到,使用默认图标");
|
||||||
warn!("窗口图标未找到");
|
warn!("窗口图标未找到");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!(" [托盘] 创建托盘图标...");
|
info!(" [托盘] 创建托盘图标...");
|
||||||
let _tray = TrayIconBuilder::new()
|
let _tray = TrayIconBuilder::new()
|
||||||
.icon(app.default_window_icon().cloned().unwrap_or_else(|| {
|
.icon(app.default_window_icon().cloned().unwrap_or_else(|| {
|
||||||
warn!("使用空图标");
|
warn!("使用空图标");
|
||||||
@ -279,30 +299,60 @@ fn setup_tray(app: &tauri::App) -> Result<()> {
|
|||||||
info!("托盘菜单事件:{:?}", event.id);
|
info!("托盘菜单事件:{:?}", event.id);
|
||||||
match event.id.as_ref() {
|
match event.id.as_ref() {
|
||||||
"show" => {
|
"show" => {
|
||||||
eprintln!(" [托盘] '显示窗口' 被点击");
|
info!(" [托盘] '显示窗口' 被点击");
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
eprintln!(" ✓ 窗口已显示并聚焦");
|
info!(" ✓ 窗口已显示并聚焦");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"record" => {
|
"record" => {
|
||||||
eprintln!(" [托盘] '开始录音' 被点击");
|
info!(" [托盘] '开始录音' 被点击");
|
||||||
// 触发录音
|
|
||||||
info!("从托盘启动录音");
|
info!("从托盘启动录音");
|
||||||
}
|
}
|
||||||
"settings" => {
|
"settings" => {
|
||||||
eprintln!(" [托盘] '设置' 被点击");
|
info!(" [托盘] '设置' 被点击");
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
eprintln!(" ✓ 窗口已显示并聚焦");
|
info!(" ✓ 窗口已显示并聚焦");
|
||||||
// 导航到设置页面
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"theme_light" => {
|
||||||
eprintln!(" [托盘] '退出' 被点击");
|
info!(" [托盘] '浅色主题' 被点击");
|
||||||
info!("从托盘退出应用");
|
let state = app.state::<AppState>();
|
||||||
|
state.set_theme(AppTheme::Light);
|
||||||
|
// 通知前端切换主题
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.emit("theme-change", "light");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"theme_dark" => {
|
||||||
|
info!(" [托盘] '深色主题' 被点击");
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
state.set_theme(AppTheme::Dark);
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.emit("theme-change", "dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"theme_system" => {
|
||||||
|
info!(" [托盘] '跟随系统' 被点击");
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
state.set_theme(AppTheme::System);
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.emit("theme-change", "system");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quit_now" => {
|
||||||
|
info!(" [托盘] '完全退出' 被点击");
|
||||||
|
// 允许完全退出
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
state.allow_exit();
|
||||||
|
// 关闭窗口
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.close();
|
||||||
|
}
|
||||||
|
// 退出应用
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@ -310,7 +360,7 @@ fn setup_tray(app: &tauri::App) -> Result<()> {
|
|||||||
})
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
eprintln!(" ✓ 托盘图标创建成功");
|
info!(" ✓ 托盘图标创建成功");
|
||||||
info!("系统托盘设置完成");
|
info!("系统托盘设置完成");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -4,6 +4,33 @@ use crate::config::HistoryEntry;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
/// 应用主题
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum AppTheme {
|
||||||
|
#[default]
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
System,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppTheme {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AppTheme::Light => "light",
|
||||||
|
AppTheme::Dark => "dark",
|
||||||
|
AppTheme::System => "system",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"light" => AppTheme::Light,
|
||||||
|
"dark" => AppTheme::Dark,
|
||||||
|
_ => AppTheme::System,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 应用最大历史记录数
|
/// 应用最大历史记录数
|
||||||
const MAX_HISTORY: usize = 100;
|
const MAX_HISTORY: usize = 100;
|
||||||
|
|
||||||
@ -18,6 +45,10 @@ pub struct AppState {
|
|||||||
history: RwLock<VecDeque<HistoryEntry>>,
|
history: RwLock<VecDeque<HistoryEntry>>,
|
||||||
/// 当前使用的模型名称
|
/// 当前使用的模型名称
|
||||||
current_model: RwLock<String>,
|
current_model: RwLock<String>,
|
||||||
|
/// 当前主题
|
||||||
|
current_theme: RwLock<AppTheme>,
|
||||||
|
/// 是否允许完全退出
|
||||||
|
allow_exit: RwLock<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@ -85,6 +116,26 @@ impl AppState {
|
|||||||
pub fn get_current_model(&self) -> String {
|
pub fn get_current_model(&self) -> String {
|
||||||
self.current_model.read().clone()
|
self.current_model.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置当前主题
|
||||||
|
pub fn set_theme(&self, theme: AppTheme) {
|
||||||
|
*self.current_theme.write() = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前主题
|
||||||
|
pub fn get_theme(&self) -> AppTheme {
|
||||||
|
*self.current_theme.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 允许完全退出
|
||||||
|
pub fn allow_exit(&self) {
|
||||||
|
*self.allow_exit.write() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查是否允许完全退出
|
||||||
|
pub fn is_exit_allowed(&self) -> bool {
|
||||||
|
*self.allow_exit.read()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for AppState {
|
impl Clone for AppState {
|
||||||
@ -94,6 +145,8 @@ impl Clone for AppState {
|
|||||||
current_recording_path: RwLock::new(self.current_recording_path.read().clone()),
|
current_recording_path: RwLock::new(self.current_recording_path.read().clone()),
|
||||||
history: RwLock::new(self.history.read().clone()),
|
history: RwLock::new(self.history.read().clone()),
|
||||||
current_model: RwLock::new(self.current_model.read().clone()),
|
current_model: RwLock::new(self.current_model.read().clone()),
|
||||||
|
current_theme: RwLock::new(*self.current_theme.read()),
|
||||||
|
allow_exit: RwLock::new(*self.allow_exit.read()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,33 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 深色主题(默认) */
|
||||||
|
.light {
|
||||||
|
--bg-primary: #f5f5f5;
|
||||||
|
--bg-secondary: #ffffff;
|
||||||
|
--bg-tertiary: #e0e0e0;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #666666;
|
||||||
|
--accent: #e94560;
|
||||||
|
--accent-hover: #ff6b6b;
|
||||||
|
--success: #22c55e;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--border: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--bg-primary: #1a1a2e;
|
||||||
|
--bg-secondary: #16213e;
|
||||||
|
--bg-tertiary: #0f3460;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--accent: #e94560;
|
||||||
|
--accent-hover: #ff6b6b;
|
||||||
|
--success: #4ade80;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--border: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #1a1a2e;
|
--bg-primary: #1a1a2e;
|
||||||
--bg-secondary: #16213e;
|
--bg-secondary: #16213e;
|
||||||
@ -240,3 +267,35 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #1a4a7a;
|
background: #1a4a7a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主题选择器 */
|
||||||
|
.theme-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,69 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import RecordPage from './pages/RecordPage'
|
import RecordPage from './pages/RecordPage'
|
||||||
import FileConvertPage from './pages/FileConvertPage'
|
import FileConvertPage from './pages/FileConvertPage'
|
||||||
import SettingsPage from './pages/SettingsPage'
|
import SettingsPage from './pages/SettingsPage'
|
||||||
|
|
||||||
type Page = 'record' | 'file' | 'settings'
|
type Page = 'record' | 'file' | 'settings'
|
||||||
|
type Theme = 'light' | 'dark' | 'system'
|
||||||
|
|
||||||
|
declare const window: Window & {
|
||||||
|
__TAURI__: {
|
||||||
|
invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
|
||||||
|
listen: (event: string, handler: (e: { payload: unknown }) => void) => Promise<() => void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('record')
|
const [currentPage, setCurrentPage] = useState<Page>('record')
|
||||||
|
const [theme, setTheme] = useState<Theme>('system')
|
||||||
|
|
||||||
|
// 获取当前主题
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTheme = async () => {
|
||||||
|
try {
|
||||||
|
const currentTheme = await window.__TAURI__.invoke<string>('get_theme')
|
||||||
|
setTheme(currentTheme as Theme)
|
||||||
|
applyTheme(currentTheme as Theme)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load theme:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadTheme()
|
||||||
|
|
||||||
|
// 监听主题变化事件
|
||||||
|
const unlisten = window.__TAURI__.listen('theme-change', (event) => {
|
||||||
|
const newTheme = event.payload as string
|
||||||
|
setTheme(newTheme as Theme)
|
||||||
|
applyTheme(newTheme as Theme)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then(fn => fn())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const applyTheme = (newTheme: Theme) => {
|
||||||
|
const root = document.documentElement
|
||||||
|
root.classList.remove('light', 'dark')
|
||||||
|
|
||||||
|
if (newTheme === 'system') {
|
||||||
|
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
root.classList.add(systemDark ? 'dark' : 'light')
|
||||||
|
} else {
|
||||||
|
root.classList.add(newTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThemeChange = async (newTheme: Theme) => {
|
||||||
|
try {
|
||||||
|
await window.__TAURI__.invoke('set_theme', { theme: newTheme })
|
||||||
|
setTheme(newTheme)
|
||||||
|
applyTheme(newTheme)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to set theme:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const renderPage = () => {
|
const renderPage = () => {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
@ -16,7 +72,7 @@ function App() {
|
|||||||
case 'file':
|
case 'file':
|
||||||
return <FileConvertPage />
|
return <FileConvertPage />
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return <SettingsPage />
|
return <SettingsPage theme={theme} onThemeChange={handleThemeChange} />
|
||||||
default:
|
default:
|
||||||
return <RecordPage />
|
return <RecordPage />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,12 @@ interface Settings {
|
|||||||
hotkeyToggle: string
|
hotkeyToggle: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
interface SettingsPageProps {
|
||||||
|
theme: 'light' | 'dark' | 'system'
|
||||||
|
onThemeChange: (theme: 'light' | 'dark' | 'system') => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsPage({ theme, onThemeChange }: SettingsPageProps) {
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
model: 'sensevoice-small',
|
model: 'sensevoice-small',
|
||||||
language: 'zh',
|
language: 'zh',
|
||||||
@ -188,15 +193,26 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<div className="setting-item">
|
<div className="setting-item">
|
||||||
<div className="setting-label">主题</div>
|
<div className="setting-label">主题</div>
|
||||||
<select
|
<div className="theme-selector">
|
||||||
className="select"
|
<button
|
||||||
value={settings.theme}
|
className={`theme-btn ${theme === 'light' ? 'active' : ''}`}
|
||||||
onChange={(e) => handleChange('theme', e.target.value)}
|
onClick={() => onThemeChange('light')}
|
||||||
>
|
>
|
||||||
<option value="dark">深色</option>
|
☀️ 浅色
|
||||||
<option value="light">浅色</option>
|
</button>
|
||||||
<option value="auto">跟随系统</option>
|
<button
|
||||||
</select>
|
className={`theme-btn ${theme === 'dark' ? 'active' : ''}`}
|
||||||
|
onClick={() => onThemeChange('dark')}
|
||||||
|
>
|
||||||
|
🌙 深色
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`theme-btn ${theme === 'system' ? 'active' : ''}`}
|
||||||
|
onClick={() => onThemeChange('system')}
|
||||||
|
>
|
||||||
|
💻 跟随系统
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user