chore: add rustfmt.toml

This commit is contained in:
lencx
2023-01-21 12:59:52 +08:00
parent 5f1c33d750
commit 321007bb87
11 changed files with 1233 additions and 1325 deletions

View File

@@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }

4
src-tauri/rustfmt.toml Normal file
View File

@@ -0,0 +1,4 @@
edition = "2021"
max_width = 120
tab_spaces = 2
newline_style = "Auto"

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
app::{fs_extra, window}, app::{fs_extra, window},
conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL}, conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL},
utils::{self, chat_root, create_file}, utils::{self, chat_root, create_file},
}; };
use log::info; use log::info;
use regex::Regex; use regex::Regex;
@@ -11,390 +11,383 @@ use walkdir::WalkDir;
#[command] #[command]
pub fn drag_window(app: AppHandle) { pub fn drag_window(app: AppHandle) {
app.get_window("core").unwrap().start_dragging().unwrap(); app.get_window("core").unwrap().start_dragging().unwrap();
} }
#[command] #[command]
pub fn dalle2_window(app: AppHandle, query: String) { pub fn dalle2_window(app: AppHandle, query: String) {
window::dalle2_window( window::dalle2_window(
&app.app_handle(), &app.app_handle(),
Some(query), Some(query),
Some("ChatGPT & DALL·E 2".to_string()), Some("ChatGPT & DALL·E 2".to_string()),
None, None,
); );
} }
#[command] #[command]
pub fn fullscreen(app: AppHandle) { pub fn fullscreen(app: AppHandle) {
let win = app.get_window("core").unwrap(); let win = app.get_window("core").unwrap();
if win.is_fullscreen().unwrap() { if win.is_fullscreen().unwrap() {
win.set_fullscreen(false).unwrap(); win.set_fullscreen(false).unwrap();
} else { } else {
win.set_fullscreen(true).unwrap(); win.set_fullscreen(true).unwrap();
} }
} }
#[command] #[command]
pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) { pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) {
let path = chat_root().join(PathBuf::from(name)); let path = chat_root().join(PathBuf::from(name));
create_file(&path).unwrap(); create_file(&path).unwrap();
fs::write(&path, blob).unwrap(); fs::write(&path, blob).unwrap();
utils::open_file(path); utils::open_file(path);
} }
#[command] #[command]
pub fn save_file(_app: AppHandle, name: String, content: String) { pub fn save_file(_app: AppHandle, name: String, content: String) {
let path = chat_root().join(PathBuf::from(name)); let path = chat_root().join(PathBuf::from(name));
create_file(&path).unwrap(); create_file(&path).unwrap();
fs::write(&path, content).unwrap(); fs::write(&path, content).unwrap();
utils::open_file(path); utils::open_file(path);
} }
#[command] #[command]
pub fn open_link(app: AppHandle, url: String) { pub fn open_link(app: AppHandle, url: String) {
api::shell::open(&app.shell_scope(), url, None).unwrap(); api::shell::open(&app.shell_scope(), url, None).unwrap();
} }
#[command] #[command]
pub fn get_chat_conf() -> ChatConfJson { pub fn get_chat_conf() -> ChatConfJson {
ChatConfJson::get_chat_conf() ChatConfJson::get_chat_conf()
} }
#[command] #[command]
pub fn get_theme() -> String { pub fn get_theme() -> String {
ChatConfJson::theme().unwrap_or(Theme::Light).to_string() ChatConfJson::theme().unwrap_or(Theme::Light).to_string()
} }
#[command] #[command]
pub fn reset_chat_conf() -> ChatConfJson { pub fn reset_chat_conf() -> ChatConfJson {
ChatConfJson::reset_chat_conf() ChatConfJson::reset_chat_conf()
} }
#[command] #[command]
pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option<bool>) { pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option<bool>) {
utils::run_check_update(app, silent, has_msg); utils::run_check_update(app, silent, has_msg);
} }
#[command] #[command]
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) { pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
ChatConfJson::amend(&serde_json::json!(data), None).unwrap(); ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
} }
#[command] #[command]
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) { pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label).unwrap(); let win = app.app_handle().get_window(label).unwrap();
tauri::api::dialog::ask( tauri::api::dialog::ask(
app.app_handle().get_window(label).as_ref(), app.app_handle().get_window(label).as_ref(),
title, title,
msg, msg,
move |is_cancel| { move |is_cancel| {
if is_cancel { if is_cancel {
win.close().unwrap(); win.close().unwrap();
} }
}, },
); );
} }
#[command] #[command]
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) { pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label); let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg); tauri::api::dialog::message(win.as_ref(), title, msg);
} }
#[command] #[command]
pub fn open_file(path: PathBuf) { pub fn open_file(path: PathBuf) {
utils::open_file(path); utils::open_file(path);
} }
#[command] #[command]
pub fn get_chat_model_cmd() -> serde_json::Value { pub fn get_chat_model_cmd() -> serde_json::Value {
let path = utils::chat_root().join("chat.model.cmd.json"); let path = utils::chat_root().join("chat.model.cmd.json");
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string()); let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
serde_json::from_str(&content).unwrap() serde_json::from_str(&content).unwrap()
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PromptRecord { pub struct PromptRecord {
pub cmd: Option<String>, pub cmd: Option<String>,
pub act: String, pub act: String,
pub prompt: String, pub prompt: String,
} }
#[command] #[command]
pub fn parse_prompt(data: String) -> Vec<PromptRecord> { pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
let mut rdr = csv::Reader::from_reader(data.as_bytes()); let mut rdr = csv::Reader::from_reader(data.as_bytes());
let mut list = vec![]; let mut list = vec![];
for result in rdr.deserialize() { for result in rdr.deserialize() {
let record: PromptRecord = result.unwrap_or_else(|err| { let record: PromptRecord = result.unwrap_or_else(|err| {
info!("parse_prompt_error: {}", err); info!("parse_prompt_error: {}", err);
PromptRecord { PromptRecord {
cmd: None, cmd: None,
act: "".to_string(), act: "".to_string(),
prompt: "".to_string(), prompt: "".to_string(),
} }
}); });
if !record.act.is_empty() { if !record.act.is_empty() {
list.push(record); list.push(record);
}
} }
list }
list
} }
#[command] #[command]
pub fn window_reload(app: AppHandle, label: &str) { pub fn window_reload(app: AppHandle, label: &str) {
app.app_handle() app
.get_window(label) .app_handle()
.unwrap() .get_window(label)
.eval("window.location.reload()") .unwrap()
.unwrap(); .eval("window.location.reload()")
.unwrap();
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ModelRecord { pub struct ModelRecord {
pub cmd: String, pub cmd: String,
pub act: String, pub act: String,
pub prompt: String, pub prompt: String,
pub tags: Vec<String>, pub tags: Vec<String>,
pub enable: bool, pub enable: bool,
} }
#[command] #[command]
pub fn cmd_list() -> Vec<ModelRecord> { pub fn cmd_list() -> Vec<ModelRecord> {
let mut list = vec![]; let mut list = vec![];
for entry in WalkDir::new(utils::chat_root().join("cache_model")) for entry in WalkDir::new(utils::chat_root().join("cache_model"))
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let file = fs::read_to_string(entry.path().display().to_string()); let file = fs::read_to_string(entry.path().display().to_string());
if let Ok(v) = file { if let Ok(v) = file {
let data: Vec<ModelRecord> = serde_json::from_str(&v).unwrap_or_else(|_| vec![]); let data: Vec<ModelRecord> = serde_json::from_str(&v).unwrap_or_else(|_| vec![]);
let enable_list = data.into_iter().filter(|v| v.enable); let enable_list = data.into_iter().filter(|v| v.enable);
list.extend(enable_list) list.extend(enable_list)
}
} }
// dbg!(&list); }
list.sort_by(|a, b| a.cmd.len().cmp(&b.cmd.len())); // dbg!(&list);
list list.sort_by(|a, b| a.cmd.len().cmp(&b.cmd.len()));
list
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct FileMetadata { pub struct FileMetadata {
pub name: String, pub name: String,
pub ext: String, pub ext: String,
pub created: u64, pub created: u64,
pub id: String, pub id: String,
} }
#[tauri::command] #[tauri::command]
pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) { pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
info!("get_download_list: {}", pathname); info!("get_download_list: {}", pathname);
let download_path = chat_root().join(PathBuf::from(pathname)); let download_path = chat_root().join(PathBuf::from(pathname));
let content = fs::read_to_string(&download_path).unwrap_or_else(|err| { let content = fs::read_to_string(&download_path).unwrap_or_else(|err| {
info!("download_list_error: {}", err); info!("download_list_error: {}", err);
fs::write(&download_path, "[]").unwrap(); fs::write(&download_path, "[]").unwrap();
"[]".to_string() "[]".to_string()
}); });
let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| { let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| {
info!("download_list_parse_error: {}", err); info!("download_list_parse_error: {}", err);
vec![] vec![]
}); });
(list, download_path) (list, download_path)
} }
#[command] #[command]
pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) { pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
info!("download_list: {}", pathname); info!("download_list: {}", pathname);
let data = get_download_list(pathname); let data = get_download_list(pathname);
let mut list = vec![]; let mut list = vec![];
let mut idmap = HashMap::new(); let mut idmap = HashMap::new();
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap); utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap);
for entry in WalkDir::new(utils::chat_root().join(dir)) for entry in WalkDir::new(utils::chat_root().join(dir))
.into_iter() .into_iter()
.filter_entry(|e| !utils::is_hidden(e)) .filter_entry(|e| !utils::is_hidden(e))
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let metadata = entry.metadata().unwrap(); let metadata = entry.metadata().unwrap();
if metadata.is_file() { if metadata.is_file() {
let file_path = entry.path().display().to_string(); let file_path = entry.path().display().to_string();
let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap(); let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap();
let caps = re.captures(&file_path).unwrap(); let caps = re.captures(&file_path).unwrap();
let fid = &caps["id"]; let fid = &caps["id"];
let fext = &caps["ext"]; let fext = &caps["ext"];
let mut file_data = FileMetadata { let mut file_data = FileMetadata {
name: fid.to_string(), name: fid.to_string(),
id: fid.to_string(), id: fid.to_string(),
ext: fext.to_string(), ext: fext.to_string(),
created: fs_extra::system_time_to_ms(metadata.created()), created: fs_extra::system_time_to_ms(metadata.created()),
}; };
if idmap.get(fid).is_some() { if idmap.get(fid).is_some() {
let name = idmap.get(fid).unwrap().get("name").unwrap().clone(); let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
match name { match name {
serde_json::Value::String(v) => { serde_json::Value::String(v) => {
file_data.name = v.clone(); file_data.name = v.clone();
v v
} }
_ => "".to_string(), _ => "".to_string(),
}; };
}
if filename.is_some() && id.is_some() {
if let Some(ref v) = id {
if fid == v {
if let Some(ref v2) = filename {
file_data.name = v2.to_string();
} }
}
if filename.is_some() && id.is_some() {
if let Some(ref v) = id {
if fid == v {
if let Some(ref v2) = filename {
file_data.name = v2.to_string();
}
}
}
}
list.push(serde_json::to_value(file_data).unwrap());
} }
}
list.push(serde_json::to_value(file_data).unwrap());
} }
}
// dbg!(&list); // dbg!(&list);
list.sort_by(|a, b| { list.sort_by(|a, b| {
let a1 = a.get("created").unwrap().as_u64().unwrap(); let a1 = a.get("created").unwrap().as_u64().unwrap();
let b1 = b.get("created").unwrap().as_u64().unwrap(); let b1 = b.get("created").unwrap().as_u64().unwrap();
a1.cmp(&b1).reverse() a1.cmp(&b1).reverse()
}); });
fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap(); fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap();
} }
#[command] #[command]
pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> { pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> {
let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app)) let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app)).await.unwrap();
.await
.unwrap();
if let Some(v) = res { if let Some(v) = res {
let data = parse_prompt(v) let data = parse_prompt(v)
.iter() .iter()
.map(move |i| ModelRecord { .map(move |i| ModelRecord {
cmd: if i.cmd.is_some() { cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap() i.cmd.clone().unwrap()
} else { } else {
utils::gen_cmd(i.act.clone()) utils::gen_cmd(i.act.clone())
}, },
act: i.act.clone(), act: i.act.clone(),
prompt: i.prompt.clone(), prompt: i.prompt.clone(),
tags: vec!["chatgpt-prompts".to_string()], tags: vec!["chatgpt-prompts".to_string()],
enable: true, enable: true,
}) })
.collect::<Vec<ModelRecord>>(); .collect::<Vec<ModelRecord>>();
let data2 = data.clone(); let data2 = data.clone();
let model = utils::chat_root().join("chat.model.json"); let model = utils::chat_root().join("chat.model.json");
let model_cmd = utils::chat_root().join("chat.model.cmd.json"); let model_cmd = utils::chat_root().join("chat.model.cmd.json");
let chatgpt_prompts = utils::chat_root() let chatgpt_prompts = utils::chat_root().join("cache_model").join("chatgpt_prompts.json");
.join("cache_model")
.join("chatgpt_prompts.json");
if !utils::exists(&model) { if !utils::exists(&model) {
fs::write( fs::write(
&model, &model,
serde_json::json!({ serde_json::json!({
"name": "ChatGPT Model", "name": "ChatGPT Model",
"link": "https://github.com/lencx/ChatGPT" "link": "https://github.com/lencx/ChatGPT"
}) })
.to_string(), .to_string(),
) )
.unwrap(); .unwrap();
}
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
)
.unwrap();
let cmd_data = cmd_list();
// chat.model.cmd.json
fs::write(
model_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let model_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
&kv,
);
// chat.model.json
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
);
window_reload(app.clone(), "core");
window_reload(app, "tray");
return Some(data2);
} }
None // chatgpt_prompts.json
fs::write(chatgpt_prompts, serde_json::to_string_pretty(&data).unwrap()).unwrap();
let cmd_data = cmd_list();
// chat.model.cmd.json
fs::write(
model_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let model_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
&kv,
);
// chat.model.json
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
);
window_reload(app.clone(), "core");
window_reload(app, "tray");
return Some(data2);
}
None
} }
#[command] #[command]
pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<ModelRecord>> { pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<ModelRecord>> {
let res = utils::get_data(&url, None).await.unwrap_or_else(|err| { let res = utils::get_data(&url, None).await.unwrap_or_else(|err| {
info!("chatgpt_http_error: {}", err); info!("chatgpt_http_error: {}", err);
None None
}); });
info!("chatgpt_http_url: {}", url); info!("chatgpt_http_url: {}", url);
if let Some(v) = res { if let Some(v) = res {
let data; let data;
if data_type == "csv" { if data_type == "csv" {
info!("chatgpt_http_csv_parse"); info!("chatgpt_http_csv_parse");
data = parse_prompt(v); data = parse_prompt(v);
} else if data_type == "json" { } else if data_type == "json" {
info!("chatgpt_http_json_parse"); info!("chatgpt_http_json_parse");
data = serde_json::from_str(&v).unwrap_or_else(|err| { data = serde_json::from_str(&v).unwrap_or_else(|err| {
info!("chatgpt_http_json_parse_error: {}", err); info!("chatgpt_http_json_parse_error: {}", err);
vec![] vec![]
}); });
} else { } else {
info!("chatgpt_http_unknown_type"); info!("chatgpt_http_unknown_type");
data = vec![]; data = vec![];
}
let data = data
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
})
.collect::<Vec<ModelRecord>>();
return Some(data);
} }
None let data = data
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
})
.collect::<Vec<ModelRecord>>();
return Some(data);
}
None
} }

