diff --git a/LICENSE b/LICENSE
index 7472c9eb2..2d1ab1312 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you.
-Copyright (C) 2017 - 2018 BoostIO
+Copyright (C) 2017 - 2019 BoostIO
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index 486349934..6ad294ed2 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -888,10 +888,7 @@ export default class CodeEditor extends React.Component {
handlePaste (editor, forceSmartPaste) {
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
- const isURL = str => {
- const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
- return matcher.test(str)
- }
+ const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
const isInLinkTag = editor => {
const startCursor = editor.getCursor('start')
@@ -964,7 +961,7 @@ export default class CodeEditor extends React.Component {
} else {
const image = clipboard.readImage()
if (!image.isEmpty()) {
- attachmentManagement.handlePastNativeImage(
+ attachmentManagement.handlePasteNativeImage(
this,
storageKey,
noteKey,
@@ -1109,7 +1106,7 @@ export default class CodeEditor extends React.Component {
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
- resolve(iconv.decode(new Buffer(buff), charset).toString())
+ resolve(iconv.decode(Buffer.from(buff), charset).toString())
} catch (e) {
reject(e)
}
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index e31548d0b..466be3fa7 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -20,10 +20,10 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
- status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
+ status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
renderValue: props.value,
keyPressed: new Set(),
- isLocked: false
+ isLocked: props.isLocked
}
this.lockEditorCode = () => this.handleLockEditor()
@@ -76,6 +76,7 @@ class MarkdownEditor extends React.Component {
}
handleContextMenu (e) {
+ if (this.state.isLocked) return
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index 4c638dc94..a6819ce92 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -832,6 +832,81 @@ export default class MarkdownPreview extends React.Component {
)
}
)
+
+ const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
+ const rect = markdownPreviewIframe.getBoundingClientRect()
+ const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
+ for (const img of imgList) {
+ img.onclick = () => {
+ const widthMagnification = document.body.clientWidth / img.width
+ const heightMagnification = document.body.clientHeight / img.height
+ const baseOnWidth = widthMagnification < heightMagnification
+ const magnification = baseOnWidth ? widthMagnification : heightMagnification
+
+ const zoomImgWidth = img.width * magnification
+ const zoomImgHeight = img.height * magnification
+ const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
+ const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
+ const originalImgTop = img.y + rect.top
+ const originalImgLeft = img.x + rect.left
+ const originalImgRect = {
+ top: `${originalImgTop}px`,
+ left: `${originalImgLeft}px`,
+ width: `${img.width}px`,
+ height: `${img.height}px`
+ }
+ const zoomInImgRect = {
+ top: `${baseOnWidth ? zoomImgTop : 0}px`,
+ left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
+ width: `${zoomImgWidth}px`,
+ height: `${zoomImgHeight}px`
+ }
+ const animationSpeed = 300
+
+ const zoomImg = document.createElement('img')
+ zoomImg.src = img.src
+ zoomImg.style = `
+ position: absolute;
+ top: ${baseOnWidth ? zoomImgTop : 0}px;
+ left: ${baseOnWidth ? 0 : zoomImgLeft}px;
+ width: ${zoomImgWidth};
+ height: ${zoomImgHeight}px;
+ `
+ zoomImg.animate([
+ originalImgRect,
+ zoomInImgRect
+ ], animationSpeed)
+
+ const overlay = document.createElement('div')
+ overlay.style = `
+ background-color: rgba(0,0,0,0.5);
+ cursor: zoom-out;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: ${document.body.clientHeight}px;
+ z-index: 100;
+ `
+ overlay.onclick = () => {
+ zoomImg.style = `
+ position: absolute;
+ top: ${originalImgTop}px;
+ left: ${originalImgLeft}px;
+ width: ${img.width}px;
+ height: ${img.height}px;
+ `
+ const zoomOutImgAnimation = zoomImg.animate([
+ zoomInImgRect,
+ originalImgRect
+ ], animationSpeed)
+ zoomOutImgAnimation.onfinish = () => overlay.remove()
+ }
+
+ overlay.appendChild(zoomImg)
+ document.body.appendChild(overlay)
+ }
+ }
}
focus () {
diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
index da767a9f4..4921b531d 100644
--- a/browser/components/markdown.styl
+++ b/browser/components/markdown.styl
@@ -61,6 +61,8 @@ body
// allow inline line breaks
.katex
white-space initial
+ .katex .katex-html
+ display inline-flex
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
@@ -163,6 +165,7 @@ p
white-space pre-line
word-wrap break-word
img
+ cursor zoom-in
max-width 100%
strong, b
font-weight bold
diff --git a/browser/lib/markdownTextHelper.js b/browser/lib/markdownTextHelper.js
index 1501e2c70..1657efd9d 100644
--- a/browser/lib/markdownTextHelper.js
+++ b/browser/lib/markdownTextHelper.js
@@ -22,7 +22,7 @@ export function strip (input) {
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
.replace(/>/g, '')
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
- .replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
+ .replace(/^#{1,6}\s*/gm, '')
.replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1')
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index ad7eb5909..3ed61eb74 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -42,9 +42,10 @@ class MarkdownNoteDetail extends React.Component {
content: '',
linesHighlighted: []
}, props.note),
- isLockButtonShown: false,
+ isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false,
- editorType: props.config.editor.type
+ editorType: props.config.editor.type,
+ switchPreview: props.config.editor.switchPreview
}
this.dispatchTimer = null
@@ -65,6 +66,9 @@ class MarkdownNoteDetail extends React.Component {
})
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
+
+ // Focus content if using blur or double click
+ if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus()
}
componentWillReceiveProps (nextProps) {
@@ -96,7 +100,12 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
- note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
+
+ let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
+ title = striptags(title)
+ title = markdown.strip(title)
+ note.title = title
+
this.updateNote(note)
}
@@ -294,7 +303,7 @@ class MarkdownNoteDetail extends React.Component {
handleToggleLockButton (event, noteStatus) {
// first argument event is not used
- if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
+ if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
} else {
this.setState({isLockButtonShown: false})
@@ -320,7 +329,8 @@ class MarkdownNoteDetail extends React.Component {
}
handleSwitchMode (type) {
- this.setState({ editorType: type }, () => {
+ // If in split mode, hide the lock button
+ this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
@@ -365,6 +375,7 @@ class MarkdownNoteDetail extends React.Component {
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
+ isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index ca513c044..33a0adf08 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -656,14 +656,18 @@ class NoteList extends React.Component {
})
)
.then((data) => {
- data.forEach((item) => {
- dispatch({
- type: 'DELETE_NOTE',
- storageKey: item.storageKey,
- noteKey: item.noteKey
+ const dispatchHandler = () => {
+ data.forEach((item) => {
+ dispatch({
+ type: 'DELETE_NOTE',
+ storageKey: item.storageKey,
+ noteKey: item.noteKey
+ })
})
- })
+ }
+ ee.once('list:next', dispatchHandler)
})
+ .then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
@@ -687,6 +691,7 @@ class NoteList extends React.Component {
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
})
+ .then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
})
diff --git a/browser/main/global.styl b/browser/main/global.styl
index e04060c28..d864993dd 100644
--- a/browser/main/global.styl
+++ b/browser/main/global.styl
@@ -97,6 +97,7 @@ modalBackColor = white
body[data-theme="dark"]
+ background-color $ui-dark-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -148,6 +149,7 @@ body[data-theme="dark"]
z-index modalZIndex + 5
body[data-theme="solarized-dark"]
+ background-color $ui-solarized-dark-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -157,6 +159,7 @@ body[data-theme="solarized-dark"]
color: $ui-solarized-dark-text-color
body[data-theme="monokai"]
+ background-color $ui-monokai-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -166,6 +169,7 @@ body[data-theme="monokai"]
color: $ui-monokai-text-color
body[data-theme="dracula"]
+ background-color $ui-dracula-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 059589d07..5558b3bd1 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -41,7 +41,7 @@ export const DEFAULT_CONFIG = {
theme: 'base16-light',
keyMap: 'sublime',
fontSize: '14',
- fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
+ fontFamily: win ? 'Consolas' : 'Monaco',
indentType: 'space',
indentSize: '2',
enableRulers: false,
@@ -211,7 +211,7 @@ function assignConfigValues (originalConfig, rcConfig) {
function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
- config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
+ config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
})
return config
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index 6a0315f7a..6fa3b51fa 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -6,6 +6,7 @@ const mdurl = require('mdurl')
const fse = require('fs-extra')
const escapeStringRegexp = require('escape-string-regexp')
const sander = require('sander')
+const url = require('url')
import i18n from 'browser/lib/i18n'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
@@ -18,15 +19,23 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
* @returns {Promise {i18n.__('We thought that it will be nice if we can pay reward for our contributors.')} {i18n.__('### We believe Meritocracy')} {i18n.__('We think developers who has skill and did great things must be rewarded properly.')} {i18n.__('We think developers who have skills and do great things must be rewarded properly.')} {i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')} {i18n.__('It sometimes looks like exploitation.')} {i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html'))
+ if (match) {
+ imageURL = match[1]
+ }
+ }
+
+ if (!imageURL) {
+ return
+ }
+
+ promise = Promise.all([getImage(imageURL)
+ .then(image => {
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')
+ canvas.width = image.width
+ canvas.height = image.height
+ context.drawImage(image, 0, 0)
+
+ return copyAttachment({type: 'base64', data: canvas.toDataURL(), sourceFilePath: imageURL}, storageKey, noteKey)
+ })
+ .then(fileName => ({
+ fileName,
+ title: imageURL,
+ isImage: true
+ }))])
}
- promise.then((fileName) => {
- const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
- codeEditor.insertAttachmentMd(imageMd)
+
+ promise.then(files => {
+ const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage))
+
+ codeEditor.insertAttachmentMd(attachments.join('\n'))
})
}
@@ -287,7 +348,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
* @param {String} noteKey Key of the current note
* @param {DataTransferItem} dataTransferItem Part of the past-event
*/
-function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
+function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
@@ -331,7 +392,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
* @param {String} noteKey Key of the current note
* @param {NativeImage} image The native image
*/
-function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
+function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
@@ -429,7 +490,14 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
* @returns {String} Input without the references
*/
function removeStorageAndNoteReferences (input, noteKey) {
- return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
+ return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
+ const temp = match
+ .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
+ .replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
+ .replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
+ .replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
+ return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
+ })
}
/**
@@ -584,8 +652,8 @@ module.exports = {
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
- handlePastImageEvent,
- handlePastNativeImage,
+ handlePasteImageEvent,
+ handlePasteNativeImage,
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences,
diff --git a/browser/main/lib/dataApi/init.js b/browser/main/lib/dataApi/init.js
index 7f81e90bd..0dbcc1828 100644
--- a/browser/main/lib/dataApi/init.js
+++ b/browser/main/lib/dataApi/init.js
@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const consts = require('browser/lib/consts')
const path = require('path')
+const fs = require('fs')
const CSON = require('@rokt33r/season')
/**
* @return {Object} all storages and notes
@@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season')
* 2. legacy
* 3. empty directory
*/
+
function init () {
const fetchStorages = function () {
let rawStorages
try {
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
+ // Remove storages who's location is inaccesible.
+ rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path))
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
} catch (e) {
console.warn('Failed to parse cached data from localStorage', e)
@@ -36,6 +40,7 @@ function init () {
const fetchNotes = function (storages) {
const findNotesFromEachStorage = storages
+ .filter(storage => fs.existsSync(storage.path))
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {
@@ -51,7 +56,11 @@ function init () {
}
})
if (unknownCount > 0) {
- CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
+ try {
+ CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
+ } catch (e) {
+ console.log('Error writting boostnote.json: ' + e + ' from init.js')
+ }
}
return notes
})
diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js
index f6389cd86..f94ee5caf 100644
--- a/browser/main/modals/PreferencesModal/Crowdfunding.js
+++ b/browser/main/modals/PreferencesModal/Crowdfunding.js
@@ -34,7 +34,7 @@ class Crowdfunding extends React.Component {
+https://github.com/BoostIO/Boostnote/pull/2794
+
+In the following, replace \
\n' +
- '
\n' +
+ '
\n' +
'
\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '
\n' + '\n' +
- '
\n' +
+ '
\n' +
'
\n' + + ' \n' + + '\n' + ' \n' + '' const storagePath = '<:storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + 'f939b2c3.jpg\n' + + '
\n' +
- '
\n' +
+ '
\n' +
'
\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '
\n' + '\n' +
- '
\n' +
+ '
\n' +
'
\n' + + ' \n' + + '\n' + ' \n' + '' const actual = systemUnderTest.fixLocalURLS(testInput, storagePath) @@ -171,6 +320,7 @@ it('should replace the all ":storage" path with the actual storage path', functi it('should replace the ":storage" path with the actual storage path when they have different path separators', function () { const storageFolder = systemUnderTest.DESTINATION_FOLDER + const noteKey = '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' const testInput = '\n' + ' \n' + @@ -179,10 +329,10 @@ it('should replace the ":storage" path with the actual storage path when they ha ' \n' + 'file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + 'f939b2c3.jpg\n' + + '
\n' +
- '
\n' +
+ '
\n' +
'
\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '
\n' + ' \n' + '' @@ -195,10 +345,10 @@ it('should replace the ":storage" path with the actual storage path when they ha ' \n' + '\n' +
- '
\n' +
+ '
\n' +
'
\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '
\n' + ' \n' + '' @@ -251,28 +401,17 @@ it('should test that getAttachmentsInMarkdownContent finds all attachments when it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute paths', function () { const dummyStoragePath = 'dummyStoragePath' - const testInput = - '\n' + - ' \n' + - ' //header\n' + - ' \n' + - ' \n' + - '\n' +
- '
\n' +
- '
\n' + - ' dummyPDF.pdf\n' + - '
\n' + - '\n' +
- '
\n' +
- '
\n' +
- '
\n' +
+ '
\n' +
'
\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '
\n' + '\n' +
- '
\n' +
+ '
\n' +
'