1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

Merge branch 'master' into drop-browser-image

This commit is contained in:
Baptiste Augrain
2019-02-04 13:36:32 +01:00
88 changed files with 2504 additions and 776 deletions

View File

@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
const FullscreenButton = ({
onClick
}) => (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button>
)
}) => {
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
return (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button>
)
}
FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired

View File

@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 35px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()

View File

@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' />
<p>{i18n.__('.html')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e)}>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
<i className='fa fa-print' />
<p>{i18n.__('Print')}</p>
</button>

View File

@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
</div>
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' />
<p>.md</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' />
<p>.txt</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' />
<p>.html</p>
</button>

View File

@@ -39,12 +39,15 @@ class MarkdownNoteDetail extends React.Component {
isMovingNote: false,
note: Object.assign({
title: '',
content: ''
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
this.toggleLockButton = this.handleToggleLockButton.bind(this)
@@ -63,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) {
@@ -71,7 +77,7 @@ class MarkdownNoteDetail extends React.Component {
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note)
note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
@@ -292,7 +298,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})
@@ -318,7 +324,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
@@ -361,7 +368,9 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
@@ -371,6 +380,7 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
@@ -433,6 +443,7 @@ class MarkdownNoteDetail extends React.Component {
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>

View File

@@ -20,6 +20,7 @@ import _ from 'lodash'
import {findNoteTitle} from 'browser/lib/findNoteTitle'
import convertModeName from 'browser/lib/convertModeName'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import FullscreenButton from './FullscreenButton'
import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton'
@@ -48,7 +49,7 @@ class SnippetNoteDetail extends React.Component {
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
}
@@ -76,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
const nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
this.setState({
snippetIndex: 0,
note: nextNote
@@ -410,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
return (e) => {
const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
snippets[index].linesHighlighted = e.options.linesHighlighted
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState(state => ({
note: state.note
@@ -596,13 +600,16 @@ class SnippetNoteDetail extends React.Component {
}
addSnippet () {
const { config } = this.props
const { config: { editor: { snippetDefaultLanguage } } } = this.props
const { note } = this.state
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
note.snippets = note.snippets.concat([{
name: '',
mode: config.editor.snippetDefaultLanguage || 'text',
content: ''
mode: defaultLanguage,
content: '',
linesHighlighted: []
}])
const snippetIndex = note.snippets.length - 1
@@ -645,11 +652,18 @@ class SnippetNoteDetail extends React.Component {
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
}
showWarning () {
showWarning (e, msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'print': 'Print'
})[msg]
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')]
})
}
@@ -661,6 +675,8 @@ class SnippetNoteDetail extends React.Component {
const storageKey = note.storage
const folderKey = note.folder
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
@@ -685,10 +701,6 @@ class SnippetNoteDetail extends React.Component {
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
@@ -697,26 +709,34 @@ class SnippetNoteDetail extends React.Component {
? <MarkdownEditor styleName='tabView-content'
value={snippet.content}
config={config}
linesHighlighted={snippet.linesHighlighted}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
storageKey={storageKey}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
value={snippet.content}
linesHighlighted={snippet.linesHighlighted}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
autoDetect={autoDetect}
/>
}
</div>
@@ -772,6 +792,7 @@ class SnippetNoteDetail extends React.Component {
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={(e) => this.handleChange(e)}
coloredTags={config.coloredTags}
/>
</div>
<div styleName='info-right'>
@@ -780,11 +801,7 @@ class SnippetNoteDetail extends React.Component {
isActive={note.isStarred}
/>
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -800,7 +817,9 @@ class SnippetNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
type={note.type}
print={this.showWarning}
/>
</div>
</div>

View File

@@ -31,7 +31,7 @@
.tabList
absolute left right
top 55px
top 70px
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
@@ -57,6 +57,9 @@
.tabList .tabButton
navWhiteButtonColor()
width 30px
border-left 1px solid $ui-borderColor
border-top 1px solid $ui-borderColor
border-right 1px solid $ui-borderColor
.tabView
absolute left right bottom
@@ -98,17 +101,34 @@
opacity 0
transition 0.1s
body[data-theme="white"]
body[data-theme="white"], body[data-theme="default"]
.root
box-shadow $note-detail-box-shadow
border none
.tabButton
&:hover
background-color alpha($ui-button--active-backgroundColor, 20%)
color $ui-text-color
transition 0.15s
body[data-theme="dark"]
.root
border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
.tabList .tabButton
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.tabButton
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color
transition 0.15s
.body
background-color $ui-dark-noteDetail-backgroundColor
@@ -118,7 +138,6 @@ body[data-theme="dark"]
border 1px solid $ui-dark-borderColor
.tabList
background-color $ui-button--active-backgroundColor
background-color $ui-dark-noteDetail-backgroundColor
.tabList .list
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor
.tabList .tabButton
border-color $ui-solarized-dark-borderColor
.tabButton
&:hover
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
transition 0.15s
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor
.tabList .tabButton
border-color $ui-monokai-borderColor
.tabButton
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor
.tabList .tabButton
border-color $ui-dracula-borderColor
.tabButton
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.tabList
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -54,7 +54,7 @@ class StarButton extends React.Component {
: '../resources/icon/icon-star.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Star')}</span>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
</button>
)
}

View File

@@ -21,6 +21,11 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 103px
width 70px
.root--active
@extend .root
transition 0.15s

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
@@ -45,8 +46,14 @@ class TagSelect extends React.Component {
value = _.isArray(value)
? value.slice()
: []
value.push(newTag)
value = _.uniq(value)
if (!_.includes(value, newTag)) {
value.push(newTag)
}
if (this.props.saveTagsAlphabetically) {
value = _.sortBy(value)
}
this.setState({
newTag: ''
@@ -179,19 +186,34 @@ class TagSelect extends React.Component {
}
render () {
const { value, className, showTagsAlphabetically } = this.props
const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
const wrapperStyle = {}
const textStyle = {}
const BLACK = '#333333'
const WHITE = '#f1f1f1'
const color = coloredTags[tag]
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
let iconRemove = '../resources/icon/icon-x.svg'
if (color) {
wrapperStyle.backgroundColor = color
textStyle.color = invertedColor
}
if (invertedColor === WHITE) {
iconRemove = '../resources/icon/icon-x-light.svg'
}
return (
<span styleName='tag'
key={tag}
style={wrapperStyle}
>
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
>
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
</button>
</span>
)
@@ -240,7 +262,8 @@ TagSelect.contextTypes = {
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
onChange: PropTypes.func,
coloredTags: PropTypes.object
}
export default CSSModules(TagSelect, styles)

View File

@@ -3,19 +3,18 @@
align-items center
user-select none
vertical-align middle
width 100%
overflow-x scroll
width 96%
overflow-x auto
white-space nowrap
margin-top 31px
top 50px
position absolute
.root::-webkit-scrollbar
display none
&::-webkit-scrollbar
height 8px
.tag
display flex
align-items center
margin 0px 2px
margin 0px 2px 2px
padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px

View File

@@ -14,7 +14,7 @@ const ToggleModeButton = ({
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)

View File

@@ -40,6 +40,11 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()

View File

@@ -11,7 +11,7 @@ const TrashButton = ({
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Trash')}</span>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
</button>
)

View File

@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 46px
.control-trashButton--in-trash
top 60px
topBarButtonRight()

View File

@@ -96,12 +96,14 @@ class Main extends React.Component {
{
name: 'example.html',
mode: 'html',
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
linesHighlighted: []
},
{
name: 'example.js',
mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: []
}
]
})
@@ -170,10 +172,21 @@ class Main extends React.Component {
delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
}
componentWillUnmount () {
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
}
toggleMenuBarVisible () {
const { config } = this.props
const { ui } = config
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
const newConfig = Object.assign(config, newUI)
ConfigManager.set(newConfig)
}
handleLeftSlideMouseDown (e) {
@@ -234,8 +247,8 @@ class Main extends React.Component {
if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
if (newListWidth < 180) {
newListWidth = 180
} else if (newListWidth > 600) {
newListWidth = 600
}

View File

@@ -2,7 +2,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
@@ -64,13 +63,14 @@ class NoteList extends React.Component {
this.focusHandler = () => {
this.refs.list.focus()
}
this.alertIfSnippetHandler = () => {
this.alertIfSnippet()
this.alertIfSnippetHandler = (event, msg) => {
this.alertIfSnippet(msg)
}
this.importFromFileHandler = this.importFromFile.bind(this)
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
this.cloneNote = this.cloneNote.bind(this)
this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this)
@@ -96,6 +96,7 @@ class NoteList extends React.Component {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
ee.on('list:clone', this.cloneNote)
ee.on('list:focus', this.focusHandler)
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler)
@@ -118,6 +119,7 @@ class NoteList extends React.Component {
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
ee.off('list:clone', this.cloneNote)
ee.off('list:focus', this.focusHandler)
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.off('import:file', this.importFromFileHandler)
@@ -173,16 +175,15 @@ class NoteList extends React.Component {
}
}
focusNote (selectedNoteKeys, noteKey) {
focusNote (selectedNoteKeys, noteKey, pathname) {
const { router } = this.context
const { location } = this.props
this.setState({
selectedNoteKeys
})
router.push({
pathname: location.pathname,
pathname,
query: {
key: noteKey
}
@@ -201,6 +202,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex()
@@ -217,7 +219,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(priorNoteKey)
}
this.focusNote(selectedNoteKeys, priorNoteKey)
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
ee.emit('list:moved')
}
@@ -228,6 +230,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -250,7 +253,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(nextNoteKey)
}
this.focusNote(selectedNoteKeys, nextNoteKey)
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
ee.emit('list:moved')
}
@@ -262,7 +265,7 @@ class NoteList extends React.Component {
}
const selectedNoteKeys = [noteHash]
this.focusNote(selectedNoteKeys, noteHash)
this.focusNote(selectedNoteKeys, noteHash, '/home')
ee.emit('list:moved')
}
@@ -276,12 +279,6 @@ class NoteList extends React.Component {
ee.emit('top:new-note')
}
// D key
if (e.keyCode === 68) {
e.preventDefault()
this.deleteNote()
}
// E key
if (e.keyCode === 69) {
e.preventDefault()
@@ -494,14 +491,21 @@ class NoteList extends React.Component {
})
}
alertIfSnippet () {
alertIfSnippet (msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'print': 'Print'
})[msg]
const targetIndex = this.getTargetIndex()
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
buttons: [i18n.__('OK'), i18n.__('Cancel')]
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')]
})
}
}
@@ -652,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)
})
@@ -683,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)
})
@@ -706,7 +715,8 @@ class NoteList extends React.Component {
type: firstNote.type,
folder: folder.key,
title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content
content: firstNote.content,
linesHighlighted: firstNote.linesHighlighted
})
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
@@ -1042,6 +1052,7 @@ class NoteList extends React.Component {
storageName={this.getNoteStorage(note).name}
viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
coloredTags={config.coloredTags}
/>
)
}
@@ -1124,4 +1135,4 @@ NoteList.propTypes = {
})
}
export default debounceRender(CSSModules(NoteList, styles))
export default CSSModules(NoteList, styles)

