1
0
mirror of https://github.com/lencx/ChatGPT.git synced 2024-10-01 01:06:13 -04:00

Merge pull request #5 from lencx/dev

This commit is contained in:
lencx 2022-12-10 03:53:10 +08:00 committed by GitHub
commit 6901f88b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 944 additions and 105 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.js linguist-vendored

View File

@ -13,15 +13,17 @@
**Latest:** **Latest:**
- `Mac`: [ChatGPT_0.1.5_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.5_x64.dmg) - `Mac`: [ChatGPT_0.1.6_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.6_x64.dmg)
- `Linux`: [chat-gpt_0.1.5_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/chat-gpt_0.1.5_amd64.deb) - `Linux`: [chat-gpt_0.1.6_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/chat-gpt_0.1.6_amd64.deb)
- `Windows`: [ChatGPT_0.1.5_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.5_x64_en-US.msi) - `Windows`: [ChatGPT_0.1.6_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.6_x64_en-US.msi)
[Other version...](https://github.com/lencx/ChatGPT/releases) [Other version...](https://github.com/lencx/ChatGPT/releases)
## Features ## Features
- multi-platform: `macOS` `Linux` `Windows` - multi-platform: `macOS` `Linux` `Windows`
- export ChatGPT history (PNG, PDF and Share Link)
- always on top (whether the window should always be on top of other windows)
- inject script - inject script
- auto updater - auto updater
- app menu - app menu
@ -30,14 +32,8 @@
## Preview ## Preview
<img width="600" src="./assets/install.png" alt="install"> <img width="360" src="./assets/install.png" alt="install"> <img width="360" src="./assets/chat.png" alt="chat">
<img width="600" src="./assets/chat.png" alt="chat"> <img width="360" src="./assets/export.png" alt="export"> <img width="360" src="./assets/auto-update.png" alt="auto update">
<img width="600" src="./assets/auto-update.png" alt="auto update">
## TODO
- [ ] export chat history
- [ ] ...
## FAQ ## FAQ
@ -45,6 +41,10 @@
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). 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).
### Developer cannot be verified?
- [Open a Mac app from an unidentified developer](https://support.apple.com/en-sg/guide/mac-help/mh40616/mac)
### How do i build it? ### How do i build it?
#### PreInstall #### PreInstall
@ -73,3 +73,7 @@ yarn dev
# bundle path: src-tauri/target/release/bundle # bundle path: src-tauri/target/release/bundle
yarn build yarn build
``` ```
## Related
- [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

View File

@ -1,5 +1,11 @@
# UPDATE LOG # UPDATE LOG
## v0.1.6
feat:
- always on top
- export ChatGPT history
## v0.1.5 ## v0.1.5
fix: mac can't use shortcut keys fix: mac can't use shortcut keys

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 653 KiB

BIN
assets/export.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -1,12 +1,14 @@
use tauri::Manager; use crate::utils;
use std::fs;
use tauri::{api, command, AppHandle, Manager};
#[tauri::command] #[command]
pub fn drag_window(app: tauri::AppHandle) { pub fn drag_window(app: AppHandle) {
app.get_window("core").unwrap().start_dragging().unwrap(); app.get_window("core").unwrap().start_dragging().unwrap();
} }
#[tauri::command] #[command]
pub fn fullscreen(app: tauri::AppHandle) { pub fn fullscreen(app: AppHandle) {
let win = app.get_window("core").unwrap(); let win = app.get_window("core").unwrap();
if win.is_fullscreen().unwrap() { if win.is_fullscreen().unwrap() {
win.set_fullscreen(false).unwrap(); win.set_fullscreen(false).unwrap();
@ -14,3 +16,15 @@ pub fn fullscreen(app: tauri::AppHandle) {
win.set_fullscreen(true).unwrap(); win.set_fullscreen(true).unwrap();
} }
} }
#[command]
pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) {
let path = api::path::download_dir().unwrap().join(name);
fs::write(&path, blob).unwrap();
utils::open_file(path);
}
#[command]
pub fn open_link(app: AppHandle, url: String) {
api::shell::open(&app.shell_scope(), url, None).unwrap();
}

View File

@ -1,22 +1,17 @@
use crate::utils; use crate::{conf, utils};
use tauri::{ use tauri::{
utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager, utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager,
Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent,
WindowMenuEvent,
}; };
// --- Menu // --- Menu
pub fn init(context: &Context<EmbeddedAssets>) -> Menu { pub fn init(chat_conf: &conf::ChatConfJson, context: &Context<EmbeddedAssets>) -> Menu {
let name = &context.package_info().name; let name = &context.package_info().name;
let app_menu = Submenu::new( let app_menu = Submenu::new(
name, name,
Menu::new() Menu::new()
.add_native_item(MenuItem::About(name.into(), AboutMetadata::default())) .add_native_item(MenuItem::About(name.into(), AboutMetadata::default()))
.add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.accelerator("CmdOrCtrl+J"),
)
.add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide) .add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers) .add_native_item(MenuItem::HideOthers)
@ -25,6 +20,28 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
.add_native_item(MenuItem::Quit), .add_native_item(MenuItem::Quit),
); );
let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always On Top")
.accelerator("CmdOrCtrl+T");
let preferences_menu = Submenu::new(
"Preferences",
Menu::new()
.add_item(
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.accelerator("CmdOrCtrl+J"),
)
.add_item(if chat_conf.always_on_top {
always_on_top.selected()
} else {
always_on_top
})
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
.accelerator("CmdOrCtrl+Z"),
),
);
let edit_menu = Submenu::new( let edit_menu = Submenu::new(
"Edit", "Edit",
Menu::new() Menu::new()
@ -74,6 +91,7 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
Menu::new() Menu::new()
.add_submenu(app_menu) .add_submenu(app_menu)
.add_submenu(preferences_menu)
.add_submenu(edit_menu) .add_submenu(edit_menu)
.add_submenu(view_menu) .add_submenu(view_menu)
.add_submenu(help_menu) .add_submenu(help_menu)
@ -83,12 +101,27 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) { pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
let win = Some(event.window()).unwrap(); let win = Some(event.window()).unwrap();
let app = win.app_handle(); let app = win.app_handle();
let state: tauri::State<conf::ChatState> = app.state();
let script_path = utils::script_path().to_string_lossy().to_string(); let script_path = utils::script_path().to_string_lossy().to_string();
let issues_url = "https://github.com/lencx/ChatGPT/issues".to_string(); let menu_id = event.menu_item_id();
match event.menu_item_id() { let core_window = app.get_window("core").unwrap();
// App let menu_handle = core_window.menu_handle();
"inject_script" => inject_script(&app, script_path),
match menu_id {
// Preferences
"inject_script" => open(&app, script_path),
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
"always_on_top" => {
let mut always_on_top = state.always_on_top.lock().unwrap();
*always_on_top = !*always_on_top;
menu_handle
.get_item(menu_id)
.set_selected(*always_on_top)
.unwrap();
win.set_always_on_top(*always_on_top).unwrap();
conf::ChatConfJson::update_chat_conf(*always_on_top);
}
// View // View
"reload" => win.eval("window.location.reload()").unwrap(), "reload" => win.eval("window.location.reload()").unwrap(),
"go_back" => win.eval("window.history.go(-1)").unwrap(), "go_back" => win.eval("window.history.go(-1)").unwrap(),
@ -111,7 +144,7 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
) )
.unwrap(), .unwrap(),
// Help // Help
"report_bug" => inject_script(&app, issues_url), "report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => { "dev_tools" => {
win.open_devtools(); win.open_devtools();
win.close_devtools(); win.close_devtools();
@ -122,35 +155,24 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
// --- SystemTray Menu // --- SystemTray Menu
pub fn tray_menu() -> SystemTray { pub fn tray_menu() -> SystemTray {
SystemTray::new().with_menu( SystemTray::new().with_menu(SystemTrayMenu::new())
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("show".to_string(), "Show ChatGPT"))
.add_item(CustomMenuItem::new("hide".to_string(), "Hide ChatGPT"))
.add_item(CustomMenuItem::new(
"inject_script".to_string(),
"Inject Script",
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
} }
// --- SystemTray Event // --- SystemTray Event
pub fn tray_handler(app: &AppHandle, event: SystemTrayEvent) { pub fn tray_handler(app: &AppHandle, event: SystemTrayEvent) {
let script_path = utils::script_path().to_string_lossy().to_string();
let win = app.get_window("core").unwrap(); let win = app.get_window("core").unwrap();
if let SystemTrayEvent::MenuItemClick { id, .. } = event { if let SystemTrayEvent::LeftClick { .. } = event {
match id.as_str() { // TODO: tray window
"quit" => std::process::exit(0), if win.is_visible().unwrap() {
"show" => win.show().unwrap(), win.hide().unwrap();
"hide" => win.hide().unwrap(), } else {
"inject_script" => inject_script(app, script_path), win.show().unwrap();
_ => (), win.set_focus().unwrap();
} }
} }
} }
pub fn inject_script(app: &AppHandle, path: String) { pub fn open(app: &AppHandle, path: String) {
tauri::api::shell::open(&app.shell_scope(), path, None).unwrap(); tauri::api::shell::open(&app.shell_scope(), path, None).unwrap();
} }

View File

@ -1,10 +1,13 @@
use crate::utils; use crate::{conf, utils};
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App}; use tauri::{utils::config::WindowUrl, window::WindowBuilder, App};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> { pub fn init(
app: &mut App,
chat_conf: conf::ChatConfJson,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let conf = utils::get_tauri_conf().unwrap(); let conf = utils::get_tauri_conf().unwrap();
let url = conf.build.dev_path.to_string(); let url = conf.build.dev_path.to_string();
@ -12,23 +15,31 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
WindowBuilder::new(app, "core", WindowUrl::App(url.into())) WindowBuilder::new(app, "core", WindowUrl::App(url.into()))
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
.initialization_script(include_str!("../core.js"))
.initialization_script(&utils::user_script())
.title_bar_style(TitleBarStyle::Overlay)
.inner_size(800.0, 600.0) .inner_size(800.0, 600.0)
.hidden_title(true) .hidden_title(true)
.user_agent("5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36") .title_bar_style(TitleBarStyle::Overlay)
.always_on_top(chat_conf.always_on_top)
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/jspdf.js"))
.initialization_script(include_str!("../assets/core.js"))
.initialization_script(include_str!("../assets/export.js"))
.user_agent(conf::USER_AGENT)
.build()?; .build()?;
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
WindowBuilder::new(app, "core", WindowUrl::App(url.into())) WindowBuilder::new(app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
.initialization_script(include_str!("../core.js"))
.initialization_script(&utils::user_script())
.inner_size(800.0, 600.0) .inner_size(800.0, 600.0)
.title("ChatGPT") .always_on_top(chat_conf.always_on_top)
.user_agent("5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36") .initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/jspdf.js"))
.initialization_script(include_str!("../assets/core.js"))
.initialization_script(include_str!("../assets/export.js"))
.user_agent(conf::USER_AGENT)
.build()?; .build()?;
Ok(()) Ok(())

View File

@ -1,41 +1,45 @@
// *** Core Script *** // *** Core Script - IPC ***
document.addEventListener('DOMContentLoaded', async () => { const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0]; function transformCallback(callback = () => {}, once = false) {
function transformCallback(callback = () => {}, once = false) { const identifier = uid();
const identifier = uid(); const prop = `_${identifier}`;
const prop = `_${identifier}`; Object.defineProperty(window, prop, {
Object.defineProperty(window, prop, { value: (result) => {
value: (result) => { if (once) {
if (once) { Reflect.deleteProperty(window, prop);
Reflect.deleteProperty(window, prop); }
} return callback(result)
return callback(result) },
}, writable: false,
writable: false, configurable: true,
configurable: true, })
}) return identifier;
return identifier; }
} async function invoke(cmd, args) {
async function invoke(cmd, args) { return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!'); const callback = transformCallback((e) => {
const callback = transformCallback((e) => { resolve(e);
resolve(e); Reflect.deleteProperty(window, `_${error}`);
Reflect.deleteProperty(window, `_${error}`); }, true)
}, true) const error = transformCallback((e) => {
const error = transformCallback((e) => { reject(e);
reject(e); Reflect.deleteProperty(window, `_${callback}`);
Reflect.deleteProperty(window, `_${callback}`); }, true)
}, true) window.__TAURI_POST_MESSAGE__({
window.__TAURI_POST_MESSAGE__({ cmd,
cmd, callback,
callback, error,
error, ...args
...args
});
}); });
} });
}
window.uid = uid;
window.invoke = invoke;
window.transformCallback = transformCallback;
async function init() {
async function platform() { async function platform() {
return invoke('platform', { return invoke('platform', {
__tauriModule: 'Os', __tauriModule: 'Os',
@ -74,4 +78,13 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
} }
}); });
}) }
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

