Merge pull request #32 from lencx/dev

This commit is contained in:
lencx
2022-12-15 13:53:37 +08:00
committed by GitHub
38 changed files with 556 additions and 289 deletions

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
*.js linguist-vendored
*.js linguist-vendored
*.tsx linguist-vendored
*.scss linguist-vendored

View File

@@ -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:

29
.gitignore vendored
View File

@@ -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?

View File

@@ -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.

View File

@@ -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 }
## 👀 预览
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/chat.png" alt="chat">
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
<img width="320" src="./assets/chat-ua.png" alt="user agent"> <img width="320" src="./assets/auto-update.png" alt="auto update">
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="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) 网站的包装,注入了一些额外功能(均在本地,未发起网络请求),如果存疑,可以检查源代码。

View File

@@ -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
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/chat.png" alt="chat">
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
<img width="320" src="./assets/chat-ua.png" alt="user agent"> <img width="320" src="./assets/auto-update.png" alt="auto update">
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="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).

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

BIN
assets/control-center.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

BIN
assets/tray-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

0
dist/.gitkeep vendored
View File

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatGPT</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -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"
}
}

11
scripts/download.js vendored
View File

@@ -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 = /<!-- download start -->/;
const endRe = /<!-- download end -->/;
@@ -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);

View File

@@ -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]

View File

@@ -1,5 +1,4 @@
use crate::{
app::window,
conf::{self, ChatConfJson},
utils,
};
@@ -26,7 +25,7 @@ pub fn init(context: &Context<EmbeddedAssets>) -> 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<EmbeddedAssets>) -> 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<tauri::Wry>) {
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<tauri::Wry>) {
} 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<tauri::Wry>) {
.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(),

View File

@@ -21,7 +21,7 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
.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<dyn std::error::Error>
.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(())

View File

@@ -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();
}

View File

@@ -1,4 +1,5 @@
// *** Core Script - IPC ***
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();

View File

@@ -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;
}
}

View File

@@ -1,77 +0,0 @@
function init() {
document.body.innerHTML = `<style>
body {
height: 100vh;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
}
h3 {
margin-bottom: 20px;
}
input {
all: unset;
width: 280px;
height: 30px;
margin-bottom: 10px;
padding: 0 5px;
border: solid 2px #d8d8d8;
background-color: #fff;
border-radius: 5px !important;
color: #4a4a4a;
}
button {
all: unset;
height: 30px;
font-size: 16px;
padding: 0 10px;
line-height: 30px;
margin: 0 5px;
color: #fff;
border-radius: 5px;
cursor: pointer;
}
#cancel {
background-color: #999;
}
#confirm {
background-color: #10a37f;
}
</style>
<h3>Switch Origin</h3>
<input id="input" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="https://chat.openai.com" />
<div class="btns">
<button id="cancel">Cancel</button>
<button id="confirm">Confirm</button>
</div>`;
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);
}

View File

@@ -1,89 +0,0 @@
function init() {
document.body.innerHTML = `<style>
body {
height: 100vh;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
}
h3 {
margin-bottom: 20px;
}
textarea {
all: unset;
width: 300px;
height: 60px;
margin-bottom: 10px;
padding: 10px;
border: solid 2px #d8d8d8;
background-color: #fff;
border-radius: 5px !important;
color: #4a4a4a;
}
button {
all: unset;
height: 30px;
font-size: 16px;
padding: 0 10px;
line-height: 30px;
margin: 0 5px;
color: #fff;
border-radius: 5px;
cursor: pointer;
}
#cancel {
background-color: #999;
}
#confirm {
background-color: #10a37f;
}
.item {
display: flex;
align-items: center;
}
label {
width: 120px;
margin-right: 10px;
}
</style>
<h3>User Agent</h3>
<div class="item">
<label>Main Window (PC)</label>
<textarea id="ua_pc" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."></textarea>
</div>
<div class="item">
<label>Tray Window (Phone)</label>
<textarea id="ua_phone" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS ..."></textarea>
</div>
<div class="btns">
<button id="cancel">Cancel</button>
<button id="confirm">Confirm</button>
</div>`;
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);
}

View File

@@ -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<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
@@ -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()
}
}

View File

@@ -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();
}
})

View File

@@ -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
}
]
}
}

24
src/layout/index.scss vendored Normal file
View File

@@ -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;
}

36
src/layout/index.tsx vendored Normal file
View File

@@ -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<ChatLayoutProps> = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const go = useNavigate();
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider theme="light" collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
<div className="chat-logo"><img src="/logo.png" /></div>
<Menu defaultSelectedKeys={['/']} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} />
</Sider>
<Layout className="chat-layout">
<Content className="chat-container">
<Routes />
</Content>
<Footer style={{ textAlign: 'center' }}>
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx</Footer>
</Layout>
</Layout>
);
};
export default ChatLayout;

20
src/main.scss vendored Normal file
View File

@@ -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;
}

16
src/main.tsx vendored Normal file
View File

@@ -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(
<StrictMode>
<Suspense fallback={null}>
<BrowserRouter>
<Layout/>
</BrowserRouter>
</Suspense>
</StrictMode>
);

44
src/routes.tsx vendored Normal file
View File

@@ -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<RouteObject & { meta: ChatRouteObject }> = [
{
path: '/',
element: <General />,
meta: {
label: 'General',
icon: <DesktopOutlined />,
},
},
{
path: '/chatgpt-prompts',
element: <ChatGPTPrompts />,
meta: {
label: 'ChatGPT Prompts',
icon: <BulbOutlined />,
},
},
];
type MenuItem = Required<MenuProps>['items'][number];
export const menuItems: MenuItem[] = routes.map(i => ({
...i.meta,
key: i.path || '',
}));
export default () => {
return useRoutes(routes);
};

7
src/view/ChatGPTPrompts.tsx vendored Normal file
View File

@@ -0,0 +1,7 @@
export default function Dashboard() {
return (
<div>
TODO: ChatGPT Prompts
</div>
)
}

102
src/view/General.tsx vendored Normal file
View File

@@ -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 (
<span>
Switch Origin <Tooltip title={`Default: ${url}`}><QuestionCircleOutlined /></Tooltip>
</span>
)
}
const disableAuto = {
autoCapitalize: 'off',
autoComplete: 'off',
spellCheck: false
}
export default function General() {
const [form] = Form.useForm();
const [platformInfo, setPlatform] = useState<string>('');
const [chatConf, setChatConf] = useState<any>(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 (
<Form
form={form}
style={{ maxWidth: 500 }}
onFinish={onFinish}
labelCol={{ span: 8 }}
wrapperCol={{ span: 15, offset: 1 }}
>
<Form.Item label="Theme" name="theme">
<Radio.Group>
<Radio value="Light">Light</Radio>
<Radio value="Dark">Dark</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="Always on Top" name="always_on_top" valuePropName="checked">
<Switch />
</Form.Item>
{platformInfo === 'darwin' && (
<Form.Item label="Titlebar" name="titlebar" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={<OriginLabel url={chatConf?.default_origin} />} name="origin">
<Input placeholder="https://chat.openai.com" {...disableAuto} />
</Form.Item>
<Form.Item label="User Agent (Window)" name="ua_window">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...disableAuto} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item>
<Form.Item label="User Agent (SystemTray)" name="ua_tray">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...disableAuto} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item>
<Form.Item>
<Space size={20}>
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Form.Item>
</Form>
)
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

28
tsconfig.json Normal file
View File

@@ -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" }]
}

9
tsconfig.node.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

28
vite.config.ts Normal file
View File

@@ -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,
},
});