View File

@@ -6,8 +6,8 @@
use serde::{ser::Serializer, Serialize}; use serde::{ser::Serializer, Serialize};
use std::{ use std::{
path::PathBuf, path::PathBuf,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tauri::command; use tauri::command;
@@ -20,101 +20,102 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
impl Serialize for Error { impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(self.to_string().as_ref()) serializer.serialize_str(self.to_string().as_ref())
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Permissions { struct Permissions {
readonly: bool, readonly: bool,
#[cfg(unix)] #[cfg(unix)]
mode: u32, mode: u32,
} }
#[cfg(unix)] #[cfg(unix)]
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct UnixMetadata { struct UnixMetadata {
dev: u64, dev: u64,
ino: u64, ino: u64,
mode: u32, mode: u32,
nlink: u64, nlink: u64,
uid: u32, uid: u32,
gid: u32, gid: u32,
rdev: u64, rdev: u64,
blksize: u64, blksize: u64,
blocks: u64, blocks: u64,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
accessed_at_ms: u64, accessed_at_ms: u64,
pub created_at_ms: u64, pub created_at_ms: u64,
modified_at_ms: u64, modified_at_ms: u64,
is_dir: bool, is_dir: bool,
is_file: bool, is_file: bool,
is_symlink: bool, is_symlink: bool,
size: u64, size: u64,
permissions: Permissions, permissions: Permissions,
#[cfg(unix)] #[cfg(unix)]
#[serde(flatten)] #[serde(flatten)]
unix: UnixMetadata, unix: UnixMetadata,
#[cfg(windows)] #[cfg(windows)]
file_attributes: u32, file_attributes: u32,
} }
pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 { pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
time.map(|t| { time
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); .map(|t| {
duration_since_epoch.as_millis() as u64 let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
duration_since_epoch.as_millis() as u64
}) })
.unwrap_or_default() .unwrap_or_default()
} }
#[command] #[command]
pub async fn metadata(path: PathBuf) -> Result<Metadata> { pub async fn metadata(path: PathBuf) -> Result<Metadata> {
let metadata = std::fs::metadata(path)?; let metadata = std::fs::metadata(path)?;
let file_type = metadata.file_type(); let file_type = metadata.file_type();
let permissions = metadata.permissions(); let permissions = metadata.permissions();
Ok(Metadata { Ok(Metadata {
accessed_at_ms: system_time_to_ms(metadata.accessed()), accessed_at_ms: system_time_to_ms(metadata.accessed()),
created_at_ms: system_time_to_ms(metadata.created()), created_at_ms: system_time_to_ms(metadata.created()),
modified_at_ms: system_time_to_ms(metadata.modified()), modified_at_ms: system_time_to_ms(metadata.modified()),
is_dir: file_type.is_dir(), is_dir: file_type.is_dir(),
is_file: file_type.is_file(), is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(), is_symlink: file_type.is_symlink(),
size: metadata.len(), size: metadata.len(),
permissions: Permissions { permissions: Permissions {
readonly: permissions.readonly(), readonly: permissions.readonly(),
#[cfg(unix)] #[cfg(unix)]
mode: permissions.mode(), mode: permissions.mode(),
}, },
#[cfg(unix)] #[cfg(unix)]
unix: UnixMetadata { unix: UnixMetadata {
dev: metadata.dev(), dev: metadata.dev(),
ino: metadata.ino(), ino: metadata.ino(),
mode: metadata.mode(), mode: metadata.mode(),
nlink: metadata.nlink(), nlink: metadata.nlink(),
uid: metadata.uid(), uid: metadata.uid(),
gid: metadata.gid(), gid: metadata.gid(),
rdev: metadata.rdev(), rdev: metadata.rdev(),
blksize: metadata.blksize(), blksize: metadata.blksize(),
blocks: metadata.blocks(), blocks: metadata.blocks(),
}, },
#[cfg(windows)] #[cfg(windows)]
file_attributes: metadata.file_attributes(), file_attributes: metadata.file_attributes(),
}) })
} }
// #[command] // #[command]

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
app::{cmd, window}, app::{cmd, window},
conf::{self, ChatConfJson}, conf::{self, ChatConfJson},
utils, utils,
}; };
use tauri::{ use tauri::{
AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent, SystemTrayMenuItem, WindowMenuEvent,
}; };
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt}; use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
@@ -14,461 +14,404 @@ use tauri::AboutMetadata;
// --- Menu // --- Menu
pub fn init() -> Menu { pub fn init() -> Menu {
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
let name = "ChatGPT"; let name = "ChatGPT";
let app_menu = Submenu::new( let app_menu = Submenu::new(
name, name,
Menu::with_items([ Menu::with_items([
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
MenuItem::About(name.into(), AboutMetadata::default()).into(), MenuItem::About(name.into(), AboutMetadata::default()).into(),
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
CustomMenuItem::new("about".to_string(), "About ChatGPT").into(), CustomMenuItem::new("about".to_string(), "About ChatGPT").into(),
CustomMenuItem::new("check_update".to_string(), "Check for Updates").into(), CustomMenuItem::new("check_update".to_string(), "Check for Updates").into(),
MenuItem::Services.into(), MenuItem::Services.into(),
MenuItem::Hide.into(), MenuItem::Hide.into(),
MenuItem::HideOthers.into(), MenuItem::HideOthers.into(),
MenuItem::ShowAll.into(), MenuItem::ShowAll.into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
MenuItem::Quit.into(), MenuItem::Quit.into(),
]), ]),
); );
let stay_on_top = let stay_on_top = CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T"); let stay_on_top_menu = if chat_conf.stay_on_top {
let stay_on_top_menu = if chat_conf.stay_on_top { stay_on_top.selected()
stay_on_top.selected() } else {
} else { stay_on_top
stay_on_top };
};
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
let theme_system = CustomMenuItem::new("theme_system".to_string(), "System"); let theme_system = CustomMenuItem::new("theme_system".to_string(), "System");
let is_dark = chat_conf.theme == "Dark"; let is_dark = chat_conf.theme == "Dark";
let is_system = chat_conf.theme == "System"; let is_system = chat_conf.theme == "System";
let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt"); let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt");
let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent"); let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent");
let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable"); let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable");
let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search"); let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search");
let popup_search_menu = if chat_conf.popup_search { let popup_search_menu = if chat_conf.popup_search {
popup_search.selected() popup_search.selected()
} else { } else {
popup_search popup_search
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let titlebar = let titlebar = CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); #[cfg(target_os = "macos")]
#[cfg(target_os = "macos")] let titlebar_menu = if chat_conf.titlebar {
let titlebar_menu = if chat_conf.titlebar { titlebar.selected()
titlebar.selected() } else {
} else { titlebar
titlebar };
};
let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray"); let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray");
let system_tray_menu = if chat_conf.tray { let system_tray_menu = if chat_conf.tray {
system_tray.selected() system_tray.selected()
} else { } else {
system_tray system_tray
}; };
let preferences_menu = Submenu::new( let preferences_menu = Submenu::new(
"Preferences", "Preferences",
Menu::with_items([ Menu::with_items([
CustomMenuItem::new("control_center".to_string(), "Control Center") CustomMenuItem::new("control_center".to_string(), "Control Center")
.accelerator("CmdOrCtrl+Shift+P") .accelerator("CmdOrCtrl+Shift+P")
.into(), .into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
stay_on_top_menu.into(), stay_on_top_menu.into(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
titlebar_menu.into(), titlebar_menu.into(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(), CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
system_tray_menu.into(), system_tray_menu.into(),
CustomMenuItem::new("inject_script".to_string(), "Inject Script") CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.accelerator("CmdOrCtrl+J") .accelerator("CmdOrCtrl+J")
.into(), .into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
Submenu::new( Submenu::new(
"Theme", "Theme",
Menu::new()
.add_item(if is_dark || is_system {
theme_light
} else {
theme_light.selected()
})
.add_item(if is_dark {
theme_dark.selected()
} else {
theme_dark
})
.add_item(if is_system {
theme_system.selected()
} else {
theme_system
}),
)
.into(),
Submenu::new(
"Auto Update",
Menu::new()
.add_item(if chat_conf.auto_update == "Prompt" {
update_prompt.selected()
} else {
update_prompt
})
.add_item(if chat_conf.auto_update == "Silent" {
update_silent.selected()
} else {
update_silent
}), // .add_item(if chat_conf.auto_update == "Disable" {
// update_disable.selected()
// } else {
// update_disable
// })
)
.into(),
MenuItem::Separator.into(),
popup_search_menu.into(),
CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
MenuItem::Separator.into(),
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G")
.into(),
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
.accelerator("CmdOrCtrl+Shift+D")
.into(),
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
.accelerator("CmdOrCtrl+Shift+R")
.into(),
MenuItem::Separator.into(),
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
.accelerator("CmdOrCtrl+Shift+A")
.into(),
CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
]),
);
let edit_menu = Submenu::new(
"Edit",
Menu::new() Menu::new()
.add_native_item(MenuItem::Undo) .add_item(if is_dark || is_system {
.add_native_item(MenuItem::Redo) theme_light
.add_native_item(MenuItem::Separator) } else {
.add_native_item(MenuItem::Cut) theme_light.selected()
.add_native_item(MenuItem::Copy) })
.add_native_item(MenuItem::Paste) .add_item(if is_dark { theme_dark.selected() } else { theme_dark })
.add_native_item(MenuItem::SelectAll), .add_item(if is_system {
); theme_system.selected()
} else {
let view_menu = Submenu::new( theme_system
"View", }),
)
.into(),
Submenu::new(
"Auto Update",
Menu::new() Menu::new()
.add_item( .add_item(if chat_conf.auto_update == "Prompt" {
CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"), update_prompt.selected()
) } else {
.add_item( update_prompt
CustomMenuItem::new("go_forward".to_string(), "Go Forward") })
.accelerator("CmdOrCtrl+Right"), .add_item(if chat_conf.auto_update == "Silent" {
) update_silent.selected()
.add_item( } else {
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen") update_silent
.accelerator("CmdOrCtrl+Up"), }), // .add_item(if chat_conf.auto_update == "Disable" {
) // update_disable.selected()
.add_item( // } else {
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen") // update_disable
.accelerator("CmdOrCtrl+Down"), // })
) )
.add_native_item(MenuItem::Separator) .into(),
.add_item( MenuItem::Separator.into(),
CustomMenuItem::new("reload".to_string(), "Refresh the Screen") popup_search_menu.into(),
.accelerator("CmdOrCtrl+R"), CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
), MenuItem::Separator.into(),
); CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G")
let window_menu = Submenu::new( .into(),
"Window", CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
Menu::new() .accelerator("CmdOrCtrl+Shift+D")
.add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2")) .into(),
.add_native_item(MenuItem::Separator) CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
.add_native_item(MenuItem::Minimize) .accelerator("CmdOrCtrl+Shift+R")
.add_native_item(MenuItem::Zoom), .into(),
); MenuItem::Separator.into(),
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
let help_menu = Submenu::new( .accelerator("CmdOrCtrl+Shift+A")
"Help", .into(),
Menu::new() CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
.add_item(CustomMenuItem::new( ]),
"chatgpt_log".to_string(), );
"ChatGPT Log",
))
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
.add_item(
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Shift+I"),
),
);
let edit_menu = Submenu::new(
"Edit",
Menu::new() Menu::new()
.add_submenu(app_menu) .add_native_item(MenuItem::Undo)
.add_submenu(preferences_menu) .add_native_item(MenuItem::Redo)
.add_submenu(window_menu) .add_native_item(MenuItem::Separator)
.add_submenu(edit_menu) .add_native_item(MenuItem::Cut)
.add_submenu(view_menu) .add_native_item(MenuItem::Copy)
.add_submenu(help_menu) .add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::SelectAll),
);
let view_menu = Submenu::new(
"View",
Menu::new()
.add_item(CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"))
.add_item(CustomMenuItem::new("go_forward".to_string(), "Go Forward").accelerator("CmdOrCtrl+Right"))
.add_item(CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen").accelerator("CmdOrCtrl+Up"))
.add_item(
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen").accelerator("CmdOrCtrl+Down"),
)
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("reload".to_string(), "Refresh the Screen").accelerator("CmdOrCtrl+R")),
);
let window_menu = Submenu::new(
"Window",
Menu::new()
.add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2"))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom),
);
let help_menu = Submenu::new(
"Help",
Menu::new()
.add_item(CustomMenuItem::new("chatgpt_log".to_string(), "ChatGPT Log"))
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
.add_item(
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools").accelerator("CmdOrCtrl+Shift+I"),
),
);
Menu::new()
.add_submenu(app_menu)
.add_submenu(preferences_menu)
.add_submenu(window_menu)
.add_submenu(edit_menu)
.add_submenu(view_menu)
.add_submenu(help_menu)
} }
// --- Menu Event // --- Menu Event
pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) { pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
let win = Some(event.window()).unwrap(); let win = Some(event.window()).unwrap();
let app = win.app_handle(); let app = win.app_handle();
let script_path = utils::script_path().to_string_lossy().to_string(); let script_path = utils::script_path().to_string_lossy().to_string();
let menu_id = event.menu_item_id(); let menu_id = event.menu_item_id();
let menu_handle = win.menu_handle(); let menu_handle = win.menu_handle();
match menu_id { match menu_id {
// App // App
"about" => { "about" => {
let tauri_conf = utils::get_tauri_conf().unwrap(); let tauri_conf = utils::get_tauri_conf().unwrap();
tauri::api::dialog::message( tauri::api::dialog::message(
app.get_window("core").as_ref(), app.get_window("core").as_ref(),
"ChatGPT", "ChatGPT",
format!("Version {}", tauri_conf.package.version.unwrap()), format!("Version {}", tauri_conf.package.version.unwrap()),
); );
}
"check_update" => {
utils::run_check_update(app, false, None);
}
// Preferences
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::chat_root()),
"clear_conf" => utils::clear_conf(&app),
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
"popup_search" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let popup_search = !chat_conf.popup_search;
menu_handle.get_item(menu_id).set_selected(popup_search).unwrap();
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None).unwrap();
cmd::window_reload(app.clone(), "core");
cmd::window_reload(app, "tray");
}
"sync_prompts" => {
tauri::api::dialog::ask(
app.get_window("core").as_ref(),
"Sync Prompts",
"Data sync will enable all prompts, are you sure you want to sync?",
move |is_restart| {
if is_restart {
app
.get_window("core")
.unwrap()
.eval("window.__sync_prompts && window.__sync_prompts()")
.unwrap()
}
},
);
}
"hide_dock_icon" => ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap(),
"titlebar" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(&serde_json::json!({ "titlebar": !chat_conf.titlebar }), None).unwrap();
tauri::api::process::restart(&app.env());
}
"system_tray" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap();
tauri::api::process::restart(&app.env());
}
"theme_light" | "theme_dark" | "theme_system" => {
let theme = match menu_id {
"theme_dark" => "Dark",
"theme_system" => "System",
_ => "Light",
};
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
}
"update_prompt" | "update_silent" | "update_disable" => {
// for id in ["update_prompt", "update_silent", "update_disable"] {
for id in ["update_prompt", "update_silent"] {
menu_handle.get_item(id).set_selected(false).unwrap();
}
let auto_update = match menu_id {
"update_silent" => {
menu_handle.get_item("update_silent").set_selected(true).unwrap();
"Silent"
} }
"check_update" => { "update_disable" => {
utils::run_check_update(app, false, None); menu_handle.get_item("update_disable").set_selected(true).unwrap();
"Disable"
} }
// Preferences _ => {
"control_center" => window::control_window(&app), menu_handle.get_item("update_prompt").set_selected(true).unwrap();
"restart" => tauri::api::process::restart(&app.env()), "Prompt"
"inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::chat_root()),
"clear_conf" => utils::clear_conf(&app),
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
"popup_search" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let popup_search = !chat_conf.popup_search;
menu_handle
.get_item(menu_id)
.set_selected(popup_search)
.unwrap();
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None)
.unwrap();
cmd::window_reload(app.clone(), "core");
cmd::window_reload(app, "tray");
} }
"sync_prompts" => { };
tauri::api::dialog::ask( ChatConfJson::amend(&serde_json::json!({ "auto_update": auto_update }), None).unwrap();
app.get_window("core").as_ref(), }
"Sync Prompts", "stay_on_top" => {
"Data sync will enable all prompts, are you sure you want to sync?", let chat_conf = conf::ChatConfJson::get_chat_conf();
move |is_restart| { let stay_on_top = !chat_conf.stay_on_top;
if is_restart { menu_handle.get_item(menu_id).set_selected(stay_on_top).unwrap();
app.get_window("core") win.set_always_on_top(stay_on_top).unwrap();
.unwrap() ChatConfJson::amend(&serde_json::json!({ "stay_on_top": stay_on_top }), None).unwrap();
.eval("window.__sync_prompts && window.__sync_prompts()") }
.unwrap() // Window
} "dalle2" => window::dalle2_window(&app, None, None, Some(false)),
}, // View
); "reload" => win.eval("window.location.reload()").unwrap(),
} "go_back" => win.eval("window.history.go(-1)").unwrap(),
"hide_dock_icon" => { "go_forward" => win.eval("window.history.go(1)").unwrap(),
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap() // core: document.querySelector('main .overflow-y-auto')
} "scroll_top" => win
"titlebar" => { .eval(
let chat_conf = conf::ChatConfJson::get_chat_conf(); r#"window.scroll({
ChatConfJson::amend(
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
None,
)
.unwrap();
tauri::api::process::restart(&app.env());
}
"system_tray" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap();
tauri::api::process::restart(&app.env());
}
"theme_light" | "theme_dark" | "theme_system" => {
let theme = match menu_id {
"theme_dark" => "Dark",
"theme_system" => "System",
_ => "Light",
};
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
}
"update_prompt" | "update_silent" | "update_disable" => {
// for id in ["update_prompt", "update_silent", "update_disable"] {
for id in ["update_prompt", "update_silent"] {
menu_handle.get_item(id).set_selected(false).unwrap();
}
let auto_update = match menu_id {
"update_silent" => {
menu_handle
.get_item("update_silent")
.set_selected(true)
.unwrap();
"Silent"
}
"update_disable" => {
menu_handle
.get_item("update_disable")
.set_selected(true)
.unwrap();
"Disable"
}
_ => {
menu_handle
.get_item("update_prompt")
.set_selected(true)
.unwrap();
"Prompt"
}
};
ChatConfJson::amend(&serde_json::json!({ "auto_update": auto_update }), None).unwrap();
}
"stay_on_top" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let stay_on_top = !chat_conf.stay_on_top;
menu_handle
.get_item(menu_id)
.set_selected(stay_on_top)
.unwrap();
win.set_always_on_top(stay_on_top).unwrap();
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": stay_on_top }), None).unwrap();
}
// Window
"dalle2" => window::dalle2_window(&app, None, None, Some(false)),
// View
"reload" => win.eval("window.location.reload()").unwrap(),
"go_back" => win.eval("window.history.go(-1)").unwrap(),
"go_forward" => win.eval("window.history.go(1)").unwrap(),
// core: document.querySelector('main .overflow-y-auto')
"scroll_top" => win
.eval(
r#"window.scroll({
top: 0, top: 0,
left: 0, left: 0,
behavior: "smooth" behavior: "smooth"
})"#, })"#,
) )
.unwrap(), .unwrap(),
"scroll_bottom" => win "scroll_bottom" => win
.eval( .eval(
r#"window.scroll({ r#"window.scroll({
top: document.body.scrollHeight, top: document.body.scrollHeight,
left: 0, left: 0,
behavior: "smooth"})"#, behavior: "smooth"})"#,
) )
.unwrap(), .unwrap(),
// Help // Help
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")), "chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")),
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()), "update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
"report_bug" => open(&app, conf::ISSUES_URL.to_string()), "report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => { "dev_tools" => {
win.open_devtools(); win.open_devtools();
win.close_devtools(); win.close_devtools();
}
_ => (),
} }
_ => (),
}
} }
// --- SystemTray Menu // --- SystemTray Menu
pub fn tray_menu() -> SystemTray { pub fn tray_menu() -> SystemTray {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
SystemTray::new().with_menu( SystemTray::new().with_menu(
SystemTrayMenu::new() SystemTrayMenu::new()
.add_item(CustomMenuItem::new( .add_item(CustomMenuItem::new("control_center".to_string(), "Control Center"))
"control_center".to_string(), .add_native_item(SystemTrayMenuItem::Separator)
"Control Center", .add_item(CustomMenuItem::new("show_dock_icon".to_string(), "Show Dock Icon"))
)) .add_item(CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon"))
.add_native_item(SystemTrayMenuItem::Separator) .add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_item(CustomMenuItem::new( .add_native_item(SystemTrayMenuItem::Separator)
"show_dock_icon".to_string(), .add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
"Show Dock Icon", )
)) } else {
.add_item(CustomMenuItem::new( SystemTray::new().with_menu(
"hide_dock_icon".to_string(), SystemTrayMenu::new()
"Hide Dock Icon", .add_item(CustomMenuItem::new("control_center".to_string(), "Control Center"))
)) .add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT")) .add_native_item(SystemTrayMenuItem::Separator)
.add_native_item(SystemTrayMenuItem::Separator) .add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")), )
) }
} else {
SystemTray::new().with_menu(
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"control_center".to_string(),
"Control Center",
))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
}
} }
// --- SystemTray Event // --- SystemTray Event
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) { pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
on_tray_event(handle, &event); on_tray_event(handle, &event);
let app = handle.clone(); let app = handle.clone();
match event { match event {
SystemTrayEvent::LeftClick { .. } => { SystemTrayEvent::LeftClick { .. } => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let chat_conf = conf::ChatConfJson::get_chat_conf();
if !chat_conf.hide_dock_icon { if !chat_conf.hide_dock_icon {
let core_win = handle.get_window("core").unwrap(); let core_win = handle.get_window("core").unwrap();
core_win.minimize().unwrap(); core_win.minimize().unwrap();
} }
let tray_win = handle.get_window("tray").unwrap(); let tray_win = handle.get_window("tray").unwrap();
tray_win.move_window(Position::TrayCenter).unwrap(); tray_win.move_window(Position::TrayCenter).unwrap();
if tray_win.is_visible().unwrap() { if tray_win.is_visible().unwrap() {
tray_win.hide().unwrap(); tray_win.hide().unwrap();
} else { } else {
tray_win.show().unwrap(); tray_win.show().unwrap();
} }
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app))
.unwrap();
}
"hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
if !chat_conf.hide_dock_icon {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app))
.unwrap();
}
}
"show_core" => {
let core_win = app.get_window("core").unwrap();
let tray_win = app.get_window("tray").unwrap();
if !core_win.is_visible().unwrap() {
core_win.show().unwrap();
core_win.set_focus().unwrap();
tray_win.hide().unwrap();
}
}
"quit" => std::process::exit(0),
_ => (),
},
_ => (),
} }
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app)).unwrap();
}
"hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
if !chat_conf.hide_dock_icon {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap();
}
}
"show_core" => {
let core_win = app.get_window("core").unwrap();
let tray_win = app.get_window("tray").unwrap();
if !core_win.is_visible().unwrap() {
core_win.show().unwrap();
core_win.set_focus().unwrap();
tray_win.hide().unwrap();
}
}
"quit" => std::process::exit(0),
_ => (),
},
_ => (),
}
} }
pub fn open(app: &AppHandle, path: String) { pub fn open(app: &AppHandle, path: String) {
tauri::api::shell::open(&app.shell_scope(), path, None).unwrap(); tauri::api::shell::open(&app.shell_scope(), path, None).unwrap();
} }

