diff --git a/.gitattributes b/.gitattributes index a293ab2..4fabca3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -*.js linguist-vendored \ No newline at end of file +*.js linguist-vendored +*.tsx linguist-vendored +*.scss linguist-vendored \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6bf105..32d67d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,7 @@ jobs: ${{ runner.os }}-yarn- - name: Install app dependencies and build it - run: yarn + run: yarn && yarn build:fe - uses: tauri-apps/tauri-action@v0.3 env: diff --git a/.gitignore b/.gitignore index 20036f9..707eccc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,32 @@ -.DS_Store -*.lock - package-lock.json node_modules/ yarn.lock +*.lock +# rust target/ Cargo.lock + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/AWESOME.md b/AWESOME.md index bb72e9d..9d75821 100644 --- a/AWESOME.md +++ b/AWESOME.md @@ -10,6 +10,7 @@ - [ChatGPT Export and Share](https://github.com/liady/ChatGPT-pdf) - A Chrome extension for downloading your ChatGPT history to PNG, PDF or creating a sharable link - [ChatGPT for Google](https://github.com/wong2/chat-gpt-google-extension) - A browser extension to display ChatGPT response alongside Google Search results - [ChatGPT Extension](https://github.com/kazuki-sf/ChatGPT_Extension) - ChatGPT Extension is a really simple Chrome Extension (manifest v3) that you can access OpenAI's ChatGPT from anywhere on the web. +- [ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) - Chrome Extension that Integrates ChatGPT (Unofficial) into Google Search `VSCode` @@ -18,3 +19,8 @@ `Bot` - [ChatGPT Telegram Bot](https://github.com/altryne/chatGPT-telegram-bot) - This is a very early attempt at having chatGPT work within a telegram bot + +## Tools + +- [commitgpt](https://github.com/RomanHotsiy/commitgpt) - Automatically generate commit messages using ChatGPT +- [ShareGPT](https://sharegpt.com/) - ShareGPT: Share your wildest ChatGPT conversations with one click. diff --git a/README-ZH.md b/README-ZH.md index 0f8ce5b..7932a69 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -20,9 +20,9 @@ **最新版:** -- `Mac`: [ChatGPT_0.2.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/ChatGPT_0.2.1_x64.dmg) -- `Linux`: [chat-gpt_0.2.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/chat-gpt_0.2.1_amd64.deb) -- `Windows`: [ChatGPT_0.2.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/ChatGPT_0.2.1_x64_en-US.msi) +- `Mac`: [ChatGPT_v0.3.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/vv0.3.0/ChatGPT_v0.3.0_x64.dmg) +- `Linux`: [chat-gpt_v0.3.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/vv0.3.0/chat-gpt_v0.3.0_amd64.deb) +- `Windows`: [ChatGPT_v0.3.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/vv0.3.0/ChatGPT_v0.3.0_x64_en-US.msi) [其他版本...](https://github.com/lencx/ChatGPT/releases) @@ -60,9 +60,12 @@ cask "popcorn-time", args: { "no-quarantine": true } - `Theme` - `Light`, `Dark` (仅支持 macOS 和 Windows) - `Always On Top`: 窗口置顶 - `Titlebar`: 是否显示 `Titlebar`,仅 macOS 支持 - - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): 自定义 `user agent` 防止网站安全检测,默认值为空。 - `Inject Script`: 用于修改网站的用户自定义脚本 - - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): 切换网站源地址,默认为 `https://chat.openai.com`。需要注意的是镜像网站的 UI 需要和原网站一致,否则可能会导致某些功能不工作 + - `Control Center`: ChatGPT 应用的控制中心,它将为应用提供无限的可能 + - 设置 `Theme`,`Always on Top`,`Titlebar` 等 + - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): 自定义 `user agent` 防止网站安全检测,默认值为空 + - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): 切换网站源地址,默认为 `https://chat.openai.com`。需要注意的是镜像网站的 UI 需要和原网站一致,否则可能会导致某些功能不工作 + - `Go to Config`: 打开 ChatGPT 配置目录 (`path: ~/.chatgpt/*`) - `Clear Config`: 清除 ChatGPT 配置数据 (`path: ~/.chatgpt/*`), 这是危险操作,请提前备份数据 - `Restart ChatGPT`: 重启应用。如果注入脚本编辑完成,或者应用可卡死可以通过此菜单重新启动应用 - `Awesome ChatGPT`: 一个很棒的 ChatGPT 推荐列表 @@ -75,9 +78,10 @@ cask "popcorn-time", args: { "no-quarantine": true } ## 👀 预览 -install chat +install control center export tray -user agent auto update +tray login auto update + --- @@ -95,6 +99,10 @@ cask "popcorn-time", args: { "no-quarantine": true } 如果升级应用后无法打开,请尝试清除配置,它位于此目录 `~/.chatgpt/*`。 +### 主窗口已经登录,但是系统托盘窗口显示未登录 + +可通过菜单项里的 `Restart ChatGPT` 重启应用来修复这个问题(`Menu -> Preferences -> Restart ChatGPT`)。 + ### 它是否安全? 它是安全的,仅仅只是对 [OpenAI ChatGPT](https://chat.openai.com) 网站的包装,注入了一些额外功能(均在本地,未发起网络请求),如果存疑,可以检查源代码。 diff --git a/README.md b/README.md index 8f49697..a144726 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ **Latest:** -- `Mac`: [ChatGPT_0.2.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/ChatGPT_0.2.1_x64.dmg) -- `Linux`: [chat-gpt_0.2.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/chat-gpt_0.2.1_amd64.deb) -- `Windows`: [ChatGPT_0.2.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.2.1/ChatGPT_0.2.1_x64_en-US.msi) +- `Mac`: [ChatGPT_vv0.3.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/vvv0.3.0/ChatGPT_vv0.3.0_x64.dmg) +- `Linux`: [chat-gpt_vv0.3.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/vvv0.3.0/chat-gpt_vv0.3.0_amd64.deb) +- `Windows`: [ChatGPT_vv0.3.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/vvv0.3.0/ChatGPT_vv0.3.0_x64_en-US.msi) [Other version...](https://github.com/lencx/ChatGPT/releases) @@ -60,9 +60,12 @@ cask "popcorn-time", args: { "no-quarantine": true } - `Theme` - `Light`, `Dark` (Only macOS and Windows are supported). - `Always on Top`: The window is always on top of other windows. - `Titlebar`: Whether to display the titlebar, supported by macOS only. - - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Custom `user agent`, which may be required in some scenarios. The default value is the empty string. - `Inject Script`: Using scripts to modify pages. - - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Switch the site source address, the default is `https://chat.openai.com`, please make sure the mirror site UI is the same as the original address. Otherwise, some functions may not be available. + - `Control Center`: The control center of ChatGPT application, it will give unlimited imagination to the application. + - `Theme`, `Always on Top`, `Titlebar`, ... + - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Custom `user agent`, which may be required in some scenarios. The default value is the empty string. + - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Switch the site source address, the default is `https://chat.openai.com`, please make sure the mirror site UI is the same as the original address. Otherwise, some functions may not be available. + - `Go to Config`: Open the configuration file directory (`path: ~/.chatgpt/*`). - `Clear Config`: Clear the configuration file (`path: ~/.chatgpt/*`), dangerous operation, please backup the data in advance. - `Restart ChatGPT`: Restart the application, for example: the program is stuck or the injection script can take effect by restarting the application after editing. - `Awesome ChatGPT`: Recommended Related Resources. @@ -81,9 +84,9 @@ cask "popcorn-time", args: { "no-quarantine": true } ## 👀 Preview -install chat +install control center export tray -user agent auto update +tray login auto update --- @@ -95,6 +98,10 @@ cask "popcorn-time", args: { "no-quarantine": true } If you cannot open the application after the upgrade, please try to clear the configuration file, which is in the `~/.chatgpt/*` directory. +### Out of sync login status between multiple windows + +If you have already logged in in the main window, but the system tray window shows that you are not logged in, you can fix it by restarting the application (`Menu -> Preferences -> Restart ChatGPT`). + ### Is it safe? It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website, no other data transfer exists (you can check the source code). diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index f82c11c..53f128c 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -1,5 +1,18 @@ # UPDATE LOG +## v0.3.0 + +fix: can't open ChatGPT + +feat: menu enhancement +- the control center of ChatGPT application +- open the configuration file directory + +## v0.2.2 + +feat: +- menu: go to config + ## v0.2.1 feat: menu optimization diff --git a/assets/auto-update.png b/assets/auto-update.png index ebf1c9c..9cede36 100644 Binary files a/assets/auto-update.png and b/assets/auto-update.png differ diff --git a/assets/chat-ua.png b/assets/chat-ua.png deleted file mode 100644 index fa6a05b..0000000 Binary files a/assets/chat-ua.png and /dev/null differ diff --git a/assets/chat.png b/assets/chat.png deleted file mode 100644 index efdd6ab..0000000 Binary files a/assets/chat.png and /dev/null differ diff --git a/assets/control-center.png b/assets/control-center.png new file mode 100644 index 0000000..90b8d0d Binary files /dev/null and b/assets/control-center.png differ diff --git a/assets/tray-login.png b/assets/tray-login.png new file mode 100644 index 0000000..0dad35b Binary files /dev/null and b/assets/tray-login.png differ diff --git a/dist/.gitkeep b/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/index.html b/index.html new file mode 100644 index 0000000..f28c8f0 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + ChatGPT + + + +
+ + + diff --git a/package.json b/package.json index 79f4083..16b0a5b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "chatgpt", "version": "0.0.0", "scripts": { + "dev:fe": "vite", + "build:fe": "tsc && vite build", "dev": "yarn tauri dev", "build": "yarn tauri build", "updater": "tr updater", @@ -27,8 +29,25 @@ "type": "git", "url": "https://github.com/lencx/ChatGPT" }, + "dependencies": { + "@tauri-apps/api": "^1.2.0", + "antd": "^5.0.6", + "lodash": "^4.17.21", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.4.5" + }, "devDependencies": { - "@tauri-apps/cli": "^1.2.1", - "@tauri-release/cli": "^0.2.3" + "@tauri-apps/cli": "^1.2.2", + "@tauri-release/cli": "^0.2.3", + "@types/lodash": "^4.14.191", + "@types/node": "^18.7.10", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^3.0.0", + "sass": "^1.56.2", + "typescript": "^4.9.4", + "vite": "^4.0.0", + "vite-tsconfig-paths": "^4.0.2" } } diff --git a/scripts/download.js b/scripts/download.js index ae49d94..41e3347 100644 --- a/scripts/download.js +++ b/scripts/download.js @@ -2,8 +2,8 @@ const fs = require('fs'); const argv = process.argv.slice(2); -async function init() { - const content = fs.readFileSync('README.md', 'utf8').split('\n'); +async function rewrite(filename) { + const content = fs.readFileSync(filename, 'utf8').split('\n'); const startRe = //; const endRe = //; @@ -20,7 +20,12 @@ async function init() { } } - fs.writeFileSync('README.md', content.join('\n'), 'utf8'); + fs.writeFileSync(filename, content.join('\n'), 'utf8'); +} + +async function init() { + rewrite('README.md'); + rewrite('README-ZH.md'); } init().catch(console.error); \ No newline at end of file diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 4c883c7..b407979 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -35,9 +35,8 @@ pub fn get_chat_conf() -> ChatConfJson { } #[command] -pub fn form_confirm(app: AppHandle, data: serde_json::Value) { - ChatConfJson::amend(&serde_json::json!(data)).unwrap(); - tauri::api::process::restart(&app.env()); +pub fn form_confirm(_app: AppHandle, data: serde_json::Value) { + ChatConfJson::amend(&serde_json::json!(data), None).unwrap(); } #[command] diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index 9534089..cb769ad 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -1,5 +1,4 @@ use crate::{ - app::window, conf::{self, ChatConfJson}, utils, }; @@ -26,7 +25,7 @@ pub fn init(context: &Context) -> Menu { .add_native_item(MenuItem::Quit), ); - let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always On Top") + let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always on Top") .accelerator("CmdOrCtrl+T"); let titlebar = CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); @@ -67,27 +66,25 @@ pub fn init(context: &Context) -> Menu { #[cfg(target_os = "macos")] titlebar_menu.into(), MenuItem::Separator.into(), - // fix: Checking if the site connection is secure - // @link: https://github.com/lencx/ChatGPT/issues/17 - CustomMenuItem::new("user_agent".to_string(), "User Agent") - .accelerator("CmdOrCtrl+U") - .into(), - CustomMenuItem::new("switch_origin".to_string(), "Switch Origin") - .accelerator("CmdOrCtrl+O") - .into(), CustomMenuItem::new("inject_script".to_string(), "Inject Script") .accelerator("CmdOrCtrl+J") .into(), + CustomMenuItem::new("control_center".to_string(), "Control Center") + .accelerator("CmdOrCtrl+Shift+P") + .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+D") + .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+Z") + .accelerator("CmdOrCtrl+Shift+A") .into(), ]), ); @@ -162,15 +159,19 @@ pub fn menu_handler(event: WindowMenuEvent) { match menu_id { // Preferences + "control_center" => app.get_window("main").unwrap().show().unwrap(), "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), - "switch_origin" => window::origin_window(&app), - "user_agent" => window::ua_window(&app), "awesome" => open(&app, conf::AWESOME_URL.to_string()), "titlebar" => { let chat_conf = conf::ChatConfJson::get_chat_conf(); - ChatConfJson::amend(&serde_json::json!({ "titlebar": !chat_conf.titlebar })).unwrap(); + ChatConfJson::amend( + &serde_json::json!({ "titlebar": !chat_conf.titlebar }), + None, + ) + .unwrap(); tauri::api::process::restart(&app.env()); } "theme_light" | "theme_dark" => { @@ -179,8 +180,7 @@ pub fn menu_handler(event: WindowMenuEvent) { } else { "Light" }; - ChatConfJson::amend(&serde_json::json!({ "theme": theme })).unwrap(); - tauri::api::process::restart(&app.env()); + ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap(); } "always_on_top" => { let mut always_on_top = state.always_on_top.lock().unwrap(); @@ -190,7 +190,11 @@ pub fn menu_handler(event: WindowMenuEvent) { .set_selected(*always_on_top) .unwrap(); win.set_always_on_top(*always_on_top).unwrap(); - ChatConfJson::amend(&serde_json::json!({ "always_on_top": *always_on_top })).unwrap(); + ChatConfJson::amend( + &serde_json::json!({ "always_on_top": *always_on_top }), + None, + ) + .unwrap(); } // View "reload" => win.eval("window.location.reload()").unwrap(), diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 6812ce9..d7beb2e 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -21,7 +21,7 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .initialization_script(include_str!("../assets/jspdf.js")) .initialization_script(include_str!("../assets/core.js")) .initialization_script(include_str!("../assets/export.js")) - .user_agent(&chat_conf.ua_pc) + .user_agent(&chat_conf.ua_window) .build()?; #[cfg(not(target_os = "macos"))] @@ -37,7 +37,7 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .initialization_script(include_str!("../assets/jspdf.js")) .initialization_script(include_str!("../assets/core.js")) .initialization_script(include_str!("../assets/export.js")) - .user_agent(&chat_conf.ua_pc) + .user_agent(&chat_conf.ua_window) .build()?; Ok(()) diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index b0c5f08..ea6b27e 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -17,37 +17,9 @@ pub fn mini_window(handle: &tauri::AppHandle) { .initialization_script(include_str!("../assets/jspdf.js")) .initialization_script(include_str!("../assets/core.js")) .initialization_script(include_str!("../assets/export.js")) - .user_agent(&chat_conf.ua_phone) + .user_agent(&chat_conf.ua_tray) .build() .unwrap() .hide() .unwrap(); } - -pub fn origin_window(handle: &tauri::AppHandle) { - let chat_conf = conf::ChatConfJson::get_chat_conf(); - WindowBuilder::new(handle, "origin", WindowUrl::App(chat_conf.origin.into())) - .resizable(false) - .fullscreen(false) - .inner_size(400.0, 300.0) - .always_on_top(true) - .decorations(false) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/origin.js")) - .build() - .unwrap(); -} - -pub fn ua_window(handle: &tauri::AppHandle) { - let chat_conf = conf::ChatConfJson::get_chat_conf(); - WindowBuilder::new(handle, "ua", WindowUrl::App(chat_conf.origin.into())) - .resizable(false) - .fullscreen(false) - .inner_size(540.0, 480.0) - .always_on_top(true) - .decorations(false) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/ua.js")) - .build() - .unwrap(); -} diff --git a/src-tauri/src/assets/core.js b/src-tauri/src/assets/core.js index 854b1aa..143c1a8 100644 --- a/src-tauri/src/assets/core.js +++ b/src-tauri/src/assets/core.js @@ -1,4 +1,5 @@ // *** Core Script - IPC *** + const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0]; function transformCallback(callback = () => {}, once = false) { const identifier = uid(); diff --git a/src-tauri/src/assets/export.js b/src-tauri/src/assets/export.js index 41a1851..bd78fb6 100644 --- a/src-tauri/src/assets/export.js +++ b/src-tauri/src/assets/export.js @@ -1,5 +1,5 @@ // *** Core Script - Export *** -// @ref: https://github.com/liady/ChatGPT-pdf/blob/main/src/content_script.js +// @ref: https://github.com/liady/ChatGPT-pdf async function init() { const chatConf = await invoke('get_chat_conf') || {}; @@ -78,22 +78,9 @@ function downloadThread({ as = Format.PNG } = {}) { const pixelRatio = window.devicePixelRatio; const minRatio = as === Format.PDF ? 2 : 2.5; window.devicePixelRatio = Math.max(pixelRatio, minRatio); + html2canvas(elements.thread, { letterRendering: true, - onclone: function (cloneDoc) { - //Make small fix of position to all the text containers - let listOfTexts = cloneDoc.getElementsByClassName("min-h-[20px]"); - Array.from(listOfTexts).forEach((text) => { - text.style.position = "relative"; - text.style.top = "-8px"; - }); - - //Delete copy button from code blocks - let listOfCopyBtns = cloneDoc.querySelectorAll("button.flex"); - Array.from(listOfCopyBtns).forEach( - (btn) => (btn.style.visibility = "hidden") - ); - }, }).then(async function (canvas) { elements.restoreLocation(); window.devicePixelRatio = pixelRatio; @@ -166,6 +153,8 @@ class Elements { img.setAttribute("srcset_old", srcset); img.setAttribute("srcset", ""); }); + //Fix to the text shifting down when generating the canvas + document.body.style.lineHeight = "0.5"; } restoreLocation() { this.hiddens.forEach((el) => { @@ -182,6 +171,7 @@ class Elements { img.setAttribute("srcset", srcset); img.setAttribute("srcset_old", ""); }); + document.body.style.lineHeight = null; } } diff --git a/src-tauri/src/assets/origin.js b/src-tauri/src/assets/origin.js deleted file mode 100644 index 2933ffc..0000000 --- a/src-tauri/src/assets/origin.js +++ /dev/null @@ -1,77 +0,0 @@ -function init() { - document.body.innerHTML = ` -

Switch Origin

- -
- - -
`; - - const srcipt = document.createElement('script'); - srcipt.innerHTML = `const input = document.getElementById('input'); - const cancelBtn = document.getElementById('cancel'); - const confirmBtn = document.getElementById('confirm'); - cancelBtn.addEventListener('click', () => { - window.invoke('form_cancel', { label: 'origin', title: 'Switch Origin', msg: 'Are you sure you want to cancel editing?' }); - }) - confirmBtn.addEventListener('click', () => { - if (/^https?:\\/\\//.test(input.value)) { - window.invoke('form_confirm', { data: { origin: input.value } }); - } else { - window.invoke('form_msg', { label: 'origin', title: 'Switch Origin', msg: 'Invalid URL!' }); - } - })`; - document.head.appendChild(srcipt); -} - -// run init -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/ua.js b/src-tauri/src/assets/ua.js deleted file mode 100644 index 097c5e7..0000000 --- a/src-tauri/src/assets/ua.js +++ /dev/null @@ -1,89 +0,0 @@ -function init() { - document.body.innerHTML = ` -

User Agent

-
- - -
-
- - -
-
- - -
`; - - const srcipt = document.createElement('script'); - srcipt.innerHTML = `const ua_pc = document.getElementById('ua_pc'); - const ua_phone = document.getElementById('ua_phone'); - const cancelBtn = document.getElementById('cancel'); - const confirmBtn = document.getElementById('confirm'); - cancelBtn.addEventListener('click', () => { - window.invoke('form_cancel', { label: 'ua', title: 'User Agent', msg: 'Are you sure you want to cancel editing?' }); - }) - confirmBtn.addEventListener('click', () => { - window.invoke('form_confirm', { data: { ua_pc: ua_pc.value, ua_phone: ua_phone.value } }); - })`; - document.head.appendChild(srcipt); -} - -// run init -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/conf.rs b/src-tauri/src/conf.rs index c73cb0e..482a37e 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -2,7 +2,7 @@ use crate::utils::{chat_root, create_file, exists}; use anyhow::Result; use serde_json::Value; use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex}; -use tauri::Theme; +use tauri::{Manager, Theme}; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; @@ -19,8 +19,8 @@ pub const DEFAULT_CHAT_CONF: &str = r#"{ "titlebar": true, "default_origin": "https://chat.openai.com", "origin": "https://chat.openai.com", - "ua_pc": "", - "ua_phone": "" + "ua_window": "", + "ua_tray": "" }"#; pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ "always_on_top": false, @@ -28,8 +28,8 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ "titlebar": false, "default_origin": "https://chat.openai.com", "origin": "https://chat.openai.com", - "ua_pc": "", - "ua_phone": "" + "ua_window": "", + "ua_tray": "" }"#; pub struct ChatState { @@ -51,8 +51,8 @@ pub struct ChatConfJson { pub theme: String, pub default_origin: String, pub origin: String, - pub ua_pc: String, - pub ua_phone: String, + pub ua_window: String, + pub ua_tray: String, } impl ChatConfJson { @@ -60,15 +60,30 @@ impl ChatConfJson { /// path: ~/.chatgpt/chat.conf.json pub fn init() -> PathBuf { let conf_file = ChatConfJson::conf_path(); + let content = if cfg!(target_os = "macos") { + DEFAULT_CHAT_CONF_MAC + } else { + DEFAULT_CHAT_CONF + }; + if !exists(&conf_file) { create_file(&conf_file).unwrap(); - - #[cfg(target_os = "macos")] - fs::write(&conf_file, DEFAULT_CHAT_CONF_MAC).unwrap(); - - #[cfg(not(target_os = "macos"))] - fs::write(&conf_file, DEFAULT_CHAT_CONF).unwrap(); + fs::write(&conf_file, content).unwrap(); + 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(); + } + }; + conf_file } @@ -77,15 +92,31 @@ impl ChatConfJson { } pub fn get_chat_conf() -> Self { - let config_file = fs::read_to_string(ChatConfJson::conf_path()) - .unwrap_or_else(|_| DEFAULT_CHAT_CONF.to_string()); - let config: Value = - serde_json::from_str(&config_file).expect("failed to parse chat.conf.json"); - serde_json::from_value(config).unwrap_or_else(|_| ChatConfJson::chat_conf_default()) + 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 + }; + + 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() + } + } } // https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3 - pub fn amend(new_rules: &Value) -> Result<()> { + pub fn amend(new_rules: &Value, app: Option) -> Result<()> { let config = ChatConfJson::get_chat_conf(); let config: Value = serde_json::to_value(&config)?; let mut config: BTreeMap = serde_json::from_value(config)?; @@ -99,6 +130,20 @@ impl ChatConfJson { ChatConfJson::conf_path(), serde_json::to_string_pretty(&config)?, )?; + + 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(()) } @@ -120,8 +165,4 @@ impl ChatConfJson { TitleBarStyle::Overlay } } - - pub fn chat_conf_default() -> Self { - serde_json::from_value(serde_json::json!(DEFAULT_CHAT_CONF)).unwrap() - } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ce3ad3e..d1cab6c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -36,10 +36,15 @@ fn main() { .on_window_event(|event| { // https://github.com/tauri-apps/tauri/discussions/2684 if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() { - // TODO: https://github.com/tauri-apps/tauri/issues/3084 - // event.window().hide().unwrap(); - // https://github.com/tauri-apps/tao/pull/517 - event.window().minimize().unwrap(); + let win = event.window(); + if win.label() == "main" { + win.hide().unwrap(); + } else { + // TODO: https://github.com/tauri-apps/tauri/issues/3084 + // event.window().hide().unwrap(); + // https://github.com/tauri-apps/tao/pull/517 + event.window().minimize().unwrap(); + } api.prevent_close(); } }) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f83df14..6e31ac3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,13 +1,13 @@ { "build": { - "beforeDevCommand": "", - "beforeBuildCommand": "", - "devPath": "../dist", + "beforeDevCommand": "npm run dev:fe", + "beforeBuildCommand": "npm run build:fe", + "devPath": "http://localhost:1420", "distDir": "../dist" }, "package": { "productName": "ChatGPT", - "version": "0.2.1" + "version": "0.3.0" }, "tauri": { "allowlist": { @@ -63,6 +63,16 @@ "https://lencx.github.io/ChatGPT/install.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMjY4OUI5MTVFNjBEMDUKUldRRkRlWVZ1WWttc1NGWEE0RFNSb0RqdnhsekRJZTkwK2hVLzhBZTZnaHExSEZ1ZEdzWkpXTHkK" - } + }, + "windows": [ + { + "label": "main", + "url": "index.html", + "title": "ChatGPT", + "visible": false, + "width": 800, + "height": 600 + } + ] } } \ No newline at end of file diff --git a/src/layout/index.scss b/src/layout/index.scss new file mode 100644 index 0000000..0da7e39 --- /dev/null +++ b/src/layout/index.scss @@ -0,0 +1,24 @@ +.chat-logo { + text-align: center; + padding: 5px 0; + + img { + width: 48px; + height: 48px; + } +} + +.chat-container { + padding: 20px; +} + +.ant-menu { + .ant-menu-item { + background-color: #f8f8f8; + } +} + +.ant-layout-footer { + color: #666 !important; + opacity: 0.7; +} \ No newline at end of file diff --git a/src/layout/index.tsx b/src/layout/index.tsx new file mode 100644 index 0000000..f6afada --- /dev/null +++ b/src/layout/index.tsx @@ -0,0 +1,36 @@ +import { FC, useState } from 'react'; +import { Layout, Menu } from 'antd'; +import { useNavigate } from 'react-router-dom'; + +import Routes, { menuItems } from '@/routes'; + +import './index.scss'; + +const { Content, Footer, Sider } = Layout; + +interface ChatLayoutProps { + children?: React.ReactNode; +} + +const ChatLayout: FC = ({ children }) => { + const [collapsed, setCollapsed] = useState(false); + const go = useNavigate(); + + return ( + + setCollapsed(value)}> +
+ go(i.key)} /> + + + + + + + + + ); +}; + +export default ChatLayout; \ No newline at end of file diff --git a/src/main.scss b/src/main.scss new file mode 100644 index 0000000..96312fb --- /dev/null +++ b/src/main.scss @@ -0,0 +1,20 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #2a2a2a; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +html, body { + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..488f0cd --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode, Suspense } from 'react'; +import { BrowserRouter } from 'react-router-dom'; +import ReactDOM from 'react-dom/client'; + +import Layout from '@/layout'; +import './main.scss'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + +); diff --git a/src/routes.tsx b/src/routes.tsx new file mode 100644 index 0000000..c885548 --- /dev/null +++ b/src/routes.tsx @@ -0,0 +1,44 @@ +import { useRoutes } from 'react-router-dom'; +import { + DesktopOutlined, + BulbOutlined +} from '@ant-design/icons'; +import type { RouteObject } from 'react-router-dom'; +import type { MenuProps } from 'antd'; + +import General from '@view/General'; +import ChatGPTPrompts from '@view/ChatGPTPrompts'; + +export type ChatRouteObject = { + label: string; + icon?: React.ReactNode, +}; + +export const routes: Array = [ + { + path: '/', + element: , + meta: { + label: 'General', + icon: , + }, + }, + { + path: '/chatgpt-prompts', + element: , + meta: { + label: 'ChatGPT Prompts', + icon: , + }, + }, +]; + +type MenuItem = Required['items'][number]; +export const menuItems: MenuItem[] = routes.map(i => ({ + ...i.meta, + key: i.path || '', +})); + +export default () => { + return useRoutes(routes); +}; \ No newline at end of file diff --git a/src/view/ChatGPTPrompts.tsx b/src/view/ChatGPTPrompts.tsx new file mode 100644 index 0000000..3e4087f --- /dev/null +++ b/src/view/ChatGPTPrompts.tsx @@ -0,0 +1,7 @@ +export default function Dashboard() { + return ( +
+ TODO: ChatGPT Prompts +
+ ) +} \ No newline at end of file diff --git a/src/view/General.tsx b/src/view/General.tsx new file mode 100644 index 0000000..d1934ec --- /dev/null +++ b/src/view/General.tsx @@ -0,0 +1,102 @@ +import { useEffect, useState } from 'react'; +import { Form, Radio, Switch, Input, Button, Space, message, Tooltip } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { invoke } from '@tauri-apps/api'; +import { platform } from '@tauri-apps/api/os'; +import { ask } from '@tauri-apps/api/dialog'; +import { relaunch } from '@tauri-apps/api/process'; +import { clone, omit, isEqual } from 'lodash'; + +const OriginLabel = ({ url }: { url: string }) => { + return ( + + Switch Origin + + ) +} + +const disableAuto = { + autoCapitalize: 'off', + autoComplete: 'off', + spellCheck: false +} + +export default function General() { + const [form] = Form.useForm(); + const [platformInfo, setPlatform] = useState(''); + const [chatConf, setChatConf] = useState(null); + + const init = async () => { + setPlatform(await platform()); + const chatData = await invoke('get_chat_conf'); + setChatConf(chatData); + } + + useEffect(() => { + init(); + }, []) + + useEffect(() => { + form.setFieldsValue(clone(chatConf)); + }, [chatConf]) + + const onCancel = () => { + form.setFieldsValue(chatConf); + }; + + const onFinish = async (values: any) => { + if (!isEqual(omit(chatConf, ['default_origin']), values)) { + await invoke('form_confirm', { data: values, label: 'main' }); + const isOk = await ask(`Configuration saved successfully, whether to restart?`, { + title: 'ChatGPT Preferences' + }); + if (isOk) { + relaunch(); + return; + } + message.success('Configuration saved successfully'); + } + }; + + return ( +
+ + + Light + Dark + + + + + + {platformInfo === 'darwin' && ( + + + + )} + } name="origin"> + + + + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..474aa73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@view/*": ["src/view/*"], + "@comps/*": ["src/components/*"], + "@layout/*": ["src/layout/*"], + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9d31e2a --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..debf8b2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tsconfigPaths from "vite-tsconfig-paths"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [tsconfigPaths(), react()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + }, + // to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ["VITE_", "TAURI_"], + build: { + // Tauri supports es2021 + target: ["es2021", "chrome100", "safari13"], + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? "esbuild" : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG, + }, +});