1
0
mirror of https://github.com/BoostIo/Boostnote synced 2026-02-04 03:27:18 +00:00

Merge branch 'master' into fixIssue2534

This commit is contained in:
Gonçalo Santos
2019-11-22 00:19:56 +00:00
207 changed files with 230308 additions and 5178 deletions

View File

@@ -0,0 +1,69 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FromUrlButton.styl'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FromUrlButton extends React.Component {
constructor (props) {
super(props)
this.state = {
isActive: false
}
}
handleMouseDown (e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
this.setState({
isActive: false
})
}
render () {
const { className } = this.props
return (
<button className={_.isString(className)
? 'FromUrlButton ' + className
: 'FromUrlButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-external.svg'
: '../resources/icon/icon-external.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>
</button>
)
}
}
FromUrlButton.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CSSModules(FromUrlButton, styles)

View File

@@ -0,0 +1,41 @@
.root
top 45px
topBarButtonRight()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 125px
width 90px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.root--active
@extend .root
transition 0.15s
color $ui-favorite-star-button-color
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
.icon
transition transform 0.15s
height 13px
body[data-theme="dark"]
.root
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)

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

@@ -14,7 +14,7 @@ class InfoPanel extends React.Component {
render () {
const {
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
} = this.props
return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
@@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
</div>
<div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' />
</button>
@@ -70,22 +70,27 @@ 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) => exportAsPdf(e, 'export-pdf')}>
<i className='fa fa-file-pdf-o' />
<p>{i18n.__('.pdf')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
<i className='fa fa-print' />
<p>{i18n.__('Print')}</p>
</button>
@@ -104,6 +109,7 @@ InfoPanel.propTypes = {
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired,

View File

@@ -15,7 +15,7 @@
right 25px
position absolute
padding 20px 25px 0 25px
width 300px
// width 300px
overflow auto
background-color $ui-noteList-backgroundColor
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)

View File

@@ -5,7 +5,7 @@ import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
}) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div>
@@ -31,22 +31,22 @@ 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>
<button styleName='export--unable'>
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
<i className='fa fa-file-pdf-o' />
<p>.pdf</p>
</button>
@@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = {
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired
}
export default CSSModules(InfoPanelTrashed, styles)

View File

@@ -9,7 +9,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdownTextHelper'
import StatusBar from '../StatusBar'
@@ -30,6 +29,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
@@ -39,12 +40,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)
@@ -71,12 +75,26 @@ 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()
})
}
// Focus content if using blur or double click
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
const {switchPreview} = nextProps.config.editor
if (this.state.switchPreview !== switchPreview) {
this.setState({
switchPreview
})
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
console.log('setting focus', switchPreview)
this.focus()
}
}
}
componentWillUnmount () {
@@ -94,7 +112,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)
}
@@ -148,12 +171,12 @@ class MarkdownNoteDetail extends React.Component {
originNote: note,
note: newNote
})
hashHistory.replace({
dispatch(replace({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: newNote.key
}
})
})
}))
this.setState({
isMovingNote: false
})
@@ -190,6 +213,40 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('export:save-html')
}
exportAsPdf () {
ee.emit('export:save-pdf')
}
handleKeyDown (e) {
switch (e.keyCode) {
// tab key
case 9:
if (e.ctrlKey && !e.shiftKey) {
e.preventDefault()
this.jumpNextTab()
} else if (e.ctrlKey && e.shiftKey) {
e.preventDefault()
this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
e.preventDefault()
this.focusEditor()
}
break
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
}
}
break
}
}
handleTrashButtonClick (e) {
const { note } = this.state
const { isTrashed } = note
@@ -253,7 +310,7 @@ class MarkdownNoteDetail extends React.Component {
}
getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
}
handleDeleteKeyDown (e) {
@@ -262,7 +319,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})
@@ -288,7 +345,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
@@ -331,7 +389,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 {
@@ -341,6 +401,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}
/>
@@ -348,7 +409,7 @@ class MarkdownNoteDetail extends React.Component {
}
render () {
const { data, location, config } = this.props
const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
const folderKey = note.folder
@@ -381,13 +442,14 @@ class MarkdownNoteDetail extends React.Component {
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
</div>
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<div>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
@@ -402,12 +464,15 @@ class MarkdownNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
@@ -420,7 +485,7 @@ class MarkdownNoteDetail extends React.Component {
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img styleName='iconInfo' src={imgSrc} />
<img src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
@@ -440,13 +505,14 @@ class MarkdownNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${location.query.key})`}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
wordCount={note.content.split(' ').length}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
@@ -458,6 +524,7 @@ class MarkdownNoteDetail extends React.Component {
<div className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
>
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}

View File

@@ -80,4 +80,5 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -107,4 +107,12 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.info
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
background-color $ui-dracula-noteDetail-backgroundColor
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none

View File

@@ -10,7 +10,7 @@ const PermanentDeleteButton = ({
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>
)

View File

@@ -8,7 +8,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import {hashHistory} from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
@@ -18,8 +17,8 @@ import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager'
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'
@@ -30,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
const electron = require('electron')
const { remote } = electron
@@ -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
@@ -164,12 +166,12 @@ class SnippetNoteDetail extends React.Component {
originNote: note,
note: newNote
})
hashHistory.replace({
dispatch(replace({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: newNote.key
}
})
})
}))
this.setState({
isMovingNote: false
})
@@ -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
@@ -434,6 +438,18 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor()
}
break
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
}
}
break
// L key
case 76:
{
@@ -502,6 +518,19 @@ class SnippetNoteDetail extends React.Component {
])
}
handleWrapLineButtonClick (e) {
context.popup([
{
label: 'on',
click: (e) => this.handleWrapLineItemClick(e, true)
},
{
label: 'off',
click: (e) => this.handleWrapLineItemClick(e, false)
}
])
}
handleIndentSizeItemClick (e, indentSize) {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
@@ -534,6 +563,22 @@ class SnippetNoteDetail extends React.Component {
})
}
handleWrapLineItemClick (e, lineWrapping) {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
lineWrapping
})
ConfigManager.set({
editor
})
dispatch({
type: 'SET_CONFIG',
config: {
editor
}
})
}
focus () {
this.refs.description.focus()
}
@@ -584,13 +629,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
@@ -633,22 +681,32 @@ 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',
'export-pdf': 'PDF 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')]
})
}
render () {
const { data, config, location } = this.props
const { data, dispatch, config, location } = this.props
const { note } = this.state
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)
@@ -673,10 +731,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}}
@@ -685,26 +739,35 @@ 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}
lineWrapping={config.editor.lineWrapping}
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>
@@ -738,13 +801,14 @@ class SnippetNoteDetail extends React.Component {
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
exportAsPdf={this.showWarning}
/>
</div>
</div>
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<div>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
@@ -759,7 +823,9 @@ class SnippetNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={(e) => this.handleChange(e)}
coloredTags={config.coloredTags}
/>
</div>
<div styleName='info-right'>
@@ -768,11 +834,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)} />
@@ -783,12 +845,15 @@ class SnippetNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${location.query.key})`}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
exportAsPdf={this.showWarning}
type={note.type}
print={this.showWarning}
/>
</div>
</div>
@@ -865,6 +930,12 @@ class SnippetNoteDetail extends React.Component {
size: {config.editor.indentSize}&nbsp;
<i className='fa fa-caret-down' />
</button>
<button
onClick={(e) => this.handleWrapLineButtonClick(e)}
>
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}&nbsp;
<i className='fa fa-caret-down' />
</button>
</div>
<StatusBar

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
@@ -37,4 +42,4 @@ body[data-theme="dark"]
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
color alpha($ui-favorite-star-button-color, 0.6)

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'
@@ -7,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component {
constructor (props) {
@@ -45,8 +47,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: ''
@@ -89,8 +97,11 @@ class TagSelect extends React.Component {
}
handleTagLabelClick (tag) {
const { router } = this.context
router.push(`/tags/${tag}`)
const { dispatch } = this.props
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick (tag) {
@@ -179,19 +190,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>
)
@@ -233,14 +259,12 @@ class TagSelect extends React.Component {
}
}
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = {
dispatch: PropTypes.func,
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

@@ -8,19 +8,19 @@ const ToggleModeButton = ({
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div>
<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 styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
<img 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>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
editorType: PropTypes.string.isRequired
}
export default CSSModules(ToggleModeButton, styles)

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()
@@ -70,3 +75,10 @@ body[data-theme="dracula"]
.active
background-color #bd93f9
box-shadow 2px 0px 7px #222222
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none

View File

@@ -10,8 +10,8 @@ const TrashButton = ({
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Trash')}</span>
<img src='../resources/icon/icon-trash.svg' />
<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

@@ -10,6 +10,7 @@ import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
import debounceRender from 'react-debounce-render'
import searchFromNotes from 'browser/lib/search'
import queryString from 'query-string'
const OSX = global.process.platform === 'darwin'
@@ -36,11 +37,11 @@ class Detail extends React.Component {
}
render () {
const { location, data, params, config } = this.props
const { location, data, match: { params }, config } = this.props
const noteKey = location.search !== '' && queryString.parse(location.search).key
let note = null
if (location.query.key != null) {
const noteKey = location.query.key
if (location.search !== '') {
const allNotes = data.noteMap.map(note => note)
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
let displayedNotes = allNotes
@@ -49,16 +50,14 @@ class Detail extends React.Component {
const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
: searchFromNotes(allNotes, searchStr)
}
if (location.pathname.match(/\/tags/)) {
} else if (location.pathname.match(/^\/tags/)) {
const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
}
if (location.pathname.match(/\/trashed/)) {
if (location.pathname.match(/^\/trashed/)) {
displayedNotes = trashedNotes
} else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)

View File

@@ -0,0 +1,16 @@
import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'
const DevTools = createDevTools(
<DockMonitor
toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
defaultIsVisible={false}
>
<LogMonitor theme='tomorrow' />
</DockMonitor>
)
export default DevTools

View File

@@ -0,0 +1,8 @@
/* eslint-disable no-undef */
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
module.exports = require('./index.dev').default
} else {
// eslint-disable-next-line global-require
module.exports = require('./index.prod').default
}

View File

@@ -0,0 +1,6 @@
import React from 'react'
const DevTools = () => <div />
DevTools.instrument = () => {}
export default DevTools

View File

@@ -12,12 +12,12 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager'
import {chooseTheme, applyTheme} from 'browser/main/lib/ThemeManager'
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
import { push } from 'connected-react-router'
const path = require('path')
const electron = require('electron')
const { remote } = electron
@@ -97,12 +97,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('hello').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: []
}
]
})
@@ -131,7 +133,7 @@ class Main extends React.Component {
.then(() => data.storage)
})
.then(storage => {
hashHistory.push('/storages/' + storage.key)
store.dispatch(push('/storages/' + storage.key))
})
.catch(err => {
throw err
@@ -167,13 +169,25 @@ class Main extends React.Component {
}
})
// eslint-disable-next-line no-undef
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 +248,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
}
@@ -298,7 +312,7 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
@@ -328,7 +342,7 @@ class Main extends React.Component {
'dispatch',
'config',
'data',
'params',
'match',
'location'
])}
/>
@@ -338,7 +352,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
/>
@@ -360,7 +374,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}