View File

@@ -4,90 +4,90 @@ use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcut
use wry::application::accelerator::Accelerator; use wry::application::accelerator::Accelerator;
pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> { pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("stepup"); info!("stepup");
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
let url = chat_conf.origin.to_string(); let url = chat_conf.origin.to_string();
let theme = ChatConfJson::theme(); let theme = ChatConfJson::theme();
let handle = app.app_handle(); let handle = app.app_handle();
tauri::async_runtime::spawn(async move {
window::tray_window(&handle);
});
if let Some(v) = chat_conf.global_shortcut {
info!("global_shortcut: `{}`", v);
match v.parse::<Accelerator>() {
Ok(_) => {
info!("global_shortcut_register");
let handle = app.app_handle();
let mut shortcut = app.global_shortcut_manager();
shortcut
.register(&v, move || {
if let Some(w) = handle.get_window("core") {
if w.is_visible().unwrap() {
w.hide().unwrap();
} else {
w.show().unwrap();
w.set_focus().unwrap();
}
}
})
.unwrap_or_else(|err| {
info!("global_shortcut_register_error: {}", err);
});
}
Err(err) => {
info!("global_shortcut_parse_error: {}", err);
}
}
} else {
info!("global_shortcut_unregister");
};
if chat_conf.hide_dock_icon {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else {
let app = app.handle();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
window::tray_window(&handle); let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0);
if cfg!(target_os = "macos") {
main_win = main_win.hidden_title(true);
}
main_win
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../vendors/html2canvas.js"))
.initialization_script(include_str!("../vendors/jspdf.js"))
.initialization_script(include_str!("../vendors/turndown.js"))
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
.initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/popup.core.js"))
.initialization_script(include_str!("../scripts/export.js"))
.initialization_script(include_str!("../scripts/markdown.export.js"))
.initialization_script(include_str!("../scripts/cmd.js"))
.user_agent(&chat_conf.ua_window)
.build()
.unwrap();
}); });
}
if let Some(v) = chat_conf.global_shortcut { // auto_update
info!("global_shortcut: `{}`", v); if chat_conf.auto_update != "Disable" {
match v.parse::<Accelerator>() { info!("stepup::run_check_update");
Ok(_) => { let app = app.handle();
info!("global_shortcut_register"); utils::run_check_update(app, chat_conf.auto_update == "Silent", None);
let handle = app.app_handle(); }
let mut shortcut = app.global_shortcut_manager();
shortcut
.register(&v, move || {
if let Some(w) = handle.get_window("core") {
if w.is_visible().unwrap() {
w.hide().unwrap();
} else {
w.show().unwrap();
w.set_focus().unwrap();
}
}
})
.unwrap_or_else(|err| {
info!("global_shortcut_register_error: {}", err);
});
}
Err(err) => {
info!("global_shortcut_parse_error: {}", err);
}
}
} else {
info!("global_shortcut_unregister");
};
if chat_conf.hide_dock_icon { Ok(())
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else {
let app = app.handle();
tauri::async_runtime::spawn(async move {
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0);
if cfg!(target_os = "macos") {
main_win = main_win.hidden_title(true);
}
main_win
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../vendors/html2canvas.js"))
.initialization_script(include_str!("../vendors/jspdf.js"))
.initialization_script(include_str!("../vendors/turndown.js"))
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
.initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/popup.core.js"))
.initialization_script(include_str!("../scripts/export.js"))
.initialization_script(include_str!("../scripts/markdown.export.js"))
.initialization_script(include_str!("../scripts/cmd.js"))
.user_agent(&chat_conf.ua_window)
.build()
.unwrap();
});
}
// auto_update
if chat_conf.auto_update != "Disable" {
info!("stepup::run_check_update");
let app = app.handle();
utils::run_check_update(app, chat_conf.auto_update == "Silent", None);
}
Ok(())
} }

