diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a25678f..a7818a9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,18 +16,18 @@ tauri-build = {version = "1.2.1", features = [] } [dependencies] anyhow = "1.0.66" serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] } -tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] } log = "0.4.17" csv = "1.1.6" thiserror = "1.0.38" walkdir = "2.3.2" regex = "1.7.0" -tokio = { version = "1.23.0", features = ["macros"] } reqwest = "0.11.13" -wry = "0.23.4" +wry = "0.24.1" dark-light = "1.0.0" +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.23.0", features = ["macros"] } +tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] } +tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] } [dependencies.tauri-plugin-log] git = "https://github.com/lencx/tauri-plugin-log" branch = "dev" @@ -36,6 +36,8 @@ features = ["colored"] git = "https://github.com/lencx/tauri-plugin-autostart" branch = "dev" +# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] } + [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 5472e38..b90fad3 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -1,10 +1,11 @@ use crate::{ - app::window, + app::{fs_extra, window}, conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL}, - utils, + utils::{self, chat_root, create_file}, }; use log::info; -use std::{collections::HashMap, fs, path::PathBuf}; +use regex::Regex; +use std::{collections::HashMap, fs, path::PathBuf, vec}; use tauri::{api, command, AppHandle, Manager, Theme}; use walkdir::WalkDir; @@ -35,14 +36,16 @@ pub fn fullscreen(app: AppHandle) { #[command] pub fn download(_app: AppHandle, name: String, blob: Vec) { - let path = api::path::download_dir().unwrap().join(name); + let path = chat_root().join(PathBuf::from(name)); + create_file(&path).unwrap(); fs::write(&path, blob).unwrap(); utils::open_file(path); } #[command] pub fn save_file(_app: AppHandle, name: String, content: String) { - let path = api::path::download_dir().unwrap().join(name); + let path = chat_root().join(PathBuf::from(name)); + create_file(&path).unwrap(); fs::write(&path, content).unwrap(); utils::open_file(path); } @@ -174,6 +177,76 @@ pub fn cmd_list() -> Vec { list } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct FileMetadata { + pub name: String, + pub ext: String, + pub created: u64, + pub id: String, +} + +#[command] +pub fn download_list(filename: Option, id: Option) { + info!("download_list"); + let download_path = chat_root().join("chat.download.json"); + let content = fs::read_to_string(&download_path).unwrap_or_else(|err| { + info!("download_list_error: {}", err); + fs::write(&download_path, "[]").unwrap(); + "[]".to_string() + }); + let mut list = serde_json::from_str::>(&content) + .unwrap_or_else(|err| { + info!("download_list_parse_error: {}", err); + vec![] + }); + + let list2 = &list; + let mut my_hashmap = HashMap::new(); + utils::vec_to_hashmap(list2.clone().into_iter(), "id", &mut my_hashmap); + + for entry in WalkDir::new(utils::chat_root().join("download")) + .into_iter() + .filter_entry(|e| !utils::is_hidden(e)) + .filter_map(|e| e.ok()) + { + let metadata = entry.metadata().unwrap(); + if metadata.is_file() { + let file_path = entry.path().display().to_string(); + let re = Regex::new(r"(?P[\d\w]+).(?P\w+)$").unwrap(); + let caps = re.captures(&file_path).unwrap(); + let fid = &caps["id"]; + let fext = &caps["ext"]; + + let mut file_data = FileMetadata { + name: fid.to_string(), + id: fid.to_string(), + ext: fext.to_string(), + created: fs_extra::system_time_to_ms(metadata.created()), + }; + if my_hashmap.get(fid).is_some() && filename.is_some() && id.is_some() { + if let Some(ref v) = id { + if fid == v { + if let Some(ref v2) = filename { + file_data.name = v2.to_string(); + } + } + } + } + list.push(serde_json::to_value(file_data).unwrap()); + } + } + + dbg!(&list); + + list.sort_by(|a, b| { + let a1 = a.get("created").unwrap().as_u64().unwrap(); + let b1 = b.get("created").unwrap().as_u64().unwrap(); + a1.cmp(&b1).reverse() + }); + + fs::write(download_path, serde_json::to_string_pretty(&list).unwrap()).unwrap(); +} + #[command] pub async fn sync_prompts(app: AppHandle, time: u64) -> Option> { let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app)) diff --git a/src-tauri/src/app/fs_extra.rs b/src-tauri/src/app/fs_extra.rs index 72453fb..9ed9cf0 100644 --- a/src-tauri/src/app/fs_extra.rs +++ b/src-tauri/src/app/fs_extra.rs @@ -60,7 +60,7 @@ struct UnixMetadata { #[serde(rename_all = "camelCase")] pub struct Metadata { accessed_at_ms: u64, - created_at_ms: u64, + pub created_at_ms: u64, modified_at_ms: u64, is_dir: bool, is_file: bool, @@ -74,7 +74,7 @@ pub struct Metadata { file_attributes: u32, } -fn system_time_to_ms(time: std::io::Result) -> u64 { +pub fn system_time_to_ms(time: std::io::Result) -> u64 { time.map(|t| { let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); duration_since_epoch.as_millis() as u64 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4f96dd6..08ddcf3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,6 +30,8 @@ async fn main() { trace: Color::Cyan, }; + cmd::download_list(None, None); + let chat_conf = ChatConfJson::get_chat_conf(); let mut builder = tauri::Builder::default() @@ -73,6 +75,7 @@ async fn main() { cmd::window_reload, cmd::dalle2_window, cmd::cmd_list, + cmd::download_list, fs_extra::metadata, ]) .setup(setup::init) diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index c1272ba..a8cfaf8 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -81,6 +81,7 @@ function shouldAddButtons(actionsArea) { function removeButtons() { const downloadButton = document.getElementById("download-png-button"); const downloadPdfButton = document.getElementById("download-pdf-button"); + const downloadMdButton = document.getElementById("download-markdown-button"); // const downloadHtmlButton = document.getElementById("download-html-button"); if (downloadButton) { downloadButton.remove(); @@ -88,6 +89,9 @@ function removeButtons() { if (downloadPdfButton) { downloadPdfButton.remove(); } + if (downloadPdfButton) { + downloadMdButton.remove(); + } // if (downloadHtmlButton) { // downloadHtmlButton.remove(); // } @@ -95,6 +99,18 @@ function removeButtons() { function addActionsButtons(actionsArea, TryAgainButton) { const downloadButton = TryAgainButton.cloneNode(true); + // Export markdown + const exportMd = TryAgainButton.cloneNode(true); + exportMd.id = "download-markdown-button"; + downloadButton.setAttribute("share-ext", "true"); + exportMd.title = "Export Markdown"; + exportMd.innerHTML = setIcon('md'); + exportMd.onclick = () => { + exportMarkdown(); + }; + actionsArea.appendChild(exportMd); + + // Generate PNG downloadButton.id = "download-png-button"; downloadButton.setAttribute("share-ext", "true"); // downloadButton.innerText = "Generate PNG"; @@ -104,6 +120,8 @@ function addActionsButtons(actionsArea, TryAgainButton) { downloadThread(); }; actionsArea.appendChild(downloadButton); + + // Generate PDF const downloadPdfButton = TryAgainButton.cloneNode(true); downloadPdfButton.id = "download-pdf-button"; downloadButton.setAttribute("share-ext", "true"); @@ -126,17 +144,11 @@ function addActionsButtons(actionsArea, TryAgainButton) { // sendRequest(); // }; // actionsArea.appendChild(exportHtml); - const exportMd = TryAgainButton.cloneNode(true); - exportMd.id = "download-markdown-button"; - downloadButton.setAttribute("share-ext", "true"); - // exportHtml.innerText = "Share Link"; - exportMd.title = "Download Markdown"; - exportMd.innerHTML = setIcon('md'); - exportMd.onclick = () => { - const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); - invoke('save_file', { name: `chatgpt-${Date.now()}.md`, content: data }); - }; - actionsArea.appendChild(exportMd); +} + +async function exportMarkdown() { + const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); + await invoke('save_file', { name: `notes/${Date.now().toString(36)}.md`, content: data }); } function downloadThread({ as = Format.PNG } = {}) { @@ -162,16 +174,17 @@ function downloadThread({ as = Format.PNG } = {}) { }); } -function handleImg(imgData) { +async function handleImg(imgData) { const binaryData = atob(imgData.split("base64,")[1]); const data = []; for (let i = 0; i < binaryData.length; i++) { data.push(binaryData.charCodeAt(i)); } - invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: data }); + await invoke('download', { name: `download/img/${Date.now().toString(36)}.png`, blob: data }); + await invoke('download_list'); } -function handlePdf(imgData, canvas, pixelRatio) { +async function handlePdf(imgData, canvas, pixelRatio) { const { jsPDF } = window.jspdf; const orientation = canvas.width > canvas.height ? "l" : "p"; var pdf = new jsPDF(orientation, "pt", [ @@ -183,7 +196,8 @@ function handlePdf(imgData, canvas, pixelRatio) { pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); - invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download', { name: `download/pdf/${Date.now().toString(36)}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download_list'); } class Elements { diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 608936f..a5625ec 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -230,3 +230,23 @@ pub async fn silent_install(app: AppHandle, update: UpdateResponse) -> Ok(()) } + +pub fn is_hidden(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with('.')) + .unwrap_or(false) +} + +pub fn vec_to_hashmap( + vec: impl Iterator, + key: &str, + map: &mut HashMap, +) { + for v in vec { + if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) { + map.insert(kval.to_string(), v); + } + } +} diff --git a/src/view/download/config.tsx b/src/view/download/config.tsx index 2bd47a5..c164b11 100644 --- a/src/view/download/config.tsx +++ b/src/view/download/config.tsx @@ -36,3 +36,10 @@ export const syncColumns = () => [ } } ]; + +// { +// id: '', +// name: '', +// type: '.png', +// created: '2022.01.01', +// } \ No newline at end of file diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx index 86d1d39..03ce0d2 100644 --- a/src/view/download/index.tsx +++ b/src/view/download/index.tsx @@ -5,17 +5,21 @@ import { path, shell } from '@tauri-apps/api'; import useInit from '@/hooks/useInit'; import useColumns from '@/hooks/useColumns'; import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; -import { chatRoot } from '@/utils'; +import { chatRoot, readJSON } from '@/utils'; import { syncColumns } from './config'; import './index.scss'; export default function SyncPrompts() { const { rowSelection, selectedRowIDs } = useTable(); const [downloadPath, setDownloadPath] = useState(''); + const [downloadData, setDownloadData] = useState([]); const { columns, ...opInfo } = useColumns(syncColumns()); useInit(async () => { - setDownloadPath(await path.join(await chatRoot(), 'download')); + const file = await path.join(await chatRoot(), 'chat.download.json'); + setDownloadPath(file); + const data = await readJSON(file, { isRoot: true, isList: true }); + setDownloadData(data); }); return ( @@ -29,7 +33,7 @@ export default function SyncPrompts() { rowKey="name" columns={columns} scroll={{ x: 'auto' }} - dataSource={[]} + dataSource={downloadData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} />