View File

@@ -21,42 +21,39 @@ class NewNoteButton extends React.Component {
this.state = {
}
this.newNoteHandler = () => {
this.handleNewNoteButtonClick()
}
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
}
componentDidMount () {
eventEmitter.on('top:new-note', this.newNoteHandler)
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
}
componentWillUnmount () {
eventEmitter.off('top:new-note', this.newNoteHandler)
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
}
handleNewNoteButtonClick (e) {
const { location, dispatch, config } = this.props
const { location, dispatch, match: { params }, config } = this.props
const { storage, folder } = this.resolveTargetFolder()
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
createMarkdownNote(storage.key, folder.key, dispatch, location)
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
createSnippetNote(storage.key, folder.key, dispatch, location, config)
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
} else {
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location,
params,
config
})
}
}
resolveTargetFolder () {
const { data, params } = this.props
const { data, match: { params } } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (const kv of data.storageMap) {
@@ -92,8 +89,8 @@ class NewNoteButton extends React.Component {
>
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
onClick={this.handleNewNoteButtonClick}>
<img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span>

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'
@@ -15,23 +14,42 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
import searchFromNotes from 'browser/lib/search'
import fs from 'fs'
import path from 'path'
import { hashHistory } from 'react-router'
import { push, replace } from 'connected-react-router'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context'
import queryString from 'query-string'
const { remote } = require('electron')
const { dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt)
}
function sortByAlphabetical (a, b) {
const matchA = regexMatchStartingTitleNumber.exec(a.title)
const matchB = regexMatchStartingTitleNumber.exec(b.title)
if (matchA && matchA.length === 2 && matchB && matchB.length === 2) {
// Both note titles are starting with a float. We will compare it now.
const floatA = parseFloat(matchA[1])
const floatB = parseFloat(matchB[1])
const diff = floatA - floatB
if (diff !== 0) {
return diff
}
// The float values are equal. We will compare the full title.
}
return a.title.localeCompare(b.title)
}
@@ -64,13 +82,15 @@ 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.handleNoteListBlur = this.handleNoteListBlur.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 +116,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 +139,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)
@@ -125,15 +147,15 @@ class NoteList extends React.Component {
}
componentDidUpdate (prevProps) {
const { location } = this.props
const { dispatch, location } = this.props
const { selectedNoteKeys } = this.state
const visibleNoteKeys = this.notes.map(note => note.key)
const note = this.notes[0]
const prevKey = prevProps.location.query.key
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
const note = this.notes && this.notes[0]
const key = location.search && queryString.parse(location.search).key
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
if (note && location.query.key == null) {
const { router } = this.context
if (note && location.search === '') {
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
// A visible note is an active note
@@ -143,17 +165,17 @@ class NoteList extends React.Component {
ee.emit('list:moved')
}
router.replace({
dispatch(replace({ // was passed with context - we can use connected router here
pathname: location.pathname,
query: {
search: queryString.stringify({
key: noteKey
}
})
})
}))
return
}
// Auto scroll
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
if (_.isString(key) && prevKey === key) {
const targetIndex = this.getTargetIndex()
if (targetIndex > -1) {
const list = this.refs.list
@@ -173,20 +195,19 @@ class NoteList extends React.Component {
}
}
focusNote (selectedNoteKeys, noteKey) {
const { router } = this.context
const { location } = this.props
focusNote (selectedNoteKeys, noteKey, pathname) {
const { dispatch } = this.props
this.setState({
selectedNoteKeys
})
router.push({
pathname: location.pathname,
query: {
dispatch(push({
pathname,
search: queryString.stringify({
key: noteKey
}
})
})
}))
}
getNoteKeyFromTargetIndex (targetIndex) {
@@ -201,6 +222,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex()
@@ -217,7 +239,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 +250,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,19 +273,28 @@ class NoteList extends React.Component {
selectedNoteKeys.push(nextNoteKey)
}
this.focusNote(selectedNoteKeys, nextNoteKey)
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
ee.emit('list:moved')
}
jumpNoteByHashHandler (event, noteHash) {
const { data } = this.props
// first argument event isn't used.
if (this.notes === null || this.notes.length === 0) {
return
}
const selectedNoteKeys = [noteHash]
this.focusNote(selectedNoteKeys, noteHash)
let locationToSelect = '/home'
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
if (noteByHash !== undefined) {
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
}
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
ee.emit('list:moved')
}
@@ -276,12 +308,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()
@@ -323,9 +349,15 @@ class NoteList extends React.Component {
}
}
getNotes () {
const { data, params, location } = this.props
handleNoteListBlur () {
this.setState({
shiftKeyDown: false,
ctrlKeyDown: false
})
}
getNotes () {
const { data, match: { params }, location } = this.props
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
@@ -366,7 +398,7 @@ class NoteList extends React.Component {
// get notes in the current folder
getContextNotes () {
const { data, params } = this.props
const { data, match: { params } } = this.props
const storageKey = params.storageKey
const folderKey = params.folderKey
const storage = data.storageMap.get(storageKey)
@@ -406,8 +438,7 @@ class NoteList extends React.Component {
}
handleNoteClick (e, uniqueKey) {
const { router } = this.context
const { location } = this.props
const { dispatch, location } = this.props
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
const { ctrlKeyDown, shiftKeyDown } = this.state
const hasSelectedNoteKey = selectedNoteKeys.length > 0
@@ -458,16 +489,16 @@ class NoteList extends React.Component {
prevShiftNoteIndex
})
router.push({
dispatch(push({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: uniqueKey
}
})
})
}))
}
handleSortByChange (e) {
const { dispatch, params: { folderKey } } = this.props
const { dispatch, match: { params: { folderKey } } } = this.props
const config = {
[folderKey]: { sortBy: e.target.value }
@@ -494,14 +525,22 @@ class NoteList extends React.Component {
})
}
alertIfSnippet () {
alertIfSnippet (msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'export-pdf': 'PDF 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 +691,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 +726,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 +750,12 @@ 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,
description: firstNote.description,
snippets: firstNote.snippets,
tags: firstNote.tags,
isStarred: firstNote.isStarred
})
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
@@ -722,10 +771,10 @@ class NoteList extends React.Component {
selectedNoteKeys: [note.key]
})
hashHistory.push({
dispatch(push({
pathname: location.pathname,
query: {key: note.key}
})
search: queryString.stringify({key: note.key})
}))
})
}
@@ -735,13 +784,13 @@ class NoteList extends React.Component {
}
navigate (sender, pathname) {
const { router } = this.context
router.push({
const { dispatch } = this.props
dispatch(push({
pathname,
query: {
search: queryString.stringify({
// key: noteKey
}
})
})
}))
}
save (note) {
@@ -871,7 +920,7 @@ class NoteList extends React.Component {
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
}
// Add notes to the current folder
// Add notes to the current folder
addNotesFromFiles (filepaths) {
const { dispatch, location } = this.props
const { storage, folder } = this.resolveTargetFolder()
@@ -895,13 +944,20 @@ class NoteList extends React.Component {
}
dataApi.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: getNoteKey(note)}
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
.then((newcontent) => {
note.content = newcontent
dataApi.updateNote(storage.key, note.key, note)
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
search: queryString.stringify({key: getNoteKey(note)})
}))
})
})
})
@@ -911,14 +967,15 @@ class NoteList extends React.Component {
getTargetIndex () {
const { location } = this.props
const key = queryString.parse(location.search).key
const targetIndex = _.findIndex(this.notes, (note) => {
return getNoteKey(note) === location.query.key
return getNoteKey(note) === key
})
return targetIndex
}
resolveTargetFolder () {
const { data, params } = this.props
const { data, match: { params } } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
@@ -966,7 +1023,7 @@ class NoteList extends React.Component {
}
render () {
const { location, config, params: { folderKey } } = this.props
const { location, config, match: { params: { folderKey } } } = this.props
let { notes } = this.props
const { selectedNoteKeys } = this.state
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
@@ -1042,6 +1099,7 @@ class NoteList extends React.Component {
storageName={this.getNoteStorage(note).name}
viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
coloredTags={config.coloredTags}
/>
)
}
@@ -1088,7 +1146,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
>
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
<img src='../resources/icon/icon-column.svg' />
</button>
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active'
@@ -1096,7 +1154,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
>
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' />
<img src='../resources/icon/icon-column-list.svg' />
</button>
</div>
</div>
@@ -1105,6 +1163,7 @@ class NoteList extends React.Component {
tabIndex='-1'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
onKeyUp={this.handleNoteListKeyUp}
onBlur={this.handleNoteListBlur}
>
{noteList}
</div>
@@ -1124,4 +1183,4 @@ NoteList.propTypes = {
})
}
export default debounceRender(CSSModules(NoteList, styles))
export default CSSModules(NoteList, styles)

View File

@@ -8,7 +8,7 @@ const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<img src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>
)

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
import modal from 'browser/main/lib/modal'
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
@@ -12,6 +11,7 @@ import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { push } from 'connected-react-router'
const { remote } = require('electron')
const { dialog } = remote
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
const { storage } = this.props
this.state = {
isOpen: !!storage.isOpen
isOpen: !!storage.isOpen,
draggedOver: null
}
}
@@ -133,14 +134,14 @@ class StorageItem extends React.Component {
}
handleHeaderInfoClick (e) {
const { storage } = this.props
hashHistory.push('/storages/' + storage.key)
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key))
}
handleFolderButtonClick (folderKey) {
return (e) => {
const { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
}
}
@@ -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)
}
@@ -274,7 +299,7 @@ class StorageItem extends React.Component {
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
@@ -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)
}}
/>
)
})
@@ -331,14 +362,14 @@ class StorageItem extends React.Component {
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
>
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
<img src='../resources/icon/icon-plus.svg' />
</button>
}
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
<span>
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
</span>
{isFolded &&
@@ -349,7 +380,7 @@ class StorageItem extends React.Component {
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
<div>
{folderList}
</div>
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import { push } from 'connected-react-router'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl'
@@ -20,13 +21,31 @@ 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'
import { every, sortBy } from 'lodash'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
return every(activeTags, v => tags.indexOf(v) >= 0)
}
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)
@@ -38,14 +57,14 @@ class SideNav extends React.Component {
deleteTag (tag) {
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
ype: 'warning',
type: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (selectedButton === 0) {
const { data, dispatch, location, params } = this.props
const { data, dispatch, location, match: { params } } = this.props
const notes = data.noteMap
.map(note => note)
@@ -75,7 +94,7 @@ class SideNav extends React.Component {
if (index !== -1) {
tags.splice(index, 1)
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
}
}
})
@@ -87,13 +106,13 @@ class SideNav extends React.Component {
}
handleHomeButtonClick (e) {
const { router } = this.context
router.push('/home')
const { dispatch } = this.props
dispatch(push('/home'))
}
handleStarredButtonClick (e) {
const { router } = this.context
router.push('/starred')
const { dispatch } = this.props
dispatch(push('/starred'))
}
handleTagContextMenu (e, tag) {
@@ -104,9 +123,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
@@ -118,18 +192,18 @@ class SideNav extends React.Component {
}
handleTrashedButtonClick (e) {
const { router } = this.context
router.push('/trashed')
const { dispatch } = this.props
dispatch(push('/trashed'))
}
handleSwitchFoldersButtonClick () {
const { router } = this.context
router.push('/home')
const { dispatch } = this.props
dispatch(push('/home'))
}
handleSwitchTagsButtonClick () {
const { router } = this.context
router.push('/alltags')
const { dispatch } = this.props
dispatch(push('/alltags'))
}
onSortEnd (storage) {
@@ -198,6 +272,7 @@ class SideNav extends React.Component {
<div styleName='tagList'>
{this.tagListComponent(data)}
</div>
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div>
)
}
@@ -207,9 +282,10 @@ 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(
let tagList = sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
).filter(
tag => tag.size > 0
@@ -222,7 +298,7 @@ class SideNav extends React.Component {
})
}
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
tagList = sortBy(tagList, item => (0 - item.size))
}
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
tagList = tagList.filter(
@@ -237,10 +313,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]}
/>
)
})
@@ -274,8 +351,8 @@ class SideNav extends React.Component {
}
handleClickTagListItem (name) {
const { router } = this.context
router.push(`/tags/${encodeURIComponent(name)}`)
const { dispatch } = this.props
dispatch(push(`/tags/${encodeURIComponent(name)}`))
}
handleSortTagsByChange (e) {
@@ -293,8 +370,7 @@ class SideNav extends React.Component {
}
handleClickNarrowToTag (tag) {
const { router } = this.context
const { location } = this.props
const { dispatch, location } = this.props
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) {
@@ -302,7 +378,7 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
}
emptyTrash (entries) {
@@ -333,6 +409,7 @@ class SideNav extends React.Component {
render () {
const { data, location, config, dispatch } = this.props
const { colorPicker: colorPickerState } = this.state
const isFolded = config.isSideNavFolded
@@ -349,9 +426,23 @@ 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/)
const isTagActive = /tag/.test(location.pathname)
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
@@ -368,6 +459,7 @@ class SideNav extends React.Component {
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}
{colorPicker}
</div>
)
}

View File

@@ -47,6 +47,14 @@
.update-icon
color $brand-color
body[data-theme="default"]
.zoom
color $ui-text-color
body[data-theme="white"]
.zoom
color $ui-text-color
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor

View File

@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import EventEmitter from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote, ipcRenderer } = electron
@@ -13,6 +14,26 @@ const { dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
class StatusBar extends React.Component {
constructor (props) {
super(props)
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
}
componentDidMount () {
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
}
componentWillUnmount () {
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
}
updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
})
}
handleZoomInMenuItem () {
const zoomFactor = ZoomManager.getZoom() + 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomOutMenuItem () {
const zoomFactor = ZoomManager.getZoom() - 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomResetMenuItem () {
this.handleZoomMenuItemClick(1.0)
}
render () {
const { config, status } = this.context

View File

@@ -6,6 +6,9 @@ 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'
import CInput from 'react-composition-input'
import { push } from 'connected-react-router'
class TopBar extends React.Component {
constructor (props) {
@@ -14,22 +17,36 @@ class TopBar extends React.Component {
this.state = {
search: '',
searchOptions: [],
isSearching: false,
isAlphabet: false,
isIME: false,
isConfirmTranslation: false
isSearching: false
}
const { dispatch } = this.props
this.focusSearchHandler = () => {
this.handleOnSearchFocus()
}
this.codeInitHandler = this.handleCodeInit.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleSearchFocus = this.handleSearchFocus.bind(this)
this.handleSearchBlur = this.handleSearchBlur.bind(this)
this.handleSearchChange = this.handleSearchChange.bind(this)
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
this.debouncedUpdateKeyword = debounce((keyword) => {
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
}, 1000 / 60, {
maxWait: 1000 / 8
})
}
componentDidMount () {
const { params } = this.props
const searchWord = params.searchword
const { match: { params } } = this.props
const searchWord = params && params.searchword
if (searchWord !== undefined) {
this.setState({
search: searchWord,
@@ -46,22 +63,22 @@ class TopBar extends React.Component {
}
handleSearchClearButton (e) {
const { router } = this.context
const { dispatch } = this.props
this.setState({
search: '',
isSearching: false
})
this.refs.search.childNodes[0].blur
router.push('/searched')
dispatch(push('/searched'))
e.preventDefault()
this.debouncedUpdateKeyword('')
}
handleKeyDown (e) {
// reset states
this.setState({
isAlphabet: false,
isIME: false
})
// Re-apply search field on ENTER key
if (e.keyCode === 13) {
this.debouncedUpdateKeyword(e.target.value)
}
// Clear search on ESC
if (e.keyCode === 27) {
@@ -79,52 +96,11 @@ class TopBar extends React.Component {
ee.emit('list:prior')
e.preventDefault()
}
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
isAlphabet: true
})
// When the key is an IME input (Japanese, Chinese)
} else if (e.keyCode === 229) {
this.setState({
isIME: true
})
}
}
handleKeyUp (e) {
const { router } = this.context
// reset states
this.setState({
isConfirmTranslation: false
})
// When the key is translation confirmation (Enter, Space)
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
this.setState({
isConfirmTranslation: true
})
const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({
search: 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)}`)
} else {
e.preventDefault()
}
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
const keyword = e.target.value
this.debouncedUpdateKeyword(keyword)
}
handleSearchFocus (e) {
@@ -132,6 +108,7 @@ class TopBar extends React.Component {
isSearching: true
})
}
handleSearchBlur (e) {
e.stopPropagation()
@@ -161,7 +138,7 @@ class TopBar extends React.Component {
}
handleCodeInit () {
ee.emit('top:search', this.refs.searchInput.value)
ee.emit('top:search', this.refs.searchInput.value || '')
}
render () {
@@ -174,24 +151,23 @@ class TopBar extends React.Component {
<div styleName='control'>
<div styleName='control-search'>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
onFocus={this.handleSearchFocus}
onBlur={this.handleSearchBlur}
tabIndex='-1'
ref='search'
>
<input
<CInput
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
onInputChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
placeholder={i18n.__('Search')}
type='text'
className='searchInput'
/>
{this.state.search !== '' &&
<button styleName='control-search-input-clear'
onClick={(e) => this.handleSearchClearButton(e)}
onClick={this.handleSearchClearButton}
>
<i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
@@ -206,8 +182,8 @@ class TopBar extends React.Component {
'dispatch',
'data',
'config',
'params',
'location'
'location',
'match'
])}
/>}
</div>

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

@@ -1,11 +1,13 @@
import { Provider } from 'react-redux'
import Main from './Main'
import store from './store'
import React from 'react'
import { store, history } from './store'
import React, { Fragment } from 'react'
import ReactDOM from 'react-dom'
require('!!style!css!stylus?sourceMap!./global.styl')
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import { Route, Switch, Redirect } from 'react-router-dom'
import { ConnectedRouter } from 'connected-react-router'
import DevTools from './DevTools'
require('./lib/ipcClient')
require('../lib/customMeta')
import i18n from 'browser/lib/i18n'
@@ -77,7 +79,6 @@ document.addEventListener('click', function (e) {
})
const el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
function notify (...args) {
return new window.Notification(...args)
@@ -98,29 +99,24 @@ function updateApp () {
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<Route path='/' component={Main}>
<IndexRedirect to='/home' />
<Route path='home' />
<Route path='starred' />
<Route path='searched'>
<Route path=':searchword' />
</Route>
<Route path='trashed' />
<Route path='alltags' />
<Route path='tags'>
<IndexRedirect to='/alltags' />
<Route path=':tagname' />
</Route>
<Route path='storages'>
<IndexRedirect to='/home' />
<Route path=':storageKey'>
<IndexRoute />
<Route path='folders/:folderKey' />
</Route>
</Route>
</Route>
</Router>
<ConnectedRouter history={history}>
<Fragment>
<Switch>
<Redirect path='/' to='/home' exact />
<Route path='/(home|alltags|starred|trashed)' component={Main} />
<Route path='/searched' component={Main} exact />
<Route path='/searched/:searchword' component={Main} />
<Redirect path='/tags' to='/alltags' exact />
<Route path='/tags/:tagname' component={Main} />
{/* storages */}
<Redirect path='/storages' to='/home' exact />
<Route path='/storages/:storageKey' component={Main} exact />
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
</Switch>
<DevTools />
</Fragment>
</ConnectedRouter>
</Provider>
), el, function () {
const loadingCover = document.getElementById('loadingCover')

View File

@@ -8,9 +8,14 @@ const win = global.process.platform === 'win32'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
const electronConfig = new (require('electron-config'))()
let isInitialized = false
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
"default": true
}`
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
@@ -22,10 +27,17 @@ export const DEFAULT_CONFIG = {
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true,
autoUpdateEnabled: true,
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',
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
toggleMenuBar: 'Alt'
},
ui: {
language: 'en',
@@ -37,18 +49,23 @@ export const DEFAULT_CONFIG = {
scheduleEnd: 360,
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',
lineWrapping: true,
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``~~__',
matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false,
@@ -56,7 +73,18 @@ export const DEFAULT_CONFIG = {
fetchUrlTitle: true,
enableTableEditor: false,
enableFrontMatterTitle: true,
frontMatterTitleField: 'title'
frontMatterTitleField: 'title',
spellcheck: false,
enableSmartPaste: false,
enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
prettierConfig: ` {
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}`,
deleteUnusedAttachments: true
},
preview: {
fontSize: '14',
@@ -74,8 +102,10 @@ export const DEFAULT_CONFIG = {
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
customCSS: '/* Drop Your Custom CSS Code Here */',
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
mermaidHTMLLabel: false,
lineThroughCheckbox: true
},
blog: {
@@ -85,7 +115,8 @@ export const DEFAULT_CONFIG = {
token: '',
username: '',
password: ''
}
},
coloredTags: {}
}
function validate (config) {
@@ -98,7 +129,6 @@ function validate (config) {
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
@@ -118,6 +148,8 @@ function get () {
_save(config)
}
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
if (!isInitialized) {
isInitialized = true
let editorTheme = document.getElementById('editorTheme')
@@ -128,16 +160,12 @@ function get () {
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
if (config.editor.theme !== 'default') {
if (config.editor.theme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
if (theme) {
editorTheme.setAttribute('href', theme.path)
} else {
config.editor.theme = 'default'
}
}
@@ -146,7 +174,13 @@ function get () {
function set (updates) {
const currentConfig = get()
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
const arrangedUpdates = updates
if (updates.preview !== undefined && updates.preview.customCSS === '') {
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
}
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
@@ -162,18 +196,15 @@ function set (updates) {
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
? newConfig.editor.theme
: 'default'
if (newTheme !== 'default') {
if (newTheme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
}
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
if (newTheme) {
editorTheme.setAttribute('href', newTheme.path)
}
electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled)
ipcRenderer.send('config-renew', {
config: get()
})
@@ -196,7 +227,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

@@ -6,7 +6,9 @@ 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'
import { isString } from 'lodash'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments'
@@ -18,15 +20,23 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
* @returns {Promise<Image>} Image element created
*/
function getImage (file) {
return new Promise((resolve) => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
if (isString(file)) {
return new Promise(resolve => {
const img = new Image()
img.onload = () => resolve(img)
img.src = file
})
} else {
return new Promise(resolve => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
}
}
/**
@@ -76,7 +86,7 @@ function getOrientation (file) {
return view.getUint16(offset + (i * 12) + 8, little)
}
}
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
break
} else {
offset += view.getUint16(offset, false)
@@ -151,23 +161,28 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
try {
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
if (!fs.existsSync(sourceFilePath) && !isBase64) {
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
return reject('source file does not exist')
}
const targetStorage = findStorage.findStorage(storageKey)
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
let destinationName
if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
} else {
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
destinationName = path.basename(sourceURL.pathname)
}
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
if (isBase64) {
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
const dataBuffer = new Buffer(base64Data, 'base64')
const dataBuffer = Buffer.from(base64Data, 'base64')
outputFile.write(dataBuffer, () => {
resolve(destinationName)
})
@@ -227,9 +242,20 @@ 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) {
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))
const encodedWin32SeparatorRegex = /%5C/g
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
/*
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) {
return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
})
}
@@ -253,22 +279,87 @@ function generateAttachmentMarkdown (fileName, path, showPreview) {
* @param {Event} dropEvent DropEvent
*/
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const file = dropEvent.dataTransfer.files[0]
const filePath = file.path
const originalFileName = path.basename(filePath)
const fileType = file['type']
const isImage = fileType.startsWith('image')
let promise
if (isImage) {
promise = fixRotate(file).then(base64data => {
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
})
if (dropEvent.dataTransfer.files.length > 0) {
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
const filePath = file.path
const fileType = file.type // EX) 'image/gif' or 'text/html'
if (fileType.startsWith('image')) {
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
fileName,
title: path.basename(filePath),
isImage: true
}))
} else {
return getOrientation(file)
.then((orientation) => {
if (orientation === -1) { // The image rotation is correct and does not need adjustment
return copyAttachment(filePath, storageKey, noteKey)
} else {
return fixRotate(file).then(data => copyAttachment({
type: 'base64',
data: data,
sourceFilePath: filePath
}, storageKey, noteKey))
}
})
.then(fileName =>
({
fileName,
title: path.basename(filePath),
isImage: true
})
)
}
} else {
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
fileName,
title: path.basename(filePath),
isImage: false
}))
}
}))
} else {
promise = copyAttachment(filePath, storageKey, noteKey)
let imageURL = dropEvent.dataTransfer.getData('text/plain')
if (!imageURL) {
const match = /<img[^>]*[\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'))
})
}
@@ -279,7 +370,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')
}
@@ -316,6 +407,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
@@ -342,6 +471,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
return result
}
/**
* @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with
* @param {String} markDownContent content in which the attachment paths should be found
* @param {String} filepath The path of the file with attachments to import
* @param {String} storageKey Storage key of the destination storage
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
*/
function importAttachments (markDownContent, filepath, storageKey, noteKey) {
return new Promise((resolve, reject) => {
const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g
let attachPath = nameRegex.exec(markDownContent)
const promiseArray = []
const attachmentPaths = []
const groupIndex = 2
while (attachPath) {
let attachmentPath = attachPath[groupIndex]
attachmentPaths.push(attachmentPath)
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
attachPath = nameRegex.exec(markDownContent)
}
let numResolvedPromises = 0
if (promiseArray.length === 0) {
resolve(markDownContent)
}
for (let j = 0; j < promiseArray.length; j++) {
promiseArray[j]
.then((fileName) => {
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
})
.catch((e) => {
console.error('File does not exist in path: ' + attachmentPaths[j])
})
.finally(() => {
numResolvedPromises++
if (numResolvedPromises === promiseArray.length) {
resolve(markDownContent)
}
})
}
})
}
/**
* @description Moves the attachments of the current note to the new location.
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
@@ -383,7 +560,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)
})
}
/**
@@ -437,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
}
})
})
} else {
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
}
}
/**
* @description Get all existing attachments related to a specific note
including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid
* @param markdownContent markdownContent of the current note
* @param storageKey StorageKey of the current note
* @param noteKey NoteKey of the currentNote
* @return {Promise<Array<{path: String, isInUse: bool}>>} Promise returning the
list of attachments with their properties */
function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) {
return null
}
const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
const attachmentsInNoteOnlyFileNames = []
if (attachmentsInNote) {
for (let i = 0; i < attachmentsInNote.length; i++) {
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
}
}
if (fs.existsSync(attachmentFolder)) {
return new Promise((resolve, reject) => {
fs.readdir(attachmentFolder, (err, files) => {
if (err) {
console.error('Error reading directory "' + attachmentFolder + '". Error:')
console.error(err)
reject(err)
return
}
const attachments = []
for (const file of files) {
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
attachments.push({ path: absolutePathOfFile, isInUse: false })
} else {
attachments.push({ path: absolutePathOfFile, isInUse: true })
}
}
resolve(attachments)
})
})
} else {
return null
}
}
/**
* @description Remove all specified attachment paths
* @param attachments attachment paths
* @return {Promise} Promise after all attachments are removed */
function removeAttachmentsByPaths (attachments) {
const promises = []
for (const attachment of attachments) {
const promise = new Promise((resolve, reject) => {
fs.unlink(attachment, (err) => {
if (err) {
console.error('Could not delete "%s"', attachment)
console.error(err)
reject(err)
return
}
resolve()
})
})
promises.push(promise)
}
return Promise.all(promises)
}
/**
* Clones the attachments of a given note.
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
@@ -538,12 +790,16 @@ module.exports = {
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
handlePastImageEvent,
handlePasteImageEvent,
handlePasteNativeImage,
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
importAttachments,
removeStorageAndNoteReferences,
removeAttachmentsByPaths,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote,
getAttachmentsPathAndStatus,
moveAttachments,
cloneAttachments,
isAttachmentLink,

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

@@ -0,0 +1,86 @@
const http = require('http')
const https = require('https')
const { createTurndownService } = require('../../../lib/turndown')
const createNote = require('./createNote')
import { push } from 'connected-react-router'
import ee from 'browser/main/lib/eventEmitter'
function validateUrl (str) {
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
return true
} else {
return false
}
}
const ERROR_MESSAGES = {
ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.',
VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com',
UNEXPECTED: 'Unexpected error! Please check console for details!'
}
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
return new Promise((resolve, reject) => {
const td = createTurndownService()
if (!validateUrl(url)) {
reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR})
}
const request = url.startsWith('https') ? https : http
const req = request.request(url, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
const markdownHTML = td.turndown(data)
if (dispatch !== null) {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
})
.then((note) => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
query: {key: noteHash}
}))
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
resolve({result: true, error: null})
})
} else {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
}).then((note) => {
resolve({result: true, note, error: null})
})
}
})
})
req.on('error', (e) => {
console.error('error in parsing URL', e)
reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED})
})
req.end()
})
}
module.exports = createNoteFromUrl

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

@@ -3,7 +3,6 @@ const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const deleteSingleNote = require('./deleteNote')

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
@@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
.then(function exportNotes (data) {
const { storage, notes } = data
notes
return Promise.all(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)
.map(note => {
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return exportNote(note.key, storage.path, note.content, notePath, null)
})
return {
).then(() => ({
storage,
folderKey,
fileType,
exportDir
}
}))
})
}

View File

@@ -4,37 +4,56 @@ 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)
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
} else {
exportedData = Promise.resolve(exportedData)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
.then(() => exportedData)
.then(data => {
return saveToFile(data, targetPath)
}).catch((err) => {
rollbackExport(tasks)
throw err

View File

@@ -11,6 +11,7 @@ const dataApi = {
exportFolder: require('./exportFolder'),
exportStorage: require('./exportStorage'),
createNote: require('./createNote'),
createNoteFromUrl: require('./createNoteFromUrl'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),

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

@@ -1,7 +1,6 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')

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

@@ -1,7 +1,7 @@
import React from 'react'
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom'
import store from '../store'
import { store } from '../store'
class ModalBase extends React.Component {
constructor (props) {

View File

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

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './CreateFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import consts from 'browser/lib/consts'
import ModalEscButton from 'browser/components/ModalEscButton'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'

View File

@@ -0,0 +1,118 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './CreateMarkdownFromURLModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
class CreateMarkdownFromURLModal extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
showerror: false,
errormessage: ''
}
}
componentDidMount () {
this.refs.name.focus()
this.refs.name.select()
}
handleCloseButtonClick (e) {
this.props.close()
}
handleChange (e) {
this.setState({
name: this.refs.name.value
})
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
handleInputKeyDown (e) {
switch (e.keyCode) {
case 13:
this.confirm()
}
}
handleConfirmButtonClick (e) {
this.confirm()
}
showError (message) {
this.setState({
showerror: true,
errormessage: message
})
}
hideError () {
this.setState({
showerror: false,
errormessage: ''
})
}
confirm () {
this.hideError()
const { storage, folder, dispatch, location } = this.props
dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => {
this.props.close()
}).catch((result) => {
this.showError(result.error)
})
}
render () {
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>{i18n.__('Import Markdown From URL')}</div>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='control'>
<div styleName='control-folder'>
<div styleName='control-folder-label'>{i18n.__('Insert URL Here')}</div>
<input styleName='control-folder-input'
ref='name'
value={this.state.name}
onChange={(e) => this.handleChange(e)}
onKeyDown={(e) => this.handleInputKeyDown(e)}
/>
</div>
<button styleName='control-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
{i18n.__('Import')}
</button>
<div className='error' styleName='error'>{this.state.errormessage}</div>
</div>
</div>
)
}
}
CreateMarkdownFromURLModal.propTypes = {
storage: PropTypes.string,
folder: PropTypes.string,
dispatch: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string
})
}
export default CSSModules(CreateMarkdownFromURLModal, styles)

View File

@@ -0,0 +1,160 @@
.root
modal()
width 500px
height 270px
overflow hidden
position relative
.header
height 80px
margin-bottom 10px
margin-top 20px
font-size 18px
line-height 50px
background-color $ui-backgroundColor
color $ui-text-color
.title
font-size 36px
font-weight 600
.control-folder-label
text-align left
font-size 14px
color $ui-text-color
.control-folder-input
display block
height 40px
width 490px
padding 0 5px
margin 10px 0
border 1px solid $ui-input--create-folder-modal
border-radius 2px
background-color transparent
outline none
vertical-align middle
font-size 16px
&:disabled
background-color $ui-input--disabled-backgroundColor
&:focus, &:active
border-color $ui-active-color
.control-confirmButton
display block
height 35px
width 140px
border none
border-radius 2px
padding 0 25px
margin 20px auto
font-size 14px
colorPrimaryButton()
body[data-theme="dark"]
.root
modalDark()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-dark-text-color
.control-folder-label
color $ui-dark-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorDarkPrimaryButton()
body[data-theme="solarized-dark"]
.root
modalSolarizedDark()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-solarized-dark-text-color
.control-folder-label
color $ui-solarized-dark-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorSolarizedDarkPrimaryButton()
.error
text-align center
color #F44336
body[data-theme="monokai"]
.root
modalMonokai()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-monokai-text-color
.control-folder-label
color $ui-monokai-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.root
modalDracula()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dracula-borderColor
color $ui-dracula-text-color
.control-folder-label
color $ui-dracula-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorDraculaPrimaryButton()

View File

@@ -3,12 +3,15 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
import { openModal } from 'browser/main/lib/modal'
import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal'
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
import queryString from 'query-string'
class NewNoteModal extends React.Component {
constructor (props) {
super(props)
this.lock = false
this.state = {}
}
@@ -20,13 +23,29 @@ class NewNoteModal extends React.Component {
this.props.close()
}
handleMarkdownNoteButtonClick (e) {
handleCreateMarkdownFromUrlClick (e) {
this.props.close()
const { storage, folder, dispatch, location } = this.props
createMarkdownNote(storage, folder, dispatch, location).then(() => {
setTimeout(this.props.close, 200)
openModal(CreateMarkdownFromURLModal, {
storage: storage,
folder: folder,
dispatch,
location
})
}
handleMarkdownNoteButtonClick (e) {
const { storage, folder, dispatch, location, config } = this.props
const params = location.search !== '' && queryString.parse(location.search)
if (!this.lock) {
this.lock = true
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
}
}
handleMarkdownNoteButtonKeyDown (e) {
if (e.keyCode === 9) {
e.preventDefault()
@@ -36,9 +55,13 @@ class NewNoteModal extends React.Component {
handleSnippetNoteButtonClick (e) {
const { storage, folder, dispatch, location, config } = this.props
createSnippetNote(storage, folder, dispatch, location, config).then(() => {
setTimeout(this.props.close, 200)
})
const params = location.search !== '' && queryString.parse(location.search)
if (!this.lock) {
this.lock = true
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
}
}
handleSnippetNoteButtonKeyDown (e) {
@@ -106,10 +129,8 @@ class NewNoteModal extends React.Component {
</button>
</div>
<div styleName='description'>
<i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}
</div>
<div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div>
<div styleName='from-url' onClick={(e) => this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL</div>
</div>
)
}

View File

@@ -19,6 +19,7 @@
.control
padding 25px 0px
text-align center
display: flex
.control-button
width 240px
@@ -47,6 +48,12 @@
text-align center
margin-bottom 25px
.from-url
color $ui-inactive-text-color
text-align center
margin-bottom 25px
cursor pointer
body[data-theme="dark"]
.root
modalDark()
@@ -61,7 +68,7 @@ body[data-theme="dark"]
&:focus
colorDarkPrimaryButton()
.description
.description, .from-url
color $ui-inactive-text-color
body[data-theme="solarized-dark"]
@@ -78,7 +85,7 @@ body[data-theme="solarized-dark"]
&:focus
colorDarkPrimaryButton()
.description
.description, .from-url
color $ui-solarized-dark-text-color
body[data-theme="monokai"]
@@ -95,7 +102,7 @@ body[data-theme="monokai"]
&:focus
colorDarkPrimaryButton()
.description
.description, .from-url
color $ui-monokai-text-color
body[data-theme="dracula"]

View File

@@ -2,7 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import PropTypes from 'prop-types'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'

View File

@@ -122,6 +122,14 @@ div[id^="firstRow"]
margin-bottom 15px
margin-top 30px
.group-header--sub
@extend .group-header
margin-bottom 10px
.group-header2--sub
@extend .group-header2
margin-bottom 10px
.group-section
margin-bottom 20px
display flex
@@ -252,10 +260,12 @@ body[data-theme="dark"]
color $ui-dark-text-color
.group-header
.group-header--sub
color $ui-dark-text-color
border-color $ui-dark-borderColor
.group-header2
.group-header2--sub
color $ui-dark-text-color
.group-section-control-input
@@ -288,10 +298,12 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
.group-header
.group-header--sub
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
.group-header2
.group-header2--sub
color $ui-solarized-dark-text-color
.group-section-control-input
@@ -323,10 +335,12 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
.group-header
.group-header--sub
color $ui-monokai-text-color
border-color $ui-monokai-borderColor
.group-header2
.group-header2--sub
color $ui-monokai-text-color
.group-section-control-input
@@ -358,10 +372,12 @@ body[data-theme="dracula"]
color $ui-dracula-text-color
.group-header
.group-header--sub
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
.group-header2
.group-header2--sub
color $ui-dracula-text-color
.group-section-control-input

View File

@@ -22,19 +22,17 @@ class Crowdfunding extends React.Component {
render () {
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
<div styleName='group-header'>{i18n.__('Crowdfunding')}</div>
<p>{i18n.__('Thank you for using Boostnote!')}</p>
<br />
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
<br />
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
<div styleName='group-header2--sub'>{i18n.__('Sustainable Open Source Ecosystem')}</div>
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. Weve got tons of Github stars and hundred of contributors in two years.')}</p>
<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>
<div styleName='group-header2--sub'>{i18n.__('We believe Meritocracy')}</div>
<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

@@ -1,14 +1,8 @@
@import('./Tab')
@import('./ConfigTab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
p
font-size 16px
line-height 1.4
.cf-link
height 35px

View File

@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import ReactDOM from 'react-dom'
import styles from './FolderItem.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import { SketchPicker } from 'react-color'
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
@@ -225,7 +225,7 @@ class FolderItem extends React.Component {
<div styleName='folderItem-left'
style={{borderColor: folder.color}}
>
<span styleName='folderItem-left-name'>{folder.name}</span>
<span>{folder.name}</span>
<span styleName='folderItem-left-key'>({folder.key})</span>
</div>
<div styleName='folderItem-right'>
@@ -288,10 +288,10 @@ class Handle extends React.Component {
class SortableFolderItemComponent extends React.Component {
render () {
const StyledHandle = CSSModules(Handle, this.props.styles)
const StyledHandle = CSSModules(Handle, styles)
const DragHandle = SortableHandle(StyledHandle)
const StyledFolderItem = CSSModules(FolderItem, this.props.styles)
const StyledFolderItem = CSSModules(FolderItem, styles)
return (
<div>

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

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './FolderList.styl'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import FolderItem from './FolderItem'
import { SortableContainer } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
@@ -22,7 +22,7 @@ class FolderList extends React.Component {
})
return (
<div styleName='folderList'>
<div>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
@@ -76,11 +76,16 @@ class HotkeyTab extends React.Component {
handleHotkeyChange (e) {
const { config } = this.state
config.hotkey = {
config.hotkey = Object.assign({}, 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,
prettifyMarkdown: this.refs.prettifyMarkdown.value,
toggleMenuBar: this.refs.toggleMenuBar.value,
insertDate: this.refs.insertDate.value,
insertDateTime: this.refs.insertDateTime.value
})
this.setState({
config
})
@@ -127,6 +132,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 +165,49 @@ 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-section'>
<div styleName='group-section-label'>{i18n.__('Prettify Markdown')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='prettifyMarkdown'
value={config.hotkey.prettifyMarkdown}
type='text' />
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='insertDate'
value={config.hotkey.insertDate}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date and Time')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='insertDateTime'
value={config.hotkey.insertDateTime}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
@@ -61,6 +61,15 @@ class InfoTab extends React.Component {
})
}
toggleAutoUpdate () {
const newConfig = {
autoUpdateEnabled: !this.state.config.autoUpdateEnabled
}
this.setState({ config: newConfig })
ConfigManager.set(newConfig)
}
infoMessage () {
const { amaMessage } = this.state
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
@@ -69,10 +78,14 @@ class InfoTab extends React.Component {
render () {
return (
<div styleName='root'>
<div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='group-header'>{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)}
@@ -103,7 +116,7 @@ class InfoTab extends React.Component {
<hr />
<div styleName='header--sub'>{i18n.__('About')}</div>
<div styleName='group-header--sub'>{i18n.__('About')}</div>
<div styleName='top'>
<div styleName='icon-space'>
@@ -129,16 +142,18 @@ 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')}
</li>
</ul>
<div><label><input type='checkbox' onChange={this.toggleAutoUpdate.bind(this)} checked={this.state.config.autoUpdateEnabled} />{i18n.__('Enable Auto Update')}</label></div>
<hr styleName='separate-line' />
<div styleName='policy'>{i18n.__('Analytics')}</div>
<div styleName='group-header2--sub'>{i18n.__('Analytics')}</div>
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<br />

View File

@@ -1,16 +1,4 @@
@import('./Tab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
.top
text-align left
margin-bottom 20px
@import('./ConfigTab.styl')
.icon-space
margin 20px 0
@@ -45,13 +33,21 @@
.separate-line
margin 40px 0
.policy
width 100%
font-size 20px
margin-bottom 10px
.policy-submit
margin-top 10px
height 35px
border-radius 2px
border none
background-color alpha(#1EC38B, 90%)
padding-left 20px
padding-right 20px
text-decoration none
color white
font-weight 600
font-size 16px
&:hover
background-color #1EC38B
transition 0.2s
.policy-confirm
margin-top 10px
@@ -60,11 +56,14 @@
body[data-theme="dark"]
.root
color alpha($tab--dark-text-color, 80%)
.appId
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.appId
color $ui-solarized-dark-text-color
.list
a
color $ui-solarized-dark-active-color
@@ -72,6 +71,8 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.root
color $ui-monokai-text-color
.appId
color $ui-monokai-text-color
.list
a
color $ui-monokai-active-color
@@ -79,6 +80,8 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
.appId
color $ui-dracula-text-color
.list
a
color $ui-dracula-active-color
color $ui-dracula-active-color

View File

@@ -4,6 +4,7 @@ import _ from 'lodash'
import styles from './SnippetTab.styl'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import snippetManager from '../../../lib/SnippetManager'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) =>
@@ -28,9 +29,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'
@@ -64,7 +65,9 @@ class SnippetEditor extends React.Component {
}
saveSnippet () {
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
dataApi.updateSnippet(this.snippet)
.then(snippets => snippetManager.assignSnippets(snippets))
.catch((err) => { throw err })
}
render () {

View File

@@ -91,7 +91,7 @@ class SnippetTab extends React.Component {
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Snippets')}</div>
<div styleName='group-header'>{i18n.__('Snippets')}</div>
<SnippetList
onSnippetSelect={this.handleSnippetSelect.bind(this)}
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
@@ -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

@@ -1,14 +1,5 @@
@import('./Tab')
@import('./ConfigTab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
.group
margin-bottom 45px
@@ -127,7 +118,7 @@
background darken(#f5f5f5, 5)
.snippet-detail
width 70%
width 67%
height calc(100% - 200px)
position absolute
left 33%

View File

@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import FolderList from './FolderList'
import i18n from 'browser/lib/i18n'

View File

@@ -101,4 +101,12 @@ body[data-theme="solarized-dark"]
.header-control-button
border-color $ui-solarized-dark-button-backgroundColor
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
color $ui-solarized-dark-text-color
body[data-theme="dracula"]
.header
border-color $ui-dracula-borderColor
.header-control-button
colorDraculaDefaultButton()
border-color $ui-dracula-borderColor

View File

@@ -3,8 +3,11 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import StorageItem from './StorageItem'
import i18n from 'browser/lib/i18n'
import { humanFileSize } from 'browser/lib/utils'
import fs from 'fs'
const electron = require('electron')
const { shell, remote } = electron
@@ -35,8 +38,29 @@ class StoragesTab extends React.Component {
name: 'Unnamed',
type: 'FILESYSTEM',
path: ''
}
},
attachments: []
}
this.loadAttachmentStorage()
}
loadAttachmentStorage () {
const promises = []
this.props.data.noteMap.map(note => {
const promise = attachmentManagement.getAttachmentsPathAndStatus(
note.content,
note.storage,
note.key
)
if (promise) promises.push(promise)
})
Promise.all(promises)
.then(data => {
const result = data.reduce((acc, curr) => acc.concat(curr), [])
this.setState({attachments: result})
})
.catch(console.error)
}
handleAddStorageButton (e) {
@@ -57,8 +81,39 @@ class StoragesTab extends React.Component {
e.preventDefault()
}
handleRemoveUnusedAttachments (attachments) {
attachmentManagement.removeAttachmentsByPaths(attachments)
.then(() => this.loadAttachmentStorage())
.catch(console.error)
}
renderList () {
const { data, boundingBox } = this.props
const { attachments } = this.state
const unusedAttachments = attachments.filter(attachment => !attachment.isInUse)
const inUseAttachments = attachments.filter(attachment => attachment.isInUse)
const totalUnusedAttachments = unusedAttachments.length
const totalInuseAttachments = inUseAttachments.length
const totalAttachments = totalUnusedAttachments + totalInuseAttachments
const totalUnusedAttachmentsSize = unusedAttachments
.reduce((acc, curr) => {
const stats = fs.statSync(curr.path)
const fileSizeInBytes = stats.size
return acc + fileSizeInBytes
}, 0)
const totalInuseAttachmentsSize = inUseAttachments
.reduce((acc, curr) => {
const stats = fs.statSync(curr.path)
const fileSizeInBytes = stats.size
return acc + fileSizeInBytes
}, 0)
const totalAttachmentsSize = totalUnusedAttachmentsSize + totalInuseAttachmentsSize
const unusedAttachmentPaths = unusedAttachments
.reduce((acc, curr) => acc.concat(curr.path), [])
if (!boundingBox) { return null }
const storageList = data.storageMap.map((storage) => {
@@ -82,6 +137,20 @@ class StoragesTab extends React.Component {
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
</button>
</div>
<div styleName='header'>{i18n.__('Attachment storage')}</div>
<p styleName='list-attachment-label'>
Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items)
</p>
<p styleName='list-attachment-label'>
In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items)
</p>
<p styleName='list-attachment-label'>
Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items)
</p>
<button styleName='list-attachement-clear-button'
onClick={() => this.handleRemoveUnusedAttachments(unusedAttachmentPaths)}>
{i18n.__('Clear unused attachments')}
</button>
</div>
)
}

View File

@@ -1,8 +1,4 @@
@import('./Tab')
.root
padding 15px
color $ui-text-color
@import('./ConfigTab')
.list
margin-bottom 15px
@@ -37,6 +33,17 @@
colorDefaultButton()
font-size $tab--button-font-size
border-radius 2px
.list-attachment-label
margin-bottom 10px
color $ui-text-color
.list-attachement-clear-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
padding 0 20px
.addStorage
margin-bottom 15px
@@ -158,8 +165,8 @@ body[data-theme="dark"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
.list-attachement-clear-button
colorDarkPrimaryButton()
body[data-theme="solarized-dark"]
.root
@@ -198,6 +205,8 @@ body[data-theme="solarized-dark"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-solarized-dark-borderColor
.list-attachement-clear-button
colorSolarizedDarkPrimaryButton()
body[data-theme="monokai"]
.root
@@ -236,6 +245,8 @@ body[data-theme="monokai"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-monokai-borderColor
.list-attachement-clear-button
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.root
@@ -273,4 +284,6 @@ body[data-theme="dracula"]
colorDarkPrimaryButton()
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-dracula-borderColor
border-color $ui-dracula-borderColor
.list-attachement-clear-button
colorDraculaPrimaryButton()

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import consts from 'browser/lib/consts'
import ReactCodeMirror from 'react-codemirror'
import CodeMirror from 'codemirror'
@@ -29,7 +29,13 @@ class UiTab extends React.Component {
componentDidMount () {
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript')
// Set CM editor Sizes
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
this.prettierConfigCM.getCodeMirror().setSize('400px', '400px')
this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px')
this.handleSettingDone = () => {
this.setState({UiAlert: {
type: 'success',
@@ -75,12 +81,14 @@ class UiTab extends React.Component {
scheduleEnd: this.refs.scheduleEnd.value,
language: this.refs.uiLanguage.value,
defaultNote: this.refs.defaultNote.value,
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked,
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
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
@@ -94,6 +102,7 @@ class UiTab extends React.Component {
enableRulers: this.refs.enableEditorRulers.value === 'true',
rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','),
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
lineWrapping: this.refs.editorLineWrapping.checked,
switchPreview: this.refs.editorSwitchPreview.value,
keyMap: this.refs.editorKeyMap.value,
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
@@ -101,7 +110,16 @@ class UiTab extends React.Component {
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
enableTableEditor: this.refs.enableTableEditor.checked,
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value
frontMatterTitleField: this.refs.frontMatterTitleField.value,
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,
enableMarkdownLint: this.refs.enableMarkdownLint.checked,
customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue(),
prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(),
deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -119,6 +137,7 @@ class UiTab extends React.Component {
breaks: this.refs.previewBreaks.checked,
smartArrows: this.refs.previewSmartArrows.checked,
sanitize: this.refs.previewSanitize.value,
mermaidHTMLLabel: this.refs.previewMermaidHTMLLabel.checked,
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
lineThroughCheckbox: this.refs.lineThroughCheckbox.checked,
customCSS: this.customCSSCM.getCodeMirror().getValue()
@@ -128,8 +147,13 @@ class UiTab extends React.Component {
const newCodemirrorTheme = this.refs.editorTheme.value
if (newCodemirrorTheme !== codemirrorTheme) {
checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`)
const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme)
if (theme) {
checkHighLight.setAttribute('href', theme.path)
}
}
this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }, () => {
const {ui, editor, preview} = this.props.config
this.currentConfig = {ui, editor, preview}
@@ -323,6 +347,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)}
@@ -358,6 +392,7 @@ class UiTab extends React.Component {
</div>
: null
}
<div styleName='group-header2'>Tags</div>
<div styleName='group-checkBoxSection'>
@@ -404,6 +439,17 @@ class UiTab extends React.Component {
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.tagNewNoteWithFilteringTags}
ref='tagNewNoteWithFilteringTags'
type='checkbox'
/>&nbsp;
{i18n.__('New notes are tagged with the filtering tags')}
</label>
</div>
<div styleName='group-header2'>Editor</div>
<div styleName='group-section'>
@@ -417,7 +463,7 @@ class UiTab extends React.Component {
>
{
themes.map((theme) => {
return (<option value={theme} key={theme}>{theme}</option>)
return (<option value={theme.name} key={theme.name}>{theme.name}</option>)
})
}
</select>
@@ -554,6 +600,7 @@ class UiTab extends React.Component {
ref='editorSnippetDefaultLanguage'
onChange={(e) => this.handleUIChange(e)}
>
<option key='Auto Detect' value='Auto Detect'>{i18n.__('Auto Detect')}</option>
{
_.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => (<option key={name} value={name}>{name}</option>))
}
@@ -597,6 +644,17 @@ class UiTab extends React.Component {
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.lineWrapping}
ref='editorLineWrapping'
type='checkbox'
/>&nbsp;
{i18n.__('Wrap line in Snippet Note')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -630,6 +688,108 @@ class UiTab extends React.Component {
</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)}
checked={this.state.config.editor.spellcheck}
ref='spellcheck'
type='checkbox'
/>&nbsp;
{i18n.__('Enable spellcheck - Experimental feature!! :)')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.deleteUnusedAttachments}
ref='deleteUnusedAttachments'
type='checkbox'
/>&nbsp;
{i18n.__('Delete attachments, that are not referenced in the text anymore')}
</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-section'>
<div styleName='group-section-label'>
{i18n.__('Custom MarkdownLint Rules')}
</div>
<div styleName='group-section-control'>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.enableMarkdownLint}
ref='enableMarkdownLint'
type='checkbox'
/>&nbsp;
{i18n.__('Enable MarkdownLint')}
<div style={{fontFamily, display: this.state.config.editor.enableMarkdownLint ? 'block' : 'none'}}>
<ReactCodeMirror
width='400px'
height='200px'
onChange={e => this.handleUIChange(e)}
ref={e => (this.customMarkdownLintConfigCM = e)}
value={config.editor.customMarkdownLintConfig}
options={{
lineNumbers: true,
mode: 'application/json',
theme: codemirrorTheme,
lint: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers']
}} />
</div>
</div>
</div>
<div styleName='group-header2'>{i18n.__('Preview')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
@@ -657,6 +817,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'>
@@ -666,7 +827,7 @@ class UiTab extends React.Component {
>
{
themes.map((theme) => {
return (<option value={theme} key={theme}>{theme}</option>)
return (<option value={theme.name} key={theme.name}>{theme.name}</option>)
})
}
</select>
@@ -759,6 +920,16 @@ class UiTab extends React.Component {
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.mermaidHTMLLabel}
ref='previewMermaidHTMLLabel'
type='checkbox'
/>&nbsp;
{i18n.__('Enable HTML label in mermaid flowcharts')}
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('LaTeX Inline Open Delimiter')}
@@ -850,7 +1021,27 @@ class UiTab extends React.Component {
</div>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Prettier Config')}
</div>
<div styleName='group-section-control'>
<div style={{fontFamily}}>
<ReactCodeMirror
width='400px'
height='400px'
onChange={e => this.handleUIChange(e)}
ref={e => (this.prettierConfigCM = e)}
value={config.editor.prettierConfig}
options={{
lineNumbers: true,
mode: 'application/json',
lint: true,
theme: codemirrorTheme
}} />
</div>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}>{i18n.__('Save')}

View File

@@ -147,7 +147,7 @@ class Preferences extends React.Component {
key={tab.target}
onClick={(e) => this.handleNavButtonClick(tab.target)(e)}
>
<span styleName='nav-button-label'>
<span>
{tab.label}
</span>
{isUiHotkeyTab ? this.haveToSaveNotif(tab[tab.label].type, tab[tab.label].message) : null}

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RenameFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'

View File

@@ -1,8 +1,10 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import { combineReducers, createStore, compose, applyMiddleware } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'
import { createHashHistory as createHistory } from 'history'
import ConfigManager from 'browser/main/lib/ConfigManager'
import { Map, Set } from 'browser/lib/Mutable'
import _ from 'lodash'
import DevTools from './DevTools'
function defaultDataMap () {
return {
@@ -44,7 +46,9 @@ function data (state = defaultDataMap(), action) {
const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey)
folderNoteSet.add(uniqueKey)
assignToTags(note.tags, state, uniqueKey)
if (!note.isTrashed) {
assignToTags(note.tags, state, uniqueKey)
}
})
return state
case 'UPDATE_NOTE':
@@ -463,13 +467,17 @@ function getOrInitItem (target, key) {
return results
}
const history = createHistory()
const reducer = combineReducers({
data,
config,
status,
routing: routerReducer
router: connectRouter(history)
})
const store = createStore(reducer)
const store = createStore(reducer, undefined, process.env.NODE_ENV === 'development'
? compose(applyMiddleware(routerMiddleware(history)), DevTools.instrument())
: applyMiddleware(routerMiddleware(history)))
export default store
export { store, history }