View File

@@ -4,104 +4,95 @@ use std::time::SystemTime;
use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager}; use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager};
pub fn tray_window(handle: &tauri::AppHandle) { pub fn tray_window(handle: &tauri::AppHandle) {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let chat_conf = conf::ChatConfJson::get_chat_conf();
let theme = conf::ChatConfJson::theme(); let theme = conf::ChatConfJson::theme();
let app = handle.clone(); let app = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into())) WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into()))
.title("ChatGPT") .title("ChatGPT")
.resizable(false) .resizable(false)
.fullscreen(false) .fullscreen(false)
.inner_size(360.0, 540.0) .inner_size(360.0, 540.0)
.decorations(false) .decorations(false)
.always_on_top(true) .always_on_top(true)
.theme(theme) .theme(theme)
.initialization_script(&utils::user_script()) .initialization_script(&utils::user_script())
.initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../scripts/core.js")) .initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/cmd.js")) .initialization_script(include_str!("../scripts/cmd.js"))
.initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(include_str!("../scripts/popup.core.js"))
.user_agent(&chat_conf.ua_tray) .user_agent(&chat_conf.ua_tray)
.build() .build()
.unwrap() .unwrap()
.hide() .hide()
.unwrap(); .unwrap();
}); });
} }
pub fn dalle2_window( pub fn dalle2_window(handle: &tauri::AppHandle, query: Option<String>, title: Option<String>, is_new: Option<bool>) {
handle: &tauri::AppHandle, info!("dalle2_query: {:?}", query);
query: Option<String>, let theme = conf::ChatConfJson::theme();
title: Option<String>, let app = handle.clone();
is_new: Option<bool>,
) {
info!("dalle2_query: {:?}", query);
let theme = conf::ChatConfJson::theme();
let app = handle.clone();
let query = if query.is_some() { let query = if query.is_some() {
format!( format!(
"window.addEventListener('DOMContentLoaded', function() {{\nwindow.__CHATGPT_QUERY__='{}';\n}})", "window.addEventListener('DOMContentLoaded', function() {{\nwindow.__CHATGPT_QUERY__='{}';\n}})",
query.unwrap() query.unwrap()
) )
} else { } else {
"".to_string() "".to_string()
}; };
let label = if is_new.unwrap_or(true) { let label = if is_new.unwrap_or(true) {
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
format!("dalle2_{}", timestamp) format!("dalle2_{}", timestamp)
} else { } else {
"dalle2".to_string() "dalle2".to_string()
}; };
if app.get_window("dalle2").is_none() { if app.get_window("dalle2").is_none() {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
WindowBuilder::new( WindowBuilder::new(&app, label, WindowUrl::App("https://labs.openai.com".into()))
&app, .title(title.unwrap_or_else(|| "DALL·E 2".to_string()))
label, .resizable(true)
WindowUrl::App("https://labs.openai.com".into()), .fullscreen(false)
) .inner_size(800.0, 600.0)
.title(title.unwrap_or_else(|| "DALL·E 2".to_string())) .always_on_top(false)
.resizable(true) .theme(theme)
.fullscreen(false) .initialization_script(include_str!("../scripts/core.js"))
.inner_size(800.0, 600.0) .initialization_script(&query)
.always_on_top(false) .initialization_script(include_str!("../scripts/dalle2.js"))
.theme(theme) .build()
.initialization_script(include_str!("../scripts/core.js")) .unwrap();
.initialization_script(&query) });
.initialization_script(include_str!("../scripts/dalle2.js")) } else {
.build() let dalle2_win = app.get_window("dalle2").unwrap();
.unwrap(); dalle2_win.show().unwrap();
}); dalle2_win.set_focus().unwrap();
} else { }
let dalle2_win = app.get_window("dalle2").unwrap();
dalle2_win.show().unwrap();
dalle2_win.set_focus().unwrap();
}
} }
pub fn control_window(handle: &tauri::AppHandle) { pub fn control_window(handle: &tauri::AppHandle) {
let app = handle.clone(); let app = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if app.app_handle().get_window("main").is_none() { if app.app_handle().get_window("main").is_none() {
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into())) WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into()))
.title("Control Center") .title("Control Center")
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
.inner_size(1000.0, 700.0) .inner_size(1000.0, 700.0)
.min_inner_size(800.0, 600.0) .min_inner_size(800.0, 600.0)
.build() .build()
.unwrap(); .unwrap();
} else { } else {
let main_win = app.app_handle().get_window("main").unwrap(); let main_win = app.app_handle().get_window("main").unwrap();
main_win.show().unwrap(); main_win.show().unwrap();
main_win.set_focus().unwrap(); main_win.set_focus().unwrap();
} }
}); });
} }