View File

@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
const { storage } = this.props
this.state = {
isOpen: !!storage.isOpen
isOpen: !!storage.isOpen,
draggedOver: null
}
}
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
folderKey: data.folderKey,
fileType: data.fileType
})
return data
})
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
}
}
handleDragEnter (e) {
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
handleDragEnter (e, key) {
e.preventDefault()
if (this.state.draggedOver === key) { return }
this.setState({
draggedOver: key
})
}
handleDragLeave (e) {
e.target.style.opacity = '1'
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
e.preventDefault()
if (this.state.draggedOver === null) { return }
this.setState({
draggedOver: null
})
}
dropNote (storage, folder, dispatch, location, noteData) {
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
}
handleDrop (e, storage, folder, dispatch, location) {
e.target.style.opacity = '1'
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
e.preventDefault()
if (this.state.draggedOver !== null) {
this.setState({
draggedOver: null
})
}
const noteData = JSON.parse(e.dataTransfer.getData('note'))
this.dropNote(storage, folder, dispatch, location, noteData)
}
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
<SortableStorageItemChild
key={folder.key}
index={index}
isActive={isActive}
isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
handleDragEnter={this.handleDragEnter}
handleDragLeave={this.handleDragLeave}
handleDrop={(e) => {
this.handleDrop(e, storage, folder, dispatch, location)
}}
handleDragEnter={(e) => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={(e) => {
this.handleDragLeave(e, folder)
}}
/>
)
})

