diff --git a/src-tauri/icons/tray-icon.png b/src-tauri/icons/tray-icon.png new file mode 100644 index 0000000..81d2aeb Binary files /dev/null and b/src-tauri/icons/tray-icon.png differ diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 1003ce1..70061fa 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -1,4 +1,4 @@ -use crate::utils; +use crate::{conf::ChatConfJson, utils}; use std::fs; use tauri::{api, command, AppHandle, Manager}; @@ -28,3 +28,8 @@ pub fn download(_app: AppHandle, name: String, blob: Vec) { pub fn open_link(app: AppHandle, url: String) { api::shell::open(&app.shell_scope(), url, None).unwrap(); } + +#[command] +pub fn get_chat_conf() -> ChatConfJson { + ChatConfJson::get_chat_conf() +} diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index ef88c3f..32ccf21 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -1,4 +1,7 @@ -use crate::{conf, utils}; +use crate::{ + conf::{self, ChatConfJson}, + utils, +}; use tauri::{ utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent, @@ -13,6 +16,11 @@ pub fn init(chat_conf: &conf::ChatConfJson, context: &Context) - Menu::new() .add_native_item(MenuItem::About(name.into(), AboutMetadata::default())) .add_native_item(MenuItem::Separator) + .add_item( + CustomMenuItem::new("restart".to_string(), "Restart ChatGPT") + .accelerator("CmdOrCtrl+Shift+R"), + ) + .add_native_item(MenuItem::Services) .add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Hide) .add_native_item(MenuItem::HideOthers) @@ -23,24 +31,52 @@ pub fn init(chat_conf: &conf::ChatConfJson, context: &Context) - let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always On Top") .accelerator("CmdOrCtrl+T"); + let titlebar = + CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); + let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); + let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); + let is_dark = chat_conf.theme == "Dark"; + + let always_on_top_menu = if chat_conf.always_on_top { + always_on_top.selected() + } else { + always_on_top + }; + let titlebar_menu = if chat_conf.titlebar { + titlebar.selected() + } else { + titlebar + }; let preferences_menu = Submenu::new( "Preferences", - Menu::new() - .add_item( - CustomMenuItem::new("inject_script".to_string(), "Inject Script") - .accelerator("CmdOrCtrl+J"), + Menu::with_items([ + Submenu::new( + "Theme", + Menu::new() + .add_item(if is_dark { + theme_light + } else { + theme_light.selected() + }) + .add_item(if is_dark { + theme_dark.selected() + } else { + theme_dark + }), ) - .add_item(if chat_conf.always_on_top { - always_on_top.selected() - } else { - always_on_top - }) - .add_native_item(MenuItem::Separator) - .add_item( - CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT") - .accelerator("CmdOrCtrl+Z"), - ), + .into(), + always_on_top_menu.into(), + #[cfg(target_os = "macos")] + titlebar_menu.into(), + CustomMenuItem::new("inject_script".to_string(), "Inject Script") + .accelerator("CmdOrCtrl+J") + .into(), + MenuItem::Separator.into(), + CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT") + .accelerator("CmdOrCtrl+Z") + .into(), + ]), ); let edit_menu = Submenu::new( @@ -73,6 +109,7 @@ pub fn init(chat_conf: &conf::ChatConfJson, context: &Context) - CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen") .accelerator("CmdOrCtrl+Down"), ) + .add_native_item(MenuItem::Zoom) .add_native_item(MenuItem::Separator) .add_item( CustomMenuItem::new("reload".to_string(), "Refresh the Screen") @@ -110,9 +147,25 @@ pub fn menu_handler(event: WindowMenuEvent) { let menu_handle = core_window.menu_handle(); match menu_id { + // App + "restart" => tauri::api::process::restart(&app.env()), // Preferences "inject_script" => open(&app, script_path), "awesome" => open(&app, conf::AWESOME_URL.to_string()), + "titlebar" => { + let chat_conf = conf::ChatConfJson::get_chat_conf(); + ChatConfJson::amend(&serde_json::json!({ "titlebar": !chat_conf.titlebar })).unwrap(); + tauri::api::process::restart(&app.env()); + } + "theme_light" | "theme_dark" => { + let theme = if menu_id == "theme_dark" { + "Dark" + } else { + "Light" + }; + ChatConfJson::amend(&serde_json::json!({ "theme": theme })).unwrap(); + tauri::api::process::restart(&app.env()); + } "always_on_top" => { let mut always_on_top = state.always_on_top.lock().unwrap(); *always_on_top = !*always_on_top; @@ -121,7 +174,7 @@ pub fn menu_handler(event: WindowMenuEvent) { .set_selected(*always_on_top) .unwrap(); win.set_always_on_top(*always_on_top).unwrap(); - conf::ChatConfJson::update_chat_conf(*always_on_top); + ChatConfJson::amend(&serde_json::json!({ "always_on_top": *always_on_top })).unwrap(); } // View "reload" => win.eval("window.location.reload()").unwrap(), diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 84cdc5a..8cc5fd6 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -1,15 +1,13 @@ use crate::{app::window, conf, utils}; use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, Manager}; -#[cfg(target_os = "macos")] -use tauri::TitleBarStyle; - pub fn init( app: &mut App, chat_conf: conf::ChatConfJson, ) -> std::result::Result<(), Box> { - let conf = utils::get_tauri_conf().unwrap(); - let url = conf.build.dev_path.to_string(); + let tauri_conf = utils::get_tauri_conf().unwrap(); + let url = tauri_conf.build.dev_path.to_string(); + let theme = conf::ChatConfJson::theme(); window::mini_window(&app.app_handle()); #[cfg(target_os = "macos")] @@ -18,8 +16,9 @@ pub fn init( .fullscreen(false) .inner_size(800.0, 600.0) .hidden_title(true) - .title_bar_style(TitleBarStyle::Overlay) + .theme(theme) .always_on_top(chat_conf.always_on_top) + .title_bar_style(conf::ChatConfJson::titlebar()) .initialization_script(&utils::user_script()) .initialization_script(include_str!("../assets/html2canvas.js")) .initialization_script(include_str!("../assets/jspdf.js")) @@ -34,6 +33,7 @@ pub fn init( .resizable(true) .fullscreen(false) .inner_size(800.0, 600.0) + .theme(theme) .always_on_top(chat_conf.always_on_top) .initialization_script(&utils::user_script()) .initialization_script(include_str!("../assets/html2canvas.js")) diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index e969d71..1789934 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -2,8 +2,10 @@ use crate::{conf, utils}; use tauri::{utils::config::WindowUrl, window::WindowBuilder}; pub fn mini_window(handle: &tauri::AppHandle) { - let conf = utils::get_tauri_conf().unwrap(); - let url = conf.build.dev_path.to_string(); + let tauri_conf = utils::get_tauri_conf().unwrap(); + let url = tauri_conf.build.dev_path.to_string(); + // let chat_conf = conf::ChatConfJson::get_chat_conf(); + let theme = conf::ChatConfJson::theme(); WindowBuilder::new(handle, "mini", WindowUrl::App(url.into())) .resizable(false) @@ -11,13 +13,13 @@ pub fn mini_window(handle: &tauri::AppHandle) { .inner_size(360.0, 540.0) .decorations(false) .always_on_top(true) + .theme(theme) .initialization_script(&utils::user_script()) .initialization_script(include_str!("../assets/html2canvas.js")) .initialization_script(include_str!("../assets/jspdf.js")) .initialization_script(include_str!("../assets/core.js")) .initialization_script(include_str!("../assets/export.js")) .user_agent(conf::PHONE_USER_AGENT) - .menu(tauri::Menu::new()) .build() .unwrap() .hide() diff --git a/src-tauri/src/assets/core.js b/src-tauri/src/assets/core.js index fe79a00..46c83d4 100644 --- a/src-tauri/src/assets/core.js +++ b/src-tauri/src/assets/core.js @@ -54,7 +54,8 @@ async function init() { } const _platform = await platform(); - if (/darwin/.test(_platform)) { + const chatConf = await invoke('get_chat_conf') || {}; + if (/darwin/.test(_platform) && !chatConf.titlebar) { const topStyleDom = document.createElement("style"); topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`; document.head.appendChild(topStyleDom); diff --git a/src-tauri/src/conf.rs b/src-tauri/src/conf.rs index 8486d05..18dcb64 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -1,7 +1,8 @@ use crate::utils::{chat_root, create_file, exists}; -use std::fs; -use std::path::PathBuf; -use std::sync::Mutex; +use anyhow::Result; +use serde_json::Value; +use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex}; +use tauri::{Theme, TitleBarStyle}; pub const USER_AGENT: &str = "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"; pub const PHONE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"; @@ -13,7 +14,7 @@ pub struct ChatState { } impl ChatState { - pub fn default(chat_conf: &ChatConfJson) -> Self { + pub fn default(chat_conf: ChatConfJson) -> Self { ChatState { always_on_top: Mutex::new(chat_conf.always_on_top), } @@ -23,6 +24,8 @@ impl ChatState { #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct ChatConfJson { pub always_on_top: bool, + pub theme: String, + pub titlebar: bool, } impl ChatConfJson { @@ -32,7 +35,22 @@ impl ChatConfJson { let conf_file = ChatConfJson::conf_path(); if !exists(&conf_file) { create_file(&conf_file).unwrap(); - fs::write(&conf_file, r#"{"always_on_top": false}"#).unwrap(); + + #[cfg(target_os = "macos")] + let content = r#"{ + "always_on_top": false, + "theme": "Light", + "titlebar": false + }"#; + + #[cfg(not(target_os = "macos"))] + let content = r#"{ + "always_on_top": false, + "theme": "Light", + "titlebar": true + }"#; + + fs::write(&conf_file, content).unwrap(); } conf_file } @@ -43,24 +61,49 @@ impl ChatConfJson { pub fn get_chat_conf() -> Self { let config_file = fs::read_to_string(ChatConfJson::conf_path()).unwrap(); - let config: serde_json::Value = + let config: Value = serde_json::from_str(&config_file).expect("failed to parse chat.conf.json"); serde_json::from_value(config).unwrap_or_else(|_| ChatConfJson::chat_conf_default()) } - pub fn update_chat_conf(always_on_top: bool) { - let mut conf = ChatConfJson::get_chat_conf(); - conf.always_on_top = always_on_top; - fs::write( - ChatConfJson::conf_path(), - serde_json::to_string(&conf).unwrap(), - ) - .unwrap(); + // https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3 + pub fn amend(new_rules: &Value) -> Result<()> { + let config = ChatConfJson::get_chat_conf(); + let config: Value = serde_json::to_value(&config)?; + let mut config: BTreeMap = serde_json::from_value(config)?; + let new_rules: BTreeMap = serde_json::from_value(new_rules.clone())?; + + for (k, v) in new_rules { + config.insert(k, v); + } + + fs::write(ChatConfJson::conf_path(), serde_json::to_string(&config)?)?; + Ok(()) + } + + pub fn theme() -> Option { + let conf = ChatConfJson::get_chat_conf(); + if conf.theme == "Dark" { + Some(Theme::Dark) + } else { + Some(Theme::Light) + } + } + + pub fn titlebar() -> TitleBarStyle { + let conf = ChatConfJson::get_chat_conf(); + if conf.titlebar { + TitleBarStyle::Transparent + } else { + TitleBarStyle::Overlay + } } pub fn chat_conf_default() -> Self { serde_json::from_value(serde_json::json!({ "always_on_top": false, + "theme": "Light", + "titlebar": true })) .unwrap() } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 57b55e0..3ec00ed 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -17,12 +17,13 @@ fn main() { let chat_conf2 = chat_conf.clone(); tauri::Builder::default() - .manage(conf::ChatState::default(&chat_conf)) + .manage(conf::ChatState::default(chat_conf.clone())) .invoke_handler(tauri::generate_handler![ cmd::drag_window, cmd::fullscreen, cmd::download, - cmd::open_link + cmd::open_link, + cmd::get_chat_conf, ]) .setup(|app| setup::init(app, chat_conf2)) .plugin(tauri_plugin_positioner::init()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ef29397..5d4b0fd 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,8 +14,8 @@ "all": true }, "systemTray": { - "iconPath": "icons/icon.png", - "iconAsTemplate": false + "iconPath": "icons/tray-icon.png", + "iconAsTemplate": true }, "bundle": { "active": true,