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