View File

@@ -20,6 +20,7 @@ import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { remote } from 'electron'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
@@ -27,6 +28,22 @@ function matchActiveTags (tags, activeTags) {
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
constructor (props) {
super(props)
this.state = {
colorPicker: {
show: false,
color: null,
tagName: null,
targetRect: null
}
}
this.dismissColorPicker = this.dismissColorPicker.bind(this)
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
}
componentDidMount () {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
@@ -104,9 +121,64 @@ class SideNav extends React.Component {
click: this.deleteTag.bind(this, tag)
})
menu.push({
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
})
context.popup(menu)
}
dismissColorPicker () {
this.setState({
colorPicker: {
show: false
}
})
}
displayColorPicker (tagName, rect) {
const { config } = this.props
this.setState({
colorPicker: {
show: true,
color: config.coloredTags[tagName],
tagName,
targetRect: rect
}
})
}
handleColorPickerConfirm (color) {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleColorPickerReset () {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
const newColoredTags = Object.assign({}, coloredTags)
delete newColoredTags[tagName]
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
@@ -207,6 +279,7 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location, config } = this.props
const { colorPicker } = this.state
const activeTags = this.getActiveTags(location.pathname)
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = _.sortBy(data.tagNoteMap.map(
@@ -237,10 +310,11 @@ class SideNav extends React.Component {
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name)}
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
isRelated={tag.related}
key={tag.name}
count={tag.size}
color={config.coloredTags[tag.name]}
/>
)
})
@@ -333,6 +407,7 @@ class SideNav extends React.Component {
render () {
const { data, location, config, dispatch } = this.props
const { colorPicker: colorPickerState } = this.state
const isFolded = config.isSideNavFolded
@@ -349,6 +424,20 @@ class SideNav extends React.Component {
useDragHandle
/>
})
let colorPicker
if (colorPickerState.show) {
colorPicker = (
<ColorPicker
color={colorPickerState.color}
targetRect={colorPickerState.targetRect}
onConfirm={this.handleColorPickerConfirm}
onCancel={this.dismissColorPicker}
onReset={this.handleColorPickerReset}
/>
)
}
const style = {}
if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/)
@@ -368,6 +457,7 @@ class SideNav extends React.Component {
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}
{colorPicker}
</div>
)
}

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n'
import debounce from 'lodash/debounce'
class TopBar extends React.Component {
constructor (props) {
@@ -25,6 +26,10 @@ class TopBar extends React.Component {
}
this.codeInitHandler = this.handleCodeInit.bind(this)
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
maxWait: 1000 / 8
})
}
componentDidMount () {
@@ -94,7 +99,6 @@ class TopBar extends React.Component {
}
handleKeyUp (e) {
const { router } = this.context
// reset states
this.setState({
isConfirmTranslation: false
@@ -106,21 +110,21 @@ class TopBar extends React.Component {
isConfirmTranslation: true
})
const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({
search: keyword
})
this.updateKeyword(keyword)
}
}
handleSearchChange (e) {
const { router } = this.context
const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push(`/searched/${encodeURIComponent(keyword)}`)
const keyword = this.refs.searchInput.value
this.updateKeyword(keyword)
} else {
e.preventDefault()
}
}
updateKeyword (keyword) {
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({
search: keyword
})

View File

@@ -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

View File

@@ -25,25 +25,31 @@ export const DEFAULT_CONFIG = {
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
toggleMenuBar: 'Alt'
},
ui: {
language: 'en',
theme: 'default',
showCopyNotification: true,
disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
showMenuBar: false
},
editor: {
theme: 'base16-light',
keyMap: 'sublime',
fontSize: '14',
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
fontFamily: win ? 'Consolas' : 'Monaco',
indentType: 'space',
indentSize: '2',
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``',
matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false,
@@ -52,7 +58,8 @@ export const DEFAULT_CONFIG = {
enableTableEditor: false,
enableFrontMatterTitle: true,
frontMatterTitleField: 'title',
spellcheck: false
spellcheck: false,
enableSmartPaste: false
},
preview: {
fontSize: '14',
@@ -81,7 +88,8 @@ export const DEFAULT_CONFIG = {
token: '',
username: '',
password: ''
}
},
coloredTags: {}
}
function validate (config) {
@@ -203,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

View File

@@ -241,7 +241,15 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/
function fixLocalURLS (renderedHTML, storagePath) {
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
/*
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
*/
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
})
@@ -270,7 +278,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
let promise
if (dropEvent.dataTransfer.files.length > 0) {
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
if (file['type'].startsWith('image')) {
if (file['type'].startsWith('image') && !file['type'].endsWith('gif')) {
return fixRotate(file)
.then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey)
.then(fileName => ({
@@ -330,7 +338,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')
}
@@ -367,6 +375,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
reader.readAsDataURL(blob)
}
/**
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
* @param {String} storageKey Key of the current storage
* @param {String} noteKey Key of the current note
* @param {NativeImage} image The native image
*/
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
if (!storageKey) {
throw new Error('storageKey has to be given')
}
if (!noteKey) {
throw new Error('noteKey has to be given')
}
if (!image) {
throw new Error('image has to be given')
}
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const imageName = `${uniqueSlug()}.png`
const imagePath = path.join(destinationDir, imageName)
const binaryData = image.toPNG()
fs.writeFileSync(imagePath, binaryData, 'binary')
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
codeEditor.insertAttachmentMd(imageMd)
}
/**
* @description Returns all attachment paths of the given markdown
* @param {String} markdownContent content in which the attachment paths should be found
@@ -434,7 +480,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)
})
}
/**
@@ -589,7 +642,8 @@ module.exports = {
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
handlePastImageEvent,
handlePasteImageEvent,
handlePasteNativeImage,
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences,

View File

@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
const input = fs.createReadStream(srcPath)
const input = fs.createReadStream(decodeURI(srcPath))
const output = fs.createWriteStream(dstPath)
output.on('error', reject)

View File

@@ -16,6 +16,7 @@ function validateInput (input) {
switch (input.type) {
case 'MARKDOWN_NOTE':
if (!_.isString(input.content)) input.content = ''
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
break
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
@@ -23,7 +24,8 @@ function validateInput (input) {
input.snippets = [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
}
break

View File

@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
id: crypto.randomBytes(16).toString('hex'),
name: 'Unnamed snippet',
prefix: [],
content: ''
content: '',
linesHighlighted: []
}
fetchSnippet(null, snippetFile).then((snippets) => {
snippets.push(newSnippet)

View File

@@ -1,9 +1,9 @@
import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import exportNote from './exportNote'
import filenamify from 'filenamify'
import * as path from 'path'
import * as fs from 'fs'
/**
* @param {String} storageKey
@@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => {
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content)
.forEach(note => {
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
exportNote(note.key, storage.path, note.content, notePath, null)
})
return {

View File

@@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs')
const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/**
* Export note together with images
* Export note together with attachments
*
* 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
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
* and copies attachments to it. Changes links to images in the content of the note
*
* @param {String} nodeKey key of the node that should be exported
* @param {String} storageKey or storage path
* @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []
if (!storagePath) {
throw new Error('Storage path is not found')
}
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
let exportedData = noteContent
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
nodeKey
)
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)

View File

@@ -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
})

View File

@@ -69,7 +69,8 @@ function importAll (storage, data) {
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
@@ -87,7 +88,8 @@ function importAll (storage, data) {
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
content: article.content,
linesHighlighted: article.linesHighlighted
}]
}
notes.push(newNote)

View File

@@ -39,6 +39,9 @@ function validateInput (input) {
if (input.content != null) {
if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
else validatedInput.linesHighlighted = input.linesHighlighted
}
return validatedInput
case 'SNIPPET_NOTE':
@@ -51,7 +54,8 @@ function validateInput (input) {
validatedInput.snippets = [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
} else {
validatedInput.snippets = input.snippets
@@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) {
snippets: [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
}
: {
type: 'MARKDOWN_NOTE',
content: ''
content: '',
linesHighlighted: []
}
noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')

View File

@@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) {
if (
currentSnippet.name === snippet.name &&
currentSnippet.prefix === snippet.prefix &&
currentSnippet.content === snippet.content
currentSnippet.content === snippet.content &&
currentSnippet.linesHighlighted === snippet.linesHighlighted
) {
// if everything is the same then don't write to disk
resolve(snippets)
@@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) {
currentSnippet.name = snippet.name
currentSnippet.prefix = snippet.prefix
currentSnippet.content = snippet.content
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippets)

View File

@@ -6,5 +6,8 @@ module.exports = {
},
'deleteNote': () => {
ee.emit('hotkey:deletenote')
},
'toggleMenuBar': () => {
ee.emit('menubar:togglemenubar')
}
}

View File

@@ -34,7 +34,7 @@ class Crowdfunding extends React.Component {
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
<br />
<p>{i18n.__('### We believe Meritocracy')}</p>
<p>{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}</p>
<p>{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}</p>
<p>{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.')}</p>
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
<p>{i18n.__('Weve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>

View File

@@ -62,7 +62,7 @@
.folderItem-right-button
vertical-align middle
height 25px
margin-top 2.5px
margin-top 2px
colorDefaultButton()
border-radius 2px
border $ui-border

View File

@@ -79,7 +79,9 @@ class HotkeyTab extends React.Component {
config.hotkey = {
toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value,
deleteNote: this.refs.deleteNote.value
deleteNote: this.refs.deleteNote.value,
pasteSmartly: this.refs.pasteSmartly.value,
toggleMenuBar: this.refs.toggleMenuBar.value
}
this.setState({
config
@@ -127,6 +129,17 @@ class HotkeyTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Show/Hide Menu Bar')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleMenuBar'
value={config.hotkey.toggleMenuBar}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
<div styleName='group-section-control'>
@@ -149,6 +162,17 @@ class HotkeyTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Paste HTML')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='pasteSmartly'
value={config.hotkey.pasteSmartly}
type='text'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -73,6 +73,11 @@ class InfoTab extends React.Component {
<div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='top'>
<ul styleName='list'>
<li>
<a href='https://issuehunt.io/repos/53266139'
onClick={(e) => this.handleLinkClick(e)}
>{i18n.__('Bounty on IssueHunt')}</a>
</li>
<li>
<a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)}
@@ -129,7 +134,7 @@ class InfoTab extends React.Component {
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
</li>
<li styleName='cc'>
{i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
{i18n.__('Copyright (C) 2017 - 2019 BoostIO')}
</li>
<li styleName='cc'>
{i18n.__('License: GPL v3')}

View File

@@ -28,9 +28,9 @@ class SnippetEditor extends React.Component {
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs,
override: true
},
mode: 'null'

View File

@@ -136,6 +136,9 @@ class SnippetTab extends React.Component {
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
scrollPastEnd={config.editor.scrollPastEnd}
onRef={ref => { this.snippetEditor = ref }} />
</div>

View File

@@ -75,6 +75,7 @@ class UiTab extends React.Component {
showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
showMenuBar: this.refs.showMenuBar.checked,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
@@ -96,7 +97,11 @@ class UiTab extends React.Component {
enableTableEditor: this.refs.enableTableEditor.checked,
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value,
spellcheck: this.refs.spellcheck.checked
matchingPairs: this.refs.matchingPairs.value,
matchingTriples: this.refs.matchingTriples.value,
explodingPairs: this.refs.explodingPairs.value,
spellcheck: this.refs.spellcheck.checked,
enableSmartPaste: this.refs.enableSmartPaste.checked
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -234,6 +239,16 @@ class UiTab extends React.Component {
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.showMenuBar}
ref='showMenuBar'
type='checkbox'
/>&nbsp;
{i18n.__('Show menu bar')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -477,6 +492,7 @@ class UiTab extends React.Component {
ref='editorSnippetDefaultLanguage'
onChange={(e) => this.handleUIChange(e)}
>
<option key='Auto Detect' value='Auto Detect'>Auto Detect</option>
{
_.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => (<option key={name} value={name}>{name}</option>))
}
@@ -552,6 +568,18 @@ class UiTab extends React.Component {
{i18n.__('Enable smart table editor')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.enableSmartPaste}
ref='enableSmartPaste'
type='checkbox'
/>&nbsp;
{i18n.__('Enable HTML paste')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -563,6 +591,48 @@ class UiTab extends React.Component {
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Matching character pairs')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={this.state.config.editor.matchingPairs}
ref='matchingPairs'
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Matching character triples')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={this.state.config.editor.matchingTriples}
ref='matchingTriples'
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Exploding character pairs')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={this.state.config.editor.explodingPairs}
ref='explodingPairs'
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-header2'>{i18n.__('Preview')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
@@ -590,6 +660,7 @@ class UiTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Code Block Theme')}</div>
<div styleName='group-section-control'>