View File

@@ -15,8 +15,7 @@ pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md"; pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md";
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md"; pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx"; pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx";
pub const GITHUB_PROMPTS_CSV_URL: &str = pub const GITHUB_PROMPTS_CSV_URL: &str = "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
pub const DEFAULT_CHAT_CONF: &str = r#"{ pub const DEFAULT_CHAT_CONF: &str = r#"{
"stay_on_top": false, "stay_on_top": false,
"auto_update": "Prompt", "auto_update": "Prompt",
@@ -48,153 +47,150 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson { pub struct ChatConfJson {
// support macOS only // support macOS only
pub titlebar: bool, pub titlebar: bool,
pub hide_dock_icon: bool, pub hide_dock_icon: bool,
// macOS and Windows, Light/Dark/System // macOS and Windows, Light/Dark/System
pub theme: String, pub theme: String,
// auto update policy, Prompt/Silent/Disable // auto update policy, Prompt/Silent/Disable
pub auto_update: String, pub auto_update: String,
pub tray: bool, pub tray: bool,
pub popup_search: bool, pub popup_search: bool,
pub stay_on_top: bool, pub stay_on_top: bool,
pub default_origin: String, pub default_origin: String,
pub origin: String, pub origin: String,
pub ua_window: String, pub ua_window: String,
pub ua_tray: String, pub ua_tray: String,
pub global_shortcut: Option<String>, pub global_shortcut: Option<String>,
} }
impl ChatConfJson { impl ChatConfJson {
/// init chat.conf.json /// init chat.conf.json
/// path: ~/.chatgpt/chat.conf.json /// path: ~/.chatgpt/chat.conf.json
pub fn init() -> PathBuf { pub fn init() -> PathBuf {
info!("chat_conf_init"); info!("chat_conf_init");
let conf_file = ChatConfJson::conf_path(); let conf_file = ChatConfJson::conf_path();
let content = if cfg!(target_os = "macos") { let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC DEFAULT_CHAT_CONF_MAC
} else { } else {
DEFAULT_CHAT_CONF DEFAULT_CHAT_CONF
}; };
if !exists(&conf_file) { if !exists(&conf_file) {
create_file(&conf_file).unwrap(); create_file(&conf_file).unwrap();
fs::write(&conf_file, content).unwrap(); fs::write(&conf_file, content).unwrap();
return conf_file; return conf_file;
}
let conf_file = ChatConfJson::conf_path();
let file_content = fs::read_to_string(&conf_file).unwrap();
match serde_json::from_str(&file_content) {
Ok(v) => v,
Err(err) => {
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" {
return conf_file;
} }
fs::write(&conf_file, content).unwrap();
}
};
let conf_file = ChatConfJson::conf_path(); conf_file
let file_content = fs::read_to_string(&conf_file).unwrap(); }
match serde_json::from_str(&file_content) {
Ok(v) => v,
Err(err) => {
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" {
return conf_file;
}
fs::write(&conf_file, content).unwrap();
}
};
conf_file pub fn conf_path() -> PathBuf {
} chat_root().join("chat.conf.json")
}
pub fn conf_path() -> PathBuf { pub fn get_chat_conf() -> Self {
chat_root().join("chat.conf.json") let conf_file = ChatConfJson::conf_path();
} let file_content = fs::read_to_string(&conf_file).unwrap();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
pub fn get_chat_conf() -> Self { match serde_json::from_value(match serde_json::from_str(&file_content) {
let conf_file = ChatConfJson::conf_path(); Ok(v) => v,
let file_content = fs::read_to_string(&conf_file).unwrap(); Err(_) => {
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
match serde_json::from_value(match serde_json::from_str(&file_content) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap()
}
}) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
}
}
}
pub fn reset_chat_conf() -> Self {
let conf_file = ChatConfJson::conf_path();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
fs::write(&conf_file, content).unwrap(); fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap() serde_json::from_str(content).unwrap()
}
}) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
}
}
}
pub fn reset_chat_conf() -> Self {
let conf_file = ChatConfJson::conf_path();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap()
}
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
for (k, v) in new_rules {
config.insert(k, v);
} }
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3 fs::write(ChatConfJson::conf_path(), serde_json::to_string_pretty(&config)?)?;
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
for (k, v) in new_rules { if let Some(handle) = app {
config.insert(k, v); tauri::api::process::restart(&handle.env());
} // tauri::api::dialog::ask(
// handle.get_window("core").as_ref(),
fs::write( // "ChatGPT Restart",
ChatConfJson::conf_path(), // "Whether to restart immediately?",
serde_json::to_string_pretty(&config)?, // move |is_restart| {
)?; // if is_restart {
// }
if let Some(handle) = app { // },
tauri::api::process::restart(&handle.env()); // );
// tauri::api::dialog::ask(
// handle.get_window("core").as_ref(),
// "ChatGPT Restart",
// "Whether to restart immediately?",
// move |is_restart| {
// if is_restart {
// }
// },
// );
}
Ok(())
} }
pub fn theme() -> Option<Theme> { Ok(())
let conf = ChatConfJson::get_chat_conf(); }
let theme = match conf.theme.as_str() {
"System" => match dark_light::detect() {
// Dark mode
dark_light::Mode::Dark => Theme::Dark,
// Light mode
dark_light::Mode::Light => Theme::Light,
// Unspecified
dark_light::Mode::Default => Theme::Light,
},
"Dark" => Theme::Dark,
_ => Theme::Light,
};
Some(theme) pub fn theme() -> Option<Theme> {
} let conf = ChatConfJson::get_chat_conf();
let theme = match conf.theme.as_str() {
"System" => match dark_light::detect() {
// Dark mode
dark_light::Mode::Dark => Theme::Dark,
// Light mode
dark_light::Mode::Light => Theme::Light,
// Unspecified
dark_light::Mode::Default => Theme::Light,
},
"Dark" => Theme::Dark,
_ => Theme::Light,
};
#[cfg(target_os = "macos")] Some(theme)
pub fn titlebar() -> TitleBarStyle { }
let conf = ChatConfJson::get_chat_conf();
if conf.titlebar { #[cfg(target_os = "macos")]
TitleBarStyle::Transparent pub fn titlebar() -> TitleBarStyle {
} else { let conf = ChatConfJson::get_chat_conf();
TitleBarStyle::Overlay if conf.titlebar {
} TitleBarStyle::Transparent
} else {
TitleBarStyle::Overlay
} }
}
} }

