mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Export note with local images
Looks through the note and searches for local images. Copies them to ‘images’ folder in the export path and replaces affected links. #1261
This commit is contained in:
32
browser/components/MarkdownPreview.js
Normal file → Executable file
32
browser/components/MarkdownPreview.js
Normal file → Executable file
@@ -8,7 +8,6 @@ import Raphael from 'raphael'
|
|||||||
import flowchart from 'flowchart'
|
import flowchart from 'flowchart'
|
||||||
import SequenceDiagram from 'js-sequence-diagrams'
|
import SequenceDiagram from 'js-sequence-diagrams'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||||
import fs from 'fs'
|
|
||||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
import mdurl from 'mdurl'
|
import mdurl from 'mdurl'
|
||||||
@@ -116,8 +115,6 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
|
||||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
|
||||||
this.printHandler = () => this.handlePrint()
|
this.printHandler = () => this.handlePrint()
|
||||||
|
|
||||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||||
@@ -165,35 +162,10 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveAsText () {
|
|
||||||
this.exportAsDocument('txt')
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveAsMd () {
|
|
||||||
this.exportAsDocument('md')
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePrint () {
|
handlePrint () {
|
||||||
this.refs.root.contentWindow.print()
|
this.refs.root.contentWindow.print()
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAsDocument (fileType) {
|
|
||||||
const options = {
|
|
||||||
filters: [
|
|
||||||
{ name: 'Documents', extensions: [fileType] }
|
|
||||||
],
|
|
||||||
properties: ['openFile', 'createDirectory']
|
|
||||||
}
|
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
|
||||||
(filename) => {
|
|
||||||
if (filename) {
|
|
||||||
fs.writeFile(filename, this.props.value, (err) => {
|
|
||||||
if (err) throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fixDecodedURI (node) {
|
fixDecodedURI (node) {
|
||||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
||||||
const { innerText, href } = node
|
const { innerText, href } = node
|
||||||
@@ -221,8 +193,6 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
|
||||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
|
||||||
eventEmitter.on('print', this.printHandler)
|
eventEmitter.on('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +202,6 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
|
||||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
|
||||||
eventEmitter.off('print', this.printHandler)
|
eventEmitter.off('print', this.printHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
39
browser/main/Detail/MarkdownNoteDetail.js
Normal file → Executable file
@@ -23,6 +23,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
|||||||
import { formatDate } from 'browser/lib/date-formatter'
|
import { formatDate } from 'browser/lib/date-formatter'
|
||||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||||
import striptags from 'striptags'
|
import striptags from 'striptags'
|
||||||
|
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
@@ -44,6 +45,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
|
|
||||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||||
|
this.saveAsText = this.handleSaveAsText.bind(this)
|
||||||
|
this.saveAsMd = this.handleSaveAsMd.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -52,6 +55,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.on('export:save-text', this.saveAsText)
|
||||||
|
ee.on('export:save-md', this.saveAsMd)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@@ -72,6 +77,8 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
|
|
||||||
componentDidUnmount () {
|
componentDidUnmount () {
|
||||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||||
|
ee.off('export:save-text', this.saveAsTextHandler)
|
||||||
|
ee.off('export:save-md', this.saveAsMdHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
@@ -170,6 +177,30 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('export:save-text')
|
ee.emit('export:save-text')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportAsDocument (fileType) {
|
||||||
|
const options = {
|
||||||
|
filters: [
|
||||||
|
{ name: 'Documents', extensions: [fileType] }
|
||||||
|
],
|
||||||
|
properties: ['openFile', 'createDirectory']
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||||
|
(filename) => {
|
||||||
|
if (filename) {
|
||||||
|
const note = this.props.note
|
||||||
|
|
||||||
|
exportNote(note.storage, note.content, filename)
|
||||||
|
.then((res) => {
|
||||||
|
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||||
|
}).catch((err) => {
|
||||||
|
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleTrashButtonClick (e) {
|
handleTrashButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
const { isTrashed } = note
|
const { isTrashed } = note
|
||||||
@@ -207,6 +238,14 @@ class MarkdownNoteDetail extends React.Component {
|
|||||||
ee.emit('list:next')
|
ee.emit('list:next')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSaveAsText () {
|
||||||
|
this.exportAsDocument('txt')
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveAsMd () {
|
||||||
|
this.exportAsDocument('md')
|
||||||
|
}
|
||||||
|
|
||||||
handleUndoButtonClick (e) {
|
handleUndoButtonClick (e) {
|
||||||
const { note } = this.state
|
const { note } = this.state
|
||||||
|
|
||||||
|
|||||||
37
browser/main/lib/dataApi/exportImage.js
Executable file
37
browser/main/lib/dataApi/exportImage.js
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Export an image
|
||||||
|
* @param {String} storagePath
|
||||||
|
* @param {String} srcFilename
|
||||||
|
* @param {String} dstPath
|
||||||
|
* @param {String} dstFilename if not present, destination filename will be equal to srcFilename
|
||||||
|
* @return {Promise} an image path
|
||||||
|
*/
|
||||||
|
function exportImage (storagePath, srcFilename, dstPath, dstFilename = '') {
|
||||||
|
dstFilename = dstFilename || srcFilename
|
||||||
|
|
||||||
|
const src = path.join(storagePath, 'images', srcFilename)
|
||||||
|
|
||||||
|
if (!path.extname(dstFilename)) {
|
||||||
|
dstFilename += path.extname(srcFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dstImagesFolder = path.join(dstPath, 'images')
|
||||||
|
const dst = path.join(dstImagesFolder, dstFilename)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!fs.existsSync(dstImagesFolder)) fs.mkdirSync(dstImagesFolder)
|
||||||
|
|
||||||
|
const input = fs.createReadStream(src)
|
||||||
|
const output = fs.createWriteStream(dst)
|
||||||
|
|
||||||
|
output.on('error', reject)
|
||||||
|
input.on('error', reject)
|
||||||
|
input.on('end', resolve)
|
||||||
|
input.pipe(output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exportImage
|
||||||
67
browser/main/lib/dataApi/exportNote.js
Executable file
67
browser/main/lib/dataApi/exportNote.js
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
import exportImage from 'browser/main/lib/dataApi/exportImage'
|
||||||
|
import {findStorage} from 'browser/lib/findStorage'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export note together with images
|
||||||
|
*
|
||||||
|
* If images is stored in the storage, creates 'images' subfolder in target directory
|
||||||
|
* and copies images to it. Changes links to images in the content of the note
|
||||||
|
*
|
||||||
|
* @param {String} storageKey
|
||||||
|
* @param {String} noteContent Content to export
|
||||||
|
* @param {String} targetPath Path to exported file
|
||||||
|
* @return {Promise.<*[]>}
|
||||||
|
*/
|
||||||
|
function exportNote (storageKey, noteContent, targetPath) {
|
||||||
|
const targetStorage = findStorage(storageKey)
|
||||||
|
const storagedImagesRe = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||||
|
const exportTasks = []
|
||||||
|
const images = []
|
||||||
|
|
||||||
|
const exportedData = noteContent.replace(storagedImagesRe, (match, dstFilename, srcFilename) => {
|
||||||
|
if (!path.extname(dstFilename)) {
|
||||||
|
dstFilename += path.extname(srcFilename)
|
||||||
|
}
|
||||||
|
const imagePath = path.join('images', dstFilename)
|
||||||
|
|
||||||
|
exportTasks.push(
|
||||||
|
exportImage(targetStorage.path, srcFilename, path.dirname(targetPath), dstFilename)
|
||||||
|
)
|
||||||
|
images.push(imagePath)
|
||||||
|
return ``
|
||||||
|
})
|
||||||
|
|
||||||
|
exportTasks.push(exportFile(exportedData, targetPath))
|
||||||
|
return Promise.all(exportTasks)
|
||||||
|
.catch((err) => {
|
||||||
|
rollbackExport(images)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFile (data, filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(filename, data, (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
resolve(filename)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove exported images
|
||||||
|
* @param imagesPaths
|
||||||
|
*/
|
||||||
|
function rollbackExport (imagesPaths) {
|
||||||
|
imagesPaths.forEach((path) => {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
fs.unlink(path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default exportNote
|
||||||
Reference in New Issue
Block a user