diff --git a/package.json b/package.json index fdfb4623..09dbbe61 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "got": "^11.8.6", "mime-types": "^2.1.35", "mkdirp": "^3.0.0", + "node-ssh": "^13.1.0", "node-watch": "^0.7.4", "shiki": "^0.14.4", "upath": "^2.0.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2690c509..eadddcd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ dependencies: mkdirp: specifier: ^3.0.0 version: 3.0.0 + node-ssh: + specifier: ^13.1.0 + version: 13.1.0 node-watch: specifier: ^0.7.4 version: 0.7.4 @@ -1207,6 +1210,12 @@ packages: /@types/semver@7.5.1: resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} + /@types/ssh2@1.11.14: + resolution: {integrity: sha512-O/U38mvV4jVVrdtZz8KpmitkmeD/PUDeDNNueQhm34166dmaqb1iZ3sfarSxBArM2/iX4PZVJY3EOta0Zks9hw==} + dependencies: + '@types/node': 18.17.5 + dev: false + /@types/unist@2.0.8: resolution: {integrity: sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==} dev: true @@ -1623,6 +1632,12 @@ packages: engines: {node: '>=8'} dev: true + /asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: false + /assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -1674,6 +1689,12 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true + /bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -1744,6 +1765,12 @@ packages: dev: true optional: true + /buildcheck@0.0.6: + resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + engines: {node: '>=10.0.0'} + dev: false + optional: true + /builder-util-runtime@9.1.1: resolution: {integrity: sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==} engines: {node: '>=12.0.0'} @@ -2019,6 +2046,16 @@ packages: layout-base: 2.0.1 dev: true + /cpu-features@0.0.9: + resolution: {integrity: sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + buildcheck: 0.0.6 + nan: 2.18.0 + dev: false + optional: true + /crc@3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} dependencies: @@ -3426,6 +3463,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false @@ -3662,6 +3704,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: false + /markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} dev: true @@ -4245,6 +4294,11 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + /nan@2.18.0: + resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + dev: false + optional: true + /nano-css@5.3.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==} peerDependencies: @@ -4292,6 +4346,19 @@ packages: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true + /node-ssh@13.1.0: + resolution: {integrity: sha512-GLcw49yFd9+rUpP+FgX6wrF/N90cmuDl2n0i8d3L828b6riRjkb9w3SS+XvviRWbrAhLxuMKywFqxvQDZQ1bug==} + engines: {node: '>= 10'} + dependencies: + '@types/ssh2': 1.11.14 + is-stream: 2.0.1 + make-dir: 3.1.0 + sb-promise-queue: 2.1.0 + sb-scandir: 3.1.0 + shell-escape: 0.2.0 + ssh2: 1.14.0 + dev: false + /node-watch@0.7.4: resolution: {integrity: sha512-RinNxoz4W1cep1b928fuFhvAQ5ag/+1UlMDV7rbyGthBIgsiEouS4kvRayvvboxii4m8eolKOIBo3OjDqbc+uQ==} engines: {node: '>=6'} @@ -5413,7 +5480,6 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true /sanitize-filename@1.6.3: resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} @@ -5434,6 +5500,18 @@ packages: /sax@1.2.4: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + /sb-promise-queue@2.1.0: + resolution: {integrity: sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg==} + engines: {node: '>= 8'} + dev: false + + /sb-scandir@3.1.0: + resolution: {integrity: sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg==} + engines: {node: '>= 8'} + dependencies: + sb-promise-queue: 2.1.0 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -5500,6 +5578,10 @@ packages: engines: {node: '>=8'} dev: true + /shell-escape@0.2.0: + resolution: {integrity: sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==} + dev: false + /shiki@0.14.4: resolution: {integrity: sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==} dependencies: @@ -5615,6 +5697,18 @@ packages: resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} optional: true + /ssh2@1.14.0: + resolution: {integrity: sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==} + engines: {node: '>=10.16.0'} + requiresBuild: true + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.9 + nan: 2.18.0 + dev: false + /stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} dependencies: @@ -5863,6 +5957,10 @@ packages: typescript: 5.1.6 dev: true + /tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} diff --git a/src/main/api.ts b/src/main/api.ts index 27224a45..223d76e5 100644 --- a/src/main/api.ts +++ b/src/main/api.ts @@ -101,6 +101,13 @@ export const registerApi = () => { ipcMain.handle('get-system-dark', (e) => { return nativeTheme.shouldUseDarkColors }) + ipcMain.handle('set-service-config', (e, config: any) => { + if (!config) store.delete('service-config') + else store.set('service-config', config) + }) + ipcMain.handle('get-service-config', e => { + return store.get('service-config') + }) ipcMain.on('setStore', (e, key: string, value: any) => { if (typeof value === 'undefined') { store.delete(key) diff --git a/src/main/appMenus.ts b/src/main/appMenus.ts index ba2097f9..35df8ce7 100644 --- a/src/main/appMenus.ts +++ b/src/main/appMenus.ts @@ -426,7 +426,7 @@ export const createAppMenus = () => { } } ] - const devTools:MenuOptions[number]['submenu'] = is.dev ? [ + const devTools:MenuOptions[number]['submenu'] = is.dev || true ? [ {role: 'toggleDevTools'} ] : [] menus.push( diff --git a/src/main/index.ts b/src/main/index.ts index db439bb4..4e77a60a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,7 +17,7 @@ type WinOptions = { openFolder?: string openFile?: string } - +console.log('path', app.getPath('userData')) const windows = new Map() app.setAsDefaultProtocolClient('bluestone-markdown') function createWindow(initial?: WinOptions): void { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 3d4baeeb..fa6677d7 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -6,6 +6,7 @@ import {AliApi} from './sdk/ali' import {Sdk} from './sdk' import {Got} from 'got' import {ExtendOptions} from 'got/dist/source/types' +import {Service} from './service' declare global { @@ -14,6 +15,7 @@ declare global { api: { sdk: typeof Sdk, dev: boolean + service: Service, got: Got toUnix: (path: string) => string md5: (str: string | Buffer) => string diff --git a/src/preload/index.ts b/src/preload/index.ts index e756b601..b546b605 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,10 +11,12 @@ const langSet = new Set(BUNDLED_LANGUAGES.map(l => [l.id, ...(l.aliases || [])]) let highlighter:Highlighter | null = null import {toUnix} from 'upath' import mime from 'mime-types' +import {Service} from './service' let watchers = new Map() let ready:any = null const api = { langSet, + service: new Service, copyToClipboard(str: string) { clipboard.writeText(str) }, diff --git a/src/preload/service.ts b/src/preload/service.ts new file mode 100644 index 00000000..7b2b0aa6 --- /dev/null +++ b/src/preload/service.ts @@ -0,0 +1,111 @@ +import {NodeSSH} from 'node-ssh' +import {join, sep} from 'path' +import {ipcRenderer} from 'electron' +const ssh = new NodeSSH() +import {readdirSync, readFileSync} from 'fs' +import {SFTPWrapper} from 'ssh2' +export class Service { + ssh: NodeSSH | null = null + private async getAssets() { + const env = await ipcRenderer.invoke('get-env') as {webPath: string} + const files = readdirSync(env.webPath) + const scriptPath = files.find(f => f.endsWith('.js')) + const cssPath = files.find(f => f === 'style.css') + return { + script: await readFileSync(join(env.webPath, scriptPath!), {encoding:'utf-8'}), + icon: join(env.webPath, 'favicon.png'), + katex: join(env.webPath, 'katex.min.css'), + css: await readFileSync(join(env.webPath, cssPath!), {encoding:'utf-8'}) + } + } + close() { + if (this.ssh) { + this.ssh.dispose() + this.ssh = null + } + } + private async getConfig() { + return ipcRenderer.invoke('get-service-config') as Record + } + private remoteExists(sftp: SFTPWrapper, filePath: string) { + return new Promise((resolve, reject) => { + sftp.exists(filePath, (res) => { + resolve(res) + }) + }) + } + private async connectSsh(config: any) { + this.ssh = await ssh.connect({ + host: config.host, + username: config.username, + password: config.password, + port: config.port + }) + return this.ssh + } + async initialSsh(config: any) { + this.ssh = await ssh.connect({ + host: config.host, + username: config.username, + password: config.password, + port: config.port + }) + const assets = await this.getAssets() + await this.ssh.withSFTP(async sftp => { + const lib = join(config.target, 'lib') + const exist = await this.remoteExists(sftp, lib) + if (!exist) await this.ssh!.mkdir(lib) + await this.writeFileBySsh(sftp, join(lib, 'style.css'), assets.css) + await this.writeFileBySsh(sftp, join(lib, 'script.js'), assets.script) + }) + await this.uploadFile('katex.min.css', assets.katex, 'lib') + await this.uploadFile('favicon.png', assets.icon, 'lib') + this.close() + } + private writeFileBySsh(sftp: SFTPWrapper, path: string, content: string) { + return new Promise((resolve, reject) => { + sftp.writeFile(path, content, res => { + if (res instanceof Error) { + reject(res) + } else { + resolve(null) + } + }) + }) + } + async uploadDoc(name: string, content: string) { + const config = await this.getConfig() + if (config.type === 'ssh') { + if (!this.ssh) this.ssh = await this.connectSsh(config) + await this.ssh.withSFTP(async sftp => { + const doc = join(config.target, 'doc') + const exist = await this.remoteExists(sftp, doc) + if (!exist) await this.ssh!.mkdir(doc) + await this.writeFileBySsh(sftp, join(config.target, name), content) + }) + this.close() + } + } + async deleteDoc(path: string) { + const config = await this.getConfig() + if (config.type === 'ssh') { + if (!this.ssh) this.ssh = await this.connectSsh(config) + await this.ssh!.execCommand(`rm -f ${join(config.target, path)}`) + } + this.close() + } + async uploadFile(name: string, filePath: string, dir = 'assets'): Promise { + const config = await this.getConfig() + if (config.type === 'ssh') { + if (!this.ssh) this.ssh = await this.connectSsh(config) + await this.ssh.withSFTP(async sftp => { + const assets = join(config.target, dir) + const exist = await this.remoteExists(sftp, assets) + if (!exist) await this.ssh!.mkdir(assets) + }) + await this.ssh!.putFile(filePath, join(config.target, dir, name)) + } else { + + } + } +} diff --git a/src/preload/ssh.ts b/src/preload/ssh.ts new file mode 100644 index 00000000..89f989df --- /dev/null +++ b/src/preload/ssh.ts @@ -0,0 +1,70 @@ +import {NodeSSH} from 'node-ssh' +import {join, parse, sep} from 'path' +import {writeFileSync} from 'fs' +import {SFTPWrapper} from 'ssh2' +const ssh = new NodeSSH() + +interface Config { + host: string + username: string + password: string + target: string + domain: string + port?: number +} +export class SshApi { + ssh: NodeSSH | null = null + constructor( + private readonly config: Config + ) {} + + async connect() { + this.ssh = await ssh.connect({ + host: this.config.host, + username: this.config.username, + password: this.config.password, + port: this.config.port + }) + return this.ssh + } + + async uploadFile(name: string, filePath: string, contentType?: string): Promise { + if (!this.ssh) this.ssh = await this.connect() + await this.ssh!.putFile(filePath, join(this.config.target, name)) + } + async removeFile(name: string): Promise { + if (!this.ssh) this.ssh = await this.connect() + await this.ssh!.execCommand(`rm -f ${join(this.config.target, name)}`) + } + + dispose() { + if (this.ssh) { + this.ssh.dispose() + this.ssh = null + } + } + private remoteExists(sftp: SFTPWrapper, filePath: string) { + return new Promise((resolve, reject) => { + sftp.exists(filePath, (res) => { + resolve(res) + }) + }) + } + async uploadFileByText(name, content) { + if (!this.ssh) this.ssh = await this.connect() + const dir = name.split(sep).slice(0, -1).join(sep) + return this.ssh.withSFTP(async sftp => { + return new Promise(async (resolve, reject) => { + const exist = await this.remoteExists(sftp, join(this.config.target, dir)) + if (!exist) await this.ssh!.mkdir(join(this.config.target, dir)) + sftp.writeFile(join(this.config.target, name), content, res => { + if (res instanceof Error) { + reject(res) + } else { + resolve() + } + }) + }) + }) + } +} diff --git a/src/renderer/src/components/Nav.tsx b/src/renderer/src/components/Nav.tsx index 9ad16aee..c24fabd8 100644 --- a/src/renderer/src/components/Nav.tsx +++ b/src/renderer/src/components/Nav.tsx @@ -8,6 +8,7 @@ import {isMac, isWindows} from '../utils' import {Share} from '../share/Share' import {User} from '../share/User' import {configStore} from '../store/config' +import {Server} from '../server/Server' export const Nav = observer(() => { const paths = useMemo(() => { if (!treeStore.openNote) return [''] @@ -69,11 +70,8 @@ export const Nav = observer(() => {
- {/*Network services are relatively private and currently not open source. Please comment on this line of code to run it*/} - {!configStore.config.hideWebService && - - } +
MainApi.openToolMenu(treeStore.openNote?.filePath)} @@ -82,10 +80,6 @@ export const Nav = observer(() => { className={'text-lg duration-200 dark:group-hover:text-gray-300 group-hover:text-gray-700'} />
- {/*Network services are relatively private and currently not open source. Please comment on this line of code to run it*/} - {!configStore.config.hideWebService && - - }
diff --git a/src/renderer/src/editor/output/html.tsx b/src/renderer/src/editor/output/html.tsx index f7324d5e..73f0b714 100644 --- a/src/renderer/src/editor/output/html.tsx +++ b/src/renderer/src/editor/output/html.tsx @@ -15,13 +15,14 @@ const getAssets = async () => { const files = await fs.readdir(env.webPath) const scriptPath = files.find(f => f.endsWith('.js')) const cssPath = files.find(f => f === 'style.css') + const katexPath = files.find(f => f === 'katex.min.css') return { script: await fs.readFile(join(env.webPath, scriptPath!), {encoding:'utf-8'}), + katexCss: await fs.readFile(join(env.webPath, katexPath!), {encoding:'utf-8'}), css: await fs.readFile(join(env.webPath, cssPath!), {encoding:'utf-8'}) } } - -export const exportHtml = async (node: IFileItem) => { +export const exportToHtmlString = async (node: IFileItem, web = false) => { const tree = treeStore.schemaMap.get(node)?.state || [] const title = parse(node.filePath).name const {script, css} = await getAssets() @@ -35,20 +36,24 @@ export const exportHtml = async (node: IFileItem) => { ) - const html = ` + return ` - + ${title} - + ${web ? '' : ``} + ${web ? '' : ''}
${content}
- +${web ? '' : ``} ` +} +export const exportHtml = async (node: IFileItem, web = false) => { + const html = await exportToHtmlString(node, web) const save = await saveDialog({ filters: [{name: 'html', extensions: ['html']}] }) diff --git a/src/renderer/src/editor/output/html/dom/render/Article.tsx b/src/renderer/src/editor/output/html/dom/render/Article.tsx index e4958550..af3514b1 100644 --- a/src/renderer/src/editor/output/html/dom/render/Article.tsx +++ b/src/renderer/src/editor/output/html/dom/render/Article.tsx @@ -74,13 +74,13 @@ function Render(props: { {s.type === 'list-item' &&
  • {typeof s.checked === 'boolean' && - + - + }
  • diff --git a/src/renderer/src/editor/output/html/dom/render/Code.tsx b/src/renderer/src/editor/output/html/dom/render/Code.tsx index 0044e0fb..53efd8e4 100644 --- a/src/renderer/src/editor/output/html/dom/render/Code.tsx +++ b/src/renderer/src/editor/output/html/dom/render/Code.tsx @@ -48,7 +48,7 @@ export function Code({node, path}: {
     :
    diff --git a/src/renderer/src/editor/output/html/transform.ts b/src/renderer/src/editor/output/html/transform.ts
    index 604082be..8aa0b37d 100644
    --- a/src/renderer/src/editor/output/html/transform.ts
    +++ b/src/renderer/src/editor/output/html/transform.ts
    @@ -4,10 +4,11 @@ import mermaid from 'mermaid'
     import {configStore} from '../../../store/config'
     import katex from 'katex'
     import {EditorUtils} from '../../utils/editorUtils'
    -import {isAbsolute, join} from 'path'
    +import {isAbsolute, join, extname} from 'path'
     import {existsSync} from 'fs'
     import {mediaType} from '../../utils/dom'
     import {getImageData} from '../../../utils'
    +import {db} from '../../../store/db'
     
     export const transformSchema = async (schema: any[], filePath: string) => {
       const data = JSON.parse(JSON.stringify(schema)) as any[]
    diff --git a/src/renderer/src/server/Record.tsx b/src/renderer/src/server/Record.tsx
    new file mode 100644
    index 00000000..95edfb36
    --- /dev/null
    +++ b/src/renderer/src/server/Record.tsx
    @@ -0,0 +1,43 @@
    +import {observer} from 'mobx-react-lite'
    +import {Modal, Tabs} from 'antd'
    +import {useLocalState} from '../hooks/useLocalState'
    +import {IShareNote} from '../store/db'
    +
    +export const Record = observer((props: {
    +  open: boolean
    +  onClose: () => void
    +}) => {
    +  const [state, setState] = useLocalState({
    +    docs: [] as IShareNote[]
    +  })
    +  return (
    +    
    +      
    +            )
    +          },
    +          {
    +            label: 'Files',
    +            key: 'file',
    +            children: (
    +              <>
    +            )
    +          }
    +        ]}
    +      />
    +    
    +  )
    +})
    diff --git a/src/renderer/src/server/RemoveShare.tsx b/src/renderer/src/server/RemoveShare.tsx
    new file mode 100644
    index 00000000..21a420a8
    --- /dev/null
    +++ b/src/renderer/src/server/RemoveShare.tsx
    @@ -0,0 +1,23 @@
    +import {observer} from 'mobx-react-lite'
    +import {Popconfirm} from 'antd'
    +import {ReactNode} from 'react'
    +import {IShareNote} from '../store/db'
    +
    +export const RemoveShare = observer((props: {
    +  children: ReactNode
    +  doc?: IShareNote
    +  onRemove: () => void
    +}) => {
    +  return (
    +    
    +      {props.children}
    +    
    +  )
    +})
    diff --git a/src/renderer/src/server/Server.tsx b/src/renderer/src/server/Server.tsx
    new file mode 100644
    index 00000000..1588fd8e
    --- /dev/null
    +++ b/src/renderer/src/server/Server.tsx
    @@ -0,0 +1,297 @@
    +import {observer} from 'mobx-react-lite'
    +import {
    +  CopyOutlined,
    +  DatabaseOutlined,
    +  LinkOutlined,
    +  SendOutlined,
    +  SettingOutlined,
    +  StopOutlined, SyncOutlined
    +} from '@ant-design/icons'
    +import {Button, Input, notification, Popover, Space, Tabs} from 'antd'
    +import {useCallback, useEffect, useMemo} from 'react'
    +import {nanoid} from 'nanoid'
    +import Net from '../icons/Net'
    +import {useLocalState} from '../hooks/useLocalState'
    +import {treeStore} from '../store/tree'
    +import {message$, nid} from '../utils'
    +import {mediaType} from '../editor/utils/dom'
    +import {configStore} from '../store/config'
    +import {Subject} from 'rxjs'
    +import {useSubject} from '../hooks/subscribe'
    +import {ServiceSet} from './ServiceSet'
    +import {Record} from './Record'
    +import {RemoveShare} from './RemoveShare'
    +import {db, IShareNote} from '../store/db'
    +import {exportToHtmlString} from '../editor/output/html'
    +
    +export const shareSuccess$ = new Subject()
    +export const Server = observer(() => {
    +  const [state, setState] = useLocalState({
    +    popOpen: false,
    +    syncing: false,
    +    tab: 'doc',
    +    refresh: false,
    +    inputPassword: '',
    +    // books: [] as ShareBook[],
    +    mask: false,
    +    showData: false,
    +    openSetting: false,
    +    openRecord: false,
    +    curDoc: null as null | IShareNote
    +  })
    +
    +  const [api, contextHolder] = notification.useNotification()
    +
    +  useSubject(shareSuccess$, (url: string) => {
    +    shareSuccess(url)
    +  })
    +  const openNote = useMemo(() => {
    +    if (treeStore.openNote && mediaType(treeStore.openNote.filePath) === 'markdown') return treeStore.openNote.filePath
    +    return null
    +  }, [treeStore.openNote, state.refresh])
    +
    +  useEffect(() => {
    +    if (openNote) {
    +      db.shareNote.where('filePath').equals(openNote).first().then(res => {
    +        setState({curDoc: res || null})
    +      })
    +    } else {
    +      setState({curDoc: null})
    +    }
    +  }, [openNote, state.refresh])
    +
    +  const copyDocUrl = useCallback((url: string) => {
    +    window.api.copyToClipboard(url)
    +    message$.next({
    +      type: 'success',
    +      content: 'Copied to clipboard'
    +    })
    +  }, [])
    +
    +  const closeMask = useCallback(() => {
    +    setTimeout(() => {
    +      setState({mask: false})
    +    })
    +  }, [])
    +
    +  const shareSuccess = useCallback((url: string) => {
    +    const key = nanoid()
    +    api.success({
    +      key,
    +      message: 'Synchronization succeeded',
    +      duration: 3,
    +      btn: (
    +        
    +          
    +          
    +        
    +      )
    +    })
    +  }, [])
    +
    +  const share = useCallback(async () => {
    +    const note = treeStore.openNote
    +    if (note && mediaType(note.filePath) === 'markdown') {
    +      setState({syncing: true})
    +      try {
    +        const record = await db.shareNote.where('filePath').equals(note.filePath).first()
    +        const html = await exportToHtmlString(treeStore.openNote!, true)
    +        const name = record ? record.name : `doc/${nid()}.html`
    +        await window.api.service.uploadDoc(name, html)
    +        if (record) {
    +          await db.shareNote.where('filePath').equals(note.filePath).modify({
    +            updated: Date.now()
    +          })
    +        } else {
    +          await db.shareNote.add({
    +            id: nanoid(),
    +            name,
    +            filePath: note.filePath,
    +            updated: Date.now()
    +          })
    +        }
    +        setState({refresh: !state.refresh})
    +        shareSuccess(configStore.serviceConfig!.domain + '/' + name)
    +      } catch (e: any) {
    +        message$.next({
    +          type: 'error',
    +          content: e.toString()
    +        })
    +      } finally {
    +        setState({syncing: false})
    +      }
    +    }
    +  }, [])
    +
    +  return (
    +    <>
    +      {contextHolder}
    +      
    +             {
    +                setState({tab: key})
    +              }}
    +              tabBarExtraContent={(
    +                <>
    +                  
    +                
    +              )}
    +              items={[
    +                {
    +                  key: 'doc',
    +                  label: 'Share Note',
    +                  children: (
    +                    
    + + + } + {!!configStore.serviceConfig && +
    + + + {state.curDoc ? + <> + + + : + + } + + {state.curDoc && + + } +
    + } +
    + ) + } + ]} + /> + + )} + title={null} trigger="click" placement={'bottomRight'} open={state.popOpen} onOpenChange={v => { + if ((!v && !state.mask) || v) { + setState({popOpen: v}) + } + }} + arrow={false} + > +
    + +
    +
    + { + setState({openSetting: false}) + closeMask() + }} + /> + { + setState({openRecord: false}) + closeMask() + }} + /> + + ) +}) diff --git a/src/renderer/src/server/ServiceSet.tsx b/src/renderer/src/server/ServiceSet.tsx new file mode 100644 index 00000000..6b9f76d5 --- /dev/null +++ b/src/renderer/src/server/ServiceSet.tsx @@ -0,0 +1,115 @@ +import {observer} from 'mobx-react-lite' +import {Alert, Form, Input, Modal, Radio} from 'antd' +import {useLocalState} from '../hooks/useLocalState' +import {message$} from '../utils' +import {useEffect} from 'react' +import {runInAction} from 'mobx' +import {configStore} from '../store/config' + +export const ServiceSet = observer((props: { + open: boolean + onClose: () => void +}) => { + const [state, setState] = useLocalState({ + error: '', + loading: false + }) + const [form] = Form.useForm() + useEffect(() => { + if (props.open) { + window.electron.ipcRenderer.invoke('get-service-config').then(res => { + if (res) { + form.setFieldsValue(res) + } else { + form.resetFields() + } + }) + } + }, [props.open]) + return ( + { + const value = form.getFieldsValue() + setState({loading: true}) + try { + await window.electron.ipcRenderer.invoke('set-service-config', value) + if (value.type === 'ssh') { + await window.api.service.initialSsh(value) + } + setState({error: ''}) + message$.next({ + type: 'success', + content: 'Setup successful' + }) + runInAction(() => { + configStore.serviceConfig = value + }) + props.onClose() + } catch (e: any) { + console.error(e) + setState({error: e.toString()}) + window.electron.ipcRenderer.invoke('set-service-config', null) + } finally { + setState({loading: false}) + } + }} + onCancel={props.onClose} + > +
    + + + SSH + Custom + + + prevValues.type !== nextValues.type}> + {() => + <> + {form.getFieldValue('type') === 'ssh' && + <> + + + + + + + + + + + + + + + + + + + + } + + } + +
    + {state.error && + + } +
    + ) +}) diff --git a/src/renderer/src/store/config.ts b/src/renderer/src/store/config.ts index b64de514..6656c2cd 100644 --- a/src/renderer/src/store/config.ts +++ b/src/renderer/src/store/config.ts @@ -23,7 +23,7 @@ class ConfigStore { hideWebService: false } timer = 0 - + serviceConfig: null | Record = null get mas() { return process.mas || false } @@ -95,6 +95,11 @@ class ConfigStore { initial() { return new Promise(resolve => { + window.electron.ipcRenderer.invoke('get-service-config').then(res => { + if (res) { + runInAction(() => this.serviceConfig = res) + } + }) window.electron.ipcRenderer.invoke('getConfig').then(action(res => { // console.log('res', res) if (res.dark) document.documentElement.classList.add('dark') diff --git a/src/renderer/src/store/db.ts b/src/renderer/src/store/db.ts index 202251f6..ec4ecca4 100644 --- a/src/renderer/src/store/db.ts +++ b/src/renderer/src/store/db.ts @@ -21,6 +21,15 @@ export interface IQuickOpen { dirPath: string time: number } +export interface IShareNote{ + id?: string + filePath: string + name: string + hash?: string + updated: number + details?: object +} + export interface IHistory { id?: string @@ -33,14 +42,16 @@ class Db extends Dexie { public recent!: Table public quickOpen!: Table public history!: Table + public shareNote!: Table public constructor() { super('db') - this.version(4).stores({ + this.version(5).stores({ ebook: '++id,filePath,strategy,name,map,ignorePaths', recent: '&id,&filePath', quickOpen: '&id,filePath,dirPath', - history: '&id,filePath,schema,updated' + history: '&id,filePath,schema,updated', + shareNote: '&id,&filePath,name,updated,hash,details' }) } } diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index 3622d17e..b59fc73d 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -5,6 +5,8 @@ import {mediaType} from '../editor/utils/dom' import {Subject} from 'rxjs' import {ArgsProps} from 'antd/es/message' import {HookAPI} from 'antd/es/modal/useModal' +import { customAlphabet } from 'nanoid' +export const nid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 13) const kb = 1024 const mb = kb * 1024