238
src-tauri/src/assets/export.js vendored Normal file
View File

@ -0,0 +1,238 @@
// *** Core Script - Export ***
// @ref: https://github.com/liady/ChatGPT-pdf/blob/main/src/content_script.js
async function init() {
if (window.buttonsInterval) {
clearInterval(window.buttonsInterval);
}
window.buttonsInterval = setInterval(() => {
const actionsArea = document.querySelector("form>div>div");
if (!actionsArea) {
return;
}
const buttons = actionsArea.querySelectorAll("button");
const hasTryAgainButton = Array.from(buttons).some((button) => {
return !button.id?.includes("download");
});
if (hasTryAgainButton && buttons.length === 1) {
const TryAgainButton = actionsArea.querySelector("button");
addActionsButtons(actionsArea, TryAgainButton);
} else if (!hasTryAgainButton) {
removeButtons();
}
}, 200);
}
const Format = {
PNG: "png",
PDF: "pdf",
};
function addActionsButtons(actionsArea, TryAgainButton) {
const downloadButton = TryAgainButton.cloneNode(true);
downloadButton.id = "download-png-button";
downloadButton.innerText = "Generate PNG";
downloadButton.onclick = () => {
downloadThread();
};
actionsArea.appendChild(downloadButton);
const downloadPdfButton = TryAgainButton.cloneNode(true);
downloadPdfButton.id = "download-pdf-button";
downloadPdfButton.innerText = "Download PDF";
downloadPdfButton.onclick = () => {
downloadThread({ as: Format.PDF });
};
actionsArea.appendChild(downloadPdfButton);
const exportHtml = TryAgainButton.cloneNode(true);
exportHtml.id = "download-html-button";
exportHtml.innerText = "Share Link";
exportHtml.onclick = () => {
sendRequest();
};
actionsArea.appendChild(exportHtml);
}
function removeButtons() {
const downloadButton = document.getElementById("download-png-button");
const downloadPdfButton = document.getElementById("download-pdf-button");
const downloadHtmlButton = document.getElementById("download-html-button");
if (downloadButton) {
downloadButton.remove();
}
if (downloadPdfButton) {
downloadPdfButton.remove();
}
if (downloadHtmlButton) {
downloadHtmlButton.remove();
}
}
function downloadThread({ as = Format.PNG } = {}) {
const elements = new Elements();
elements.fixLocation();
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;
const imgData = canvas.toDataURL("image/png");
requestAnimationFrame(() => {
if (as === Format.PDF) {
return handlePdf(imgData, canvas, pixelRatio);
} else {
handleImg(imgData);
}
});
});
}
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)) });
}
function handlePdf(imgData, canvas, pixelRatio) {
const { jsPDF } = window.jspdf;
const orientation = canvas.width > canvas.height ? "l" : "p";
var pdf = new jsPDF(orientation, "pt", [
canvas.width / pixelRatio,
canvas.height / pixelRatio,
]);
var pdfWidth = pdf.internal.pageSize.getWidth();
var pdfHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight);
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) });
}
class Elements {
constructor() {
this.init();
}
init() {
// this.threadWrapper = document.querySelector(".cdfdFe");
this.spacer = document.querySelector(".w-full.h-48.flex-shrink-0");
this.thread = document.querySelector(
"[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div"
);
this.positionForm = document.querySelector("form").parentNode;
// this.styledThread = document.querySelector("main");
// this.threadContent = document.querySelector(".gAnhyd");
this.scroller = Array.from(
document.querySelectorAll('[class*="react-scroll-to"]')
).filter((el) => el.classList.contains("h-full"))[0];
this.hiddens = Array.from(document.querySelectorAll(".overflow-hidden"));
this.images = Array.from(document.querySelectorAll("img[srcset]"));
}
fixLocation() {
this.hiddens.forEach((el) => {
el.classList.remove("overflow-hidden");
});
this.spacer.style.display = "none";
this.thread.style.maxWidth = "960px";
this.thread.style.marginInline = "auto";
this.positionForm.style.display = "none";
this.scroller.classList.remove("h-full");
this.scroller.style.minHeight = "100vh";
this.images.forEach((img) => {
const srcset = img.getAttribute("srcset");
img.setAttribute("srcset_old", srcset);
img.setAttribute("srcset", "");
});
}
restoreLocation() {
this.hiddens.forEach((el) => {
el.classList.add("overflow-hidden");
});
this.spacer.style.display = null;
this.thread.style.maxWidth = null;
this.thread.style.marginInline = null;
this.positionForm.style.display = null;
this.scroller.classList.add("h-full");
this.scroller.style.minHeight = null;
this.images.forEach((img) => {
const srcset = img.getAttribute("srcset_old");
img.setAttribute("srcset", srcset);
img.setAttribute("srcset_old", "");
});
}
}
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);
}

