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 SequenceDiagram from 'js-sequence-diagrams'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import fs from 'fs'
|
||||
import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
@@ -116,8 +115,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
handleSaveAsText () {
|
||||
this.exportAsDocument('txt')
|
||||
}
|
||||
|
||||
handleSaveAsMd () {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handlePrint () {
|
||||
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) {
|
||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
||||
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('drop', 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)
|
||||
}
|
||||
|
||||
@@ -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('drop', 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)
|
||||
}
|
||||
|
||||
|
||||
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 { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -44,6 +45,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.saveAsText = this.handleSaveAsText.bind(this)
|
||||
this.saveAsMd = this.handleSaveAsMd.bind(this)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -52,6 +55,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.on('export:save-text', this.saveAsText)
|
||||
ee.on('export:save-md', this.saveAsMd)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -72,6 +77,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentDidUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.off('export:save-text', this.saveAsTextHandler)
|
||||
ee.off('export:save-md', this.saveAsMdHandler)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
@@ -170,6 +177,30 @@ class MarkdownNoteDetail extends React.Component {
|
||||
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) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
@@ -207,6 +238,14 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleSaveAsText () {
|
||||
this.exportAsDocument('txt')
|
||||
}
|
||||
|
||||
handleSaveAsMd () {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
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