diff --git a/README-ZH_CN.md b/README-ZH_CN.md index a901f1b..f2f6e57 100644 --- a/README-ZH_CN.md +++ b/README-ZH_CN.md @@ -201,8 +201,9 @@ Mac 上无法安装,提示开发者未验证,具体可以查看下面给出 #### 预安装 -- [Rust](https://www.rust-lang.org/) -- [VS Code](https://code.visualstudio.com/) +- [Rust (必须)](https://www.rust-lang.org/) +- [Node.js (必须)](https://nodejs.org/) +- [VS Code (可选)](https://code.visualstudio.com/) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) @@ -226,6 +227,9 @@ yarn dev yarn build ``` +- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180) +- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182) + ## ❤️ 感谢 - 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改 diff --git a/README.md b/README.md index c174690..c1644e5 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt ## ✨ Features - Multi-platform: `macOS` `Linux` `Windows` -- Export ChatGPT history (PNG, PDF and Share Link) +- Export ChatGPT history (PNG, PDF and Markdown) - Automatic application upgrade notification - Common shortcut keys - System tray hover window @@ -209,8 +209,9 @@ It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website, #### PreInstall -- [Rust](https://www.rust-lang.org/) -- [VS Code](https://code.visualstudio.com/) +- [Rust (Required)](https://www.rust-lang.org/) +- [Node.js (Required)](https://nodejs.org/) +- [VS Code (Optional)](https://code.visualstudio.com/) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) @@ -234,6 +235,9 @@ yarn dev yarn build ``` +- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180) +- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182) + ## ❤️ Thanks - The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications. diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index e8e588c..b40ac36 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -1,5 +1,14 @@ # UPDATE LOG +## v0.9.0 + +fix: +- export button does not work + +feat: +- add an export markdown button +- `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews. + ## v0.8.1 fix: diff --git a/package.json b/package.json index dd9e962..6f8ca26 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^8.0.4", "react-router-dom": "^6.4.5", + "react-syntax-highlighter": "^15.5.0", "uuid": "^9.0.0" }, "devDependencies": { @@ -50,6 +52,7 @@ "@types/node": "^18.7.10", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", + "@types/react-syntax-highlighter": "^15.5.6", "@types/uuid": "^9.0.0", "@vitejs/plugin-react": "^3.0.0", "sass": "^1.56.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a25678f..a7818a9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,18 +16,18 @@ tauri-build = {version = "1.2.1", features = [] } [dependencies] anyhow = "1.0.66" serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] } -tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] } log = "0.4.17" csv = "1.1.6" thiserror = "1.0.38" walkdir = "2.3.2" regex = "1.7.0" -tokio = { version = "1.23.0", features = ["macros"] } reqwest = "0.11.13" -wry = "0.23.4" +wry = "0.24.1" dark-light = "1.0.0" +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.23.0", features = ["macros"] } +tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] } +tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] } [dependencies.tauri-plugin-log] git = "https://github.com/lencx/tauri-plugin-log" branch = "dev" @@ -36,6 +36,8 @@ features = ["colored"] git = "https://github.com/lencx/tauri-plugin-autostart" branch = "dev" +# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] } + [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 3d656fe..b14aebd 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -1,10 +1,11 @@ use crate::{ - app::window, + app::{fs_extra, window}, conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL}, - utils, + utils::{self, chat_root, create_file}, }; use log::info; -use std::{collections::HashMap, fs, path::PathBuf}; +use regex::Regex; +use std::{collections::HashMap, fs, path::PathBuf, vec}; use tauri::{api, command, AppHandle, Manager, Theme}; use walkdir::WalkDir; @@ -35,11 +36,20 @@ pub fn fullscreen(app: AppHandle) { #[command] pub fn download(_app: AppHandle, name: String, blob: Vec) { - let path = api::path::download_dir().unwrap().join(name); + let path = chat_root().join(PathBuf::from(name)); + create_file(&path).unwrap(); fs::write(&path, blob).unwrap(); utils::open_file(path); } +#[command] +pub fn save_file(_app: AppHandle, name: String, content: String) { + let path = chat_root().join(PathBuf::from(name)); + create_file(&path).unwrap(); + fs::write(&path, content).unwrap(); + utils::open_file(path); +} + #[command] pub fn open_link(app: AppHandle, url: String) { api::shell::open(&app.shell_scope(), url, None).unwrap(); @@ -167,6 +177,93 @@ pub fn cmd_list() -> Vec { list } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct FileMetadata { + pub name: String, + pub ext: String, + pub created: u64, + pub id: String, +} + +#[tauri::command] +pub fn get_download_list(pathname: &str) -> (Vec, PathBuf) { + info!("get_download_list: {}", pathname); + let download_path = chat_root().join(PathBuf::from(pathname)); + let content = fs::read_to_string(&download_path).unwrap_or_else(|err| { + info!("download_list_error: {}", err); + fs::write(&download_path, "[]").unwrap(); + "[]".to_string() + }); + let list = serde_json::from_str::>(&content).unwrap_or_else(|err| { + info!("download_list_parse_error: {}", err); + vec![] + }); + + (list, download_path) +} + +#[command] +pub fn download_list(pathname: &str, dir: &str, filename: Option, id: Option) { + info!("download_list: {}", pathname); + let data = get_download_list(pathname); + let mut list = vec![]; + let mut idmap = HashMap::new(); + utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap); + + for entry in WalkDir::new(utils::chat_root().join(dir)) + .into_iter() + .filter_entry(|e| !utils::is_hidden(e)) + .filter_map(|e| e.ok()) + { + let metadata = entry.metadata().unwrap(); + if metadata.is_file() { + let file_path = entry.path().display().to_string(); + let re = Regex::new(r"(?P[\d\w]+).(?P\w+)$").unwrap(); + let caps = re.captures(&file_path).unwrap(); + let fid = &caps["id"]; + let fext = &caps["ext"]; + + let mut file_data = FileMetadata { + name: fid.to_string(), + id: fid.to_string(), + ext: fext.to_string(), + created: fs_extra::system_time_to_ms(metadata.created()), + }; + + if idmap.get(fid).is_some() { + let name = idmap.get(fid).unwrap().get("name").unwrap().clone(); + match name { + serde_json::Value::String(v) => { + file_data.name = v.clone(); + v + } + _ => "".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()); + } + } + + // dbg!(&list); + list.sort_by(|a, b| { + let a1 = a.get("created").unwrap().as_u64().unwrap(); + let b1 = b.get("created").unwrap().as_u64().unwrap(); + a1.cmp(&b1).reverse() + }); + + fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap(); +} + #[command] pub async fn sync_prompts(app: AppHandle, time: u64) -> Option> { let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app)) diff --git a/src-tauri/src/app/fs_extra.rs b/src-tauri/src/app/fs_extra.rs index 72453fb..9ed9cf0 100644 --- a/src-tauri/src/app/fs_extra.rs +++ b/src-tauri/src/app/fs_extra.rs @@ -60,7 +60,7 @@ struct UnixMetadata { #[serde(rename_all = "camelCase")] pub struct Metadata { accessed_at_ms: u64, - created_at_ms: u64, + pub created_at_ms: u64, modified_at_ms: u64, is_dir: bool, is_file: bool, @@ -74,7 +74,7 @@ pub struct Metadata { file_attributes: u32, } -fn system_time_to_ms(time: std::io::Result) -> u64 { +pub fn system_time_to_ms(time: std::io::Result) -> u64 { time.map(|t| { let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); duration_since_epoch.as_millis() as u64 diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index 0f8bf18..1a0faf7 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -41,10 +41,6 @@ pub fn init() -> Menu { stay_on_top }; - #[cfg(target_os = "macos")] - 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 theme_system = CustomMenuItem::new("theme_system".to_string(), "System"); @@ -62,6 +58,9 @@ pub fn init() -> Menu { popup_search }; + #[cfg(target_os = "macos")] + let titlebar = + CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); #[cfg(target_os = "macos")] let titlebar_menu = if chat_conf.titlebar { titlebar.selected() @@ -69,6 +68,13 @@ pub fn init() -> Menu { titlebar }; + let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray"); + let system_tray_menu = if chat_conf.tray { + system_tray.selected() + } else { + system_tray + }; + let preferences_menu = Submenu::new( "Preferences", Menu::with_items([ @@ -81,6 +87,7 @@ pub fn init() -> Menu { titlebar_menu.into(), #[cfg(target_os = "macos")] CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(), + system_tray_menu.into(), CustomMenuItem::new("inject_script".to_string(), "Inject Script") .accelerator("CmdOrCtrl+J") .into(), @@ -141,6 +148,7 @@ pub fn init() -> Menu { CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT") .accelerator("CmdOrCtrl+Shift+A") .into(), + CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(), ]), ); @@ -242,6 +250,7 @@ pub fn menu_handler(event: WindowMenuEvent) { "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; @@ -281,6 +290,11 @@ pub fn menu_handler(event: WindowMenuEvent) { .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", diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index ca3c0c4..b470e0c 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -61,14 +61,18 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .always_on_top(chat_conf.stay_on_top) .title_bar_style(ChatConfJson::titlebar()) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .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!("../assets/core.js")) - .initialization_script(include_str!("../assets/popup.core.js")) - .initialization_script(include_str!("../assets/export.js")) - .initialization_script(include_str!("../assets/cmd.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(); @@ -82,14 +86,18 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .theme(theme) .always_on_top(chat_conf.stay_on_top) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .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!("../assets/core.js")) - .initialization_script(include_str!("../assets/popup.core.js")) - .initialization_script(include_str!("../assets/export.js")) - .initialization_script(include_str!("../assets/cmd.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(); diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index 58ca282..81ee7f6 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -18,11 +18,12 @@ pub fn tray_window(handle: &tauri::AppHandle) { .always_on_top(true) .theme(theme) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js")) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/cmd.js")) - .initialization_script(include_str!("../assets/popup.core.js")) + .initialization_script(include_str!("../scripts/core.js")) + .initialization_script(include_str!("../scripts/cmd.js")) + .initialization_script(include_str!("../scripts/popup.core.js")) .user_agent(&chat_conf.ua_tray) .build() .unwrap() @@ -73,9 +74,10 @@ pub fn dalle2_window( .inner_size(800.0, 600.0) .always_on_top(false) .theme(theme) - .initialization_script(include_str!("../assets/core.js")) + .initialization_script(include_str!("../vendors/jq.js")) + .initialization_script(include_str!("../scripts/core.js")) .initialization_script(&query) - .initialization_script(include_str!("../assets/dalle2.js")) + .initialization_script(include_str!("../scripts/dalle2.js")) .build() .unwrap(); }); diff --git a/src-tauri/src/conf.rs b/src-tauri/src/conf.rs index e2ba14a..559933b 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -14,12 +14,14 @@ use tauri::TitleBarStyle; 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 AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md"; +pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx"; pub const GITHUB_PROMPTS_CSV_URL: &str = "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"; pub const DEFAULT_CHAT_CONF: &str = r#"{ "stay_on_top": false, "auto_update": "Prompt", "theme": "Light", + "tray": true, "titlebar": true, "popup_search": true, "global_shortcut": "", @@ -33,6 +35,7 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ "stay_on_top": false, "auto_update": "Prompt", "theme": "Light", + "tray": true, "titlebar": false, "popup_search": true, "global_shortcut": "", @@ -53,6 +56,7 @@ pub struct ChatConfJson { pub theme: String, // auto update policy, Prompt/Silent/Disable pub auto_update: String, + pub tray: bool, pub popup_search: bool, pub stay_on_top: bool, pub default_origin: String, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5d9db81..b41eee9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,7 +30,12 @@ async fn main() { trace: Color::Cyan, }; - tauri::Builder::default() + cmd::download_list("chat.download.json", "download", None, None); + cmd::download_list("chat.notes.json", "notes", None, None); + + let chat_conf = ChatConfJson::get_chat_conf(); + + let mut builder = tauri::Builder::default() // https://github.com/tauri-apps/tauri/pull/2736 .plugin( LoggerBuilder::new() @@ -45,10 +50,16 @@ async fn main() { ]) .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, @@ -65,16 +76,18 @@ async fn main() { cmd::window_reload, cmd::dalle2_window, cmd::cmd_list, + cmd::download_list, + cmd::get_download_list, fs_extra::metadata, ]) .setup(setup::init) - .plugin(tauri_plugin_positioner::init()) - .plugin(tauri_plugin_autostart::init( - MacosLauncher::LaunchAgent, - None, - )) - .menu(menu::init()) - .system_tray(menu::tray_menu()) + .menu(menu::init()); + + if chat_conf.tray { + builder = builder.system_tray(menu::tray_menu()); + } + + builder .on_menu_event(menu::menu_handler) .on_system_tray_event(menu::tray_handler) .on_window_event(|event| { diff --git a/src-tauri/src/assets/cmd.js b/src-tauri/src/scripts/cmd.js similarity index 97% rename from src-tauri/src/assets/cmd.js rename to src-tauri/src/scripts/cmd.js index 4588e5a..b390d0a 100644 --- a/src-tauri/src/assets/cmd.js +++ b/src-tauri/src/scripts/cmd.js @@ -1,6 +1,6 @@ // *** Core Script - CMD *** -function init() { +$(function() { const styleDom = document.createElement('style'); styleDom.innerHTML = `form { position: relative; @@ -71,9 +71,9 @@ function init() { width: 20px; height: 20px; } - .chatappico.pdf { - width: 24px; - height: 24px; + .chatappico.pdf, .chatappico.md { + width: 22px; + height: 22px; } @media screen and (max-width: 767px) { #download-png-button, #download-pdf-button, #download-html-button { @@ -92,7 +92,7 @@ function init() { clearInterval(window.formInterval); cmdTip(); }, 200); -} +}); async function cmdTip() { const chatModelJson = await invoke('get_chat_model_cmd') || {}; @@ -269,12 +269,3 @@ async function cmdTip() { }); }, 200); } - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} \ No newline at end of file diff --git a/src-tauri/src/assets/core.js b/src-tauri/src/scripts/core.js similarity index 93% rename from src-tauri/src/assets/core.js rename to src-tauri/src/scripts/core.js index 8cadca3..d69f37e 100644 --- a/src-tauri/src/assets/core.js +++ b/src-tauri/src/scripts/core.js @@ -40,7 +40,7 @@ window.uid = uid; window.invoke = invoke; window.transformCallback = transformCallback; -async function init() { +$(async function () { if (__TAURI_METADATA__.__currentWindow.label === 'tray') { document.getElementsByTagName('html')[0].style['font-size'] = '70%'; } @@ -91,13 +91,4 @@ async function init() { window.__sync_prompts = async function() { await invoke('sync_prompts', { time: Date.now() }); } -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src-tauri/src/assets/dalle2.js b/src-tauri/src/scripts/dalle2.js similarity index 82% rename from src-tauri/src/assets/dalle2.js rename to src-tauri/src/scripts/dalle2.js index 0bc382f..ca11cd4 100644 --- a/src-tauri/src/assets/dalle2.js +++ b/src-tauri/src/scripts/dalle2.js @@ -1,6 +1,6 @@ // *** Core Script - DALL·E 2 *** -async function init() { +$(function () { document.addEventListener("click", (e) => { const origin = e.target.closest("a"); if (!origin || !origin.target) return; @@ -28,13 +28,4 @@ async function init() { searchInput.value = query; } }, 200) -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} +}) \ No newline at end of file diff --git a/src-tauri/src/assets/export.js b/src-tauri/src/scripts/export.js similarity index 77% rename from src-tauri/src/assets/export.js rename to src-tauri/src/scripts/export.js index 19a08b0..a5539d5 100644 --- a/src-tauri/src/assets/export.js +++ b/src-tauri/src/scripts/export.js @@ -1,8 +1,8 @@ // *** Core Script - Export *** -// @ref: https://github.com/liady/ChatGPT-pdf const buttonOuterHTMLFallback = ``; -async function init() { + +$(async function () { if (window.innerWidth < 767) return; const chatConf = await invoke('get_chat_conf') || {}; if (window.buttonsInterval) { @@ -25,7 +25,7 @@ async function init() { removeButtons(); } }, 1000); -} +}) const Format = { PNG: "png", @@ -49,14 +49,19 @@ function shouldAddButtons(actionsArea) { const buttons = actionsArea.querySelectorAll("button"); const hasTryAgainButton = Array.from(buttons).some((button) => { - return !button.id?.includes("download"); + return !/download-/.test(button.id); }); - // fix: https://github.com/lencx/ChatGPT/issues/189 - if (buttons.length === 1) { + const stopBtn = buttons?.[0]?.innerText; + + if (/Stop generating/ig.test(stopBtn)) { return false; } + if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) { + return true; + } + if (hasTryAgainButton && buttons.length === 1) { return true; } @@ -80,51 +85,58 @@ function shouldAddButtons(actionsArea) { function removeButtons() { const downloadButton = document.getElementById("download-png-button"); const downloadPdfButton = document.getElementById("download-pdf-button"); - const downloadHtmlButton = document.getElementById("download-html-button"); + const downloadMdButton = document.getElementById("download-markdown-button"); if (downloadButton) { downloadButton.remove(); } if (downloadPdfButton) { downloadPdfButton.remove(); } - if (downloadHtmlButton) { - downloadHtmlButton.remove(); + if (downloadPdfButton) { + downloadMdButton.remove(); } } function addActionsButtons(actionsArea, TryAgainButton) { const downloadButton = TryAgainButton.cloneNode(true); + // Export markdown + const exportMd = TryAgainButton.cloneNode(true); + exportMd.id = "download-markdown-button"; + downloadButton.setAttribute("share-ext", "true"); + exportMd.title = "Export Markdown"; + exportMd.innerHTML = setIcon('md'); + exportMd.onclick = () => { + exportMarkdown(); + }; + actionsArea.appendChild(exportMd); + + // Generate PNG downloadButton.id = "download-png-button"; downloadButton.setAttribute("share-ext", "true"); - // downloadButton.innerText = "Generate PNG"; downloadButton.title = "Generate PNG"; downloadButton.innerHTML = setIcon('png'); downloadButton.onclick = () => { downloadThread(); }; actionsArea.appendChild(downloadButton); + + // Generate PDF const downloadPdfButton = TryAgainButton.cloneNode(true); downloadPdfButton.id = "download-pdf-button"; downloadButton.setAttribute("share-ext", "true"); - // downloadPdfButton.innerText = "Download PDF"; downloadPdfButton.title = "Download PDF"; downloadPdfButton.innerHTML = setIcon('pdf'); downloadPdfButton.onclick = () => { downloadThread({ as: Format.PDF }); }; actionsArea.appendChild(downloadPdfButton); +} - // fix: https://github.com/lencx/ChatGPT/issues/126 - // const exportHtml = TryAgainButton.cloneNode(true); - // exportHtml.id = "download-html-button"; - // downloadButton.setAttribute("share-ext", "true"); - // // exportHtml.innerText = "Share Link"; - // exportHtml.title = "Share Link"; - // exportHtml.innerHTML = setIcon('link'); - // exportHtml.onclick = () => { - // sendRequest(); - // }; - // actionsArea.appendChild(exportHtml); +async function exportMarkdown() { + const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); + const { id, filename } = getName(); + await invoke('save_file', { name: `notes/${id}.md`, content: data }); + await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' }); } function downloadThread({ as = Format.PNG } = {}) { @@ -150,16 +162,18 @@ function downloadThread({ as = Format.PNG } = {}) { }); } -function handleImg(imgData) { +async function handleImg(imgData) { const binaryData = atob(imgData.split("base64,")[1]); const data = []; for (let i = 0; i < binaryData.length; i++) { data.push(binaryData.charCodeAt(i)); } - invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: Array.from(new Uint8Array(data)) }); + const { pathname, id, filename } = getName(); + await invoke('download', { name: `download/img/${id}.png`, blob: data }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } -function handlePdf(imgData, canvas, pixelRatio) { +async function handlePdf(imgData, canvas, pixelRatio) { const { jsPDF } = window.jspdf; const orientation = canvas.width > canvas.height ? "l" : "p"; var pdf = new jsPDF(orientation, "pt", [ @@ -169,9 +183,16 @@ function handlePdf(imgData, canvas, pixelRatio) { var pdfWidth = pdf.internal.pageSize.getWidth(); var pdfHeight = pdf.internal.pageSize.getHeight(); pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); - + const { pathname, id, filename } = getName(); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); - invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); +} + +function getName() { + const id = uid().toString(36); + const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || ''; + return { filename: name ? name : id, id, pathname: 'chat.download.json' }; } class Elements { @@ -187,9 +208,7 @@ class Elements { // fix: old chat https://github.com/lencx/ChatGPT/issues/185 if (!this.thread) { - this.thread = document.querySelector( - "main .overflow-y-auto" - ); + this.thread = document.querySelector("main .overflow-y-auto"); } // h-full overflow-y-auto @@ -245,67 +264,11 @@ class Elements { } } -function selectElementByClassPrefix(classPrefix) { - const element = document.querySelector(`[class^='${classPrefix}']`); - return element; -} - -async function sendRequest() { - const data = getData(); - const uploadUrlResponse = await fetch( - "https://chatgpt-static.s3.amazonaws.com/url.txt" - ); - const uploadUrl = await uploadUrlResponse.text(); - fetch(uploadUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - .then((response) => response.json()) - .then((data) => { - invoke('open_link', { url: data.url }); - }); -} - -function getData() { - const globalCss = getCssFromSheet( - document.querySelector("link[rel=stylesheet]").sheet - ); - const localCss = - getCssFromSheet( - document.querySelector(`style[data-styled][data-styled-version]`).sheet - ) || "body{}"; - const data = { - main: document.querySelector("main").outerHTML, - // css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`, - globalCss, - localCss, - }; - return data; -} - -function getCssFromSheet(sheet) { - return Array.from(sheet.cssRules) - .map((rule) => rule.cssText) - .join(""); -} - -// run init -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} - function setIcon(type) { return { - link: ``, + // link: ``, png: ``, - pdf: `` + pdf: ``, + md: `` }[type]; } diff --git a/src-tauri/src/scripts/markdown.export.js b/src-tauri/src/scripts/markdown.export.js new file mode 100644 index 0000000..651eebc --- /dev/null +++ b/src-tauri/src/scripts/markdown.export.js @@ -0,0 +1,36 @@ +var ExportMD = (function () { + if (!TurndownService || !turndownPluginGfm) return; + const hljsREG = /^.*(hljs).*(language-[a-z0-9]+).*$/i; + const gfm = turndownPluginGfm.gfm + const turndownService = new TurndownService() + .use(gfm) + .addRule('code', { + filter: (node) => { + if (node.nodeName === 'CODE' && hljsREG.test(node.classList.value)) { + return 'code'; + } + }, + replacement: (content, node) => { + const classStr = node.getAttribute('class'); + if (hljsREG.test(classStr)) { + const lang = classStr.match(/.*language-(\w+)/)[1]; + if (lang) { + return `\`\`\`${lang}\n${content}\n\`\`\``; + } + return `\`\`\`\n${content}\n\`\`\``; + } + } + }) + .addRule('ignore', { + filter: ['button', 'img'], + replacement: () => '', + }) + .addRule('table', { + filter: 'table', + replacement: function(content, node) { + return `\`\`\`${content}\n\`\`\``; + }, + }); + + return turndownService; +}({})); diff --git a/src-tauri/src/assets/popup.core.js b/src-tauri/src/scripts/popup.core.js similarity index 91% rename from src-tauri/src/assets/popup.core.js rename to src-tauri/src/scripts/popup.core.js index 3eb7b02..953420a 100644 --- a/src-tauri/src/assets/popup.core.js +++ b/src-tauri/src/scripts/popup.core.js @@ -1,6 +1,6 @@ // *** Core Script - DALL·E 2 Core *** -async function init() { +$(async function () { const chatConf = await invoke('get_chat_conf') || {}; if (!chatConf.popup_search) return; if (!window.FloatingUIDOM) return; @@ -71,14 +71,4 @@ async function init() { }); } }); - -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} \ No newline at end of file +}) \ No newline at end of file diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 608936f..a5625ec 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -230,3 +230,23 @@ pub async fn silent_install(app: AppHandle, update: UpdateResponse) -> Ok(()) } + +pub fn is_hidden(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with('.')) + .unwrap_or(false) +} + +pub fn vec_to_hashmap( + vec: impl Iterator, + key: &str, + map: &mut HashMap, +) { + for v in vec { + if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) { + map.insert(kval.to_string(), v); + } + } +} diff --git a/src-tauri/src/vendors/jq.js b/src-tauri/src/vendors/jq.js new file mode 100644 index 0000000..b04d0fc --- /dev/null +++ b/src-tauri/src/vendors/jq.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.3 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},S=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||S).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.3",E=function(e,t){return new E.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,S)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=E)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{if(d.cssSupportsSelector&&!CSS.supports("selector(:is("+c+"))"))throw new Error;return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===E&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[E]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,S=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssSupportsSelector=ce(function(){return CSS.supports("selector(*)")&&C.querySelectorAll(":is(:jqfake)")&&!CSS.supports("selector(:is(*,:jqfake))")}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=E,!C.getElementsByName||!C.getElementsByName(E).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&S)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+E+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+E+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssSupportsSelector||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&S&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:S,!0)),N.test(r[1])&&E.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=S.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,D=E(S);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=S.createDocumentFragment().appendChild(S.createElement("div")),(fe=S.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),S.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||E.expando+"_"+Ct.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||E.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?E(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=S.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=S.implementation.createHTMLDocument("")).createElement("base")).href=S.location.href,t.head.appendChild(r)):t=S),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0 "))+"\n\n"}},t.list={filter:["ul","ol"],replacement:function(e,n){var t=n.parentNode;return"LI"===t.nodeName&&t.lastElementChild===n?"\n"+e:"\n\n"+e+"\n\n"}},t.listItem={filter:"li",replacement:function(e,n,t){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var r=t.bulletListMarker+" ",i=n.parentNode;return"OL"===i.nodeName&&(t=i.getAttribute("start"),i=Array.prototype.indexOf.call(i.children,n),r=(t?Number(t)+i:i+1)+". "),r+e+(n.nextSibling&&!/\n$/.test(e)?"\n":"")}},t.indentedCodeBlock={filter:function(e,n){return"indented"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){return"\n\n "+n.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}},t.fencedCodeBlock={filter:function(e,n){return"fenced"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){for(var r,i=((n.firstChild.getAttribute("class")||"").match(/language-(\S+)/)||[null,""])[1],o=n.firstChild.textContent,t=t.fence.charAt(0),a=3,l=new RegExp("^"+t+"{3,}","gm");r=l.exec(o);)r[0].length>=a&&(a=r[0].length+1);t=u(t,a);return"\n\n"+t+i+"\n"+o.replace(/\n$/,"")+"\n"+t+"\n\n"}},t.horizontalRule={filter:"hr",replacement:function(e,n,t){return"\n\n"+t.hr+"\n\n"}},t.inlineLink={filter:function(e,n){return"inlined"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n){var t=n.getAttribute("href"),n=s(n.getAttribute("title"));return"["+e+"]("+t+(n=n&&' "'+n+'"')+")"}},t.referenceLink={filter:function(e,n){return"referenced"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n,t){var r=n.getAttribute("href"),i=(i=s(n.getAttribute("title")))&&' "'+i+'"';switch(t.linkReferenceStyle){case"collapsed":a="["+e+"][]",l="["+e+"]: "+r+i;break;case"shortcut":a="["+e+"]",l="["+e+"]: "+r+i;break;default:var o=this.references.length+1,a="["+e+"]["+o+"]",l="["+o+"]: "+r+i}return this.references.push(l),a},references:[],append:function(e){var n="";return this.references.length&&(n="\n\n"+this.references.join("\n")+"\n\n",this.references=[]),n}},t.emphasis={filter:["em","i"],replacement:function(e,n,t){return e.trim()?t.emDelimiter+e+t.emDelimiter:""}},t.strong={filter:["strong","b"],replacement:function(e,n,t){return e.trim()?t.strongDelimiter+e+t.strongDelimiter:""}},t.code={filter:function(e){var n=e.previousSibling||e.nextSibling,n="PRE"===e.parentNode.nodeName&&!n;return"CODE"===e.nodeName&&!n},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");for(var n=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"",t="`",r=e.match(/`+/gm)||[];-1!==r.indexOf(t);)t+="`";return t+n+e+n+t}},t.image={filter:"img",replacement:function(e,n){var t=s(n.getAttribute("alt")),r=n.getAttribute("src")||"",n=s(n.getAttribute("title"));return r?"!["+t+"]("+r+(n?' "'+n+'"':"")+")":""}},f.prototype={add:function(e,n){this.array.unshift(n)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:(n=d(this.array,e,this.options))||(n=d(this._keep,e,this.options))||(n=d(this._remove,e,this.options))?n:this.defaultRule;var n},forEach:function(e){for(var n=0;n'+e+"","text/html").getElementById("turndown-root"):e.cloneNode(!0),isBlock:o,isVoid:i,isPre:n.preformattedCode?y:null}),e}function y(e){return"PRE"===e.nodeName||"CODE"===e.nodeName}function N(e,n){var t;return e.isBlock=o(e),e.isCode="CODE"===e.nodeName||e.parentNode.isCode,e.isBlank=!i(t=e)&&!function(e){return l(e,a)}(t)&&/^\s*$/i.test(t.textContent)&&!function(e){return c(e,r)}(t)&&!function(e){return c(e,a)}(t),e.flankingWhitespace=function(e,n){if(e.isBlock||n.preformattedCode&&e.isCode)return{leading:"",trailing:""};var t=function(e){e=e.match(/^(([ \t\r\n]*)(\s*))[\s\S]*?((\s*?)([ \t\r\n]*))$/);return{leading:e[1],leadingAscii:e[2],leadingNonAscii:e[3],trailing:e[4],trailingNonAscii:e[5],trailingAscii:e[6]}}(e.textContent);t.leadingAscii&&E("left",e,n)&&(t.leading=t.leadingNonAscii);t.trailingAscii&&E("right",e,n)&&(t.trailing=t.trailingNonAscii);return{leading:t.leading,trailing:t.trailing}}(e,n),e}function E(e,n,t){var r,i,n="left"===e?(r=n.previousSibling,/ $/):(r=n.nextSibling,/^ /);return r&&(3===r.nodeType?i=n.test(r.nodeValue):t.preformattedCode&&"CODE"===r.nodeName?i=!1:1!==r.nodeType||o(r)||(i=n.test(r.textContent))),i}var T=Array.prototype.reduce,R=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function C(e){if(!(this instanceof C))return new C(e);this.options=function(e){for(var n=1;n; + actions: any; +} +export const EditRow: FC = ({ rowKey, row, actions }) => { + const [isEdit, setEdit] = useState(false); + const [val, setVal] = useState(row[rowKey]); + const handleEdit = () => { + setEdit(true); + }; + const handleChange = (e: React.ChangeEvent) => { + setVal(e.target.value) + }; + + const handleSave = () => { + setEdit(false); + row[rowKey] = val; + actions?.setRecord(row, 'rowedit') + }; + + return isEdit + ? ( + + ) + : ( +
{val}
+ ); +}; diff --git a/src/hooks/useJson.ts b/src/hooks/useJson.ts new file mode 100644 index 0000000..0bfee8b --- /dev/null +++ b/src/hooks/useJson.ts @@ -0,0 +1,23 @@ +import { useState } from 'react'; + +import { readJSON, writeJSON } from '@/utils'; +import useInit from '@/hooks/useInit'; + +export default function useJson(file: string) { + const [json, setData] = useState(); + + const refreshJson = async () => { + const data = await readJSON(file); + setData(data); + return data; + }; + + const updateJson = async (data: any) => { + await writeJSON(file, data); + await refreshJson(); + }; + + useInit(refreshJson); + + return { json, refreshJson, updateJson }; +} diff --git a/src/hooks/useTable.tsx b/src/hooks/useTable.tsx index 741ab8b..4fc2c65 100644 --- a/src/hooks/useTable.tsx +++ b/src/hooks/useTable.tsx @@ -4,14 +4,35 @@ import type { TableRowSelection } from 'antd/es/table/interface'; import { safeKey } from '@/hooks/useData'; -export default function useTableRowSelection() { +type rowSelectionOptions = { + key: 'id' | string; + rowType: 'id' | 'row' | 'all'; +} +export function useTableRowSelection(options: Partial = {}) { + const { key = 'id', rowType = 'id' } = options; const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowIDs, setSelectedRowIDs] = useState([]); + const [selectedRows, setSelectedRows] = useState[]>([]); - const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record) => { - const keys = selectedRows.map((i: any) => i[safeKey]); - setSelectedRowIDs(keys); + const onSelectChange = (newSelectedRowKeys: React.Key[], newSelectedRows: Record[]) => { + const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]); setSelectedRowKeys(newSelectedRowKeys); + if (rowType === 'id') { + setSelectedRowIDs(keys); + } + if (rowType === 'row') { + setSelectedRows(newSelectedRows); + } + if (rowType === 'all') { + setSelectedRowIDs(keys); + setSelectedRows(newSelectedRows); + } + }; + + const rowReset = () => { + setSelectedRowKeys([]); + setSelectedRowIDs([]); + setSelectedRows([]); }; const rowSelection: TableRowSelection> = { @@ -24,14 +45,14 @@ export default function useTableRowSelection() { ], }; - return { rowSelection, selectedRowIDs }; + return { rowSelection, selectedRowIDs, selectedRows, rowReset }; } export const TABLE_PAGINATION = { hideOnSinglePage: true, showSizeChanger: true, showQuickJumper: true, - defaultPageSize: 5, + defaultPageSize: 10, pageSizeOptions: [5, 10, 15, 20], showTotal: (total: number) => Total {total} items, }; \ No newline at end of file diff --git a/src/layout/index.tsx b/src/layout/index.tsx index bbc7dc9..d5397f9 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import {Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd'; +import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd'; import { SyncOutlined } from '@ant-design/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { getName, getVersion } from '@tauri-apps/api/app'; @@ -29,58 +29,61 @@ export default function ChatLayout() { await invoke('run_check_update', { silent: false, hasMsg: true }); } - return ( - - - setCollapsed(value)} - style={{ - overflow: 'auto', - height: '100vh', - position: 'fixed', - left: 0, - top: 0, - bottom: 0, - zIndex: 999, - }} - > -
-
- {appInfo.appName} - - {appInfo.appVersion} - - - - -
+ const isDark = appInfo.appTheme === "dark"; - go(i.key)} - /> - - - + + setCollapsed(value)} style={{ - overflow: 'inherit' + overflow: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + zIndex: 999, }} > - - - +
+
+ {appInfo.appName} + + {appInfo.appVersion} + + + + +
+ + go(i.key)} + /> + + + + + + + - ); }; \ No newline at end of file diff --git a/src/main.scss b/src/main.scss index afba087..a2eabaa 100644 --- a/src/main.scss +++ b/src/main.scss @@ -31,10 +31,27 @@ html, body { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +.ellipsis-line { + display: inline-block; + width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rowedit { + padding: 2px 5px; + + &:hover { + box-shadow: 0 0 2px rgba(237, 122, 60, 0.8); + border-radius: 4px; + } +} + .chat-add-btn { margin-bottom: 5px; } @@ -51,6 +68,7 @@ html, body { } } +.chat-file-path, .chat-sync-path { font-size: 12px; font-weight: 500; diff --git a/src/routes.tsx b/src/routes.tsx index d7b0819..41527cb 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,10 +1,12 @@ import { useRoutes } from 'react-router-dom'; import { - DesktopOutlined, + SettingOutlined, BulbOutlined, SyncOutlined, FileSyncOutlined, UserOutlined, + DownloadOutlined, + FormOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; @@ -13,6 +15,8 @@ import UserCustom from '@/view/model/UserCustom'; import SyncPrompts from '@/view/model/SyncPrompts'; import SyncCustom from '@/view/model/SyncCustom'; import SyncRecord from '@/view/model/SyncRecord'; +import Download from '@/view/download'; +import Notes from '@/view/notes'; export type ChatRouteMetaObject = { label: string; @@ -33,7 +37,15 @@ export const routes: Array = [ element: , meta: { label: 'General', - icon: , + icon: , + }, + }, + { + path: '/notes', + element: , + meta: { + label: 'Notes', + icon: , }, }, { @@ -51,6 +63,7 @@ export const routes: Array = [ icon: , }, }, + // --- Sync { path: 'sync-prompts', element: , @@ -72,7 +85,15 @@ export const routes: Array = [ element: , hideMenu: true, }, - ] + ], + }, + { + path: 'download', + element: , + meta: { + label: 'Download', + icon: , + }, }, ]; diff --git a/src/utils.ts b/src/utils.ts index e983604..b90a7bd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,8 @@ import dayjs from 'dayjs'; export const CHAT_MODEL_JSON = 'chat.model.json'; export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json'; +export const CHAT_DOWNLOAD_JSON = 'chat.download.json'; +export const CHAT_NOTES_JSON = 'chat.notes.json'; export const CHAT_PROMPTS_CSV = 'chat.prompts.csv'; export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv'; export const DISABLE_AUTO_COMPLETE = { diff --git a/src/view/download/config.tsx b/src/view/download/config.tsx new file mode 100644 index 0000000..274c208 --- /dev/null +++ b/src/view/download/config.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import { Tag, Space, Popconfirm } from 'antd'; +import { path, shell } from '@tauri-apps/api'; + +import { EditRow } from '@/hooks/useColumns'; + +import useInit from '@/hooks/useInit'; +import { fmtDate, chatRoot } from '@/utils'; + +const colorMap: any = { + pdf: 'blue', + png: 'orange', +} + +export const downloadColumns = () => [ + { + title: 'Name', + dataIndex: 'name', + fixed: 'left', + key: 'name', + width: 240, + render: (_: string, row: any, actions: any) => ( + + ), + }, + { + title: 'Extension', + dataIndex: 'ext', + key: 'ext', + width: 120, + render: (v: string) => {v}, + }, + { + title: 'Path', + dataIndex: 'path', + key: 'path', + width: 200, + render: (_: string, row: any) => , + }, + { + title: 'Created', + dataIndex: 'created', + key: 'created', + width: 150, + render: fmtDate, + }, + { + title: 'Action', + fixed: 'right', + width: 150, + render: (_: any, row: any, actions: any) => { + return ( + + actions.setRecord(row, 'preview')}>Preview + actions.setRecord(row, 'delete')} + okText="Yes" + cancelText="No" + > + Delete + + + ) + } + } +]; + +const RenderPath = ({ row }: any) => { + const [filePath, setFilePath] = useState(''); + useInit(async () => { + setFilePath(await getPath(row)); + }) + return shell.open(filePath)}>{filePath}; +}; + +export const getPath = async (row: any) => { + const isImg = ['png'].includes(row?.ext); + return await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id) + `.${row.ext}`; +} diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx new file mode 100644 index 0000000..5156b78 --- /dev/null +++ b/src/view/download/index.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from 'react'; +import { Table, Modal, Popconfirm, Button, message } from 'antd'; +import { invoke, path, shell, fs } from '@tauri-apps/api'; + +import useInit from '@/hooks/useInit'; +import useJson from '@/hooks/useJson'; +import useData from '@/hooks/useData'; +import useColumns from '@/hooks/useColumns'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; +import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils'; +import { downloadColumns } from './config'; + +function renderFile(buff: Uint8Array, type: string) { + const renderType = { + pdf: 'application/pdf', + png: 'image/png', + }[type]; + return URL.createObjectURL(new Blob([buff], { type: renderType })); +} + +export default function Download() { + const [downloadPath, setDownloadPath] = useState(''); + const [source, setSource] = useState(''); + const [isVisible, setVisible] = useState(false); + const { opData, opInit, opReplace, opSafeKey } = useData([]); + const { columns, ...opInfo } = useColumns(downloadColumns()); + const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' }); + const { json, refreshJson, updateJson } = useJson(CHAT_DOWNLOAD_JSON); + const selectedItems = rowSelection.selectedRowKeys || []; + + useInit(async () => { + const file = await path.join(await chatRoot(), CHAT_DOWNLOAD_JSON); + setDownloadPath(file); + }); + + useEffect(() => { + if (!json || json.length <= 0) return; + opInit(json); + }, [json?.length]); + + useEffect(() => { + if (!opInfo.opType) return; + (async () => { + const record = opInfo?.opRecord; + const isImg = ['png'].includes(record?.ext); + const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`); + if (opInfo.opType === 'preview') { + const data = await fs.readBinaryFile(file); + const sourceData = renderFile(data, record?.ext); + setSource(sourceData); + setVisible(true); + return; + } + if (opInfo.opType === 'delete') { + await fs.removeFile(file); + await handleRefresh(); + } + if (opInfo.opType === 'rowedit') { + const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord); + await updateJson(data); + message.success('Name has been changed!'); + } + opInfo.resetRecord(); + })() + }, [opInfo.opType]) + + const handleDelete = async () => { + if (opData?.length === selectedRows.length) { + const downloadDir = await path.join(await chatRoot(), 'download'); + await fs.removeDir(downloadDir, { recursive: true }); + await handleRefresh(); + rowReset(); + message.success('All files have been cleared!'); + return; + } + + const rows = selectedRows.map(async (i) => { + const isImg = ['png'].includes(i?.ext); + const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : i?.ext, `${i?.id}.${i?.ext}`); + await fs.removeFile(file); + return file; + }) + Promise.all(rows).then(async () => { + await handleRefresh(); + message.success('All files selected are cleared!'); + }); + }; + + const handleRefresh = async () => { + await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' }); + const data = await refreshJson(); + opInit(data); + }; + + const handleCancel = () => { + setVisible(false); + opInfo.resetRecord(); + }; + + return ( +
+
+
+ {selectedItems.length > 0 && ( + <> + + + + Selected {selectedItems.length} items + + )} +
+
+ + + {opInfo?.opRecord?.name || ''}} + onCancel={handleCancel} + footer={false} + destroyOnClose + > + + + + ) +} \ No newline at end of file diff --git a/src/view/model/SyncPrompts/config.tsx b/src/view/model/SyncPrompts/config.tsx index 32fbf6a..b8b4ade 100644 --- a/src/view/model/SyncPrompts/config.tsx +++ b/src/view/model/SyncPrompts/config.tsx @@ -1,4 +1,4 @@ -import { Switch, Tag, Tooltip } from 'antd'; +import { Table, Switch, Tag } from 'antd'; import { genCmd } from '@/utils'; @@ -35,13 +35,14 @@ export const syncColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', // width: 300, render: (v: string) => ( - {v} + {v} ), }, ]; diff --git a/src/view/model/SyncPrompts/index.tsx b/src/view/model/SyncPrompts/index.tsx index d5a359e..7db4d2e 100644 --- a/src/view/model/SyncPrompts/index.tsx +++ b/src/view/model/SyncPrompts/index.tsx @@ -6,7 +6,7 @@ import useInit from '@/hooks/useInit'; import useData from '@/hooks/useData'; import useColumns from '@/hooks/useColumns'; import useChatModel, { useCacheModel } from '@/hooks/useChatModel'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { fmtDate, chatRoot } from '@/utils'; import { syncColumns } from './config'; import './index.scss'; @@ -14,7 +14,7 @@ import './index.scss'; const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv'; export default function SyncPrompts() { - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const [jsonPath, setJsonPath] = useState(''); const { modelJson, modelSet } = useChatModel('sync_prompts'); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); @@ -93,6 +93,7 @@ export default function SyncPrompts() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> ) diff --git a/src/view/model/SyncRecord/config.tsx b/src/view/model/SyncRecord/config.tsx index ebd2609..71321ca 100644 --- a/src/view/model/SyncRecord/config.tsx +++ b/src/view/model/SyncRecord/config.tsx @@ -1,4 +1,4 @@ -import { Switch, Tag, Tooltip } from 'antd'; +import { Switch, Tag, Table } from 'antd'; import { genCmd } from '@/utils'; @@ -37,13 +37,14 @@ export const syncColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', // width: 300, render: (v: string) => ( - {v} + {v} ), }, ]; diff --git a/src/view/model/SyncRecord/index.tsx b/src/view/model/SyncRecord/index.tsx index f3e64dc..9f22759 100644 --- a/src/view/model/SyncRecord/index.tsx +++ b/src/view/model/SyncRecord/index.tsx @@ -7,7 +7,7 @@ import { shell, path } from '@tauri-apps/api'; import useColumns from '@/hooks/useColumns'; import useData from '@/hooks/useData'; import { useCacheModel } from '@/hooks/useChatModel'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { fmtDate, chatRoot } from '@/utils'; import { getPath } from '@/view/model/SyncCustom/config'; import { syncColumns } from './config'; @@ -19,7 +19,7 @@ export default function SyncRecord() { const [jsonPath, setJsonPath] = useState(''); const state = location?.state; - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]); const { columns, ...opInfo } = useColumns(syncColumns()); @@ -79,6 +79,7 @@ export default function SyncRecord() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> ) diff --git a/src/view/model/UserCustom/config.tsx b/src/view/model/UserCustom/config.tsx index c023079..0450043 100644 --- a/src/view/model/UserCustom/config.tsx +++ b/src/view/model/UserCustom/config.tsx @@ -1,4 +1,4 @@ -import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd'; +import { Tag, Switch, Space, Popconfirm, Table } from 'antd'; export const modelColumns = () => [ { @@ -33,13 +33,14 @@ export const modelColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', width: 300, render: (v: string) => ( - {v} + {v} ), }, { diff --git a/src/view/model/UserCustom/index.tsx b/src/view/model/UserCustom/index.tsx index ed8bbad..985fe41 100644 --- a/src/view/model/UserCustom/index.tsx +++ b/src/view/model/UserCustom/index.tsx @@ -6,13 +6,13 @@ import useInit from '@/hooks/useInit'; import useData from '@/hooks/useData'; import useChatModel, { useCacheModel } from '@/hooks/useChatModel'; import useColumns from '@/hooks/useColumns'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { chatRoot, fmtDate } from '@/utils'; import { modelColumns } from './config'; import UserCustomForm from './Form'; export default function LanguageModel() { - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const [isVisible, setVisible] = useState(false); const [jsonPath, setJsonPath] = useState(''); const { modelJson, modelSet } = useChatModel('user_custom'); @@ -123,6 +123,7 @@ export default function LanguageModel() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> [ + { + title: 'Name', + dataIndex: 'name', + fixed: 'left', + key: 'name', + width: 240, + render: (_: string, row: any, actions: any) => ( + + ), + }, + { + title: 'Path', + dataIndex: 'path', + key: 'path', + width: 200, + render: (_: string, row: any) => , + }, + { + title: 'Created', + dataIndex: 'created', + key: 'created', + width: 150, + render: fmtDate, + }, + { + title: 'Action', + fixed: 'right', + width: 160, + render: (_: any, row: any, actions: any) => { + return ( + + actions.setRecord(row, 'preview')}>Preview + actions.setRecord(row, 'edit')}>Edit + actions.setRecord(row, 'delete')} + okText="Yes" + cancelText="No" + > + Delete + + + ) + } + } +]; + +const RenderPath = ({ row }: any) => { + const [filePath, setFilePath] = useState(''); + useInit(async () => { + setFilePath(await getPath(row)); + }) + return shell.open(filePath)}>{filePath}; +}; + +export const getPath = async (row: any) => { + const isImg = ['png'].includes(row?.ext); + return await path.join(await chatRoot(), 'notes', row.id) + `.${row.ext}`; +} diff --git a/src/view/notes/index.tsx b/src/view/notes/index.tsx new file mode 100644 index 0000000..b93bd35 --- /dev/null +++ b/src/view/notes/index.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react'; +import { Table, Modal, Popconfirm, Button, message } from 'antd'; +import { invoke, path, shell, fs } from '@tauri-apps/api'; +import ReactMarkdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import useInit from '@/hooks/useInit'; +import useJson from '@/hooks/useJson'; +import useData from '@/hooks/useData'; +import useColumns from '@/hooks/useColumns'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; +import { chatRoot, CHAT_NOTES_JSON } from '@/utils'; +import { notesColumns } from './config'; + +export default function Notes() { + const [notesPath, setNotesPath] = useState(''); + const [source, setSource] = useState(''); + const [isVisible, setVisible] = useState(false); + const { opData, opInit, opReplace, opSafeKey } = useData([]); + const { columns, ...opInfo } = useColumns(notesColumns()); + const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' }); + const { json, refreshJson, updateJson } = useJson(CHAT_NOTES_JSON); + const selectedItems = rowSelection.selectedRowKeys || []; + + useInit(async () => { + const file = await path.join(await chatRoot(), CHAT_NOTES_JSON); + setNotesPath(file); + }); + + useEffect(() => { + if (!json || json.length <= 0) return; + opInit(json); + }, [json?.length]); + + useEffect(() => { + if (!opInfo.opType) return; + (async () => { + const record = opInfo?.opRecord; + const file = await path.join(await chatRoot(), 'notes', `${record?.id}.${record?.ext}`); + if (opInfo.opType === 'preview') { + const data = await fs.readTextFile(file); + setSource(data); + setVisible(true); + return; + } + if (opInfo.opType === 'edit') { + alert('TODO'); + } + if (opInfo.opType === 'delete') { + await fs.removeFile(file); + await handleRefresh(); + } + if (opInfo.opType === 'rowedit') { + const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord); + await updateJson(data); + message.success('Name has been changed!'); + } + opInfo.resetRecord(); + })() + }, [opInfo.opType]) + + const handleDelete = async () => { + if (opData?.length === selectedRows.length) { + const notesDir = await path.join(await chatRoot(), 'notes'); + await fs.removeDir(notesDir, { recursive: true }); + await handleRefresh(); + rowReset(); + message.success('All files have been cleared!'); + return; + } + + const rows = selectedRows.map(async (i) => { + const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`); + await fs.removeFile(file); + return file; + }) + Promise.all(rows).then(async () => { + await handleRefresh(); + message.success('All files selected are cleared!'); + }); + }; + + const handleRefresh = async () => { + await invoke('download_list', { pathname: CHAT_NOTES_JSON, dir: 'notes' }); + const data = await refreshJson(); + opInit(data); + }; + + const handleCancel = () => { + setVisible(false); + opInfo.resetRecord(); + }; + + return ( +
+
+
+ {selectedItems.length > 0 && ( + <> + + + + Selected {selectedItems.length} items + + )} +
+
+ +
+ {opInfo?.opRecord?.name || ''}} + onCancel={handleCancel} + footer={false} + destroyOnClose + > + + ) : ( + + {children} + + ) + } + }} + /> + + + ) +} \ No newline at end of file