20
src-tauri/src/assets/html2canvas.js vendored Normal file

File diff suppressed because one or more lines are too long

397
src-tauri/src/assets/jspdf.js vendored Normal file

File diff suppressed because one or more lines are too long

66
src-tauri/src/conf.rs Normal file
View File

@ -0,0 +1,66 @@
use crate::utils::{chat_root, create_file, exists};
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
pub const USER_AGENT: &str = "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
pub struct ChatState {
pub always_on_top: Mutex<bool>,
}
impl ChatState {
pub fn default(chat_conf: &ChatConfJson) -> Self {
ChatState {
always_on_top: Mutex::new(chat_conf.always_on_top),
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson {
pub always_on_top: bool,
}
impl ChatConfJson {
/// init chat.conf.json
/// path: ~/.chatgpt/chat.conf.json
pub fn init() -> PathBuf {
let conf_file = ChatConfJson::conf_path();
if !exists(&conf_file) {
create_file(&conf_file).unwrap();
fs::write(&conf_file, r#"{"always_on_top": false}"#).unwrap();
}
conf_file
}
pub fn conf_path() -> PathBuf {
chat_root().join("chat.conf.json")
}
pub fn get_chat_conf() -> Self {
let config_file = fs::read_to_string(ChatConfJson::conf_path()).unwrap();
let config: serde_json::Value =
serde_json::from_str(&config_file).expect("failed to parse chat.conf.json");
serde_json::from_value(config).unwrap_or_else(|_| ChatConfJson::chat_conf_default())
}
pub fn update_chat_conf(always_on_top: bool) {
let mut conf = ChatConfJson::get_chat_conf();
conf.always_on_top = always_on_top;
fs::write(
ChatConfJson::conf_path(),
serde_json::to_string(&conf).unwrap(),
)
.unwrap();
}
pub fn chat_conf_default() -> Self {
serde_json::from_value(serde_json::json!({
"always_on_top": false,
}))
.unwrap()
}
}

View File

@ -4,20 +4,41 @@
)] )]
mod app; mod app;
mod conf;
mod utils; mod utils;
use app::{cmd, menu, setup}; use app::{cmd, menu, setup};
use conf::ChatConfJson;
fn main() { fn main() {
ChatConfJson::init();
let context = tauri::generate_context!(); let context = tauri::generate_context!();
let chat_conf = ChatConfJson::get_chat_conf();
let chat_conf2 = chat_conf.clone();
tauri::Builder::default() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![cmd::drag_window, cmd::fullscreen]) .manage(conf::ChatState::default(&chat_conf))
.setup(setup::init) .invoke_handler(tauri::generate_handler![
.menu(menu::init(&context)) cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::open_link
])
.setup(|app| setup::init(app, chat_conf2))
.menu(menu::init(&chat_conf, &context))
.system_tray(menu::tray_menu()) .system_tray(menu::tray_menu())
.on_menu_event(menu::menu_handler) .on_menu_event(menu::menu_handler)
.on_system_tray_event(menu::tray_handler) .on_system_tray_event(menu::tray_handler)
.on_window_event(|event| {
// 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();
api.prevent_close();
}
})
.run(context) .run(context)
.expect("error while running ChatGPT application"); .expect("error while running ChatGPT application");
} }

