From ff905375cdbad7b76a1c2a9a15c8881b0923168c Mon Sep 17 00:00:00 2001 From: stickmy Date: Mon, 18 Mar 2024 23:01:39 +0800 Subject: [PATCH] feature: add dark mode and serialization --- src-tauri/Cargo.lock | 13 +++++ src-tauri/Cargo.toml | 1 + src-tauri/src/app_conf/app_setting.rs | 64 ++++++++++++++++++++++ src-tauri/src/app_conf/mod.rs | 28 ++++++++-- src-tauri/src/commands/app_setting.rs | 13 +++++ src-tauri/src/commands/mod.rs | 3 +- src-tauri/src/error/configuration_error.rs | 12 +++- src-tauri/src/main.rs | 4 +- src-tauri/src/proxy/mod.rs | 2 + src-tauri/src/window/mod.rs | 36 +++++++++--- src/Commands/Commands.ts | 15 +++++ src/Components/TopBar/useTheme.ts | 17 +++++- 12 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 src-tauri/src/app_conf/app_setting.rs create mode 100644 src-tauri/src/commands/app_setting.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6d3ea47..654715b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -492,6 +492,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df51e72c150781d2c7d4cbcb0b803277caaa80476786994a62961a8f1010dafb" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "toml 0.5.11", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -2467,6 +2479,7 @@ dependencies = [ "bstr", "bytes", "chrono", + "config-file", "futures", "home", "http", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7aaa682..32286e9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ regex = "1.8.1" log = "0.4" simplelog = { version = "0.12.1", features = ["paris"] } home = "0.5.5" +config-file = { version = "0.2.3", features = ["json"] } [features] default = ["http2"] diff --git a/src-tauri/src/app_conf/app_setting.rs b/src-tauri/src/app_conf/app_setting.rs new file mode 100644 index 0000000..31575da --- /dev/null +++ b/src-tauri/src/app_conf/app_setting.rs @@ -0,0 +1,64 @@ +use crate::error::{ + self, + configuration_error::{AppConfIoError, ConfigurationErrorKind}, + ConfigurationError, Error, +}; +use serde::{Deserialize, Serialize}; +use snafu::ResultExt; +use std::{fs, path::Path}; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct AppSetting { + theme: String, +} + +impl Default for AppSetting { + fn default() -> Self { + Self { + theme: String::from("dark"), + } + } +} + +pub(super) fn read_app_setting>(path: P) -> Result { + let content_raw = fs::read(path) + .context(AppConfIoError {}) + .context(ConfigurationError { + scenario: "read file", + })?; + + let content_str = String::from_utf8(content_raw).map_err(|err| Error::Configuration { + scenario: "read as utf8", + source: ConfigurationErrorKind::AppSettingFmt { + msg: format!("{err}"), + }, + })?; + + let conf: AppSetting = + serde_json::from_str(content_str.as_str()).map_err(|err| Error::Configuration { + scenario: "deserialize with serde_json", + source: ConfigurationErrorKind::AppSettingFmt { + msg: format!("{err}"), + }, + })?; + + Ok(conf) +} + +pub(super) fn write_app_setting>( + path: P, + conf: AppSetting, +) -> Result<(), error::Error> { + let str = serde_json::to_string(&conf).map_err(|err| Error::Configuration { + scenario: "serialized as string", + source: ConfigurationErrorKind::AppSettingFmt { + msg: format!("{err}"), + }, + })?; + + fs::write(path, str) + .context(AppConfIoError {}) + .context(ConfigurationError { + scenario: "write to disk", + }) +} diff --git a/src-tauri/src/app_conf/mod.rs b/src-tauri/src/app_conf/mod.rs index f89390e..409e6b1 100644 --- a/src-tauri/src/app_conf/mod.rs +++ b/src-tauri/src/app_conf/mod.rs @@ -1,7 +1,13 @@ use snafu::ResultExt; use std::{fs, path::PathBuf}; -use crate::error::{self, configuration_error::AppConfError, ConfigurationError}; +use crate::error::{self, configuration_error::AppConfIoError, ConfigurationError}; + +use self::app_setting::{read_app_setting, write_app_setting}; + +pub(crate) use self::app_setting::AppSetting; + +mod app_setting; pub fn init() -> Result<(), error::Error> { ensure_app_dir() @@ -15,13 +21,27 @@ pub fn app_rule_dir() -> PathBuf { get_app_path("rule") } +fn app_setting_file() -> PathBuf { + get_app_path("settings.json") +} + +pub fn get_app_setting() -> AppSetting { + read_app_setting(app_setting_file()).unwrap_or(AppSetting::default()) +} + +pub fn save_app_setting(conf: AppSetting) -> () { + if let Err(err) = write_app_setting(app_setting_file(), conf) { + log::error!("save app setting error: {}", err); + } +} + fn get_app_path(name: &str) -> PathBuf { let mut path = app_dir(); path.push(name); path } -fn app_dir() -> PathBuf { +pub fn app_dir() -> PathBuf { const APP_DOT: &str = ".proxyman"; let mut app_dir = home::home_dir().unwrap(); @@ -37,7 +57,7 @@ fn ensure_app_dir() -> Result<(), error::Error> { Ok(meta) => { if !meta.is_dir() { fs::create_dir(app_dir) - .context(AppConfError {}) + .context(AppConfIoError {}) .context(ConfigurationError { scenario: "Ensure app dir", }) @@ -46,7 +66,7 @@ fn ensure_app_dir() -> Result<(), error::Error> { } } Err(_) => fs::create_dir(app_dir) - .context(AppConfError {}) + .context(AppConfIoError {}) .context(ConfigurationError { scenario: "Ensure app dir", }), diff --git a/src-tauri/src/commands/app_setting.rs b/src-tauri/src/commands/app_setting.rs new file mode 100644 index 0000000..4a4b87a --- /dev/null +++ b/src-tauri/src/commands/app_setting.rs @@ -0,0 +1,13 @@ +use crate::app_conf; + +#[tauri::command] +pub async fn set_app_setting(setting: app_conf::AppSetting) -> () { + log::trace!("set_app_setting, {:?}", setting); + + app_conf::save_app_setting(setting); +} + +#[tauri::command] +pub async fn get_app_setting() -> app_conf::AppSetting { + app_conf::get_app_setting() +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 774ed44..2eb085c 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ pub mod processor; pub mod ca; -pub mod global_proxy; \ No newline at end of file +pub mod global_proxy; +pub mod app_setting; diff --git a/src-tauri/src/error/configuration_error.rs b/src-tauri/src/error/configuration_error.rs index b4a4079..ee35264 100644 --- a/src-tauri/src/error/configuration_error.rs +++ b/src-tauri/src/error/configuration_error.rs @@ -5,7 +5,8 @@ use snafu::Snafu; #[snafu(visibility(pub(crate)), context(suffix(Error)))] pub enum ConfigurationErrorKind { Ssl { source: openssl::error::ErrorStack }, - AppConf { source: std::io::Error }, + AppConfIo { source: std::io::Error }, + AppSettingFmt { msg: String }, Cert { scenario: &'static str }, Unknown {}, } @@ -21,11 +22,16 @@ impl Serialize for ConfigurationErrorKind { state.serialize_field("message", source.to_string().as_str())?; state.end() } - Self::AppConf { source } => { - let mut state = serializer.serialize_struct("AppConf", 1)?; + Self::AppConfIo { source } => { + let mut state = serializer.serialize_struct("AppConfIo", 1)?; state.serialize_field("message", source.to_string().as_str())?; state.end() } + Self::AppSettingFmt { msg } => { + let mut state = serializer.serialize_struct("AppSettingFmt", 1)?; + state.serialize_field("message", msg)?; + state.end() + } Self::Cert { scenario } => { let mut state = serializer.serialize_struct("Certificate", 1)?; state.serialize_field("message", scenario)?; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 52a280a..124cd01 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,7 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use sys_events::handle_sys_events; -use tauri::{Manager}; +use tauri::Manager; mod app_conf; mod ca; @@ -19,6 +19,8 @@ mod window; fn main() { let app = tauri::Builder::default() .plugin(proxy::init()) + .menu(window::initial_menu()) + .on_menu_event(window::register_menu_events) .setup(|app| { let win = app.get_window("main").unwrap(); diff --git a/src-tauri/src/proxy/mod.rs b/src-tauri/src/proxy/mod.rs index 8de9438..d2dbf68 100644 --- a/src-tauri/src/proxy/mod.rs +++ b/src-tauri/src/proxy/mod.rs @@ -129,6 +129,8 @@ pub fn init() -> TauriPlugin { commands::processor::remove_response_mapping, commands::global_proxy::turn_on_global_proxy, commands::global_proxy::turn_off_global_proxy, + commands::app_setting::set_app_setting, + commands::app_setting::get_app_setting, ]) .build() } diff --git a/src-tauri/src/window/mod.rs b/src-tauri/src/window/mod.rs index 611dcd7..e413f0b 100644 --- a/src-tauri/src/window/mod.rs +++ b/src-tauri/src/window/mod.rs @@ -1,13 +1,33 @@ -use tauri::{LogicalSize, Size, Window}; +use std::process::Command; + +use tauri::{CustomMenuItem, LogicalSize, Menu, Size, Submenu, Window, WindowMenuEvent}; + +use crate::app_conf::{self}; pub fn initial_window(win: Window) -> () { + win.set_size(Size::Logical(LogicalSize { + width: 1200.0, + height: 800.0, + })) + .unwrap(); win.center().unwrap(); +} + +pub fn initial_menu() -> Menu { + Menu::new() + .add_submenu(Submenu::new("proxyman", Menu::new())) + .add_submenu(Submenu::new( + "帮助", + Menu::new().add_item(CustomMenuItem::new("openlog", "打开日志文件")), + )) +} - win - .set_size(Size::Logical(LogicalSize { - width: 1200.0, - height: 800.0, - })) - .unwrap(); -} \ No newline at end of file +pub fn register_menu_events(event: WindowMenuEvent) { + match event.menu_item_id() { + "openlog" => { + Command::new("open").arg(app_conf::app_dir().as_os_str()).output().unwrap(); + }, + _ => {} + }; +} diff --git a/src/Commands/Commands.ts b/src/Commands/Commands.ts index ee00376..b53db35 100644 --- a/src/Commands/Commands.ts +++ b/src/Commands/Commands.ts @@ -1,3 +1,4 @@ +import { ThemeType } from "@/Components/TopBar/useTheme"; import { invoke, InvokeArgs } from "@tauri-apps/api/tauri"; export const checkTlsCertInstalled = async () => { @@ -49,6 +50,20 @@ export const removeResponseMapping = async (req: string) => { }); }; +export interface AppSetting { + theme: ThemeType; +} + +export const setAppSetting = async (setting: AppSetting) => { + return invokeWithLogging("plugin:proxy|set_app_setting", { + setting + }); +} + +export const getAppSetting = async () => { + return invokeWithLogging("plugin:proxy|get_app_setting"); +} + const invokeWithLogging = async ( cmd: string, args?: InvokeArgs diff --git a/src/Components/TopBar/useTheme.ts b/src/Components/TopBar/useTheme.ts index 4bfe413..82a1224 100644 --- a/src/Components/TopBar/useTheme.ts +++ b/src/Components/TopBar/useTheme.ts @@ -1,8 +1,14 @@ +import { getAppSetting, setAppSetting } from "@/Commands/Commands"; import { create } from "zustand"; export type ThemeType = "light" | "dark"; -const getSystemPreferColorScheme = (): ThemeType => { +const getSystemPreferColorScheme = (delayUpdator: (theme: ThemeType) => void): ThemeType => { + // 初始化返回系统偏好, 随后采用用户设置中的主题配置 + getAppSetting().then(setting => { + delayUpdator(setting.theme); + }); + return window.matchMedia("screen and (prefers-color-scheme: light)").matches ? "light" : "dark"; @@ -12,9 +18,16 @@ export const useTheme = create<{ theme: ThemeType; setTheme: (theme: ThemeType) => void; }>((set) => ({ - theme: getSystemPreferColorScheme(), + theme: getSystemPreferColorScheme(theme => { + document.body.setAttribute("arco-theme", theme); + set({ theme }); + }), setTheme: (theme: ThemeType) => { document.body.setAttribute("arco-theme", theme); set({ theme }); + + getAppSetting().then(setting => { + setAppSetting({ ...setting, theme }); + }); }, }));