From 335d3e33ba4ed1376cefcee77aaacaa43c7825f3 Mon Sep 17 00:00:00 2001 From: lencx Date: Sun, 8 Jan 2023 12:28:54 +0800 Subject: [PATCH 01/14] chore: add jq --- src-tauri/src/app/setup.rs | 20 +- src-tauri/src/app/window.rs | 12 +- src-tauri/src/scripts/cmd.js | 271 ++++++++++++++++++++++ src-tauri/src/scripts/core.js | 94 ++++++++ src-tauri/src/scripts/dalle2.js | 31 +++ src-tauri/src/scripts/export.js | 278 +++++++++++++++++++++++ src-tauri/src/scripts/markdown.export.js | 5 + src-tauri/src/scripts/popup.core.js | 74 ++++++ src-tauri/src/vendors/jq.js | 2 + 9 files changed, 774 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/scripts/cmd.js create mode 100644 src-tauri/src/scripts/core.js create mode 100644 src-tauri/src/scripts/dalle2.js create mode 100644 src-tauri/src/scripts/export.js create mode 100644 src-tauri/src/scripts/markdown.export.js create mode 100644 src-tauri/src/scripts/popup.core.js create mode 100644 src-tauri/src/vendors/jq.js diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index ca3c0c4..3e4c8e4 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -61,14 +61,16 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .always_on_top(chat_conf.stay_on_top) .title_bar_style(ChatConfJson::titlebar()) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/html2canvas.js")) .initialization_script(include_str!("../vendors/jspdf.js")) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/popup.core.js")) - .initialization_script(include_str!("../assets/export.js")) - .initialization_script(include_str!("../assets/cmd.js")) + .initialization_script(include_str!("../scripts/core.js")) + .initialization_script(include_str!("../scripts/popup.core.js")) + .initialization_script(include_str!("../scripts/export.js")) + .initialization_script(include_str!("../scripts/markdown.export.js")) + .initialization_script(include_str!("../scripts/cmd.js")) .user_agent(&chat_conf.ua_window) .build() .unwrap(); @@ -82,14 +84,16 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .theme(theme) .always_on_top(chat_conf.stay_on_top) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/html2canvas.js")) .initialization_script(include_str!("../vendors/jspdf.js")) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/popup.core.js")) - .initialization_script(include_str!("../assets/export.js")) - .initialization_script(include_str!("../assets/cmd.js")) + .initialization_script(include_str!("../scripts/core.js")) + .initialization_script(include_str!("../scripts/popup.core.js")) + .initialization_script(include_str!("../scripts/export.js")) + .initialization_script(include_str!("../scripts/markdown.export.js")) + .initialization_script(include_str!("../scripts/cmd.js")) .user_agent(&chat_conf.ua_window) .build() .unwrap(); diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index 58ca282..81ee7f6 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -18,11 +18,12 @@ pub fn tray_window(handle: &tauri::AppHandle) { .always_on_top(true) .theme(theme) .initialization_script(&utils::user_script()) + .initialization_script(include_str!("../vendors/jq.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js")) - .initialization_script(include_str!("../assets/core.js")) - .initialization_script(include_str!("../assets/cmd.js")) - .initialization_script(include_str!("../assets/popup.core.js")) + .initialization_script(include_str!("../scripts/core.js")) + .initialization_script(include_str!("../scripts/cmd.js")) + .initialization_script(include_str!("../scripts/popup.core.js")) .user_agent(&chat_conf.ua_tray) .build() .unwrap() @@ -73,9 +74,10 @@ pub fn dalle2_window( .inner_size(800.0, 600.0) .always_on_top(false) .theme(theme) - .initialization_script(include_str!("../assets/core.js")) + .initialization_script(include_str!("../vendors/jq.js")) + .initialization_script(include_str!("../scripts/core.js")) .initialization_script(&query) - .initialization_script(include_str!("../assets/dalle2.js")) + .initialization_script(include_str!("../scripts/dalle2.js")) .build() .unwrap(); }); diff --git a/src-tauri/src/scripts/cmd.js b/src-tauri/src/scripts/cmd.js new file mode 100644 index 0000000..5ddc12a --- /dev/null +++ b/src-tauri/src/scripts/cmd.js @@ -0,0 +1,271 @@ +// *** Core Script - CMD *** + +$(function() { + const styleDom = document.createElement('style'); + styleDom.innerHTML = `form { + position: relative; + } + .chat-model-cmd-list { + position: absolute; + bottom: 60px; + max-height: 100px; + overflow: auto; + z-index: 9999; + } + .chat-model-cmd-list>div { + border: solid 2px rgba(80,80,80,.3); + border-radius: 5px; + background-color: #fff; + } + + html.dark .chat-model-cmd-list>div { + background-color: #4a4a4a; + } + html.dark .chat-model-cmd-list .cmd-item { + border-color: #666; + } + html.dark .chat-model-cmd-list .cmd-item b { + color: #e8e8e8; + } + html.dark .chat-model-cmd-list .cmd-item i { + color: #999; + } + html.dark .chat-model-cmd-list .cmd-item.selected { + background: rgba(59,130,246,.5); + } + + .chat-model-cmd-list .cmd-item { + font-size: 12px; + border-bottom: solid 1px rgba(80,80,80,.2); + padding: 2px 4px; + display: flex; + user-select: none; + cursor: pointer; + } + .chat-model-cmd-list .cmd-item:last-child { + border-bottom: none; + } + .chat-model-cmd-list .cmd-item.selected { + background: rgba(59,130,246,.3); + } + .chat-model-cmd-list .cmd-item b { + display: inline-block; + width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: 4px; + margin-right: 10px; + color: #2a2a2a; + } + .chat-model-cmd-list .cmd-item i { + width: 100%; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + color: #888; + } + .chatappico { + width: 20px; + height: 20px; + } + .chatappico.pdf { + width: 24px; + height: 24px; + } + @media screen and (max-width: 767px) { + #download-png-button, #download-pdf-button, #download-html-button { + display: none; + } + } + `; + document.head.append(styleDom); + + if (window.formInterval) { + clearInterval(window.formInterval); + } + window.formInterval = setInterval(() => { + const form = document.querySelector("form"); + if (!form) return; + clearInterval(window.formInterval); + cmdTip(); + }, 200); +}); + +async function cmdTip() { + const chatModelJson = await invoke('get_chat_model_cmd') || {}; + const data = chatModelJson.data; + if (data.length <= 0) return; + + const modelDom = document.createElement('div'); + modelDom.classList.add('chat-model-cmd-list'); + + // fix: tray window + if (__TAURI_METADATA__.__currentWindow.label === 'tray') { + modelDom.style.bottom = '54px'; + } + + document.querySelector('form').appendChild(modelDom); + const itemDom = (v) => `
/${v.cmd}${v.act}
`; + const renderList = (v) => { + modelDom.innerHTML = `
${v.map(itemDom).join('')}
`; + window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim(); + window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim(); + window.__list = modelDom.querySelectorAll('.cmd-item'); + window.__index = 0; + window.__list[window.__index].classList.add('selected'); + }; + const setPrompt = (v = '') => { + if (v.trim()) { + window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`); + } + } + const searchInput = document.querySelector('form textarea'); + + // Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`. + // If more than one command appears in the search results, the first one will be used by default. + searchInput.addEventListener('keydown', (event) => { + if (!window.__CHAT_MODEL_CMD_PROMPT__) { + return; + } + + // ------------------ Keyboard scrolling (ArrowUp | ArrowDown) -------------------------- + if (event.keyCode === 38 && window.__index > 0) { // ArrowUp + window.__list[window.__index].classList.remove('selected'); + window.__index = window.__index - 1; + window.__list[window.__index].classList.add('selected'); + window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt')); + searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`; + event.preventDefault(); + } + + if (event.keyCode === 40 && window.__index < window.__list.length - 1) { // ArrowDown + window.__list[window.__index].classList.remove('selected'); + window.__index = window.__index + 1; + window.__list[window.__index].classList.add('selected'); + window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt')); + searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`; + event.preventDefault(); + } + + const containerHeight = modelDom.offsetHeight; + const itemHeight = window.__list[0].offsetHeight + 1; + + const itemTop = window.__list[window.__index].offsetTop; + const itemBottom = itemTop + itemHeight; + if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) { + modelDom.scrollTop = itemTop; + } + + // ------------------ TAB key replaces `{q}` tag content ------------------------------- + // feat: https://github.com/lencx/ChatGPT/issues/54 + if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) { + const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || []; + + if (strGroup[1]) { + searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> '; + window.__CHAT_MODEL_STATUS__ = 1; + } + event.preventDefault(); + } + + if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB + const data = searchInput.value.split('|->'); + if (data[1]?.trim()) { + setPrompt(data[1]); + window.__CHAT_MODEL_STATUS__ = 2; + } + event.preventDefault(); + } + + // input text + if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB + searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; + modelDom.innerHTML = ''; + delete window.__CHAT_MODEL_STATUS__; + event.preventDefault(); + } + + // ------------------ type in a space to complete the fill ------------------------------------ + if (event.keyCode === 32) { + searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; + modelDom.innerHTML = ''; + delete window.__CHAT_MODEL_CMD_PROMPT__; + } + + // ------------------ send -------------------------------------------------------------------- + if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter + const data = searchInput.value.split('|->'); + setPrompt(data[1]); + + searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; + modelDom.innerHTML = ''; + delete window.__CHAT_MODEL_CMD_PROMPT__; + delete window.__CHAT_MODEL_CMD__; + delete window.__CHAT_MODEL_STATUS__; + event.preventDefault(); + } + }); + + searchInput.addEventListener('input', () => { + if (searchInput.value === '') { + delete window.__CHAT_MODEL_CMD_PROMPT__; + delete window.__CHAT_MODEL_CMD__; + delete window.__CHAT_MODEL_STATUS__; + } + + if (window.__CHAT_MODEL_STATUS__) return; + + const query = searchInput.value; + if (!query || !/^\//.test(query)) { + modelDom.innerHTML = ''; + return; + } + + // all cmd result + if (query === '/') { + renderList(data); + return; + } + + const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd)); + if (result.length > 0) { + renderList(result); + } else { + modelDom.innerHTML = ''; + delete window.__CHAT_MODEL_CMD_PROMPT__; + delete window.__CHAT_MODEL_CMD__; + delete window.__CHAT_MODEL_STATUS__; + } + }, { + capture: false, + passive: true, + once: false + }); + + if (window.searchInterval) { + clearInterval(window.searchInterval); + } + window.searchInterval = setInterval(() => { + // The `chatgpt prompt` fill can be done by clicking on the event. + const searchDom = document.querySelector("form .chat-model-cmd-list>div"); + if (!searchDom) return; + searchDom.addEventListener('click', (event) => { + // .cmd-item + const item = event.target.closest("div"); + if (item) { + const val = decodeURIComponent(item.getAttribute('data-prompt')); + searchInput.value = val; + document.querySelector('form textarea').focus(); + window.__CHAT_MODEL_CMD_PROMPT__ = val; + modelDom.innerHTML = ''; + } + }, { + capture: false, + passive: true, + once: false + }); + }, 200); +} diff --git a/src-tauri/src/scripts/core.js b/src-tauri/src/scripts/core.js new file mode 100644 index 0000000..ad8ed4b --- /dev/null +++ b/src-tauri/src/scripts/core.js @@ -0,0 +1,94 @@ +// *** Core Script - IPC *** + +const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0]; +function transformCallback(callback = () => {}, once = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once) { + Reflect.deleteProperty(window, prop); + } + return callback(result) + }, + writable: false, + configurable: true, + }) + return identifier; +} +async function invoke(cmd, args) { + return new Promise((resolve, reject) => { + if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!'); + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true) + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true) + window.__TAURI_POST_MESSAGE__({ + cmd, + callback, + error, + ...args + }); + }); +} + +window.uid = uid; +window.invoke = invoke; +window.transformCallback = transformCallback; + +$(async function () { + if (__TAURI_METADATA__.__currentWindow.label === 'tray') { + document.getElementsByTagName('html')[0].style['font-size'] = '70%'; + } + + if (__TAURI_METADATA__.__currentWindow.label !== 'core') return; + + async function platform() { + return invoke('platform', { + __tauriModule: 'Os', + message: { cmd: 'platform' } + }); + } + + const _platform = await platform(); + const chatConf = await invoke('get_chat_conf') || {}; + if (/darwin/.test(_platform) && !chatConf.titlebar) { + const topStyleDom = document.createElement("style"); + topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`; + document.head.appendChild(topStyleDom); + const topDom = document.createElement("div"); + topDom.id = "chatgpt-app-window-top"; + document.body.appendChild(topDom); + + topDom.addEventListener("mousedown", () => invoke("drag_window")); + topDom.addEventListener("touchstart", () => invoke("drag_window")); + topDom.addEventListener("dblclick", () => invoke("fullscreen")); + } + + document.addEventListener("click", (e) => { + const origin = e.target.closest("a"); + if (!origin.target) return; + if (origin && origin.href && origin.target !== '_self') { + invoke('open_link', { url: origin.href }); + } + }); + + document.addEventListener('wheel', function(event) { + const deltaX = event.wheelDeltaX; + if (Math.abs(deltaX) >= 50) { + if (deltaX > 0) { + window.history.go(-1); + } else { + window.history.go(1); + } + } + }); + + window.__sync_prompts = async function() { + await invoke('sync_prompts', { time: Date.now() }); + } +}); \ No newline at end of file diff --git a/src-tauri/src/scripts/dalle2.js b/src-tauri/src/scripts/dalle2.js new file mode 100644 index 0000000..1b77067 --- /dev/null +++ b/src-tauri/src/scripts/dalle2.js @@ -0,0 +1,31 @@ +// *** Core Script - DALL·E 2 *** + +$(function () { + document.addEventListener("click", (e) => { + const origin = e.target.closest("a"); + if (!origin.target) return; + if (origin && origin.href && origin.target !== '_self') { + if (/\/(login|signup)$/.test(window.location.href)) { + origin.target = '_self'; + } else { + invoke('open_link', { url: origin.href }); + } + } + }); + + if (window.searchInterval) { + clearInterval(window.searchInterval); + } + + window.searchInterval = setInterval(() => { + const searchInput = document.querySelector('.image-prompt-form-wrapper form>.text-input'); + if (searchInput) { + clearInterval(window.searchInterval); + + if (!window.__CHATGPT_QUERY__) return; + const query = decodeURIComponent(window.__CHATGPT_QUERY__); + searchInput.focus(); + searchInput.value = query; + } + }, 200) +}) \ No newline at end of file diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js new file mode 100644 index 0000000..b6633d2 --- /dev/null +++ b/src-tauri/src/scripts/export.js @@ -0,0 +1,278 @@ +// *** Core Script - Export *** +// @ref: https://github.com/liady/ChatGPT-pdf + +const buttonOuterHTMLFallback = ``; + +$(async function () { + if (window.innerWidth < 767) return; + const chatConf = await invoke('get_chat_conf') || {}; + if (window.buttonsInterval) { + clearInterval(window.buttonsInterval); + } + window.buttonsInterval = setInterval(() => { + const actionsArea = document.querySelector("form>div>div"); + if (!actionsArea) { + return; + } + if (shouldAddButtons(actionsArea)) { + let TryAgainButton = actionsArea.querySelector("button"); + if (!TryAgainButton) { + const parentNode = document.createElement("div"); + parentNode.innerHTML = buttonOuterHTMLFallback; + TryAgainButton = parentNode.querySelector("button"); + } + addActionsButtons(actionsArea, TryAgainButton, chatConf); + } else if (shouldRemoveButtons()) { + removeButtons(); + } + }, 200); +}) + +const Format = { + PNG: "png", + PDF: "pdf", +}; + +function shouldRemoveButtons() { + const isOpenScreen = document.querySelector("h1.text-4xl"); + if(isOpenScreen){ + return true; + } + const inConversation = document.querySelector("form button>div"); + if(inConversation){ + return true; + } + return false; +} + +function shouldAddButtons(actionsArea) { + // first, check if there's a "Try Again" button and no other buttons + const buttons = actionsArea.querySelectorAll("button"); + const hasTryAgainButton = Array.from(buttons).some((button) => { + return !button.id?.includes("download"); + }); + if (hasTryAgainButton && buttons.length === 1) { + return true; + } + + // otherwise, check if open screen is not visible + const isOpenScreen = document.querySelector("h1.text-4xl"); + if (isOpenScreen) { + return false; + } + + // check if the conversation is finished and there are no share buttons + const finishedConversation = document.querySelector("form button>svg"); + const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]"); + if (finishedConversation && !hasShareButtons.length) { + return true; + } + + return false; +} + +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 addActionsButtons(actionsArea, TryAgainButton) { + const downloadButton = TryAgainButton.cloneNode(true); + downloadButton.id = "download-png-button"; + downloadButton.setAttribute("share-ext", "true"); + // downloadButton.innerText = "Generate PNG"; + downloadButton.title = "Generate PNG"; + downloadButton.innerHTML = setIcon('png'); + downloadButton.onclick = () => { + downloadThread(); + }; + actionsArea.appendChild(downloadButton); + const downloadPdfButton = TryAgainButton.cloneNode(true); + downloadPdfButton.id = "download-pdf-button"; + downloadButton.setAttribute("share-ext", "true"); + // downloadPdfButton.innerText = "Download PDF"; + downloadPdfButton.title = "Download PDF"; + downloadPdfButton.innerHTML = setIcon('pdf'); + downloadPdfButton.onclick = () => { + downloadThread({ as: Format.PDF }); + }; + actionsArea.appendChild(downloadPdfButton); + const exportHtml = TryAgainButton.cloneNode(true); + exportHtml.id = "download-html-button"; + downloadButton.setAttribute("share-ext", "true"); + // exportHtml.innerText = "Share Link"; + exportHtml.title = "Share Link"; + exportHtml.innerHTML = setIcon('link'); + exportHtml.onclick = () => { + sendRequest(); + }; + actionsArea.appendChild(exportHtml); +} + +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, + }).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, '', 'FAST'); + + 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", ""); + }); + //Fix to the text shifting down when generating the canvas + document.body.style.lineHeight = "0.5"; + } + 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", ""); + }); + document.body.style.lineHeight = null; + } +} + +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(""); +} + +function setIcon(type) { + return { + link: ``, + png: ``, + pdf: `` + }[type]; +} diff --git a/src-tauri/src/scripts/markdown.export.js b/src-tauri/src/scripts/markdown.export.js new file mode 100644 index 0000000..eb46ac2 --- /dev/null +++ b/src-tauri/src/scripts/markdown.export.js @@ -0,0 +1,5 @@ +// *** Core Script - Markdown *** + +$(function() { + console.log("markdown"); +}); \ No newline at end of file diff --git a/src-tauri/src/scripts/popup.core.js b/src-tauri/src/scripts/popup.core.js new file mode 100644 index 0000000..953420a --- /dev/null +++ b/src-tauri/src/scripts/popup.core.js @@ -0,0 +1,74 @@ +// *** Core Script - DALL·E 2 Core *** + +$(async function () { + const chatConf = await invoke('get_chat_conf') || {}; + if (!chatConf.popup_search) return; + if (!window.FloatingUIDOM) return; + + const styleDom = document.createElement('style'); + styleDom.innerHTML = ` + #chagpt-selection-menu { + display: none; + width: max-content; + position: absolute; + top: 0; + left: 0; + background: #4a4a4a; + color: white; + font-weight: bold; + padding: 5px 8px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + } + `; + document.head.append(styleDom); + + const selectionMenu = document.createElement('div'); + selectionMenu.id = 'chagpt-selection-menu'; + selectionMenu.innerHTML = 'DALL·E 2'; + document.body.appendChild(selectionMenu); + const { computePosition, flip, offset, shift } = window.FloatingUIDOM; + + document.body.addEventListener('mousedown', async (e) => { + if (e.target.id === 'chagpt-selection-menu') { + await invoke('dalle2_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) }); + } else { + delete window.__DALLE2_CONTENT__; + } + }); + + document.body.addEventListener("mouseup", async (e) => { + selectionMenu.style.display = 'none'; + const selection = window.getSelection(); + window.__DALLE2_CONTENT__ = selection.toString().trim(); + + if (!window.__DALLE2_CONTENT__) return; + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const rect = range.getClientRects()[0]; + + const rootEl = document.createElement('div'); + rootEl.style.top = `${rect.top}px`; + rootEl.style.position = 'fixed'; + rootEl.style.left = `${rect.left}px`; + document.body.appendChild(rootEl); + + selectionMenu.style.display = 'block'; + computePosition(rootEl, selectionMenu, { + placement: 'top', + middleware: [ + flip(), + offset(5), + shift({ padding: 5 }) + ] + }).then(({x, y}) => { + Object.assign(selectionMenu.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + } + }); +}) \ No newline at end of file diff --git a/src-tauri/src/vendors/jq.js b/src-tauri/src/vendors/jq.js new file mode 100644 index 0000000..b04d0fc --- /dev/null +++ b/src-tauri/src/vendors/jq.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.3 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},S=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||S).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.3",E=function(e,t){return new E.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,S)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=E)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{if(d.cssSupportsSelector&&!CSS.supports("selector(:is("+c+"))"))throw new Error;return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===E&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[E]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,S=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssSupportsSelector=ce(function(){return CSS.supports("selector(*)")&&C.querySelectorAll(":is(:jqfake)")&&!CSS.supports("selector(:is(*,:jqfake))")}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=E,!C.getElementsByName||!C.getElementsByName(E).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&S){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&S)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+E+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+E+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssSupportsSelector||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&S&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:S,!0)),N.test(r[1])&&E.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=S.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,D=E(S);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=S.createDocumentFragment().appendChild(S.createElement("div")),(fe=S.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),S.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||E.expando+"_"+Ct.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||E.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?E(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=S.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=S.implementation.createHTMLDocument("")).createElement("base")).href=S.location.href,t.head.appendChild(r)):t=S),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0 Date: Sun, 8 Jan 2023 17:04:49 +0800 Subject: [PATCH 02/14] chore: scripts --- src-tauri/src/assets/cmd.js | 280 ---------------------- src-tauri/src/assets/core.js | 103 -------- src-tauri/src/assets/dalle2.js | 40 ---- src-tauri/src/assets/export.js | 287 ----------------------- src-tauri/src/assets/popup.core.js | 84 ------- src-tauri/src/scripts/core.js | 2 +- src-tauri/src/scripts/dalle2.js | 2 +- src-tauri/src/scripts/export.js | 108 ++++----- src-tauri/src/scripts/markdown.export.js | 8 +- src-tauri/src/vendors/turndown.js | 1 + 10 files changed, 63 insertions(+), 852 deletions(-) delete mode 100644 src-tauri/src/assets/cmd.js delete mode 100644 src-tauri/src/assets/core.js delete mode 100644 src-tauri/src/assets/dalle2.js delete mode 100644 src-tauri/src/assets/export.js delete mode 100644 src-tauri/src/assets/popup.core.js create mode 100644 src-tauri/src/vendors/turndown.js diff --git a/src-tauri/src/assets/cmd.js b/src-tauri/src/assets/cmd.js deleted file mode 100644 index 4588e5a..0000000 --- a/src-tauri/src/assets/cmd.js +++ /dev/null @@ -1,280 +0,0 @@ -// *** Core Script - CMD *** - -function init() { - const styleDom = document.createElement('style'); - styleDom.innerHTML = `form { - position: relative; - } - .chat-model-cmd-list { - position: absolute; - bottom: 60px; - max-height: 100px; - overflow: auto; - z-index: 9999; - } - .chat-model-cmd-list>div { - border: solid 2px rgba(80,80,80,.3); - border-radius: 5px; - background-color: #fff; - } - - html.dark .chat-model-cmd-list>div { - background-color: #4a4a4a; - } - html.dark .chat-model-cmd-list .cmd-item { - border-color: #666; - } - html.dark .chat-model-cmd-list .cmd-item b { - color: #e8e8e8; - } - html.dark .chat-model-cmd-list .cmd-item i { - color: #999; - } - html.dark .chat-model-cmd-list .cmd-item.selected { - background: rgba(59,130,246,.5); - } - - .chat-model-cmd-list .cmd-item { - font-size: 12px; - border-bottom: solid 1px rgba(80,80,80,.2); - padding: 2px 4px; - display: flex; - user-select: none; - cursor: pointer; - } - .chat-model-cmd-list .cmd-item:last-child { - border-bottom: none; - } - .chat-model-cmd-list .cmd-item.selected { - background: rgba(59,130,246,.3); - } - .chat-model-cmd-list .cmd-item b { - display: inline-block; - width: 100px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - border-radius: 4px; - margin-right: 10px; - color: #2a2a2a; - } - .chat-model-cmd-list .cmd-item i { - width: 100%; - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: right; - color: #888; - } - .chatappico { - width: 20px; - height: 20px; - } - .chatappico.pdf { - width: 24px; - height: 24px; - } - @media screen and (max-width: 767px) { - #download-png-button, #download-pdf-button, #download-html-button { - display: none; - } - } - `; - document.head.append(styleDom); - - if (window.formInterval) { - clearInterval(window.formInterval); - } - window.formInterval = setInterval(() => { - const form = document.querySelector("form"); - if (!form) return; - clearInterval(window.formInterval); - cmdTip(); - }, 200); -} - -async function cmdTip() { - const chatModelJson = await invoke('get_chat_model_cmd') || {}; - const data = chatModelJson.data; - if (data.length <= 0) return; - - const modelDom = document.createElement('div'); - modelDom.classList.add('chat-model-cmd-list'); - - // fix: tray window - if (__TAURI_METADATA__.__currentWindow.label === 'tray') { - modelDom.style.bottom = '54px'; - } - - document.querySelector('form').appendChild(modelDom); - const itemDom = (v) => `
/${v.cmd}${v.act}
`; - const renderList = (v) => { - modelDom.innerHTML = `
${v.map(itemDom).join('')}
`; - window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim(); - window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim(); - window.__list = modelDom.querySelectorAll('.cmd-item'); - window.__index = 0; - window.__list[window.__index].classList.add('selected'); - }; - const setPrompt = (v = '') => { - if (v.trim()) { - window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`); - } - } - const searchInput = document.querySelector('form textarea'); - - // Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`. - // If more than one command appears in the search results, the first one will be used by default. - searchInput.addEventListener('keydown', (event) => { - if (!window.__CHAT_MODEL_CMD_PROMPT__) { - return; - } - - // ------------------ Keyboard scrolling (ArrowUp | ArrowDown) -------------------------- - if (event.keyCode === 38 && window.__index > 0) { // ArrowUp - window.__list[window.__index].classList.remove('selected'); - window.__index = window.__index - 1; - window.__list[window.__index].classList.add('selected'); - window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt')); - searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`; - event.preventDefault(); - } - - if (event.keyCode === 40 && window.__index < window.__list.length - 1) { // ArrowDown - window.__list[window.__index].classList.remove('selected'); - window.__index = window.__index + 1; - window.__list[window.__index].classList.add('selected'); - window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt')); - searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`; - event.preventDefault(); - } - - const containerHeight = modelDom.offsetHeight; - const itemHeight = window.__list[0].offsetHeight + 1; - - const itemTop = window.__list[window.__index].offsetTop; - const itemBottom = itemTop + itemHeight; - if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) { - modelDom.scrollTop = itemTop; - } - - // ------------------ TAB key replaces `{q}` tag content ------------------------------- - // feat: https://github.com/lencx/ChatGPT/issues/54 - if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) { - const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || []; - - if (strGroup[1]) { - searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> '; - window.__CHAT_MODEL_STATUS__ = 1; - } - event.preventDefault(); - } - - if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB - const data = searchInput.value.split('|->'); - if (data[1]?.trim()) { - setPrompt(data[1]); - window.__CHAT_MODEL_STATUS__ = 2; - } - event.preventDefault(); - } - - // input text - if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB - searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; - modelDom.innerHTML = ''; - delete window.__CHAT_MODEL_STATUS__; - event.preventDefault(); - } - - // ------------------ type in a space to complete the fill ------------------------------------ - if (event.keyCode === 32) { - searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; - modelDom.innerHTML = ''; - delete window.__CHAT_MODEL_CMD_PROMPT__; - } - - // ------------------ send -------------------------------------------------------------------- - if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter - const data = searchInput.value.split('|->'); - setPrompt(data[1]); - - searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__; - modelDom.innerHTML = ''; - delete window.__CHAT_MODEL_CMD_PROMPT__; - delete window.__CHAT_MODEL_CMD__; - delete window.__CHAT_MODEL_STATUS__; - event.preventDefault(); - } - }); - - searchInput.addEventListener('input', () => { - if (searchInput.value === '') { - delete window.__CHAT_MODEL_CMD_PROMPT__; - delete window.__CHAT_MODEL_CMD__; - delete window.__CHAT_MODEL_STATUS__; - } - - if (window.__CHAT_MODEL_STATUS__) return; - - const query = searchInput.value; - if (!query || !/^\//.test(query)) { - modelDom.innerHTML = ''; - return; - } - - // all cmd result - if (query === '/') { - renderList(data); - return; - } - - const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd)); - if (result.length > 0) { - renderList(result); - } else { - modelDom.innerHTML = ''; - delete window.__CHAT_MODEL_CMD_PROMPT__; - delete window.__CHAT_MODEL_CMD__; - delete window.__CHAT_MODEL_STATUS__; - } - }, { - capture: false, - passive: true, - once: false - }); - - if (window.searchInterval) { - clearInterval(window.searchInterval); - } - window.searchInterval = setInterval(() => { - // The `chatgpt prompt` fill can be done by clicking on the event. - const searchDom = document.querySelector("form .chat-model-cmd-list>div"); - if (!searchDom) return; - searchDom.addEventListener('click', (event) => { - // .cmd-item - const item = event.target.closest("div"); - if (item) { - const val = decodeURIComponent(item.getAttribute('data-prompt')); - searchInput.value = val; - document.querySelector('form textarea').focus(); - window.__CHAT_MODEL_CMD_PROMPT__ = val; - modelDom.innerHTML = ''; - } - }, { - capture: false, - passive: true, - once: false - }); - }, 200); -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} \ No newline at end of file diff --git a/src-tauri/src/assets/core.js b/src-tauri/src/assets/core.js deleted file mode 100644 index 335daa8..0000000 --- a/src-tauri/src/assets/core.js +++ /dev/null @@ -1,103 +0,0 @@ -// *** Core Script - IPC *** - -const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0]; -function transformCallback(callback = () => {}, once = false) { - const identifier = uid(); - const prop = `_${identifier}`; - Object.defineProperty(window, prop, { - value: (result) => { - if (once) { - Reflect.deleteProperty(window, prop); - } - return callback(result) - }, - writable: false, - configurable: true, - }) - return identifier; -} -async function invoke(cmd, args) { - return new Promise((resolve, reject) => { - if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!'); - const callback = transformCallback((e) => { - resolve(e); - Reflect.deleteProperty(window, `_${error}`); - }, true) - const error = transformCallback((e) => { - reject(e); - Reflect.deleteProperty(window, `_${callback}`); - }, true) - window.__TAURI_POST_MESSAGE__({ - cmd, - callback, - error, - ...args - }); - }); -} - -window.uid = uid; -window.invoke = invoke; -window.transformCallback = transformCallback; - -async function init() { - if (__TAURI_METADATA__.__currentWindow.label === 'tray') { - document.getElementsByTagName('html')[0].style['font-size'] = '70%'; - } - - if (__TAURI_METADATA__.__currentWindow.label !== 'core') return; - - async function platform() { - return invoke('platform', { - __tauriModule: 'Os', - message: { cmd: 'platform' } - }); - } - - const _platform = await platform(); - const chatConf = await invoke('get_chat_conf') || {}; - if (/darwin/.test(_platform) && !chatConf.titlebar) { - const topStyleDom = document.createElement("style"); - topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`; - document.head.appendChild(topStyleDom); - const topDom = document.createElement("div"); - topDom.id = "chatgpt-app-window-top"; - document.body.appendChild(topDom); - - topDom.addEventListener("mousedown", () => invoke("drag_window")); - topDom.addEventListener("touchstart", () => invoke("drag_window")); - topDom.addEventListener("dblclick", () => invoke("fullscreen")); - } - - document.addEventListener("click", (e) => { - const origin = e.target.closest("a"); - if (!origin.target) return; - if (origin && origin.href && origin.target !== '_self') { - invoke('open_link', { url: origin.href }); - } - }); - - document.addEventListener('wheel', function(event) { - const deltaX = event.wheelDeltaX; - if (Math.abs(deltaX) >= 50) { - if (deltaX > 0) { - window.history.go(-1); - } else { - window.history.go(1); - } - } - }); - - window.__sync_prompts = async function() { - await invoke('sync_prompts', { time: Date.now() }); - } -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} \ No newline at end of file diff --git a/src-tauri/src/assets/dalle2.js b/src-tauri/src/assets/dalle2.js deleted file mode 100644 index e95d4c0..0000000 --- a/src-tauri/src/assets/dalle2.js +++ /dev/null @@ -1,40 +0,0 @@ -// *** Core Script - DALL·E 2 *** - -async function init() { - document.addEventListener("click", (e) => { - const origin = e.target.closest("a"); - if (!origin.target) return; - if (origin && origin.href && origin.target !== '_self') { - if (/\/(login|signup)$/.test(window.location.href)) { - origin.target = '_self'; - } else { - invoke('open_link', { url: origin.href }); - } - } - }); - - if (window.searchInterval) { - clearInterval(window.searchInterval); - } - - window.searchInterval = setInterval(() => { - const searchInput = document.querySelector('.image-prompt-form-wrapper form>.text-input'); - if (searchInput) { - clearInterval(window.searchInterval); - - if (!window.__CHATGPT_QUERY__) return; - const query = decodeURIComponent(window.__CHATGPT_QUERY__); - searchInput.focus(); - searchInput.value = query; - } - }, 200) -} - -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} diff --git a/src-tauri/src/assets/export.js b/src-tauri/src/assets/export.js deleted file mode 100644 index 8c2a903..0000000 --- a/src-tauri/src/assets/export.js +++ /dev/null @@ -1,287 +0,0 @@ -// *** Core Script - Export *** -// @ref: https://github.com/liady/ChatGPT-pdf - -const buttonOuterHTMLFallback = ``; -async function init() { - if (window.innerWidth < 767) return; - const chatConf = await invoke('get_chat_conf') || {}; - if (window.buttonsInterval) { - clearInterval(window.buttonsInterval); - } - window.buttonsInterval = setInterval(() => { - const actionsArea = document.querySelector("form>div>div"); - if (!actionsArea) { - return; - } - if (shouldAddButtons(actionsArea)) { - let TryAgainButton = actionsArea.querySelector("button"); - if (!TryAgainButton) { - const parentNode = document.createElement("div"); - parentNode.innerHTML = buttonOuterHTMLFallback; - TryAgainButton = parentNode.querySelector("button"); - } - addActionsButtons(actionsArea, TryAgainButton, chatConf); - } else if (shouldRemoveButtons()) { - removeButtons(); - } - }, 200); -} - -const Format = { - PNG: "png", - PDF: "pdf", -}; - -function shouldRemoveButtons() { - const isOpenScreen = document.querySelector("h1.text-4xl"); - if(isOpenScreen){ - return true; - } - const inConversation = document.querySelector("form button>div"); - if(inConversation){ - return true; - } - return false; -} - -function shouldAddButtons(actionsArea) { - // first, check if there's a "Try Again" button and no other buttons - const buttons = actionsArea.querySelectorAll("button"); - const hasTryAgainButton = Array.from(buttons).some((button) => { - return !button.id?.includes("download"); - }); - if (hasTryAgainButton && buttons.length === 1) { - return true; - } - - // otherwise, check if open screen is not visible - const isOpenScreen = document.querySelector("h1.text-4xl"); - if (isOpenScreen) { - return false; - } - - // check if the conversation is finished and there are no share buttons - const finishedConversation = document.querySelector("form button>svg"); - const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]"); - if (finishedConversation && !hasShareButtons.length) { - return true; - } - - return false; -} - -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 addActionsButtons(actionsArea, TryAgainButton) { - const downloadButton = TryAgainButton.cloneNode(true); - downloadButton.id = "download-png-button"; - downloadButton.setAttribute("share-ext", "true"); - // downloadButton.innerText = "Generate PNG"; - downloadButton.title = "Generate PNG"; - downloadButton.innerHTML = setIcon('png'); - downloadButton.onclick = () => { - downloadThread(); - }; - actionsArea.appendChild(downloadButton); - const downloadPdfButton = TryAgainButton.cloneNode(true); - downloadPdfButton.id = "download-pdf-button"; - downloadButton.setAttribute("share-ext", "true"); - // downloadPdfButton.innerText = "Download PDF"; - downloadPdfButton.title = "Download PDF"; - downloadPdfButton.innerHTML = setIcon('pdf'); - downloadPdfButton.onclick = () => { - downloadThread({ as: Format.PDF }); - }; - actionsArea.appendChild(downloadPdfButton); - const exportHtml = TryAgainButton.cloneNode(true); - exportHtml.id = "download-html-button"; - downloadButton.setAttribute("share-ext", "true"); - // exportHtml.innerText = "Share Link"; - exportHtml.title = "Share Link"; - exportHtml.innerHTML = setIcon('link'); - exportHtml.onclick = () => { - sendRequest(); - }; - actionsArea.appendChild(exportHtml); -} - -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, - }).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, '', 'FAST'); - - 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", ""); - }); - //Fix to the text shifting down when generating the canvas - document.body.style.lineHeight = "0.5"; - } - 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", ""); - }); - document.body.style.lineHeight = null; - } -} - -function selectElementByClassPrefix(classPrefix) { - const element = document.querySelector(`[class^='${classPrefix}']`); - return element; -} - -async function sendRequest() { - const data = getData(); - const uploadUrlResponse = await fetch( - "https://chatgpt-static.s3.amazonaws.com/url.txt" - ); - const uploadUrl = await uploadUrlResponse.text(); - fetch(uploadUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - .then((response) => response.json()) - .then((data) => { - invoke('open_link', { url: data.url }); - }); -} - -function getData() { - const globalCss = getCssFromSheet( - document.querySelector("link[rel=stylesheet]").sheet - ); - const localCss = - getCssFromSheet( - document.querySelector(`style[data-styled][data-styled-version]`).sheet - ) || "body{}"; - const data = { - main: document.querySelector("main").outerHTML, - // css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`, - globalCss, - localCss, - }; - return data; -} - -function getCssFromSheet(sheet) { - return Array.from(sheet.cssRules) - .map((rule) => rule.cssText) - .join(""); -} - -// run init -if ( - document.readyState === "complete" || - document.readyState === "interactive" -) { - init(); -} else { - document.addEventListener("DOMContentLoaded", init); -} - -function setIcon(type) { - return { - link: ``, - png: ``, - pdf: `` - }[type]; -} diff --git a/src-tauri/src/assets/popup.core.js b/src-tauri/src/assets/popup.core.js deleted file mode 100644 index 3eb7b02..0000000 --- a/src-tauri/src/assets/popup.core.js +++ /dev/null @@ -1,84 +0,0 @@ -// *** Core Script - DALL·E 2 Core *** - -async function init() { - const chatConf = await invoke('get_chat_conf') || {}; - if (!chatConf.popup_search) return; - if (!window.FloatingUIDOM) return; - - const styleDom = document.createElement('style'); - styleDom.innerHTML = ` - #chagpt-selection-menu { - display: none; - width: max-content; - position: absolute; - top: 0; - left: 0; - background: #4a4a4a; - color: white; - font-weight: bold; - padding: 5px 8px; - border-radius: 4px; - font-size: 12px; - cursor: pointer; - } - `; - document.head.append(styleDom); - - const selectionMenu = document.createElement('div'); - selectionMenu.id = 'chagpt-selection-menu'; - selectionMenu.innerHTML = 'DALL·E 2'; - document.body.appendChild(selectionMenu); - const { computePosition, flip, offset, shift } = window.FloatingUIDOM; - - document.body.addEventListener('mousedown', async (e) => { - if (e.target.id === 'chagpt-selection-menu') { - await invoke('dalle2_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) }); - } else { - delete window.__DALLE2_CONTENT__; - } - }); - - document.body.addEventListener("mouseup", async (e) => { - selectionMenu.style.display = 'none'; - const selection = window.getSelection(); - window.__DALLE2_CONTENT__ = selection.toString().trim(); - - if (!window.__DALLE2_CONTENT__) return; - - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - const rect = range.getClientRects()[0]; - - const rootEl = document.createElement('div'); - rootEl.style.top = `${rect.top}px`; - rootEl.style.position = 'fixed'; - rootEl.style.left = `${rect.left}px`; - document.body.appendChild(rootEl); - - selectionMenu.style.display = 'block'; - computePosition(rootEl, selectionMenu, { - placement: 'top', - middleware: [ - flip(), - offset(5), - shift({ padding: 5 }) - ] - }).then(({x, y}) => { - Object.assign(selectionMenu.style, { - left: `${x}px`, - top: `${y}px`, - }); - }); - } - }); - -} - -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/scripts/core.js b/src-tauri/src/scripts/core.js index ad8ed4b..d69f37e 100644 --- a/src-tauri/src/scripts/core.js +++ b/src-tauri/src/scripts/core.js @@ -71,7 +71,7 @@ $(async function () { document.addEventListener("click", (e) => { const origin = e.target.closest("a"); - if (!origin.target) return; + if (!origin || !origin.target) return; if (origin && origin.href && origin.target !== '_self') { invoke('open_link', { url: origin.href }); } diff --git a/src-tauri/src/scripts/dalle2.js b/src-tauri/src/scripts/dalle2.js index 1b77067..ca11cd4 100644 --- a/src-tauri/src/scripts/dalle2.js +++ b/src-tauri/src/scripts/dalle2.js @@ -3,7 +3,7 @@ $(function () { document.addEventListener("click", (e) => { const origin = e.target.closest("a"); - if (!origin.target) return; + if (!origin || !origin.target) return; if (origin && origin.href && origin.target !== '_self') { if (/\/(login|signup)$/.test(window.location.href)) { origin.target = '_self'; diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index b6633d2..c94a3a4 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -74,16 +74,16 @@ function shouldAddButtons(actionsArea) { function removeButtons() { const downloadButton = document.getElementById("download-png-button"); const downloadPdfButton = document.getElementById("download-pdf-button"); - const downloadHtmlButton = document.getElementById("download-html-button"); + // const downloadHtmlButton = document.getElementById("download-html-button"); if (downloadButton) { downloadButton.remove(); } if (downloadPdfButton) { downloadPdfButton.remove(); } - if (downloadHtmlButton) { - downloadHtmlButton.remove(); - } + // if (downloadHtmlButton) { + // downloadHtmlButton.remove(); + // } } function addActionsButtons(actionsArea, TryAgainButton) { @@ -107,16 +107,16 @@ function addActionsButtons(actionsArea, TryAgainButton) { downloadThread({ as: Format.PDF }); }; actionsArea.appendChild(downloadPdfButton); - const exportHtml = TryAgainButton.cloneNode(true); - exportHtml.id = "download-html-button"; - downloadButton.setAttribute("share-ext", "true"); - // exportHtml.innerText = "Share Link"; - exportHtml.title = "Share Link"; - exportHtml.innerHTML = setIcon('link'); - exportHtml.onclick = () => { - sendRequest(); - }; - actionsArea.appendChild(exportHtml); + // const exportHtml = TryAgainButton.cloneNode(true); + // exportHtml.id = "download-html-button"; + // downloadButton.setAttribute("share-ext", "true"); + // // exportHtml.innerText = "Share Link"; + // exportHtml.title = "Share Link"; + // exportHtml.innerHTML = setIcon('link'); + // exportHtml.onclick = () => { + // sendRequest(); + // }; + // actionsArea.appendChild(exportHtml); } function downloadThread({ as = Format.PNG } = {}) { @@ -227,51 +227,51 @@ function selectElementByClassPrefix(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 }); - }); -} +// 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 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(""); -} +// function getCssFromSheet(sheet) { +// return Array.from(sheet.cssRules) +// .map((rule) => rule.cssText) +// .join(""); +// } function setIcon(type) { return { - link: ``, + // link: ``, png: ``, pdf: `` }[type]; diff --git a/src-tauri/src/scripts/markdown.export.js b/src-tauri/src/scripts/markdown.export.js index eb46ac2..4137987 100644 --- a/src-tauri/src/scripts/markdown.export.js +++ b/src-tauri/src/scripts/markdown.export.js @@ -1,5 +1,9 @@ // *** Core Script - Markdown *** -$(function() { +(function () { console.log("markdown"); -}); \ No newline at end of file + const chatThread = $('main .items-center'); + const chatBlocks = $(chatThread, '>div'); + + console.log('«8» /src/scripts/markdown.export.js ~> ', chatBlocks); +})(window); \ No newline at end of file diff --git a/src-tauri/src/vendors/turndown.js b/src-tauri/src/vendors/turndown.js new file mode 100644 index 0000000..92acdf4 --- /dev/null +++ b/src-tauri/src/vendors/turndown.js @@ -0,0 +1 @@ +var TurndownService=function(){"use strict";function u(e,n){return Array(n+1).join(e)}var n=["ADDRESS","ARTICLE","ASIDE","AUDIO","BLOCKQUOTE","BODY","CANVAS","CENTER","DD","DIR","DIV","DL","DT","FIELDSET","FIGCAPTION","FIGURE","FOOTER","FORM","FRAMESET","H1","H2","H3","H4","H5","H6","HEADER","HGROUP","HR","HTML","ISINDEX","LI","MAIN","MENU","NAV","NOFRAMES","NOSCRIPT","OL","OUTPUT","P","PRE","SECTION","TABLE","TBODY","TD","TFOOT","TH","THEAD","TR","UL"];function o(e){return l(e,n)}var r=["AREA","BASE","BR","COL","COMMAND","EMBED","HR","IMG","INPUT","KEYGEN","LINK","META","PARAM","SOURCE","TRACK","WBR"];function i(e){return l(e,r)}var a=["A","TABLE","THEAD","TBODY","TFOOT","TH","TD","IFRAME","SCRIPT","AUDIO","VIDEO"];function l(e,n){return 0<=n.indexOf(e.nodeName)}function c(n,e){return n.getElementsByTagName&&e.some(function(e){return n.getElementsByTagName(e).length})}var t={};function s(e){return e?e.replace(/(\n+\s*)+/g,"\n"):""}function f(e){for(var n in this.options=e,this._keep=[],this._remove=[],this.blankRule={replacement:e.blankReplacement},this.keepReplacement=e.keepReplacement,this.defaultRule={replacement:e.defaultReplacement},this.array=[],e.rules)this.array.push(e.rules[n])}function d(e,n,t){for(var r=0;r "))+"\n\n"}},t.list={filter:["ul","ol"],replacement:function(e,n){var t=n.parentNode;return"LI"===t.nodeName&&t.lastElementChild===n?"\n"+e:"\n\n"+e+"\n\n"}},t.listItem={filter:"li",replacement:function(e,n,t){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var r=t.bulletListMarker+" ",i=n.parentNode;return"OL"===i.nodeName&&(t=i.getAttribute("start"),i=Array.prototype.indexOf.call(i.children,n),r=(t?Number(t)+i:i+1)+". "),r+e+(n.nextSibling&&!/\n$/.test(e)?"\n":"")}},t.indentedCodeBlock={filter:function(e,n){return"indented"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){return"\n\n "+n.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}},t.fencedCodeBlock={filter:function(e,n){return"fenced"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){for(var r,i=((n.firstChild.getAttribute("class")||"").match(/language-(\S+)/)||[null,""])[1],o=n.firstChild.textContent,t=t.fence.charAt(0),a=3,l=new RegExp("^"+t+"{3,}","gm");r=l.exec(o);)r[0].length>=a&&(a=r[0].length+1);t=u(t,a);return"\n\n"+t+i+"\n"+o.replace(/\n$/,"")+"\n"+t+"\n\n"}},t.horizontalRule={filter:"hr",replacement:function(e,n,t){return"\n\n"+t.hr+"\n\n"}},t.inlineLink={filter:function(e,n){return"inlined"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n){var t=n.getAttribute("href"),n=s(n.getAttribute("title"));return"["+e+"]("+t+(n=n&&' "'+n+'"')+")"}},t.referenceLink={filter:function(e,n){return"referenced"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n,t){var r=n.getAttribute("href"),i=(i=s(n.getAttribute("title")))&&' "'+i+'"';switch(t.linkReferenceStyle){case"collapsed":a="["+e+"][]",l="["+e+"]: "+r+i;break;case"shortcut":a="["+e+"]",l="["+e+"]: "+r+i;break;default:var o=this.references.length+1,a="["+e+"]["+o+"]",l="["+o+"]: "+r+i}return this.references.push(l),a},references:[],append:function(e){var n="";return this.references.length&&(n="\n\n"+this.references.join("\n")+"\n\n",this.references=[]),n}},t.emphasis={filter:["em","i"],replacement:function(e,n,t){return e.trim()?t.emDelimiter+e+t.emDelimiter:""}},t.strong={filter:["strong","b"],replacement:function(e,n,t){return e.trim()?t.strongDelimiter+e+t.strongDelimiter:""}},t.code={filter:function(e){var n=e.previousSibling||e.nextSibling,n="PRE"===e.parentNode.nodeName&&!n;return"CODE"===e.nodeName&&!n},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");for(var n=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"",t="`",r=e.match(/`+/gm)||[];-1!==r.indexOf(t);)t+="`";return t+n+e+n+t}},t.image={filter:"img",replacement:function(e,n){var t=s(n.getAttribute("alt")),r=n.getAttribute("src")||"",n=s(n.getAttribute("title"));return r?"!["+t+"]("+r+(n?' "'+n+'"':"")+")":""}},f.prototype={add:function(e,n){this.array.unshift(n)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:(n=d(this.array,e,this.options))||(n=d(this._keep,e,this.options))||(n=d(this._remove,e,this.options))?n:this.defaultRule;var n},forEach:function(e){for(var n=0;n'+e+"","text/html").getElementById("turndown-root"):e.cloneNode(!0),isBlock:o,isVoid:i,isPre:n.preformattedCode?y:null}),e}function y(e){return"PRE"===e.nodeName||"CODE"===e.nodeName}function N(e,n){var t;return e.isBlock=o(e),e.isCode="CODE"===e.nodeName||e.parentNode.isCode,e.isBlank=!i(t=e)&&!function(e){return l(e,a)}(t)&&/^\s*$/i.test(t.textContent)&&!function(e){return c(e,r)}(t)&&!function(e){return c(e,a)}(t),e.flankingWhitespace=function(e,n){if(e.isBlock||n.preformattedCode&&e.isCode)return{leading:"",trailing:""};var t=function(e){e=e.match(/^(([ \t\r\n]*)(\s*))[\s\S]*?((\s*?)([ \t\r\n]*))$/);return{leading:e[1],leadingAscii:e[2],leadingNonAscii:e[3],trailing:e[4],trailingNonAscii:e[5],trailingAscii:e[6]}}(e.textContent);t.leadingAscii&&E("left",e,n)&&(t.leading=t.leadingNonAscii);t.trailingAscii&&E("right",e,n)&&(t.trailing=t.trailingNonAscii);return{leading:t.leading,trailing:t.trailing}}(e,n),e}function E(e,n,t){var r,i,n="left"===e?(r=n.previousSibling,/ $/):(r=n.nextSibling,/^ /);return r&&(3===r.nodeType?i=n.test(r.nodeValue):t.preformattedCode&&"CODE"===r.nodeName?i=!1:1!==r.nodeType||o(r)||(i=n.test(r.textContent))),i}var T=Array.prototype.reduce,R=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function C(e){if(!(this instanceof C))return new C(e);this.options=function(e){for(var n=1;n Date: Sun, 8 Jan 2023 21:32:42 +0800 Subject: [PATCH 03/14] chore: fmt --- src-tauri/src/app/menu.rs | 7 +++---- src-tauri/src/main.rs | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index 0f8bf18..3c0a831 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -41,10 +41,6 @@ pub fn init() -> Menu { stay_on_top }; - #[cfg(target_os = "macos")] - let titlebar = - CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); - let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); let theme_system = CustomMenuItem::new("theme_system".to_string(), "System"); @@ -62,6 +58,9 @@ pub fn init() -> Menu { popup_search }; + #[cfg(target_os = "macos")] + let titlebar = + CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); #[cfg(target_os = "macos")] let titlebar_menu = if chat_conf.titlebar { titlebar.selected() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5d9db81..3ba7221 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -45,6 +45,11 @@ async fn main() { ]) .build(), ) + .plugin(tauri_plugin_positioner::init()) + .plugin(tauri_plugin_autostart::init( + MacosLauncher::LaunchAgent, + None, + )) .invoke_handler(tauri::generate_handler![ cmd::drag_window, cmd::fullscreen, @@ -68,11 +73,6 @@ async fn main() { fs_extra::metadata, ]) .setup(setup::init) - .plugin(tauri_plugin_positioner::init()) - .plugin(tauri_plugin_autostart::init( - MacosLauncher::LaunchAgent, - None, - )) .menu(menu::init()) .system_tray(menu::tray_menu()) .on_menu_event(menu::menu_handler) From 2764219867855bb2d4049e915a9d625cbbdbe090 Mon Sep 17 00:00:00 2001 From: lencx Date: Mon, 9 Jan 2023 13:15:54 +0800 Subject: [PATCH 04/14] feat: config tray (#178) --- src-tauri/src/app/menu.rs | 18 ++++++++++++++++++ src-tauri/src/conf.rs | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index 3c0a831..4978f57 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -68,6 +68,14 @@ pub fn init() -> Menu { titlebar }; + let system_tray = + CustomMenuItem::new("system_tray".to_string(), "System Tray"); + let system_tray_menu = if chat_conf.tray { + system_tray.selected() + } else { + system_tray + }; + let preferences_menu = Submenu::new( "Preferences", Menu::with_items([ @@ -80,6 +88,7 @@ pub fn init() -> Menu { titlebar_menu.into(), #[cfg(target_os = "macos")] CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(), + system_tray_menu.into(), CustomMenuItem::new("inject_script".to_string(), "Inject Script") .accelerator("CmdOrCtrl+J") .into(), @@ -280,6 +289,15 @@ pub fn menu_handler(event: WindowMenuEvent) { .unwrap(); tauri::api::process::restart(&app.env()); } + "system_tray" => { + let chat_conf = conf::ChatConfJson::get_chat_conf(); + ChatConfJson::amend( + &serde_json::json!({ "tray": !chat_conf.tray }), + None, + ) + .unwrap(); + tauri::api::process::restart(&app.env()); + } "theme_light" | "theme_dark" | "theme_system" => { let theme = match menu_id { "theme_dark" => "Dark", diff --git a/src-tauri/src/conf.rs b/src-tauri/src/conf.rs index e2ba14a..21a204c 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -20,6 +20,7 @@ pub const DEFAULT_CHAT_CONF: &str = r#"{ "stay_on_top": false, "auto_update": "Prompt", "theme": "Light", + "tray": true, "titlebar": true, "popup_search": true, "global_shortcut": "", @@ -33,6 +34,7 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ "stay_on_top": false, "auto_update": "Prompt", "theme": "Light", + "tray": true, "titlebar": false, "popup_search": true, "global_shortcut": "", @@ -53,6 +55,7 @@ pub struct ChatConfJson { pub theme: String, // auto update policy, Prompt/Silent/Disable pub auto_update: String, + pub tray: bool, pub popup_search: bool, pub stay_on_top: bool, pub default_origin: String, From 2dfb9bac2a08c2b3bbdda95c5680c50f5faed9eb Mon Sep 17 00:00:00 2001 From: lencx Date: Tue, 10 Jan 2023 10:45:17 +0800 Subject: [PATCH 05/14] chore: export --- src-tauri/src/app/menu.rs | 9 +- src-tauri/src/app/setup.rs | 4 + src-tauri/src/main.rs | 13 +- src-tauri/src/scripts/cmd.js | 6 +- src-tauri/src/scripts/export.js | 39 ++++- src-tauri/src/scripts/markdown.export.js | 43 ++++- src-tauri/src/vendors/turndown-plugin-gfm.js | 164 +++++++++++++++++++ src/layout/index.tsx | 99 +++++------ src/main.scss | 1 + src/routes.tsx | 13 +- src/view/download/config.tsx | 38 +++++ src/view/download/index.scss | 12 ++ src/view/download/index.tsx | 38 +++++ 13 files changed, 408 insertions(+), 71 deletions(-) create mode 100644 src-tauri/src/vendors/turndown-plugin-gfm.js create mode 100644 src/view/download/config.tsx create mode 100644 src/view/download/index.scss create mode 100644 src/view/download/index.tsx diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index 4978f57..bdcabde 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -68,8 +68,7 @@ pub fn init() -> Menu { titlebar }; - let system_tray = - CustomMenuItem::new("system_tray".to_string(), "System Tray"); + let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray"); let system_tray_menu = if chat_conf.tray { system_tray.selected() } else { @@ -291,11 +290,7 @@ pub fn menu_handler(event: WindowMenuEvent) { } "system_tray" => { let chat_conf = conf::ChatConfJson::get_chat_conf(); - ChatConfJson::amend( - &serde_json::json!({ "tray": !chat_conf.tray }), - None, - ) - .unwrap(); + ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap(); tauri::api::process::restart(&app.env()); } "theme_light" | "theme_dark" | "theme_system" => { diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 3e4c8e4..b470e0c 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -66,6 +66,8 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/html2canvas.js")) .initialization_script(include_str!("../vendors/jspdf.js")) + .initialization_script(include_str!("../vendors/turndown.js")) + .initialization_script(include_str!("../vendors/turndown-plugin-gfm.js")) .initialization_script(include_str!("../scripts/core.js")) .initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(include_str!("../scripts/export.js")) @@ -89,6 +91,8 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/html2canvas.js")) .initialization_script(include_str!("../vendors/jspdf.js")) + .initialization_script(include_str!("../vendors/turndown.js")) + .initialization_script(include_str!("../vendors/turndown-plugin-gfm.js")) .initialization_script(include_str!("../scripts/core.js")) .initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(include_str!("../scripts/export.js")) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3ba7221..5dcbcee 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,7 +30,9 @@ async fn main() { trace: Color::Cyan, }; - tauri::Builder::default() + let chat_conf = ChatConfJson::get_chat_conf(); + + let mut builder = tauri::Builder::default() // https://github.com/tauri-apps/tauri/pull/2736 .plugin( LoggerBuilder::new() @@ -73,8 +75,13 @@ async fn main() { fs_extra::metadata, ]) .setup(setup::init) - .menu(menu::init()) - .system_tray(menu::tray_menu()) + .menu(menu::init()); + + if chat_conf.tray { + builder = builder.system_tray(menu::tray_menu()); + } + + builder .on_menu_event(menu::menu_handler) .on_system_tray_event(menu::tray_handler) .on_window_event(|event| { diff --git a/src-tauri/src/scripts/cmd.js b/src-tauri/src/scripts/cmd.js index 5ddc12a..b390d0a 100644 --- a/src-tauri/src/scripts/cmd.js +++ b/src-tauri/src/scripts/cmd.js @@ -71,9 +71,9 @@ $(function() { width: 20px; height: 20px; } - .chatappico.pdf { - width: 24px; - height: 24px; + .chatappico.pdf, .chatappico.md { + width: 22px; + height: 22px; } @media screen and (max-width: 767px) { #download-png-button, #download-pdf-button, #download-html-button { diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index c94a3a4..9e25ed2 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -117,6 +117,18 @@ 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 md = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); + console.log('«128» /src/scripts/export.js ~> ', md); + + }; + actionsArea.appendChild(exportMd); } function downloadThread({ as = Format.PNG } = {}) { @@ -273,6 +285,31 @@ function setIcon(type) { return { // link: ``, png: ``, - pdf: `` + pdf: ``, + md: `` }[type]; } + + +function downloadMD() { + console.log("markdown"); + const chatThread = document.querySelector('main div>div>div'); + const chatBlocks = $(chatThread, '>div'); + + console.log('«296» /src/scripts/export.js ~> ', chatThread, chatThread.innerHTML); + + + const content = new TurndownService() + .use(turndownPluginGfm.gfm) + .addRule({ + filter: function (node, options) { + return node.nodeName === 'code' && node.classList.includes('hljs') + }, + replacement: function (content) { + return '```\n' + content + '\n```' + } + }) + .turndown(chatThread.innerHTML); + + console.log('«8» /src/scripts/markdown.export.js ~> ', content); +} \ No newline at end of file diff --git a/src-tauri/src/scripts/markdown.export.js b/src-tauri/src/scripts/markdown.export.js index 4137987..651eebc 100644 --- a/src-tauri/src/scripts/markdown.export.js +++ b/src-tauri/src/scripts/markdown.export.js @@ -1,9 +1,36 @@ -// *** Core Script - Markdown *** +var ExportMD = (function () { + if (!TurndownService || !turndownPluginGfm) return; + const hljsREG = /^.*(hljs).*(language-[a-z0-9]+).*$/i; + const gfm = turndownPluginGfm.gfm + const turndownService = new TurndownService() + .use(gfm) + .addRule('code', { + filter: (node) => { + if (node.nodeName === 'CODE' && hljsREG.test(node.classList.value)) { + return 'code'; + } + }, + replacement: (content, node) => { + const classStr = node.getAttribute('class'); + if (hljsREG.test(classStr)) { + const lang = classStr.match(/.*language-(\w+)/)[1]; + if (lang) { + return `\`\`\`${lang}\n${content}\n\`\`\``; + } + return `\`\`\`\n${content}\n\`\`\``; + } + } + }) + .addRule('ignore', { + filter: ['button', 'img'], + replacement: () => '', + }) + .addRule('table', { + filter: 'table', + replacement: function(content, node) { + return `\`\`\`${content}\n\`\`\``; + }, + }); -(function () { - console.log("markdown"); - const chatThread = $('main .items-center'); - const chatBlocks = $(chatThread, '>div'); - - console.log('«8» /src/scripts/markdown.export.js ~> ', chatBlocks); -})(window); \ No newline at end of file + return turndownService; +}({})); diff --git a/src-tauri/src/vendors/turndown-plugin-gfm.js b/src-tauri/src/vendors/turndown-plugin-gfm.js new file mode 100644 index 0000000..4ab8943 --- /dev/null +++ b/src-tauri/src/vendors/turndown-plugin-gfm.js @@ -0,0 +1,164 @@ +var turndownPluginGfm = (function (exports) { + 'use strict'; + + var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/; + + function highlightedCodeBlock (turndownService) { + turndownService.addRule('highlightedCodeBlock', { + filter: function (node) { + var firstChild = node.firstChild; + return ( + node.nodeName === 'DIV' && + highlightRegExp.test(node.className) && + firstChild && + firstChild.nodeName === 'PRE' + ) + }, + replacement: function (content, node, options) { + var className = node.className || ''; + var language = (className.match(highlightRegExp) || [null, ''])[1]; + + return ( + '\n\n' + options.fence + language + '\n' + + node.firstChild.textContent + + '\n' + options.fence + '\n\n' + ) + } + }); + } + + function strikethrough (turndownService) { + turndownService.addRule('strikethrough', { + filter: ['del', 's', 'strike'], + replacement: function (content) { + return '~' + content + '~' + } + }); + } + + var indexOf = Array.prototype.indexOf; + var every = Array.prototype.every; + var rules = {}; + + rules.tableCell = { + filter: ['th', 'td'], + replacement: function (content, node) { + return cell(content, node) + } + }; + + rules.tableRow = { + filter: 'tr', + replacement: function (content, node) { + var borderCells = ''; + var alignMap = { left: ':--', right: '--:', center: ':-:' }; + + if (isHeadingRow(node)) { + for (var i = 0; i < node.childNodes.length; i++) { + var border = '---'; + var align = ( + node.childNodes[i].getAttribute('align') || '' + ).toLowerCase(); + + if (align) border = alignMap[align] || border; + + borderCells += cell(border, node.childNodes[i]); + } + } + return '\n' + content + (borderCells ? '\n' + borderCells : '') + } + }; + + rules.table = { + // Only convert tables with a heading row. + // Tables with no heading row are kept using `keep` (see below). + filter: function (node) { + return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0]) + }, + + replacement: function (content) { + // Ensure there are no blank lines + content = content.replace('\n\n', '\n'); + return '\n\n' + content + '\n\n' + } + }; + + rules.tableSection = { + filter: ['thead', 'tbody', 'tfoot'], + replacement: function (content) { + return content + } + }; + + // A tr is a heading row if: + // - the parent is a THEAD + // - or if its the first child of the TABLE or the first TBODY (possibly + // following a blank THEAD) + // - and every cell is a TH + function isHeadingRow (tr) { + var parentNode = tr.parentNode; + return ( + parentNode.nodeName === 'THEAD' || + ( + parentNode.firstChild === tr && + (parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) && + every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' }) + ) + ) + } + + function isFirstTbody (element) { + var previousSibling = element.previousSibling; + return ( + element.nodeName === 'TBODY' && ( + !previousSibling || + ( + previousSibling.nodeName === 'THEAD' && + /^\s*$/i.test(previousSibling.textContent) + ) + ) + ) + } + + function cell (content, node) { + var index = indexOf.call(node.parentNode.childNodes, node); + var prefix = ' '; + if (index === 0) prefix = '| '; + return prefix + content + ' |' + } + + function tables (turndownService) { + turndownService.keep(function (node) { + return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0]) + }); + for (var key in rules) turndownService.addRule(key, rules[key]); + } + + function taskListItems (turndownService) { + turndownService.addRule('taskListItems', { + filter: function (node) { + return node.type === 'checkbox' && node.parentNode.nodeName === 'LI' + }, + replacement: function (content, node) { + return (node.checked ? '[x]' : '[ ]') + ' ' + } + }); + } + + function gfm (turndownService) { + turndownService.use([ + highlightedCodeBlock, + strikethrough, + tables, + taskListItems + ]); + } + + exports.gfm = gfm; + exports.highlightedCodeBlock = highlightedCodeBlock; + exports.strikethrough = strikethrough; + exports.tables = tables; + exports.taskListItems = taskListItems; + + return exports; +}({})); \ No newline at end of file diff --git a/src/layout/index.tsx b/src/layout/index.tsx index bbc7dc9..d5397f9 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import {Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd'; +import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd'; import { SyncOutlined } from '@ant-design/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { getName, getVersion } from '@tauri-apps/api/app'; @@ -29,58 +29,61 @@ export default function ChatLayout() { await invoke('run_check_update', { silent: false, hasMsg: true }); } - return ( - - - setCollapsed(value)} - style={{ - overflow: 'auto', - height: '100vh', - position: 'fixed', - left: 0, - top: 0, - bottom: 0, - zIndex: 999, - }} - > -
-
- {appInfo.appName} - - {appInfo.appVersion} - - - - -
+ const isDark = appInfo.appTheme === "dark"; - go(i.key)} - /> - - - + + setCollapsed(value)} style={{ - overflow: 'inherit' + overflow: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + zIndex: 999, }} > - - - +
+
+ {appInfo.appName} + + {appInfo.appVersion} + + + + +
+ + go(i.key)} + /> + + + + + + + - ); }; \ No newline at end of file diff --git a/src/main.scss b/src/main.scss index afba087..9bb6a33 100644 --- a/src/main.scss +++ b/src/main.scss @@ -51,6 +51,7 @@ html, body { } } +.chat-file-path, .chat-sync-path { font-size: 12px; font-weight: 500; diff --git a/src/routes.tsx b/src/routes.tsx index d7b0819..b914935 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -5,6 +5,7 @@ import { SyncOutlined, FileSyncOutlined, UserOutlined, + DownloadOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; @@ -13,6 +14,7 @@ import UserCustom from '@/view/model/UserCustom'; import SyncPrompts from '@/view/model/SyncPrompts'; import SyncCustom from '@/view/model/SyncCustom'; import SyncRecord from '@/view/model/SyncRecord'; +import Download from '@/view/download'; export type ChatRouteMetaObject = { label: string; @@ -36,6 +38,14 @@ export const routes: Array = [ icon: , }, }, + { + path: 'download', + element: , + meta: { + label: 'Download', + icon: , + }, + }, { path: '/model', meta: { @@ -51,6 +61,7 @@ export const routes: Array = [ icon: , }, }, + // --- Sync { path: 'sync-prompts', element: , @@ -72,7 +83,7 @@ export const routes: Array = [ element: , hideMenu: true, }, - ] + ], }, ]; diff --git a/src/view/download/config.tsx b/src/view/download/config.tsx new file mode 100644 index 0000000..2bd47a5 --- /dev/null +++ b/src/view/download/config.tsx @@ -0,0 +1,38 @@ +import { Switch, Tag, Tooltip, Space, Popconfirm } from 'antd'; + +export const syncColumns = () => [ + { + title: 'Name', + dataIndex: 'name', + fixed: 'left', + // width: 120, + key: 'name', + }, + { + title: 'Type', + dataIndex: 'type', + key: 'type', + render: () => { + return {}; + } + // width: 200, + }, + { + title: 'Action', + render: (_: any, row: any, actions: any) => { + return ( + + View + actions.setRecord(row, 'delete')} + okText="Yes" + cancelText="No" + > + Delete + + + ) + } + } +]; diff --git a/src/view/download/index.scss b/src/view/download/index.scss new file mode 100644 index 0000000..4e3ba63 --- /dev/null +++ b/src/view/download/index.scss @@ -0,0 +1,12 @@ +.chat-table-tip, .chat-table-btns { + display: flex; + justify-content: space-between; +} + +.chat-table-btns { + margin-bottom: 5px; + + .num { + margin-left: 10px; + } +} diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx new file mode 100644 index 0000000..86d1d39 --- /dev/null +++ b/src/view/download/index.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { Table } from 'antd'; +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 { syncColumns } from './config'; +import './index.scss'; + +export default function SyncPrompts() { + const { rowSelection, selectedRowIDs } = useTable(); + const [downloadPath, setDownloadPath] = useState(''); + const { columns, ...opInfo } = useColumns(syncColumns()); + + useInit(async () => { + setDownloadPath(await path.join(await chatRoot(), 'download')); + }); + + return ( +
+ + + + ) +} \ No newline at end of file From 7446cfc186147448855a05b4bf678059a9f8890a Mon Sep 17 00:00:00 2001 From: lencx Date: Wed, 11 Jan 2023 00:40:44 +0800 Subject: [PATCH 06/14] chore: export markdown --- src-tauri/src/app/cmd.rs | 7 +++++++ src-tauri/src/main.rs | 1 + src-tauri/src/scripts/export.js | 31 +++---------------------------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 3d656fe..5472e38 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -40,6 +40,13 @@ pub fn download(_app: AppHandle, name: String, blob: Vec) { utils::open_file(path); } +#[command] +pub fn save_file(_app: AppHandle, name: String, content: String) { + let path = api::path::download_dir().unwrap().join(name); + fs::write(&path, content).unwrap(); + utils::open_file(path); +} + #[command] pub fn open_link(app: AppHandle, url: String) { api::shell::open(&app.shell_scope(), url, None).unwrap(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5dcbcee..4f96dd6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -56,6 +56,7 @@ async fn main() { cmd::drag_window, cmd::fullscreen, cmd::download, + cmd::save_file, cmd::open_link, cmd::get_chat_conf, cmd::get_theme, diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index 9e25ed2..02deaf9 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -124,9 +124,8 @@ function addActionsButtons(actionsArea, TryAgainButton) { exportMd.title = "Download Markdown"; exportMd.innerHTML = setIcon('md'); exportMd.onclick = () => { - const md = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); - console.log('«128» /src/scripts/export.js ~> ', md); - + const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); + invoke('save_file', { name: `chatgpt-${Date.now()}.md`, content: data }); }; actionsArea.appendChild(exportMd); } @@ -160,7 +159,7 @@ function handleImg(imgData) { 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)) }); + invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: data }); } function handlePdf(imgData, canvas, pixelRatio) { @@ -289,27 +288,3 @@ function setIcon(type) { md: `` }[type]; } - - -function downloadMD() { - console.log("markdown"); - const chatThread = document.querySelector('main div>div>div'); - const chatBlocks = $(chatThread, '>div'); - - console.log('«296» /src/scripts/export.js ~> ', chatThread, chatThread.innerHTML); - - - const content = new TurndownService() - .use(turndownPluginGfm.gfm) - .addRule({ - filter: function (node, options) { - return node.nodeName === 'code' && node.classList.includes('hljs') - }, - replacement: function (content) { - return '```\n' + content + '\n```' - } - }) - .turndown(chatThread.innerHTML); - - console.log('«8» /src/scripts/markdown.export.js ~> ', content); -} \ No newline at end of file From e473268df139f02c13d4ffa31d0f5c252b90dffe Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 13 Jan 2023 01:18:40 +0800 Subject: [PATCH 07/14] chore: export --- src-tauri/Cargo.toml | 12 +++-- src-tauri/src/app/cmd.rs | 83 +++++++++++++++++++++++++++++++-- src-tauri/src/app/fs_extra.rs | 4 +- src-tauri/src/main.rs | 3 ++ src-tauri/src/scripts/export.js | 44 +++++++++++------ src-tauri/src/utils.rs | 20 ++++++++ src/view/download/config.tsx | 7 +++ src/view/download/index.tsx | 10 ++-- 8 files changed, 153 insertions(+), 30 deletions(-) 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} /> From 08ef0a2437296ab84594152feca4d42847c9fce5 Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 13 Jan 2023 11:47:40 +0800 Subject: [PATCH 08/14] fix: export (#192) --- src-tauri/src/scripts/export.js | 79 +++------------------------------ 1 file changed, 7 insertions(+), 72 deletions(-) diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index a8cfaf8..50c1078 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -1,5 +1,4 @@ // *** Core Script - Export *** -// @ref: https://github.com/liady/ChatGPT-pdf const buttonOuterHTMLFallback = ``; @@ -50,14 +49,17 @@ function shouldAddButtons(actionsArea) { const buttons = actionsArea.querySelectorAll("button"); const hasTryAgainButton = Array.from(buttons).some((button) => { - return !button.id?.includes("download"); + return !/download-/.test(button.id); }); - // fix: https://github.com/lencx/ChatGPT/issues/189 - if (buttons.length === 1) { + if (/Stop generating/ig.test(buttons[0].innerText)) { return false; } + if (buttons.length === 2 && (/Regenerate response/ig.test(buttons[0].innerText) || buttons[1].innerText === '')) { + return true; + } + if (hasTryAgainButton && buttons.length === 1) { return true; } @@ -82,7 +84,6 @@ 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(); } @@ -92,9 +93,6 @@ function removeButtons() { if (downloadPdfButton) { downloadMdButton.remove(); } - // if (downloadHtmlButton) { - // downloadHtmlButton.remove(); - // } } function addActionsButtons(actionsArea, TryAgainButton) { @@ -113,7 +111,6 @@ function addActionsButtons(actionsArea, TryAgainButton) { // Generate PNG downloadButton.id = "download-png-button"; downloadButton.setAttribute("share-ext", "true"); - // downloadButton.innerText = "Generate PNG"; downloadButton.title = "Generate PNG"; downloadButton.innerHTML = setIcon('png'); downloadButton.onclick = () => { @@ -125,25 +122,12 @@ function addActionsButtons(actionsArea, TryAgainButton) { const downloadPdfButton = TryAgainButton.cloneNode(true); downloadPdfButton.id = "download-pdf-button"; downloadButton.setAttribute("share-ext", "true"); - // downloadPdfButton.innerText = "Download PDF"; downloadPdfButton.title = "Download PDF"; downloadPdfButton.innerHTML = setIcon('pdf'); downloadPdfButton.onclick = () => { downloadThread({ as: Format.PDF }); }; actionsArea.appendChild(downloadPdfButton); - - // fix: https://github.com/lencx/ChatGPT/issues/126 - // const exportHtml = TryAgainButton.cloneNode(true); - // exportHtml.id = "download-html-button"; - // downloadButton.setAttribute("share-ext", "true"); - // // exportHtml.innerText = "Share Link"; - // exportHtml.title = "Share Link"; - // exportHtml.innerHTML = setIcon('link'); - // exportHtml.onclick = () => { - // sendRequest(); - // }; - // actionsArea.appendChild(exportHtml); } async function exportMarkdown() { @@ -213,9 +197,7 @@ class Elements { // fix: old chat https://github.com/lencx/ChatGPT/issues/185 if (!this.thread) { - this.thread = document.querySelector( - "main .overflow-y-auto" - ); + this.thread = document.querySelector("main .overflow-y-auto"); } // h-full overflow-y-auto @@ -271,53 +253,6 @@ class Elements { } } -function selectElementByClassPrefix(classPrefix) { - const element = document.querySelector(`[class^='${classPrefix}']`); - return element; -} - -// async function sendRequest() { -// const data = getData(); -// const uploadUrlResponse = await fetch( -// "https://chatgpt-static.s3.amazonaws.com/url.txt" -// ); -// const uploadUrl = await uploadUrlResponse.text(); -// fetch(uploadUrl, { -// method: "POST", -// headers: { -// "Content-Type": "application/json", -// }, -// body: JSON.stringify(data), -// }) -// .then((response) => response.json()) -// .then((data) => { -// invoke('open_link', { url: data.url }); -// }); -// } - -// function getData() { -// const globalCss = getCssFromSheet( -// document.querySelector("link[rel=stylesheet]").sheet -// ); -// const localCss = -// getCssFromSheet( -// document.querySelector(`style[data-styled][data-styled-version]`).sheet -// ) || "body{}"; -// const data = { -// main: document.querySelector("main").outerHTML, -// // css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`, -// globalCss, -// localCss, -// }; -// return data; -// } - -// function getCssFromSheet(sheet) { -// return Array.from(sheet.cssRules) -// .map((rule) => rule.cssText) -// .join(""); -// } - function setIcon(type) { return { // link: ``, From bd5f34b7f9fc4a8d6765265db7eebba41d1bb216 Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 13 Jan 2023 12:18:49 +0800 Subject: [PATCH 09/14] readme --- README-ZH_CN.md | 8 ++++++-- README.md | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README-ZH_CN.md b/README-ZH_CN.md index 19738e9..20be4dc 100644 --- a/README-ZH_CN.md +++ b/README-ZH_CN.md @@ -199,8 +199,9 @@ Mac 上无法安装,提示开发者未验证,具体可以查看下面给出 #### 预安装 -- [Rust](https://www.rust-lang.org/) -- [VS Code](https://code.visualstudio.com/) +- [Rust (必须)](https://www.rust-lang.org/) +- [Node.js (必须)](https://nodejs.org/) +- [VS Code (可选)](https://code.visualstudio.com/) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) @@ -224,6 +225,9 @@ yarn dev yarn build ``` +- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180) +- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182) + ## ❤️ 感谢 - 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改 diff --git a/README.md b/README.md index c153709..ea1ce15 100644 --- a/README.md +++ b/README.md @@ -207,8 +207,9 @@ It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website, #### PreInstall -- [Rust](https://www.rust-lang.org/) -- [VS Code](https://code.visualstudio.com/) +- [Rust (Required)](https://www.rust-lang.org/) +- [Node.js (Required)](https://nodejs.org/) +- [VS Code (Optional)](https://code.visualstudio.com/) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) @@ -232,6 +233,9 @@ yarn dev yarn build ``` +- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180) +- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182) + ## ❤️ Thanks - The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications. From a2fcfa3b89acbbeccb16267d43075c39fc81ec9d Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 13 Jan 2023 23:59:38 +0800 Subject: [PATCH 10/14] chore: export --- src-tauri/src/app/cmd.rs | 33 +++++++++++++---------- src-tauri/src/main.rs | 3 ++- src/hooks/useJson.ts | 17 ++++++++++++ src/utils.ts | 1 + src/view/download/config.tsx | 29 +++++++++++++------- src/view/download/index.tsx | 51 +++++++++++++++++++++++++++++------- 6 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 src/hooks/useJson.ts diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index b90fad3..c27fd56 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -185,24 +185,30 @@ pub struct FileMetadata { pub id: String, } -#[command] -pub fn download_list(filename: Option, id: Option) { - info!("download_list"); - let download_path = chat_root().join("chat.download.json"); +#[tauri::command] +pub fn get_download_list(pathname: &str) -> (Vec, PathBuf) { + info!("get_download_list: {}", pathname); + let download_path = chat_root().join(PathBuf::from(pathname)); let content = fs::read_to_string(&download_path).unwrap_or_else(|err| { info!("download_list_error: {}", err); fs::write(&download_path, "[]").unwrap(); "[]".to_string() }); - let mut list = serde_json::from_str::>(&content) - .unwrap_or_else(|err| { - info!("download_list_parse_error: {}", err); - vec![] - }); + let list = serde_json::from_str::>(&content).unwrap_or_else(|err| { + info!("download_list_parse_error: {}", err); + vec![] + }); - let list2 = &list; + (list, download_path) +} + +#[command] +pub fn download_list(pathname: &str, filename: Option, id: Option) { + info!("download_list: {}", pathname); + let data = get_download_list(pathname); + let mut list = vec![]; let mut my_hashmap = HashMap::new(); - utils::vec_to_hashmap(list2.clone().into_iter(), "id", &mut my_hashmap); + utils::vec_to_hashmap(data.0.into_iter(), "id", &mut my_hashmap); for entry in WalkDir::new(utils::chat_root().join("download")) .into_iter() @@ -236,15 +242,14 @@ pub fn download_list(filename: Option, id: Option) { } } - dbg!(&list); - + // 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(); + fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap(); } #[command] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 08ddcf3..8c84394 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,7 +30,7 @@ async fn main() { trace: Color::Cyan, }; - cmd::download_list(None, None); + cmd::download_list("chat.download.json", None, None); let chat_conf = ChatConfJson::get_chat_conf(); @@ -76,6 +76,7 @@ async fn main() { cmd::dalle2_window, cmd::cmd_list, cmd::download_list, + cmd::get_download_list, fs_extra::metadata, ]) .setup(setup::init) diff --git a/src/hooks/useJson.ts b/src/hooks/useJson.ts new file mode 100644 index 0000000..a06873a --- /dev/null +++ b/src/hooks/useJson.ts @@ -0,0 +1,17 @@ +import { useState } from 'react'; + +import { readJSON } from '@/utils'; +import useInit from '@/hooks/useInit'; + +export default function useJson(file: string) { + const [json, setData] = useState(); + + const refreshJson = async () => { + const data = await readJSON(file); + setData(data); + }; + + useInit(refreshJson); + + return { json, refreshJson }; +} diff --git a/src/utils.ts b/src/utils.ts index e983604..09a0648 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,7 @@ import dayjs from 'dayjs'; export const CHAT_MODEL_JSON = 'chat.model.json'; export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json'; +export const CHAT_DOWNLOAD_JSON = 'chat.download.json'; export const CHAT_PROMPTS_CSV = 'chat.prompts.csv'; export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv'; export const DISABLE_AUTO_COMPLETE = { diff --git a/src/view/download/config.tsx b/src/view/download/config.tsx index c164b11..dc915f4 100644 --- a/src/view/download/config.tsx +++ b/src/view/download/config.tsx @@ -1,28 +1,37 @@ -import { Switch, Tag, Tooltip, Space, Popconfirm } from 'antd'; +import { Tag, Space, Popconfirm } from 'antd'; + +import { fmtDate } from '@/utils'; + +const colorMap: any = { + pdf: 'blue', + png: 'orange', +} export const syncColumns = () => [ { title: 'Name', dataIndex: 'name', fixed: 'left', - // width: 120, key: 'name', }, { - title: 'Type', - dataIndex: 'type', - key: 'type', - render: () => { - return {}; - } - // width: 200, + title: 'Extension', + dataIndex: 'ext', + key: 'ext', + render: (v: string) => {v}, + }, + { + title: 'Created', + dataIndex: 'created', + key: 'created', + render: fmtDate, }, { title: 'Action', render: (_: any, row: any, actions: any) => { return ( - View + actions.setRecord(row, 'view')}>View actions.setRecord(row, 'delete')} diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx index 03ce0d2..324cc98 100644 --- a/src/view/download/index.tsx +++ b/src/view/download/index.tsx @@ -1,27 +1,52 @@ -import { useState } from 'react'; -import { Table } from 'antd'; -import { path, shell } from '@tauri-apps/api'; +import { useEffect, useState } from 'react'; +import { Table, Modal } from 'antd'; +import { path, shell, fs } from '@tauri-apps/api'; import useInit from '@/hooks/useInit'; +import useJson from '@/hooks/useJson'; import useColumns from '@/hooks/useColumns'; import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; -import { chatRoot, readJSON } from '@/utils'; +import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils'; import { syncColumns } from './config'; import './index.scss'; +function renderFile(buff: Uint8Array, type: string) { + const renderType = { + pdf: 'application/pdf', + png: 'image/png', + }[type]; + return URL.createObjectURL(new Blob([buff], { type: renderType })); +} + export default function SyncPrompts() { const { rowSelection, selectedRowIDs } = useTable(); - const [downloadPath, setDownloadPath] = useState(''); - const [downloadData, setDownloadData] = useState([]); const { columns, ...opInfo } = useColumns(syncColumns()); + const [downloadPath, setDownloadPath] = useState(''); + const { json } = useJson(CHAT_DOWNLOAD_JSON); + const [source, setSource] = useState(''); + const [isVisible, setVisible] = useState(false); useInit(async () => { const file = await path.join(await chatRoot(), 'chat.download.json'); setDownloadPath(file); - const data = await readJSON(file, { isRoot: true, isList: true }); - setDownloadData(data); }); + useEffect(() => { + if (!opInfo.opType) return; + (async () => { + const record = opInfo?.opRecord; + const isImg = ['png'].includes(record?.ext); + const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`); + if (opInfo.opType === 'view') { + const data = await fs.readBinaryFile(file); + const sourceData = renderFile(data, record?.ext); + setSource(sourceData); + setVisible(true); + } + opInfo.resetRecord(); + })() + }, [opInfo.opType]) + return (
@@ -33,10 +58,18 @@ export default function SyncPrompts() { rowKey="name" columns={columns} scroll={{ x: 'auto' }} - dataSource={downloadData} + dataSource={json} rowSelection={rowSelection} pagination={TABLE_PAGINATION} /> + setVisible(false)} + footer={false} + destroyOnClose + > + +
) } \ No newline at end of file From ae2c56805c6d8f849d692f430e8ad1fda9802973 Mon Sep 17 00:00:00 2001 From: lencx Date: Sat, 14 Jan 2023 23:31:12 +0800 Subject: [PATCH 11/14] chore: export --- src-tauri/src/app/cmd.rs | 18 +++- src-tauri/src/scripts/export.js | 25 +++-- src/hooks/{useColumns.ts => useColumns.tsx} | 43 ++++++++- src/hooks/useJson.ts | 9 +- src/hooks/useTable.tsx | 33 +++++-- src/main.scss | 19 +++- src/view/download/config.tsx | 44 +++++++-- src/view/download/index.scss | 12 --- src/view/download/index.tsx | 100 +++++++++++++++++--- src/view/model/SyncPrompts/config.tsx | 5 +- src/view/model/SyncPrompts/index.tsx | 5 +- src/view/model/SyncRecord/config.tsx | 5 +- src/view/model/SyncRecord/index.tsx | 5 +- src/view/model/UserCustom/config.tsx | 5 +- src/view/model/UserCustom/index.tsx | 5 +- 15 files changed, 264 insertions(+), 69 deletions(-) rename src/hooks/{useColumns.ts => useColumns.tsx} (52%) delete mode 100644 src/view/download/index.scss diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index c27fd56..6369ac1 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -207,8 +207,8 @@ pub fn download_list(pathname: &str, filename: Option, id: Option, id: Option { + file_data.name = v.clone(); + v + } + _ => "".to_string(), + }; + } + + if filename.is_some() && id.is_some() { if let Some(ref v) = id { if fid == v { if let Some(ref v2) = filename { diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index 50c1078..d5af93b 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -52,11 +52,13 @@ function shouldAddButtons(actionsArea) { return !/download-/.test(button.id); }); - if (/Stop generating/ig.test(buttons[0].innerText)) { + const stopBtn = buttons?.[0]?.innerText; + + if (/Stop generating/ig.test(stopBtn)) { return false; } - if (buttons.length === 2 && (/Regenerate response/ig.test(buttons[0].innerText) || buttons[1].innerText === '')) { + if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) { return true; } @@ -132,7 +134,7 @@ function addActionsButtons(actionsArea, TryAgainButton) { 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 }); + await invoke('save_file', { name: `notes/${uid().toString(36)}.md`, content: data }); } function downloadThread({ as = Format.PNG } = {}) { @@ -164,8 +166,9 @@ async function handleImg(imgData) { for (let i = 0; i < binaryData.length; i++) { data.push(binaryData.charCodeAt(i)); } - await invoke('download', { name: `download/img/${Date.now().toString(36)}.png`, blob: data }); - await invoke('download_list'); + const { pathname, id, filename } = getName(); + await invoke('download', { name: `download/img/${id}.png`, blob: data }); + await invoke('download_list', { pathname, filename, id }); } async function handlePdf(imgData, canvas, pixelRatio) { @@ -178,10 +181,16 @@ async function handlePdf(imgData, canvas, pixelRatio) { var pdfWidth = pdf.internal.pageSize.getWidth(); var pdfHeight = pdf.internal.pageSize.getHeight(); pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); - + const { pathname, id, filename } = getName(); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); - await invoke('download', { name: `download/pdf/${Date.now().toString(36)}.pdf`, blob: Array.from(new Uint8Array(data)) }); - await invoke('download_list'); + await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download_list', { pathname, filename, id }); +} + +function getName() { + const id = uid().toString(36); + const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || ''; + return { filename: name ? name : id, id, pathname: 'chat.download.json' }; } class Elements { diff --git a/src/hooks/useColumns.ts b/src/hooks/useColumns.tsx similarity index 52% rename from src/hooks/useColumns.ts rename to src/hooks/useColumns.tsx index ed6c19a..acc0dfa 100644 --- a/src/hooks/useColumns.ts +++ b/src/hooks/useColumns.tsx @@ -1,4 +1,7 @@ -import { useState, useCallback } from 'react'; +import { FC, useState, useCallback } from 'react'; +import { Input } from 'antd'; + +import { DISABLE_AUTO_COMPLETE } from '@/utils'; export default function useColumns(columns: any[] = []) { const [opType, setOpType] = useState(''); @@ -41,4 +44,40 @@ export default function useColumns(columns: any[] = []) { setExtra, opExtra, }; -} \ No newline at end of file +} + +interface EditRowProps { + rowKey: string; + row: Record; + actions: any; +} +export const EditRow: FC = ({ rowKey, row, actions }) => { + const [isEdit, setEdit] = useState(false); + const [val, setVal] = useState(row[rowKey]); + const handleEdit = () => { + setEdit(true); + }; + const handleChange = (e: React.ChangeEvent) => { + setVal(e.target.value) + }; + + const handleSave = () => { + setEdit(false); + row[rowKey] = val; + actions?.setRecord(row, 'rowedit') + }; + + return isEdit + ? ( + + ) + : ( +
{val}
+ ); +}; diff --git a/src/hooks/useJson.ts b/src/hooks/useJson.ts index a06873a..00a5bd8 100644 --- a/src/hooks/useJson.ts +++ b/src/hooks/useJson.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { readJSON } from '@/utils'; +import { readJSON, writeJSON } from '@/utils'; import useInit from '@/hooks/useInit'; export default function useJson(file: string) { @@ -11,7 +11,12 @@ export default function useJson(file: string) { setData(data); }; + const updateJson = async (data: any) => { + await writeJSON(file, data); + await refreshJson(); + }; + useInit(refreshJson); - return { json, refreshJson }; + return { json, refreshJson, updateJson }; } diff --git a/src/hooks/useTable.tsx b/src/hooks/useTable.tsx index 741ab8b..4fc2c65 100644 --- a/src/hooks/useTable.tsx +++ b/src/hooks/useTable.tsx @@ -4,14 +4,35 @@ import type { TableRowSelection } from 'antd/es/table/interface'; import { safeKey } from '@/hooks/useData'; -export default function useTableRowSelection() { +type rowSelectionOptions = { + key: 'id' | string; + rowType: 'id' | 'row' | 'all'; +} +export function useTableRowSelection(options: Partial = {}) { + const { key = 'id', rowType = 'id' } = options; const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowIDs, setSelectedRowIDs] = useState([]); + const [selectedRows, setSelectedRows] = useState[]>([]); - const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record) => { - const keys = selectedRows.map((i: any) => i[safeKey]); - setSelectedRowIDs(keys); + const onSelectChange = (newSelectedRowKeys: React.Key[], newSelectedRows: Record[]) => { + const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]); setSelectedRowKeys(newSelectedRowKeys); + if (rowType === 'id') { + setSelectedRowIDs(keys); + } + if (rowType === 'row') { + setSelectedRows(newSelectedRows); + } + if (rowType === 'all') { + setSelectedRowIDs(keys); + setSelectedRows(newSelectedRows); + } + }; + + const rowReset = () => { + setSelectedRowKeys([]); + setSelectedRowIDs([]); + setSelectedRows([]); }; const rowSelection: TableRowSelection> = { @@ -24,14 +45,14 @@ export default function useTableRowSelection() { ], }; - return { rowSelection, selectedRowIDs }; + return { rowSelection, selectedRowIDs, selectedRows, rowReset }; } export const TABLE_PAGINATION = { hideOnSinglePage: true, showSizeChanger: true, showQuickJumper: true, - defaultPageSize: 5, + defaultPageSize: 10, pageSizeOptions: [5, 10, 15, 20], showTotal: (total: number) => Total {total} items, }; \ No newline at end of file diff --git a/src/main.scss b/src/main.scss index 9bb6a33..a2eabaa 100644 --- a/src/main.scss +++ b/src/main.scss @@ -31,10 +31,27 @@ html, body { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +.ellipsis-line { + display: inline-block; + width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rowedit { + padding: 2px 5px; + + &:hover { + box-shadow: 0 0 2px rgba(237, 122, 60, 0.8); + border-radius: 4px; + } +} + .chat-add-btn { margin-bottom: 5px; } diff --git a/src/view/download/config.tsx b/src/view/download/config.tsx index dc915f4..274c208 100644 --- a/src/view/download/config.tsx +++ b/src/view/download/config.tsx @@ -1,37 +1,57 @@ +import { useState } from 'react'; import { Tag, Space, Popconfirm } from 'antd'; +import { path, shell } from '@tauri-apps/api'; -import { fmtDate } from '@/utils'; +import { EditRow } from '@/hooks/useColumns'; + +import useInit from '@/hooks/useInit'; +import { fmtDate, chatRoot } from '@/utils'; const colorMap: any = { pdf: 'blue', png: 'orange', } -export const syncColumns = () => [ +export const downloadColumns = () => [ { title: 'Name', dataIndex: 'name', fixed: 'left', key: 'name', + width: 240, + render: (_: string, row: any, actions: any) => ( + + ), }, { title: 'Extension', dataIndex: 'ext', key: 'ext', + width: 120, render: (v: string) => {v}, }, + { + title: 'Path', + dataIndex: 'path', + key: 'path', + width: 200, + render: (_: string, row: any) => , + }, { title: 'Created', dataIndex: 'created', key: 'created', + width: 150, render: fmtDate, }, { title: 'Action', + fixed: 'right', + width: 150, render: (_: any, row: any, actions: any) => { return ( - actions.setRecord(row, 'view')}>View + actions.setRecord(row, 'preview')}>Preview actions.setRecord(row, 'delete')} @@ -46,9 +66,15 @@ export const syncColumns = () => [ } ]; -// { -// id: '', -// name: '', -// type: '.png', -// created: '2022.01.01', -// } \ No newline at end of file +const RenderPath = ({ row }: any) => { + const [filePath, setFilePath] = useState(''); + useInit(async () => { + setFilePath(await getPath(row)); + }) + return shell.open(filePath)}>{filePath}; +}; + +export const getPath = async (row: any) => { + const isImg = ['png'].includes(row?.ext); + return await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id) + `.${row.ext}`; +} diff --git a/src/view/download/index.scss b/src/view/download/index.scss deleted file mode 100644 index 4e3ba63..0000000 --- a/src/view/download/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -.chat-table-tip, .chat-table-btns { - display: flex; - justify-content: space-between; -} - -.chat-table-btns { - margin-bottom: 5px; - - .num { - margin-left: 10px; - } -} diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx index 324cc98..ae3909e 100644 --- a/src/view/download/index.tsx +++ b/src/view/download/index.tsx @@ -1,14 +1,14 @@ import { useEffect, useState } from 'react'; -import { Table, Modal } from 'antd'; -import { path, shell, fs } from '@tauri-apps/api'; +import { Table, Modal, Popconfirm, Button, message } from 'antd'; +import { invoke, path, shell, fs } from '@tauri-apps/api'; import useInit from '@/hooks/useInit'; import useJson from '@/hooks/useJson'; +import useData from '@/hooks/useData'; import useColumns from '@/hooks/useColumns'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils'; -import { syncColumns } from './config'; -import './index.scss'; +import { downloadColumns } from './config'; function renderFile(buff: Uint8Array, type: string) { const renderType = { @@ -19,52 +19,124 @@ function renderFile(buff: Uint8Array, type: string) { } export default function SyncPrompts() { - const { rowSelection, selectedRowIDs } = useTable(); - const { columns, ...opInfo } = useColumns(syncColumns()); const [downloadPath, setDownloadPath] = useState(''); - const { json } = useJson(CHAT_DOWNLOAD_JSON); const [source, setSource] = useState(''); const [isVisible, setVisible] = useState(false); + const { opData, opInit, opReplace, opSafeKey } = useData([]); + const { columns, ...opInfo } = useColumns(downloadColumns()); + const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' }); + const { json, refreshJson, updateJson } = useJson(CHAT_DOWNLOAD_JSON); + const selectedItems = rowSelection.selectedRowKeys || []; useInit(async () => { - const file = await path.join(await chatRoot(), 'chat.download.json'); + const file = await path.join(await chatRoot(), CHAT_DOWNLOAD_JSON); setDownloadPath(file); }); + useEffect(() => { + if (!json || json.length <= 0) return; + opInit(json); + }, [json?.length]); + useEffect(() => { if (!opInfo.opType) return; (async () => { const record = opInfo?.opRecord; const isImg = ['png'].includes(record?.ext); const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`); - if (opInfo.opType === 'view') { + if (opInfo.opType === 'preview') { const data = await fs.readBinaryFile(file); const sourceData = renderFile(data, record?.ext); setSource(sourceData); setVisible(true); + return; + } + if (opInfo.opType === 'file') { + await shell.open(file); + } + if (opInfo.opType === 'delete') { + await fs.removeFile(file); + await handleRefresh(); + } + if (opInfo.opType === 'rowedit') { + const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord); + await updateJson(data); + message.success('Name has been changed!'); } opInfo.resetRecord(); })() }, [opInfo.opType]) + const handleDelete = async () => { + if (opData?.length === selectedRows.length) { + const downloadDir = await path.join(await chatRoot(), 'download'); + await fs.removeDir(downloadDir, { recursive: true }); + await handleRefresh(); + rowReset(); + message.success('All files have been cleared!'); + return; + } + + const rows = selectedRows.map(async (i) => { + const isImg = ['png'].includes(i?.ext); + const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : i?.ext, `${i?.id}.${i?.ext}`); + await fs.removeFile(file); + return file; + }) + Promise.all(rows).then(async () => { + await handleRefresh(); + message.success('All files selected are cleared!'); + }); + }; + + const handleRefresh = async () => { + await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON }); + refreshJson(); + }; + + const handleCancel = () => { + setVisible(false); + opInfo.resetRecord(); + }; + return (
+
+
+ {selectedItems.length > 0 && ( + <> + + + + Selected {selectedItems.length} items + + )} +
+
setVisible(false)} + title={
{opInfo?.opRecord?.name || ''}
} + onCancel={handleCancel} footer={false} destroyOnClose > diff --git a/src/view/model/SyncPrompts/config.tsx b/src/view/model/SyncPrompts/config.tsx index 32fbf6a..b8b4ade 100644 --- a/src/view/model/SyncPrompts/config.tsx +++ b/src/view/model/SyncPrompts/config.tsx @@ -1,4 +1,4 @@ -import { Switch, Tag, Tooltip } from 'antd'; +import { Table, Switch, Tag } from 'antd'; import { genCmd } from '@/utils'; @@ -35,13 +35,14 @@ export const syncColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', // width: 300, render: (v: string) => ( - {v} + {v} ), }, ]; diff --git a/src/view/model/SyncPrompts/index.tsx b/src/view/model/SyncPrompts/index.tsx index d5a359e..7db4d2e 100644 --- a/src/view/model/SyncPrompts/index.tsx +++ b/src/view/model/SyncPrompts/index.tsx @@ -6,7 +6,7 @@ import useInit from '@/hooks/useInit'; import useData from '@/hooks/useData'; import useColumns from '@/hooks/useColumns'; import useChatModel, { useCacheModel } from '@/hooks/useChatModel'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { fmtDate, chatRoot } from '@/utils'; import { syncColumns } from './config'; import './index.scss'; @@ -14,7 +14,7 @@ import './index.scss'; const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv'; export default function SyncPrompts() { - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const [jsonPath, setJsonPath] = useState(''); const { modelJson, modelSet } = useChatModel('sync_prompts'); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); @@ -93,6 +93,7 @@ export default function SyncPrompts() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> ) diff --git a/src/view/model/SyncRecord/config.tsx b/src/view/model/SyncRecord/config.tsx index ebd2609..71321ca 100644 --- a/src/view/model/SyncRecord/config.tsx +++ b/src/view/model/SyncRecord/config.tsx @@ -1,4 +1,4 @@ -import { Switch, Tag, Tooltip } from 'antd'; +import { Switch, Tag, Table } from 'antd'; import { genCmd } from '@/utils'; @@ -37,13 +37,14 @@ export const syncColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', // width: 300, render: (v: string) => ( - {v} + {v} ), }, ]; diff --git a/src/view/model/SyncRecord/index.tsx b/src/view/model/SyncRecord/index.tsx index f3e64dc..9f22759 100644 --- a/src/view/model/SyncRecord/index.tsx +++ b/src/view/model/SyncRecord/index.tsx @@ -7,7 +7,7 @@ import { shell, path } from '@tauri-apps/api'; import useColumns from '@/hooks/useColumns'; import useData from '@/hooks/useData'; import { useCacheModel } from '@/hooks/useChatModel'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { fmtDate, chatRoot } from '@/utils'; import { getPath } from '@/view/model/SyncCustom/config'; import { syncColumns } from './config'; @@ -19,7 +19,7 @@ export default function SyncRecord() { const [jsonPath, setJsonPath] = useState(''); const state = location?.state; - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]); const { columns, ...opInfo } = useColumns(syncColumns()); @@ -79,6 +79,7 @@ export default function SyncRecord() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> ) diff --git a/src/view/model/UserCustom/config.tsx b/src/view/model/UserCustom/config.tsx index c023079..0450043 100644 --- a/src/view/model/UserCustom/config.tsx +++ b/src/view/model/UserCustom/config.tsx @@ -1,4 +1,4 @@ -import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd'; +import { Tag, Switch, Space, Popconfirm, Table } from 'antd'; export const modelColumns = () => [ { @@ -33,13 +33,14 @@ export const modelColumns = () => [ action.setRecord({ ...row, enable: v }, 'enable')} /> ), }, + Table.EXPAND_COLUMN, { title: 'Prompt', dataIndex: 'prompt', key: 'prompt', width: 300, render: (v: string) => ( - {v} + {v} ), }, { diff --git a/src/view/model/UserCustom/index.tsx b/src/view/model/UserCustom/index.tsx index ed8bbad..985fe41 100644 --- a/src/view/model/UserCustom/index.tsx +++ b/src/view/model/UserCustom/index.tsx @@ -6,13 +6,13 @@ import useInit from '@/hooks/useInit'; import useData from '@/hooks/useData'; import useChatModel, { useCacheModel } from '@/hooks/useChatModel'; import useColumns from '@/hooks/useColumns'; -import useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { chatRoot, fmtDate } from '@/utils'; import { modelColumns } from './config'; import UserCustomForm from './Form'; export default function LanguageModel() { - const { rowSelection, selectedRowIDs } = useTable(); + const { rowSelection, selectedRowIDs } = useTableRowSelection(); const [isVisible, setVisible] = useState(false); const [jsonPath, setJsonPath] = useState(''); const { modelJson, modelSet } = useChatModel('user_custom'); @@ -123,6 +123,7 @@ export default function LanguageModel() { dataSource={opData} rowSelection={rowSelection} pagination={TABLE_PAGINATION} + expandable={{expandedRowRender: (record) =>
{record.prompt}
}} /> Date: Sun, 15 Jan 2023 01:18:03 +0800 Subject: [PATCH 12/14] chore: export --- package.json | 3 + src-tauri/src/app/cmd.rs | 4 +- src-tauri/src/main.rs | 3 +- src-tauri/src/scripts/export.js | 8 +- src/hooks/useJson.ts | 1 + src/routes.tsx | 22 +++-- src/utils.ts | 1 + src/view/download/index.tsx | 12 +-- src/view/notes/config.tsx | 69 ++++++++++++++ src/view/notes/index.tsx | 160 ++++++++++++++++++++++++++++++++ 10 files changed, 264 insertions(+), 19 deletions(-) create mode 100644 src/view/notes/config.tsx create mode 100644 src/view/notes/index.tsx diff --git a/package.json b/package.json index dd9e962..6f8ca26 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^8.0.4", "react-router-dom": "^6.4.5", + "react-syntax-highlighter": "^15.5.0", "uuid": "^9.0.0" }, "devDependencies": { @@ -50,6 +52,7 @@ "@types/node": "^18.7.10", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", + "@types/react-syntax-highlighter": "^15.5.6", "@types/uuid": "^9.0.0", "@vitejs/plugin-react": "^3.0.0", "sass": "^1.56.2", diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 6369ac1..b14aebd 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -203,14 +203,14 @@ pub fn get_download_list(pathname: &str) -> (Vec, PathBuf) { } #[command] -pub fn download_list(pathname: &str, filename: Option, id: Option) { +pub fn download_list(pathname: &str, dir: &str, filename: Option, id: Option) { info!("download_list: {}", pathname); let data = get_download_list(pathname); let mut list = vec![]; let mut idmap = HashMap::new(); utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap); - for entry in WalkDir::new(utils::chat_root().join("download")) + for entry in WalkDir::new(utils::chat_root().join(dir)) .into_iter() .filter_entry(|e| !utils::is_hidden(e)) .filter_map(|e| e.ok()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8c84394..b41eee9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,7 +30,8 @@ async fn main() { trace: Color::Cyan, }; - cmd::download_list("chat.download.json", None, None); + cmd::download_list("chat.download.json", "download", None, None); + cmd::download_list("chat.notes.json", "notes", None, None); let chat_conf = ChatConfJson::get_chat_conf(); diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index d5af93b..a5539d5 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -134,7 +134,9 @@ function addActionsButtons(actionsArea, TryAgainButton) { async function exportMarkdown() { const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); - await invoke('save_file', { name: `notes/${uid().toString(36)}.md`, content: data }); + const { id, filename } = getName(); + await invoke('save_file', { name: `notes/${id}.md`, content: data }); + await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' }); } function downloadThread({ as = Format.PNG } = {}) { @@ -168,7 +170,7 @@ async function handleImg(imgData) { } const { pathname, id, filename } = getName(); await invoke('download', { name: `download/img/${id}.png`, blob: data }); - await invoke('download_list', { pathname, filename, id }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } async function handlePdf(imgData, canvas, pixelRatio) { @@ -184,7 +186,7 @@ async function handlePdf(imgData, canvas, pixelRatio) { const { pathname, id, filename } = getName(); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); - await invoke('download_list', { pathname, filename, id }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } function getName() { diff --git a/src/hooks/useJson.ts b/src/hooks/useJson.ts index 00a5bd8..0bfee8b 100644 --- a/src/hooks/useJson.ts +++ b/src/hooks/useJson.ts @@ -9,6 +9,7 @@ export default function useJson(file: string) { const refreshJson = async () => { const data = await readJSON(file); setData(data); + return data; }; const updateJson = async (data: any) => { diff --git a/src/routes.tsx b/src/routes.tsx index b914935..41527cb 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,11 +1,12 @@ import { useRoutes } from 'react-router-dom'; import { - DesktopOutlined, + SettingOutlined, BulbOutlined, SyncOutlined, FileSyncOutlined, UserOutlined, DownloadOutlined, + FormOutlined, } from '@ant-design/icons'; import type { MenuProps } from 'antd'; @@ -15,6 +16,7 @@ import SyncPrompts from '@/view/model/SyncPrompts'; import SyncCustom from '@/view/model/SyncCustom'; import SyncRecord from '@/view/model/SyncRecord'; import Download from '@/view/download'; +import Notes from '@/view/notes'; export type ChatRouteMetaObject = { label: string; @@ -35,15 +37,15 @@ export const routes: Array = [ element: , meta: { label: 'General', - icon: , + icon: , }, }, { - path: 'download', - element: , + path: '/notes', + element: , meta: { - label: 'Download', - icon: , + label: 'Notes', + icon: , }, }, { @@ -85,6 +87,14 @@ export const routes: Array = [ }, ], }, + { + path: 'download', + element: , + meta: { + label: 'Download', + icon: , + }, + }, ]; type MenuItem = Required['items'][number]; diff --git a/src/utils.ts b/src/utils.ts index 09a0648..b90a7bd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import dayjs from 'dayjs'; export const CHAT_MODEL_JSON = 'chat.model.json'; export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json'; export const CHAT_DOWNLOAD_JSON = 'chat.download.json'; +export const CHAT_NOTES_JSON = 'chat.notes.json'; export const CHAT_PROMPTS_CSV = 'chat.prompts.csv'; export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv'; export const DISABLE_AUTO_COMPLETE = { diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx index ae3909e..5156b78 100644 --- a/src/view/download/index.tsx +++ b/src/view/download/index.tsx @@ -18,7 +18,7 @@ function renderFile(buff: Uint8Array, type: string) { return URL.createObjectURL(new Blob([buff], { type: renderType })); } -export default function SyncPrompts() { +export default function Download() { const [downloadPath, setDownloadPath] = useState(''); const [source, setSource] = useState(''); const [isVisible, setVisible] = useState(false); @@ -51,9 +51,6 @@ export default function SyncPrompts() { setVisible(true); return; } - if (opInfo.opType === 'file') { - await shell.open(file); - } if (opInfo.opType === 'delete') { await fs.removeFile(file); await handleRefresh(); @@ -90,8 +87,9 @@ export default function SyncPrompts() { }; const handleRefresh = async () => { - await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON }); - refreshJson(); + await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' }); + const data = await refreshJson(); + opInit(data); }; const handleCancel = () => { @@ -107,7 +105,7 @@ export default function SyncPrompts() { <> [ + { + title: 'Name', + dataIndex: 'name', + fixed: 'left', + key: 'name', + width: 240, + render: (_: string, row: any, actions: any) => ( + + ), + }, + { + title: 'Path', + dataIndex: 'path', + key: 'path', + width: 200, + render: (_: string, row: any) => , + }, + { + title: 'Created', + dataIndex: 'created', + key: 'created', + width: 150, + render: fmtDate, + }, + { + title: 'Action', + fixed: 'right', + width: 160, + render: (_: any, row: any, actions: any) => { + return ( + + actions.setRecord(row, 'preview')}>Preview + actions.setRecord(row, 'edit')}>Edit + actions.setRecord(row, 'delete')} + okText="Yes" + cancelText="No" + > + Delete + + + ) + } + } +]; + +const RenderPath = ({ row }: any) => { + const [filePath, setFilePath] = useState(''); + useInit(async () => { + setFilePath(await getPath(row)); + }) + return shell.open(filePath)}>{filePath}; +}; + +export const getPath = async (row: any) => { + const isImg = ['png'].includes(row?.ext); + return await path.join(await chatRoot(), 'notes', row.id) + `.${row.ext}`; +} diff --git a/src/view/notes/index.tsx b/src/view/notes/index.tsx new file mode 100644 index 0000000..b93bd35 --- /dev/null +++ b/src/view/notes/index.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react'; +import { Table, Modal, Popconfirm, Button, message } from 'antd'; +import { invoke, path, shell, fs } from '@tauri-apps/api'; +import ReactMarkdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import useInit from '@/hooks/useInit'; +import useJson from '@/hooks/useJson'; +import useData from '@/hooks/useData'; +import useColumns from '@/hooks/useColumns'; +import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; +import { chatRoot, CHAT_NOTES_JSON } from '@/utils'; +import { notesColumns } from './config'; + +export default function Notes() { + const [notesPath, setNotesPath] = useState(''); + const [source, setSource] = useState(''); + const [isVisible, setVisible] = useState(false); + const { opData, opInit, opReplace, opSafeKey } = useData([]); + const { columns, ...opInfo } = useColumns(notesColumns()); + const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' }); + const { json, refreshJson, updateJson } = useJson(CHAT_NOTES_JSON); + const selectedItems = rowSelection.selectedRowKeys || []; + + useInit(async () => { + const file = await path.join(await chatRoot(), CHAT_NOTES_JSON); + setNotesPath(file); + }); + + useEffect(() => { + if (!json || json.length <= 0) return; + opInit(json); + }, [json?.length]); + + useEffect(() => { + if (!opInfo.opType) return; + (async () => { + const record = opInfo?.opRecord; + const file = await path.join(await chatRoot(), 'notes', `${record?.id}.${record?.ext}`); + if (opInfo.opType === 'preview') { + const data = await fs.readTextFile(file); + setSource(data); + setVisible(true); + return; + } + if (opInfo.opType === 'edit') { + alert('TODO'); + } + if (opInfo.opType === 'delete') { + await fs.removeFile(file); + await handleRefresh(); + } + if (opInfo.opType === 'rowedit') { + const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord); + await updateJson(data); + message.success('Name has been changed!'); + } + opInfo.resetRecord(); + })() + }, [opInfo.opType]) + + const handleDelete = async () => { + if (opData?.length === selectedRows.length) { + const notesDir = await path.join(await chatRoot(), 'notes'); + await fs.removeDir(notesDir, { recursive: true }); + await handleRefresh(); + rowReset(); + message.success('All files have been cleared!'); + return; + } + + const rows = selectedRows.map(async (i) => { + const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`); + await fs.removeFile(file); + return file; + }) + Promise.all(rows).then(async () => { + await handleRefresh(); + message.success('All files selected are cleared!'); + }); + }; + + const handleRefresh = async () => { + await invoke('download_list', { pathname: CHAT_NOTES_JSON, dir: 'notes' }); + const data = await refreshJson(); + opInit(data); + }; + + const handleCancel = () => { + setVisible(false); + opInfo.resetRecord(); + }; + + return ( +
+
+
+ {selectedItems.length > 0 && ( + <> + + + + Selected {selectedItems.length} items + + )} +
+
+ +
+ {opInfo?.opRecord?.name || ''}} + onCancel={handleCancel} + footer={false} + destroyOnClose + > + + ) : ( + + {children} + + ) + } + }} + /> + + + ) +} \ No newline at end of file From 3c848b4dedafa434b9e632abd36b3bd760353907 Mon Sep 17 00:00:00 2001 From: lencx Date: Sun, 15 Jan 2023 02:12:29 +0800 Subject: [PATCH 13/14] chore: export --- README.md | 2 +- UPDATE_LOG.md | 9 +++++++++ src-tauri/src/app/menu.rs | 2 ++ src-tauri/src/conf.rs | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4f770e..c1644e5 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt ## ✨ Features - Multi-platform: `macOS` `Linux` `Windows` -- Export ChatGPT history (PNG, PDF and Share Link) +- Export ChatGPT history (PNG, PDF and Markdown) - Automatic application upgrade notification - Common shortcut keys - System tray hover window diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index e8e588c..b40ac36 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -1,5 +1,14 @@ # UPDATE LOG +## v0.9.0 + +fix: +- export button does not work + +feat: +- add an export markdown button +- `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews. + ## v0.8.1 fix: diff --git a/src-tauri/src/app/menu.rs b/src-tauri/src/app/menu.rs index bdcabde..1a0faf7 100644 --- a/src-tauri/src/app/menu.rs +++ b/src-tauri/src/app/menu.rs @@ -148,6 +148,7 @@ pub fn init() -> Menu { CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT") .accelerator("CmdOrCtrl+Shift+A") .into(), + CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(), ]), ); @@ -249,6 +250,7 @@ pub fn menu_handler(event: WindowMenuEvent) { "go_conf" => utils::open_file(utils::chat_root()), "clear_conf" => utils::clear_conf(&app), "awesome" => open(&app, conf::AWESOME_URL.to_string()), + "buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()), "popup_search" => { let chat_conf = conf::ChatConfJson::get_chat_conf(); let popup_search = !chat_conf.popup_search; diff --git a/src-tauri/src/conf.rs b/src-tauri/src/conf.rs index 21a204c..559933b 100644 --- a/src-tauri/src/conf.rs +++ b/src-tauri/src/conf.rs @@ -14,6 +14,7 @@ use tauri::TitleBarStyle; pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues"; pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md"; pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md"; +pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx"; pub const GITHUB_PROMPTS_CSV_URL: &str = "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"; pub const DEFAULT_CHAT_CONF: &str = r#"{ From 96f7e321373600399e9433c183a6e1a35b7ded04 Mon Sep 17 00:00:00 2001 From: lencx Date: Sun, 15 Jan 2023 02:12:56 +0800 Subject: [PATCH 14/14] v0.9.0 --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 803da8d..1fad34b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "ChatGPT", - "version": "0.8.1" + "version": "0.9.0" }, "tauri": { "allowlist": {