View File

@@ -1,7 +1,4 @@
#![cfg_attr( #![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod app; mod app;
mod conf; mod conf;
@@ -12,104 +9,101 @@ use conf::ChatConfJson;
use tauri::api::path; use tauri::api::path;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_log::{ use tauri_plugin_log::{
fern::colors::{Color, ColoredLevelConfig}, fern::colors::{Color, ColoredLevelConfig},
LogTarget, LogTarget,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
ChatConfJson::init(); ChatConfJson::init();
// If the file does not exist, creating the file will block menu synchronization // If the file does not exist, creating the file will block menu synchronization
utils::create_chatgpt_prompts(); utils::create_chatgpt_prompts();
let context = tauri::generate_context!(); let context = tauri::generate_context!();
let colors = ColoredLevelConfig { let colors = ColoredLevelConfig {
error: Color::Red, error: Color::Red,
warn: Color::Yellow, warn: Color::Yellow,
debug: Color::Blue, debug: Color::Blue,
info: Color::BrightGreen, info: Color::BrightGreen,
trace: Color::Cyan, trace: Color::Cyan,
}; };
cmd::download_list("chat.download.json", "download", None, None); cmd::download_list("chat.download.json", "download", None, None);
cmd::download_list("chat.notes.json", "notes", None, None); cmd::download_list("chat.notes.json", "notes", None, None);
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
// https://github.com/tauri-apps/tauri/pull/2736 // https://github.com/tauri-apps/tauri/pull/2736
.plugin( .plugin(
tauri_plugin_log::Builder::default() tauri_plugin_log::Builder::default()
.targets([ .targets([
// LogTarget::LogDir, // LogTarget::LogDir,
// LOG PATH: ~/.chatgpt/ChatGPT.log // LOG PATH: ~/.chatgpt/ChatGPT.log
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")), LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
LogTarget::Stdout, LogTarget::Stdout,
LogTarget::Webview, LogTarget::Webview,
])
.level(log::LevelFilter::Debug)
.with_colors(colors)
.build(),
)
.plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
None,
))
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::save_file,
cmd::open_link,
cmd::get_chat_conf,
cmd::get_theme,
cmd::reset_chat_conf,
cmd::run_check_update,
cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
cmd::open_file,
cmd::get_chat_model_cmd,
cmd::parse_prompt,
cmd::sync_prompts,
cmd::sync_user_prompts,
cmd::window_reload,
cmd::dalle2_window,
cmd::cmd_list,
cmd::download_list,
cmd::get_download_list,
fs_extra::metadata,
]) ])
.setup(setup::init) .level(log::LevelFilter::Debug)
.menu(menu::init()); .with_colors(colors)
.build(),
)
.plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None))
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::save_file,
cmd::open_link,
cmd::get_chat_conf,
cmd::get_theme,
cmd::reset_chat_conf,
cmd::run_check_update,
cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
cmd::open_file,
cmd::get_chat_model_cmd,
cmd::parse_prompt,
cmd::sync_prompts,
cmd::sync_user_prompts,
cmd::window_reload,
cmd::dalle2_window,
cmd::cmd_list,
cmd::download_list,
cmd::get_download_list,
fs_extra::metadata,
])
.setup(setup::init)
.menu(menu::init());
if chat_conf.tray { if chat_conf.tray {
builder = builder.system_tray(menu::tray_menu()); builder = builder.system_tray(menu::tray_menu());
} }
builder builder
.on_menu_event(menu::menu_handler) .on_menu_event(menu::menu_handler)
.on_system_tray_event(menu::tray_handler) .on_system_tray_event(menu::tray_handler)
.on_window_event(|event| { .on_window_event(|event| {
// https://github.com/tauri-apps/tauri/discussions/2684 // https://github.com/tauri-apps/tauri/discussions/2684
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() { if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
let win = event.window(); let win = event.window();
if win.label() == "core" { if win.label() == "core" {
// TODO: https://github.com/tauri-apps/tauri/issues/3084 // TODO: https://github.com/tauri-apps/tauri/issues/3084
// event.window().hide().unwrap(); // event.window().hide().unwrap();
// https://github.com/tauri-apps/tao/pull/517 // https://github.com/tauri-apps/tao/pull/517
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
event.window().minimize().unwrap(); event.window().minimize().unwrap();
// fix: https://github.com/lencx/ChatGPT/issues/93 // fix: https://github.com/lencx/ChatGPT/issues/93
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
event.window().hide().unwrap(); event.window().hide().unwrap();
} else { } else {
win.close().unwrap(); win.close().unwrap();
} }
api.prevent_close(); api.prevent_close();
} }
}) })
.run(context) .run(context)
.expect("error while running ChatGPT application"); .expect("error while running ChatGPT application");
} }