View File

@ -1,8 +1,15 @@
use anyhow::Result; use anyhow::Result;
use std::fs::{self, File}; use std::{
use std::path::{Path, PathBuf}; fs::{self, File},
path::{Path, PathBuf},
process::Command,
};
use tauri::utils::config::Config; use tauri::utils::config::Config;
pub fn chat_root() -> PathBuf {
tauri::api::path::home_dir().unwrap().join(".chatgpt")
}
pub fn get_tauri_conf() -> Option<Config> { pub fn get_tauri_conf() -> Option<Config> {
let config_file = include_str!("../tauri.conf.json"); let config_file = include_str!("../tauri.conf.json");
let config: Config = let config: Config =
@ -22,8 +29,7 @@ pub fn create_file(path: &Path) -> Result<File> {
} }
pub fn script_path() -> PathBuf { pub fn script_path() -> PathBuf {
let root = tauri::api::path::home_dir().unwrap().join(".chatgpt"); let script_file = chat_root().join("main.js");
let script_file = root.join("main.js");
if !exists(&script_file) { if !exists(&script_file) {
create_file(&script_file).unwrap(); create_file(&script_file).unwrap();
fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap(); fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap();
@ -39,3 +45,19 @@ pub fn user_script() -> String {
user_script_content user_script_content
) )
} }
pub fn open_file(path: PathBuf) {
#[cfg(target_os = "macos")]
Command::new("open").arg("-R").arg(path).spawn().unwrap();
#[cfg(target_os = "windows")]
Command::new("explorer")
.arg("/select,")
.arg(path)
.spawn()
.unwrap();
// https://askubuntu.com/a/31071
#[cfg(target_os = "linux")]
Command::new("xdg-open").arg(path).spawn().unwrap();
}

View File

@ -2,12 +2,12 @@
"build": { "build": {
"beforeDevCommand": "", "beforeDevCommand": "",
"beforeBuildCommand": "", "beforeBuildCommand": "",
"devPath": "https://chat.openai.com", "devPath": "https://chat.openai.com/",
"distDir": "../dist" "distDir": "../dist"
}, },
"package": { "package": {
"productName": "ChatGPT", "productName": "ChatGPT",
"version": "0.1.5" "version": "0.1.6"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@ -44,6 +44,10 @@
"shortDescription": "ChatGPT", "shortDescription": "ChatGPT",
"targets": "all", "targets": "all",
"windows": { "windows": {
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "" "timestampUrl": ""