diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 1990e57e..7d03b60d 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -23,9 +23,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - with: - token: ${{secrets.MD_TOKEN}} - submodules: true - name: Setup Node.js environment uses: actions/setup-node@v2.1.2 with: diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 7344851b..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/renderer/src/share"] - path = src/renderer/src/share - url = git@github.com:1943time/bluestone-user.git diff --git a/README.md b/README.md index f2edfeda..ed7b5b72 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Supports light and dark color theme. and generate your Markdown files into onlin - Support exporting html and pdf. - Can drag document elements to change their order. - Supports pasting of HTML, markdown code, and plain text, when pasting HTML, it will automatically download the images in the HTML and convert the path. -- Non-intrusive sharing function, one-click to share a single markdown document or combine multiple markdown files into a combined document containing chapters, bluestone will automatically manage all dependencies. +- Through simple configuration, it is easy to synchronize the markdown documents generated by the editor to your own server or cloud storage, making it easy to quickly share documents with others. ## Drag ![](./docs/assets/drag.gif) diff --git a/package.json b/package.json index 09dbbe61..b595e987 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluestone", - "version": "0.9.1", + "version": "1.3.5", "description": "", "main": "./out/main/index.js", "license": "AGPL-3.0", diff --git a/src/main/api.ts b/src/main/api.ts index 223d76e5..408f0905 100644 --- a/src/main/api.ts +++ b/src/main/api.ts @@ -8,7 +8,6 @@ import icon from '../../resources/icon.png?asset' export const baseUrl = is.dev && process.env['ELECTRON_RENDERER_URL'] ? process.env['ELECTRON_RENDERER_URL'] : join(__dirname, '../renderer/index.html') const workerPath = join(__dirname, '../renderer/worker.html') import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions -import {openAuth, listener} from './auth' export const windowOptions: BrowserWindowConstructorOptions = { show: false, @@ -41,7 +40,7 @@ export const isDark = (config?: any) => { const isBoolean = (v: any) => typeof v === 'boolean' export const registerApi = () => { - listener(store) + store.delete('service-config') ipcMain.on('to-worker', (e, ...args:any[]) => { const window = BrowserWindow.fromWebContents(e.sender)! window?.getBrowserView()?.webContents.send('task', ...args) @@ -52,9 +51,6 @@ export const registerApi = () => { ipcMain.handle('get-path', (e, type: Parameters[0]) => { return app.getPath(type) }) - ipcMain.on('open-auth', (e, type: 'github') => { - openAuth(type) - }) ipcMain.handle('get-env', () => { return { isPackaged: app.isPackaged, @@ -88,8 +84,7 @@ export const registerApi = () => { mas: process.mas || false, headingMarkLine: isBoolean(config.headingMarkLine) ? config.headingMarkLine : true, dragToSort: isBoolean(config.dragToSort) ? config.dragToSort : true, - autoRebuild: isBoolean(config.autoRebuild) ? config.autoRebuild : true, - hideWebService: isBoolean(config.hideWebService) ? config.hideWebService : false + autoRebuild: isBoolean(config.autoRebuild) ? config.autoRebuild : true } }) diff --git a/src/main/index.ts b/src/main/index.ts index 4e77a60a..0b6f9127 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,7 +17,6 @@ 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 { @@ -126,7 +125,7 @@ app.whenReady().then(() => { createWindow() } }) - // console.log(app.getPath('userData')) + ipcMain.on('set-win', (e, data: WinOptions) => { const window = BrowserWindow.fromWebContents(e.sender)! if (!windows.get(window.id)) return diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index fa6677d7..3dfa3c4b 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -6,7 +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' +import {Service} from './service/service' declare global { diff --git a/src/preload/index.ts b/src/preload/index.ts index b546b605..eb66a84b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,7 +11,7 @@ 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' +import {Service} from './service/service' let watchers = new Map() let ready:any = null const api = { diff --git a/src/preload/service/scriptService.ts b/src/preload/service/scriptService.ts new file mode 100644 index 00000000..d29944b2 --- /dev/null +++ b/src/preload/service/scriptService.ts @@ -0,0 +1,82 @@ +import {ipcRenderer} from 'electron' +import {join} from 'path' +import {readFileSync, writeFileSync} from 'fs' +import {Service} from './service' +import got from 'got' +import mimeTypes from 'mime-types' + +interface Instance { + uploadDoc(ctx: { + json: object + randomName: string + filePath: string + htmlBuffer: Buffer + }): Promise<{name: string}> + + removeFile(ctx: {name: string, filePath: string}): Promise<{success: true}> +} +export class ScriptService { + instance: null | Instance = null + constructor( + readonly service: Service + ) { + this.initial() + } + initial() { + this.service.getConfig().then(async res => { + if (res?.type === 'custom') { + const path = await this.getScriptPath() + delete require.cache[require.resolve(path)] + const script = require(path) + this.instance = new script.Service({ + got, mimeTypes + }) + } + }) + } + private async getScriptPath() { + const dataPath = await ipcRenderer.invoke('get-path', 'userData') as string + return join(dataPath, 'bsService.js') + } + async saveScript(script: string, domain: string) { + const path = await this.getScriptPath() + writeFileSync(path, script) + await this.test(path, domain) + } + async test(path: string, domain: string) { + delete require.cache[require.resolve(path)] + const script = require(path) + if (!script?.Service) throw new Error('Please export the Service instance in the script') + const service = new script.Service({ + got, mimeTypes + }) + if (!service.uploadDependencyLibrary) throw new Error('Please implement the uploadDependencyLibrary method') + if (!service.uploadDoc) throw new Error('Please implement the uploadDoc method') + if (!service.removeFile) throw new Error('Please implement the removeFile method') + const assets = await this.service.getAssets() + await service.uploadDependencyLibrary('lib/script.js', readFileSync(assets.script)) + await service.uploadDependencyLibrary('lib/favicon.png', readFileSync(assets.script)) + await service.uploadDependencyLibrary('lib/style.css', readFileSync(assets.css)) + await service.uploadDependencyLibrary('lib/katex.min.css', readFileSync(assets.katex)) + const res = await service.uploadDoc({ + json: [{text: ' '}], + randomName: 'doc/test.html', + filePath: 'user/test.md', + htmlBuffer: Buffer.from(' ', 'utf-8') + }) + if (!res?.name) throw new Error('the method uploadDoc must return the format {name: string}') + await got.get(domain + '/' + 'doc/test.html').catch(e => { + throw new Error(`url ${domain}/doc/text.html is not accessible`) + }) + await service.removeFile({ + name: 'doc/test.html', + filePath: 'user/test.md' + }) + await got.get(domain + '/' + 'doc/test.html').then(e => { + throw new Error(`invalid`) + }).catch((e: any) => { + if (e.message === 'invalid') throw new Error('Invalid deletion of doc/text.html, please check the removeFile method') + return null + }) + } +} diff --git a/src/preload/service.ts b/src/preload/service/service.ts similarity index 65% rename from src/preload/service.ts rename to src/preload/service/service.ts index 7b2b0aa6..c51e41cf 100644 --- a/src/preload/service.ts +++ b/src/preload/service/service.ts @@ -1,21 +1,28 @@ import {NodeSSH} from 'node-ssh' import {join, sep} from 'path' import {ipcRenderer} from 'electron' +import {toUnix} from 'upath' const ssh = new NodeSSH() import {readdirSync, readFileSync} from 'fs' import {SFTPWrapper} from 'ssh2' +import {ScriptService} from './scriptService' +const unixJoin = (...args:string[]) => toUnix(join(...args)) export class Service { ssh: NodeSSH | null = null - private async getAssets() { + script: ScriptService + constructor() { + this.script = new ScriptService(this) + } + 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'}) + script: unixJoin(env.webPath, scriptPath!), + icon: unixJoin(env.webPath, 'favicon.png'), + katex: unixJoin(env.webPath, 'katex.min.css'), + css: unixJoin(env.webPath, cssPath!) } } close() { @@ -24,7 +31,7 @@ export class Service { this.ssh = null } } - private async getConfig() { + async getConfig() { return ipcRenderer.invoke('get-service-config') as Record } private remoteExists(sftp: SFTPWrapper, filePath: string) { @@ -52,14 +59,14 @@ export class Service { }) const assets = await this.getAssets() await this.ssh.withSFTP(async sftp => { - const lib = join(config.target, 'lib') + const lib = unixJoin(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') + await this.uploadFile('style.css', assets.css, 'lib') + await this.uploadFile('script.js', assets.script, 'lib') this.close() } private writeFileBySsh(sftp: SFTPWrapper, path: string, content: string) { @@ -73,39 +80,52 @@ export class Service { }) }) } - async uploadDoc(name: string, content: string) { + async uploadDoc(data: { + name: string, content: string, json: object, filePath: 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 doc = unixJoin(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) + await this.writeFileBySsh(sftp, unixJoin(config.target, data.name), data.content) }) this.close() + return {name: data.name} + } else { + return this.script.instance?.uploadDoc({ + filePath: data.filePath, + json: data.json, + randomName: data.name, + htmlBuffer: Buffer.from(data.content, 'utf-8') + }) } } - async deleteDoc(path: string) { + async deleteDoc(name: string, filePath: 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)}`) + await this.ssh!.execCommand(`rm -f ${unixJoin(config.target, name)}`) + this.close() + } else { + await this.script.instance?.removeFile({ + name, + filePath + }) } - 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 assets = unixJoin(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 { - + await this.ssh!.putFile(filePath, unixJoin(config.target, dir, name)) } } } diff --git a/src/renderer/src/components/AceCode.tsx b/src/renderer/src/components/AceCode.tsx index 16a65ef0..f3e75ab4 100644 --- a/src/renderer/src/components/AceCode.tsx +++ b/src/renderer/src/components/AceCode.tsx @@ -1,23 +1,29 @@ import AceEditor from 'react-ace' import "ace-builds/src-noconflict/mode-json"; +import "ace-builds/src-noconflict/mode-javascript.js"; import "ace-builds/src-noconflict/theme-cloud9_night"; import "ace-builds/src-noconflict/theme-cloud9_day"; import {configStore} from '../store/config' export function AceCode(props: { value?: string + mode: 'json' | 'javascript' + height?: string onChange?: (v: string) => void }) { return (
{ + editor.renderer.setPadding(10) + editor.renderer.setScrollMargin(10, 0, 0, 0) + }} + height={props.height || '400px'} width={'100%'} tabSize={2} value={props.value} diff --git a/src/renderer/src/components/Nav.tsx b/src/renderer/src/components/Nav.tsx index c24fabd8..fa71ac38 100644 --- a/src/renderer/src/components/Nav.tsx +++ b/src/renderer/src/components/Nav.tsx @@ -5,9 +5,6 @@ import {Fragment, useMemo} from 'react' import {MainApi} from '../api/main' import {Update} from './Update' 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(() => { diff --git a/src/renderer/src/components/Set.tsx b/src/renderer/src/components/Set.tsx index ec7a7e1d..1592cca7 100644 --- a/src/renderer/src/components/Set.tsx +++ b/src/renderer/src/components/Set.tsx @@ -79,19 +79,6 @@ export const Set = observer(() => { configStore.setConfig('autoRebuild', e.target.checked)}/>
-
-
- Hide Web Services - If you need to share documents with others, web services can share markdown documents online in a minimalist way, - more. - - )}/> -
-
- configStore.setConfig('hideWebService', e.target.checked)}/> -
-
Heading Mark Line diff --git a/src/renderer/src/editor/Editor.tsx b/src/renderer/src/editor/Editor.tsx index d60af430..98548c12 100644 --- a/src/renderer/src/editor/Editor.tsx +++ b/src/renderer/src/editor/Editor.tsx @@ -5,7 +5,6 @@ import {MElement, MLeaf} from './elements' import {codeCache, SetNodeToDecorations, useHighlight} from './plugins/useHighlight' import {useKeyboard} from './plugins/useKeyboard' import {useOnchange} from './plugins/useOnchange' -import './parser' import {htmlParser} from './plugins/htmlParser' import {observer} from 'mobx-react-lite' import {IFileItem} from '../index' diff --git a/src/renderer/src/server/Record.tsx b/src/renderer/src/server/Record.tsx index 95edfb36..cfd7dabc 100644 --- a/src/renderer/src/server/Record.tsx +++ b/src/renderer/src/server/Record.tsx @@ -1,39 +1,97 @@ import {observer} from 'mobx-react-lite' -import {Modal, Tabs} from 'antd' +import {Button, Modal, Table, Tabs} from 'antd' import {useLocalState} from '../hooks/useLocalState' -import {IShareNote} from '../store/db' +import {db, IShareNote} from '../store/db' +import {useCallback, useEffect} from 'react' +import dayjs from 'dayjs' +import {CopyOutlined, DeleteOutlined, StopOutlined} from '@ant-design/icons' +import {RemoveShare} from './RemoveShare' +import {message$} from '../utils' export const Record = observer((props: { open: boolean onClose: () => void }) => { const [state, setState] = useLocalState({ - docs: [] as IShareNote[] + docs: [] as IShareNote[], + page: 1, + pageSize: 10, + total: 0, + config: null as any }) + const getList = useCallback(async () => { + const total = await db.shareNote.count() + const list = await db.shareNote.offset((state.page - 1) * state.pageSize).limit(state.pageSize).toArray() + setState({docs: list, total: total}) + }, []) + + useEffect(() => { + if (props.open) { + setState({page: 1}) + window.electron.ipcRenderer.invoke('get-service-config').then(res => { + setState({config: res}) + if (res) { + getList() + } + }) + } + }, [props.open]) return ( - - ) + title: 'Name', + dataIndex: 'name', + render: v => {v} + }, + { + title: 'FilePath', + dataIndex: 'filePath' + }, + { + title: 'Updated', + dataIndex: 'updated', + render: v => dayjs(v).format('MM-DD HH:mm') }, { - label: 'Files', - key: 'file', - children: ( - <> + title: 'Action', + dataIndex: 'action', + render: (_, record) => ( +
+
) } ]} diff --git a/src/renderer/src/server/Server.tsx b/src/renderer/src/server/Server.tsx index 1588fd8e..e296487d 100644 --- a/src/renderer/src/server/Server.tsx +++ b/src/renderer/src/server/Server.tsx @@ -112,7 +112,12 @@ export const Server = observer(() => { 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) + await window.api.service.uploadDoc({ + name: name, + content: html, + filePath: treeStore.openNote!.filePath, + json: treeStore.schemaMap.get(treeStore.openNote!)?.state || [] + }) if (record) { await db.shareNote.where('filePath').equals(note.filePath).modify({ updated: Date.now() @@ -170,14 +175,16 @@ export const Server = observer(() => { label: 'Share Note', children: (
- +
+ + + } {state.error && - + }
) diff --git a/src/renderer/src/share b/src/renderer/src/share deleted file mode 160000 index 34c82f0e..00000000 --- a/src/renderer/src/share +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 34c82f0eab2d4a6a22f102a7fce55ae627eab299 diff --git a/src/renderer/src/store/config.ts b/src/renderer/src/store/config.ts index 6656c2cd..cfb98f72 100644 --- a/src/renderer/src/store/config.ts +++ b/src/renderer/src/store/config.ts @@ -19,8 +19,7 @@ class ConfigStore { mas: false, dragToSort: true, spellCheck: false, - autoRebuild: true, - hideWebService: false + autoRebuild: true } timer = 0 serviceConfig: null | Record = null