View File

@@ -3,215 +3,180 @@ use log::info;
use regex::Regex; use regex::Regex;
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::{self, File}, fs::{self, File},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
}; };
use tauri::updater::UpdateResponse; use tauri::updater::UpdateResponse;
use tauri::{utils::config::Config, AppHandle, Manager, Wry}; use tauri::{utils::config::Config, AppHandle, Manager, Wry};
pub fn chat_root() -> PathBuf { pub fn chat_root() -> PathBuf {
tauri::api::path::home_dir().unwrap().join(".chatgpt") tauri::api::path::home_dir().unwrap().join(".chatgpt")
} }
pub fn get_tauri_conf() -> Option<Config> { pub fn get_tauri_conf() -> Option<Config> {
let config_file = include_str!("../tauri.conf.json"); let config_file = include_str!("../tauri.conf.json");
let config: Config = let config: Config = serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
serde_json::from_str(config_file).expect("failed to parse tauri.conf.json"); Some(config)
Some(config)
} }
pub fn exists(path: &Path) -> bool { pub fn exists(path: &Path) -> bool {
Path::new(path).exists() Path::new(path).exists()
} }
pub fn create_file(path: &Path) -> Result<File> { pub fn create_file(path: &Path) -> Result<File> {
if let Some(p) = path.parent() { if let Some(p) = path.parent() {
fs::create_dir_all(p)? fs::create_dir_all(p)?
} }
File::create(path).map_err(Into::into) File::create(path).map_err(Into::into)
} }
pub fn create_chatgpt_prompts() { pub fn create_chatgpt_prompts() {
let sync_file = chat_root().join("cache_model").join("chatgpt_prompts.json"); let sync_file = chat_root().join("cache_model").join("chatgpt_prompts.json");
if !exists(&sync_file) { if !exists(&sync_file) {
create_file(&sync_file).unwrap(); create_file(&sync_file).unwrap();
fs::write(&sync_file, "[]").unwrap(); fs::write(&sync_file, "[]").unwrap();
} }
} }
pub fn script_path() -> PathBuf { pub fn script_path() -> PathBuf {
let script_file = chat_root().join("main.js"); let script_file = chat_root().join("main.js");
if !exists(&script_file) { if !exists(&script_file) {
create_file(&script_file).unwrap(); create_file(&script_file).unwrap();
fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap(); fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap();
} }
script_file script_file
} }
pub fn user_script() -> String { pub fn user_script() -> String {
let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string()); let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string());
format!( format!(
"window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})", "window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
user_script_content user_script_content
) )
} }
pub fn open_file(path: PathBuf) { pub fn open_file(path: PathBuf) {
info!("open_file: {}", path.to_string_lossy()); info!("open_file: {}", path.to_string_lossy());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Command::new("open").arg("-R").arg(path).spawn().unwrap(); Command::new("open").arg("-R").arg(path).spawn().unwrap();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
Command::new("explorer") Command::new("explorer").arg("/select,").arg(path).spawn().unwrap();
.arg("/select,")
.arg(path)
.spawn()
.unwrap();
// https://askubuntu.com/a/31071 // https://askubuntu.com/a/31071
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Command::new("xdg-open").arg(path).spawn().unwrap(); Command::new("xdg-open").arg(path).spawn().unwrap();
} }
pub fn clear_conf(app: &tauri::AppHandle) { pub fn clear_conf(app: &tauri::AppHandle) {
let root = chat_root(); let root = chat_root();
let app2 = app.clone(); let app2 = app.clone();
let msg = format!("Path: {}\nAre you sure to clear all ChatGPT configurations? Please backup in advance if necessary!", root.to_string_lossy()); let msg = format!(
tauri::api::dialog::ask( "Path: {}\nAre you sure to clear all ChatGPT configurations? Please backup in advance if necessary!",
app.get_window("core").as_ref(), root.to_string_lossy()
"Clear Config", );
msg, tauri::api::dialog::ask(app.get_window("core").as_ref(), "Clear Config", msg, move |is_ok| {
move |is_ok| { if is_ok {
if is_ok { fs::remove_dir_all(root).unwrap();
fs::remove_dir_all(root).unwrap(); tauri::api::process::restart(&app2.env());
tauri::api::process::restart(&app2.env()); }
} });
},
);
} }
pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value { pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value {
match v { match v {
Value::Object(m) => { Value::Object(m) => {
let mut m = m.clone(); let mut m = m.clone();
for (k, v) in fields { for (k, v) in fields {
m.insert(k.clone(), v.clone()); m.insert(k.clone(), v.clone());
} }
Value::Object(m) Value::Object(m)
}
v => v.clone(),
} }
v => v.clone(),
}
} }
pub fn gen_cmd(name: String) -> String { pub fn gen_cmd(name: String) -> String {
let re = Regex::new(r"[^a-zA-Z0-9]").unwrap(); let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
re.replace_all(&name, "_").to_lowercase() re.replace_all(&name, "_").to_lowercase()
} }
pub async fn get_data( pub async fn get_data(url: &str, app: Option<&tauri::AppHandle>) -> Result<Option<String>, reqwest::Error> {
url: &str, let res = reqwest::get(url).await?;
app: Option<&tauri::AppHandle>, let is_ok = res.status() == 200;
) -> Result<Option<String>, reqwest::Error> { let body = res.text().await?;
let res = reqwest::get(url).await?;
let is_ok = res.status() == 200;
let body = res.text().await?;
if is_ok { if is_ok {
Ok(Some(body)) Ok(Some(body))
} else { } else {
info!("chatgpt_http_error: {}", body); info!("chatgpt_http_error: {}", body);
if let Some(v) = app { if let Some(v) = app {
tauri::api::dialog::message(v.get_window("core").as_ref(), "ChatGPT HTTP", body); tauri::api::dialog::message(v.get_window("core").as_ref(), "ChatGPT HTTP", body);
}
Ok(None)
} }
Ok(None)
}
} }
pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>) { pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>) {
info!("run_check_update: silent={} has_msg={:?}", silent, has_msg); info!("run_check_update: silent={} has_msg={:?}", silent, has_msg);
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let result = app.updater().check().await; let result = app.updater().check().await;
let update_resp = result.unwrap(); let update_resp = result.unwrap();
if update_resp.is_update_available() { if update_resp.is_update_available() {
if silent { if silent {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
silent_install(app, update_resp).await.unwrap(); silent_install(app, update_resp).await.unwrap();
}); });
} else { } else {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
prompt_for_install(app, update_resp).await.unwrap(); prompt_for_install(app, update_resp).await.unwrap();
}); });
} }
} else if let Some(v) = has_msg { } else if let Some(v) = has_msg {
if v { if v {
tauri::api::dialog::message( tauri::api::dialog::message(
app.app_handle().get_window("core").as_ref(), app.app_handle().get_window("core").as_ref(),
"ChatGPT", "ChatGPT",
"Your ChatGPT is up to date", "Your ChatGPT is up to date",
); );
} }
} }
}); });
} }
// Copy private api in tauri/updater/mod.rs. TODO: refactor to public api // Copy private api in tauri/updater/mod.rs. TODO: refactor to public api
// Prompt a dialog asking if the user want to install the new version // Prompt a dialog asking if the user want to install the new version
// Maybe we should add an option to customize it in future versions. // Maybe we should add an option to customize it in future versions.
pub async fn prompt_for_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> { pub async fn prompt_for_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("prompt_for_install"); info!("prompt_for_install");
let windows = app.windows(); let windows = app.windows();
let parent_window = windows.values().next(); let parent_window = windows.values().next();
let package_info = app.package_info().clone(); let package_info = app.package_info().clone();
let body = update.body().unwrap(); let body = update.body().unwrap();
// todo(lemarier): We should review this and make sure we have // todo(lemarier): We should review this and make sure we have
// something more conventional. // something more conventional.
let should_install = tauri::api::dialog::blocking::ask( let should_install = tauri::api::dialog::blocking::ask(
parent_window, parent_window,
format!(r#"A new version of {} is available! "#, package_info.name), format!(r#"A new version of {} is available! "#, package_info.name),
format!( format!(
r#"{} {} is now available -- you have {}. r#"{} {} is now available -- you have {}.
Would you like to install it now? Would you like to install it now?
Release Notes: Release Notes:
{}"#, {}"#,
package_info.name, package_info.name,
update.latest_version(), update.latest_version(),
package_info.version, package_info.version,
body body
), ),
); );
if should_install {
// Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
update.download_and_install().await?;
// Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask(
parent_window,
"Ready to Restart",
"The installation was successful, do you want to restart the application now?",
);
if should_exit {
app.restart();
}
}
Ok(())
}
pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("silent_install");
let windows = app.windows();
let parent_window = windows.values().next();
if should_install {
// Launch updater download process // Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart // macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
@@ -220,33 +185,54 @@ pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) ->
// Ask user if we need to restart the application // Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask( let should_exit = tauri::api::dialog::blocking::ask(
parent_window, parent_window,
"Ready to Restart", "Ready to Restart",
"The silent installation was successful, do you want to restart the application now?", "The installation was successful, do you want to restart the application now?",
); );
if should_exit { if should_exit {
app.restart(); app.restart();
} }
}
Ok(()) Ok(())
}
pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("silent_install");
let windows = app.windows();
let parent_window = windows.values().next();
// Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
update.download_and_install().await?;
// Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask(
parent_window,
"Ready to Restart",
"The silent installation was successful, do you want to restart the application now?",
);
if should_exit {
app.restart();
}
Ok(())
} }
pub fn is_hidden(entry: &walkdir::DirEntry) -> bool { pub fn is_hidden(entry: &walkdir::DirEntry) -> bool {
entry entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
} }
pub fn vec_to_hashmap( pub fn vec_to_hashmap(
vec: impl Iterator<Item = serde_json::Value>, vec: impl Iterator<Item = serde_json::Value>,
key: &str, key: &str,
map: &mut HashMap<String, serde_json::Value>, map: &mut HashMap<String, serde_json::Value>,
) { ) {
for v in vec { for v in vec {
if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) { if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
map.insert(kval.to_string(), v); map.insert(kval.to_string(), v);
}
} }
}
} }

View File

@@ -12,7 +12,7 @@ export const awesomeColumns = () => [
title: 'URL', title: 'URL',
dataIndex: 'url', dataIndex: 'url',
key: 'url', key: 'url',
width: 120, width: 200,
}, },
// { // {
// title: 'Icon', // title: 'Icon',
@@ -33,7 +33,7 @@ export const awesomeColumns = () => [
title: 'Category', title: 'Category',
dataIndex: 'category', dataIndex: 'category',
key: 'category', key: 'category',
width: 200, width: 120,
render: (v: string) => <Tag color="geekblue">{v}</Tag> render: (v: string) => <Tag color="geekblue">{v}</Tag>
}, },
{ {