mirror of
https://github.com/BoostIo/Boostnote
synced 2026-01-27 15:47:15 +00:00
Merge branch 'master' into html-to-md
# Conflicts: # browser/main/modals/NewNoteModal.js # package-lock.json # package.json # yarn.lock
This commit is contained in:
@@ -23,7 +23,7 @@ body[data-theme="dark"]
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
.empty-message
|
||||
color $ui-dracula-text-color
|
||||
@@ -36,7 +36,7 @@
|
||||
height 34px
|
||||
width 20px
|
||||
line-height 34px
|
||||
|
||||
|
||||
.search-input
|
||||
vertical-align middle
|
||||
position relative
|
||||
@@ -71,7 +71,7 @@
|
||||
overflow ellipsis
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item--active
|
||||
@extend .search-optionList-item
|
||||
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-monokai-inactive-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.search-optionList
|
||||
color #f8f8f2
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
color $ui-dracula-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color $ui-dracula-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dracula-inactive-text-color
|
||||
|
||||
@@ -4,14 +4,18 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FullscreenButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
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')}</span>
|
||||
</button>
|
||||
)
|
||||
}) => {
|
||||
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
|
||||
return (
|
||||
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
FullscreenButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 35px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
@@ -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'}}>
|
||||
@@ -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,
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
.control-infoButton-panel
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top: 50px
|
||||
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)
|
||||
@@ -32,6 +33,7 @@
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top 50px
|
||||
right 0px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
@@ -255,3 +257,43 @@ body[data-theme="monokai"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-dracula-borderColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dracula-borderColor, 20%)
|
||||
color $ui-dracula-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
@@ -29,6 +28,9 @@ import { formatDate } from 'browser/lib/date-formatter'
|
||||
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) {
|
||||
@@ -38,15 +40,19 @@ 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)
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -59,22 +65,41 @@ class MarkdownNoteDetail extends React.Component {
|
||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||
this.handleSwitchMode(reversedType)
|
||||
})
|
||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
const isNewNote = nextProps.note.key !== this.props.note.key
|
||||
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
|
||||
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 () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
@@ -87,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)))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -122,6 +152,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
const { dispatch } = this.props
|
||||
const { note } = this.state
|
||||
const value = this.refs.folder.value
|
||||
const splitted = value.split('-')
|
||||
@@ -141,12 +172,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
|
||||
})
|
||||
@@ -183,6 +214,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
|
||||
@@ -255,13 +320,18 @@ 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})
|
||||
}
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const editor = this.refs.content.refs.code.editor
|
||||
markdownToc.generateInEditor(editor)
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
this.focus()
|
||||
}
|
||||
@@ -276,16 +346,42 @@ 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
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteNote () {
|
||||
this.handleTrashButtonClick()
|
||||
}
|
||||
|
||||
handleClearTodo () {
|
||||
const { note } = this.state
|
||||
const splitted = note.content.split('\n')
|
||||
|
||||
const clearTodoContent = splitted.map((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/\[x\]/i)) {
|
||||
return line.replace(/\[x\]/i, '[ ]')
|
||||
} else {
|
||||
return line
|
||||
}
|
||||
}).join('\n')
|
||||
|
||||
note.content = clearTodoContent
|
||||
this.refs.content.setValue(note.content)
|
||||
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
renderEditor () {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return <MarkdownEditor
|
||||
ref='content'
|
||||
@@ -294,7 +390,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 {
|
||||
@@ -304,6 +402,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}
|
||||
/>
|
||||
@@ -311,7 +410,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location } = this.props
|
||||
const { data, location, config } = this.props
|
||||
const { note, editorType } = this.state
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
@@ -344,6 +443,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
exportAsPdf={this.exportAsPdf}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,9 +462,13 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
@@ -401,12 +505,13 @@ 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}
|
||||
exportAsPdf={this.exportAsPdf}
|
||||
wordCount={note.content.split(' ').length}
|
||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||
type={note.type}
|
||||
@@ -419,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}
|
||||
|
||||
@@ -76,3 +76,16 @@ body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
div
|
||||
> button, div
|
||||
-webkit-user-drag none
|
||||
user-select none
|
||||
> img, span
|
||||
-webkit-user-drag none
|
||||
user-select none
|
||||
|
||||
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
|
||||
display flex
|
||||
align-items center
|
||||
padding 0 20px
|
||||
z-index 99
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
@@ -97,8 +98,13 @@ body[data-theme="solarized-dark"]
|
||||
.info
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.info
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -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'
|
||||
@@ -29,10 +28,13 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
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
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
const { dialog } = remote
|
||||
|
||||
class SnippetNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -47,11 +49,12 @@ 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))
|
||||
})
|
||||
}
|
||||
|
||||
this.scrollToNextTabThreshold = 0.7
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -65,6 +68,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||
})
|
||||
}
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -73,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
|
||||
@@ -91,6 +96,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const { note, snippetIndex } = this.state
|
||||
const currentMode = note.snippets[snippetIndex].mode
|
||||
if (currentMode.includes('Markdown')) {
|
||||
const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
|
||||
markdownToc.generateInEditor(currentEditor)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
@@ -99,7 +114,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
note.title = findNoteTitle(note.description, false)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
@@ -151,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
|
||||
})
|
||||
@@ -341,12 +356,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs['code-' + this.state.snippetIndex].reload()
|
||||
|
||||
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||
console.log('no need for arrows')
|
||||
this.moveTabBarBy(0)
|
||||
} else {
|
||||
const lastTab = this.allTabs.lastChild
|
||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||
console.log('need to scroll')
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||
@@ -399,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
|
||||
@@ -423,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:
|
||||
{
|
||||
@@ -441,7 +468,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -451,14 +478,14 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
handleModeButtonClick (e, index) {
|
||||
const menu = new Menu()
|
||||
const templetes = []
|
||||
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
templetes.push({
|
||||
label: mode.name,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
}))
|
||||
})
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
context.popup(templetes)
|
||||
}
|
||||
|
||||
handleIndentTypeButtonClick (e) {
|
||||
@@ -573,12 +600,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { config: { editor: { snippetDefaultLanguage } } } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
content: ''
|
||||
mode: defaultLanguage,
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
|
||||
@@ -613,7 +644,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
@@ -622,11 +652,19 @@ 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')]
|
||||
})
|
||||
}
|
||||
@@ -638,6 +676,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
|
||||
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -662,10 +702,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}}
|
||||
@@ -674,25 +710,34 @@ class SnippetNoteDetail extends React.Component {
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
value={snippet.content}
|
||||
config={config}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
|
||||
value={snippet.content}
|
||||
linesHighlighted={snippet.linesHighlighted}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
matchingPairs={config.editor.matchingPairs}
|
||||
matchingTriples={config.editor.matchingTriples}
|
||||
explodingPairs={config.editor.explodingPairs}
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
autoDetect={autoDetect}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -726,6 +771,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
exportAsHtml={this.showWarning}
|
||||
exportAsPdf={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -744,7 +790,11 @@ class SnippetNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
@@ -753,11 +803,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)} />
|
||||
|
||||
@@ -768,12 +814,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>
|
||||
|
||||
@@ -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,39 @@ 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
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,65 +1,39 @@
|
||||
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'
|
||||
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'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newTag: ''
|
||||
newTag: '',
|
||||
suggestions: []
|
||||
}
|
||||
|
||||
this.handleAddTag = this.handleAddTag.bind(this)
|
||||
this.onInputBlur = this.onInputBlur.bind(this)
|
||||
this.onInputChange = this.onInputChange.bind(this)
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
|
||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
|
||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
handleNewTagInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.refs.newTag.value.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNewTagBlur (e) {
|
||||
this.submitTag()
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
addNewTag (newTag) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||
|
||||
newTag = newTag.trim().replace(/ +/g, '_')
|
||||
if (newTag.charAt(0) === '#') {
|
||||
newTag.substring(1)
|
||||
}
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -68,11 +42,18 @@ class TagSelect extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
let { value } = this.props
|
||||
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: ''
|
||||
@@ -82,10 +63,41 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleNewTagInputChange (e) {
|
||||
this.setState({
|
||||
newTag: this.refs.newTag.value
|
||||
})
|
||||
buildSuggestions () {
|
||||
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
||||
(tag, name) => ({
|
||||
name,
|
||||
nameLC: name.toLowerCase(),
|
||||
size: tag.size
|
||||
})
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
), ['name'])
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
|
||||
this.buildSuggestions()
|
||||
|
||||
ee.on('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
handleAddTag () {
|
||||
this.refs.newTag.input.focus()
|
||||
}
|
||||
|
||||
handleTagLabelClick (tag) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${tag}`)
|
||||
}
|
||||
|
||||
handleTagRemoveButtonClick (tag) {
|
||||
@@ -94,6 +106,60 @@ class TagSelect extends React.Component {
|
||||
}, tag)
|
||||
}
|
||||
|
||||
onInputBlur (e) {
|
||||
this.submitNewTag()
|
||||
}
|
||||
|
||||
onInputChange (e, { newValue, method }) {
|
||||
this.setState({
|
||||
newTag: newValue
|
||||
})
|
||||
}
|
||||
|
||||
onInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.state.newTag.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested () {
|
||||
this.setState({
|
||||
suggestions: []
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested ({ value }) {
|
||||
const valueLC = value.toLowerCase()
|
||||
const suggestions = _.filter(
|
||||
this.suggestions,
|
||||
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
||||
)
|
||||
|
||||
this.setState({
|
||||
suggestions
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionSelected (event, { suggestion, suggestionValue }) {
|
||||
this.addNewTag(suggestionValue)
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
removeTagByCallback (callback, tag = null) {
|
||||
let { value } = this.props
|
||||
|
||||
@@ -107,26 +173,55 @@ class TagSelect extends React.Component {
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.buildSuggestions()
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitNewTag () {
|
||||
this.addNewTag(this.refs.newTag.input.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, className } = this.props
|
||||
const { value, className, showTagsAlphabetically, coloredTags } = this.props
|
||||
|
||||
const tagList = _.isArray(value)
|
||||
? value.map((tag) => {
|
||||
? (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'>#{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>
|
||||
)
|
||||
})
|
||||
: []
|
||||
|
||||
const { newTag, suggestions } = this.state
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
@@ -135,24 +230,40 @@ class TagSelect extends React.Component {
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
<Autosuggest
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder={i18n.__('Add tag...')}
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
getSuggestionValue={suggestion => suggestion.name}
|
||||
renderSuggestion={suggestion => (
|
||||
<div>
|
||||
{suggestion.name}
|
||||
</div>
|
||||
)}
|
||||
inputProps={{
|
||||
placeholder: i18n.__('Add tag...'),
|
||||
value: newTag,
|
||||
onChange: this.onInputChange,
|
||||
onKeyDown: this.onInputKeyDown,
|
||||
onBlur: this.onInputBlur
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.contextTypes = {
|
||||
router: PropTypes.shape({})
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
onChange: PropTypes.func,
|
||||
coloredTags: PropTypes.object
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -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
|
||||
@@ -39,16 +38,9 @@
|
||||
|
||||
.tag-label
|
||||
font-size 13px
|
||||
color: $ui-text-color
|
||||
color $ui-text-color
|
||||
padding 4px 16px 4px 8px
|
||||
|
||||
.newTag
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
cursor pointer
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tag
|
||||
@@ -62,11 +54,6 @@ body[data-theme="dark"]
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.tag
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
@@ -78,14 +65,9 @@ body[data-theme="solarized-dark"]
|
||||
.tag-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
background-color $ui-monokai-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
@@ -93,8 +75,14 @@ body[data-theme="monokai"]
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.tag
|
||||
background-color $ui-dracula-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-dracula-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.tag-label
|
||||
color $ui-dracula-borderColor
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
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>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
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>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
|
||||
@@ -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()
|
||||
@@ -59,7 +64,14 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #272822
|
||||
background-color #373831
|
||||
.active
|
||||
background-color #1EC38B
|
||||
background-color #f92672
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.control-toggleModeButton
|
||||
background-color #44475a
|
||||
.active
|
||||
background-color #bd93f9
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
@@ -11,7 +11,7 @@ const TrashButton = ({
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
|
||||
<span styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.tooltip:lang(ja)
|
||||
@extend .tooltip
|
||||
right 46px
|
||||
|
||||
.control-trashButton--in-trash
|
||||
top 60px
|
||||
topBarButtonRight()
|
||||
|
||||
@@ -8,6 +8,9 @@ import SnippetNoteDetail from './SnippetNoteDetail'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
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'
|
||||
|
||||
@@ -34,11 +37,38 @@ class Detail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, data, 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
|
||||
note = data.noteMap.get(noteKey)
|
||||
|
||||
if (location.search !== '') {
|
||||
const allNotes = data.noteMap.map(note => note)
|
||||
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
|
||||
let displayedNotes = allNotes
|
||||
|
||||
if (location.pathname.match(/\/searched/)) {
|
||||
const searchStr = params.searchword
|
||||
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
|
||||
: searchFromNotes(allNotes, searchStr)
|
||||
}
|
||||
|
||||
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/)) {
|
||||
displayedNotes = trashedNotes
|
||||
} else {
|
||||
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
|
||||
}
|
||||
|
||||
const noteKeys = displayedNotes.map(note => note.key)
|
||||
if (noteKeys.includes(noteKey)) {
|
||||
note = data.noteMap.get(noteKey)
|
||||
}
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
@@ -99,4 +129,4 @@ Detail.propTypes = {
|
||||
ignorePreviewPointerEvents: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(Detail, styles)
|
||||
export default debounceRender(CSSModules(Detail, styles))
|
||||
|
||||
16
browser/main/DevTools/index.dev.js
Normal file
16
browser/main/DevTools/index.dev.js
Normal 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
|
||||
8
browser/main/DevTools/index.js
Normal file
8
browser/main/DevTools/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-undef */
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// eslint-disable-next-line global-require
|
||||
module.exports = require('./index.prod').default
|
||||
} else {
|
||||
// eslint-disable-next-line global-require
|
||||
module.exports = require('./index.dev').default
|
||||
}
|
||||
6
browser/main/DevTools/index.prod.js
Normal file
6
browser/main/DevTools/index.prod.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
const DevTools = () => <div />
|
||||
DevTools.instrument = () => {}
|
||||
|
||||
export default DevTools
|
||||
@@ -12,17 +12,16 @@ 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 { push } from 'connected-react-router'
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
class Main extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -57,13 +56,13 @@ class Main extends React.Component {
|
||||
init () {
|
||||
dataApi
|
||||
.addStorage({
|
||||
name: 'My Storage',
|
||||
name: 'My Storage Location',
|
||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
return data
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
if (data.storage.folders[0] != null) {
|
||||
return data
|
||||
} else {
|
||||
@@ -72,7 +71,7 @@ class Main extends React.Component {
|
||||
color: '#1278BD',
|
||||
name: 'Default'
|
||||
})
|
||||
.then((_data) => {
|
||||
.then(_data => {
|
||||
return {
|
||||
storage: _data.storage,
|
||||
notes: data.notes
|
||||
@@ -80,8 +79,7 @@ class Main extends React.Component {
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
console.log(data)
|
||||
.then(data => {
|
||||
store.dispatch({
|
||||
type: 'ADD_STORAGE',
|
||||
storage: data.storage,
|
||||
@@ -98,16 +96,18 @@ class Main extends React.Component {
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
|
||||
linesHighlighted: []
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
|
||||
linesHighlighted: []
|
||||
}
|
||||
]
|
||||
})
|
||||
.then((note) => {
|
||||
.then(note => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -120,7 +120,7 @@ class Main extends React.Component {
|
||||
title: 'Welcome to Boostnote!',
|
||||
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
|
||||
})
|
||||
.then((note) => {
|
||||
.then(note => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -131,10 +131,10 @@ class Main extends React.Component {
|
||||
.then(defaultMarkdownNote)
|
||||
.then(() => data.storage)
|
||||
})
|
||||
.then((storage) => {
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
.then(storage => {
|
||||
store.dispatch(push('/storages/' + storage.key))
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
@@ -142,12 +142,7 @@ class Main extends React.Component {
|
||||
componentDidMount () {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
const supportedThemes = [
|
||||
'dark',
|
||||
'white',
|
||||
'solarized-dark',
|
||||
'monokai'
|
||||
]
|
||||
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
|
||||
|
||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||
document.body.setAttribute('data-theme', config.ui.theme)
|
||||
@@ -162,24 +157,36 @@ class Main extends React.Component {
|
||||
}
|
||||
applyShortcuts()
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
dataApi.init().then(data => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
@@ -199,34 +206,40 @@ class Main extends React.Component {
|
||||
handleMouseUp (e) {
|
||||
// Change width of NoteList component.
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isRightSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({listWidth: newListWidth})
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isRightSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ listWidth: newListWidth })
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Change width of SideNav component.
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
this.setState({
|
||||
isLeftSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isLeftSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,8 +247,8 @@ class Main extends React.Component {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
const offset = this.refs.body.getBoundingClientRect().left
|
||||
let newListWidth = e.pageX - offset
|
||||
if (newListWidth < 10) {
|
||||
newListWidth = 10
|
||||
if (newListWidth < 180) {
|
||||
newListWidth = 180
|
||||
} else if (newListWidth > 600) {
|
||||
newListWidth = 600
|
||||
}
|
||||
@@ -271,8 +284,8 @@ class Main extends React.Component {
|
||||
}
|
||||
|
||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
||||
this.setState({mainBodyWidth: mainBody.style.left})
|
||||
this.setState({ noteDetailWidth: noteDetail.style.left })
|
||||
this.setState({ mainBodyWidth: mainBody.style.left })
|
||||
noteDetail.style.left = '0px'
|
||||
mainBody.style.left = '0px'
|
||||
noteList.style.display = 'none'
|
||||
@@ -294,64 +307,73 @@ class Main extends React.Component {
|
||||
<div
|
||||
className='Main'
|
||||
styleName='root'
|
||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.navWidth}}
|
||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||
}
|
||||
style={{ left: this.state.navWidth }}
|
||||
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
}
|
||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
</div>}
|
||||
<div
|
||||
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
id='main-body'
|
||||
ref='body'
|
||||
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
|
||||
style={{
|
||||
left: config.isSideNavFolded
|
||||
? foldedNavigationWidth
|
||||
: this.state.navWidth
|
||||
}}
|
||||
>
|
||||
<TopBar style={{width: this.state.listWidth}}
|
||||
<TopBar
|
||||
style={{ width: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'config',
|
||||
'data',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
<NoteList
|
||||
style={{ width: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
||||
style={{left: this.state.listWidth - 1}}
|
||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isRightSliderFocused
|
||||
? 'slider-right--active'
|
||||
: 'slider-right'
|
||||
}
|
||||
style={{ left: this.state.listWidth - 1 }}
|
||||
onMouseDown={e => this.handleRightSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth}}
|
||||
style={{ left: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'match',
|
||||
'location'
|
||||
])}
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
@@ -374,4 +396,4 @@ Main.propTypes = {
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
export default connect(x => x)(CSSModules(Main, styles))
|
||||
|
||||
@@ -79,3 +79,7 @@ body[data-theme="solarized-dark"]
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
@@ -20,35 +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 } = this.props
|
||||
const { location, dispatch, match: { params }, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
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) {
|
||||
@@ -84,7 +89,7 @@ class NewNoteButton extends React.Component {
|
||||
>
|
||||
<div styleName='control'>
|
||||
<button styleName='control-newNoteButton'
|
||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||
onClick={this.handleNewNoteButtonClick}>
|
||||
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
|
||||
<span styleName='control-newNoteButton-tooltip'>
|
||||
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
|
||||
|
||||
@@ -84,7 +84,7 @@ body[data-theme="dark"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
@@ -109,7 +109,7 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
@@ -138,3 +138,27 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.control-sortBy-select
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-dracula-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button--active
|
||||
color $ui-dracula-text-color
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
@@ -14,22 +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 { Menu, MenuItem, dialog } = remote
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -54,7 +74,6 @@ class NoteList extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.selectNextNoteHandler = () => {
|
||||
console.log('fired next')
|
||||
this.selectNextNote()
|
||||
}
|
||||
this.selectPriorNoteHandler = () => {
|
||||
@@ -63,13 +82,14 @@ class NoteList extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
this.refs.list.focus()
|
||||
}
|
||||
this.alertIfSnippetHandler = () => {
|
||||
this.alertIfSnippet()
|
||||
this.alertIfSnippetHandler = (event, msg) => {
|
||||
this.alertIfSnippet(msg)
|
||||
}
|
||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
|
||||
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
|
||||
this.cloneNote = this.cloneNote.bind(this)
|
||||
this.deleteNote = this.deleteNote.bind(this)
|
||||
this.focusNote = this.focusNote.bind(this)
|
||||
this.pinToTop = this.pinToTop.bind(this)
|
||||
@@ -78,10 +98,13 @@ class NoteList extends React.Component {
|
||||
this.getViewType = this.getViewType.bind(this)
|
||||
this.restoreNote = this.restoreNote.bind(this)
|
||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||
this.navigate = this.navigate.bind(this)
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
ctrlKeyDown: false,
|
||||
shiftKeyDown: false,
|
||||
prevShiftNoteIndex: -1,
|
||||
selectedNoteKeys: []
|
||||
}
|
||||
|
||||
@@ -92,10 +115,12 @@ 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)
|
||||
ee.on('list:jump', this.jumpNoteByHash)
|
||||
ee.on('list:navigate', this.navigate)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -113,6 +138,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)
|
||||
@@ -120,15 +146,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
|
||||
@@ -138,17 +164,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
|
||||
@@ -168,20 +194,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) {
|
||||
@@ -196,6 +221,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
@@ -212,7 +238,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(priorNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -223,6 +249,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
|
||||
@@ -245,25 +272,34 @@ 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')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
if (e.metaKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
@@ -271,12 +307,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()
|
||||
@@ -303,6 +333,8 @@ class NoteList extends React.Component {
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: true })
|
||||
} else if (e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,11 +342,14 @@ class NoteList extends React.Component {
|
||||
if (!e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: false })
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: false })
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
const { data, params, location } = this.props
|
||||
|
||||
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
|
||||
@@ -355,7 +390,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)
|
||||
@@ -386,40 +421,79 @@ class NoteList extends React.Component {
|
||||
return pinnedNotes.concat(unpinnedNotes)
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
getNoteIndexByKey (noteKey) {
|
||||
return this.notes.findIndex((note) => {
|
||||
if (!note) return -1
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
return note.key === noteKey
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { dispatch, location } = this.props
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
|
||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
this.setState({
|
||||
selectedNoteKeys: newSelectedNoteKeys
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!shiftKeyDown) {
|
||||
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||
selectedNoteKeys = []
|
||||
}
|
||||
|
||||
if (!shiftKeyDown) {
|
||||
prevShiftNoteIndex = -1
|
||||
}
|
||||
|
||||
selectedNoteKeys.push(uniqueKey)
|
||||
|
||||
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||
// or previous first shift note index
|
||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||
|
||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||
|
||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
|
||||
selectedNoteKeys = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
selectedNoteKeys.push(this.notes[i].key)
|
||||
}
|
||||
|
||||
if (prevShiftNoteIndex < 0) {
|
||||
prevShiftNoteIndex = firstShiftNoteIndex
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
selectedNoteKeys,
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
dispatch(push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
search: queryString.stringify({
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
const { dispatch } = this.props
|
||||
const { dispatch, match: { params: { folderKey } } } = this.props
|
||||
|
||||
const config = {
|
||||
sortBy: e.target.value
|
||||
[folderKey]: { sortBy: e.target.value }
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
@@ -443,14 +517,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')]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -490,55 +572,51 @@ class NoteList extends React.Component {
|
||||
const updateLabel = i18n.__('Update Blog')
|
||||
const openBlogLabel = i18n.__('Open Blog')
|
||||
|
||||
const menu = new Menu()
|
||||
const templates = []
|
||||
|
||||
if (location.pathname.match(/\/trash/)) {
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: restoreNote,
|
||||
click: this.restoreNote
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
}, {
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
}))
|
||||
})
|
||||
} else {
|
||||
if (!location.pathname.match(/\/starred/)) {
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: pinLabel,
|
||||
click: this.pinToTop
|
||||
}))
|
||||
})
|
||||
}
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: deleteLabel,
|
||||
click: this.deleteNote
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
}, {
|
||||
label: cloneNote,
|
||||
click: this.cloneNote.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
}, {
|
||||
label: copyNoteLink,
|
||||
click: this.copyNoteLink(note)
|
||||
}))
|
||||
click: this.copyNoteLink.bind(this, note)
|
||||
})
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: updateLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
}, {
|
||||
label: openBlogLabel,
|
||||
click: () => this.openBlog.bind(this)(note)
|
||||
}))
|
||||
})
|
||||
} else {
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
menu.popup()
|
||||
context.popup(templates)
|
||||
}
|
||||
|
||||
updateSelectedNotes (updateFunc, cleanSelection = true) {
|
||||
@@ -605,18 +683,21 @@ 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)
|
||||
})
|
||||
console.log('Notes were all deleted')
|
||||
} else {
|
||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||
|
||||
@@ -636,8 +717,8 @@ class NoteList extends React.Component {
|
||||
})
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
console.log('Notes went to trash')
|
||||
})
|
||||
.then(() => ee.emit('list:next'))
|
||||
.catch((err) => {
|
||||
console.error('Notes could not go to trash: ' + err)
|
||||
})
|
||||
@@ -661,7 +742,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)
|
||||
@@ -677,10 +763,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})
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -689,6 +775,16 @@ class NoteList extends React.Component {
|
||||
return copy(noteLink)
|
||||
}
|
||||
|
||||
navigate (sender, pathname) {
|
||||
const { dispatch } = this.props
|
||||
dispatch(push({
|
||||
pathname,
|
||||
search: queryString.stringify({
|
||||
// key: noteKey
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
save (note) {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
@@ -816,7 +912,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()
|
||||
@@ -840,13 +936,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)})
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -856,14 +959,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
|
||||
@@ -911,12 +1015,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, config } = this.props
|
||||
const { location, config, match: { params: { folderKey } } } = this.props
|
||||
let { notes } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const sortFunc = config.sortBy === 'CREATED_AT'
|
||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||
const sortFunc = sortBy === 'CREATED_AT'
|
||||
? sortByCreatedAt
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
: sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||
@@ -948,17 +1053,26 @@ class NoteList extends React.Component {
|
||||
|
||||
const viewType = this.getViewType()
|
||||
|
||||
const autoSelectFirst =
|
||||
notes.length === 1 ||
|
||||
selectedNoteKeys.length === 0 ||
|
||||
notes.every(note => !selectedNoteKeys.includes(note.key))
|
||||
|
||||
const noteList = notes
|
||||
.map(note => {
|
||||
.map((note, index) => {
|
||||
if (note == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDefault = config.listStyle === 'DEFAULT'
|
||||
const uniqueKey = getNoteKey(note)
|
||||
const isActive = selectedNoteKeys.includes(uniqueKey)
|
||||
|
||||
const isActive =
|
||||
selectedNoteKeys.includes(uniqueKey) ||
|
||||
notes.length === 1 ||
|
||||
(autoSelectFirst && index === 0)
|
||||
const dateDisplay = moment(
|
||||
config.sortBy === 'CREATED_AT'
|
||||
sortBy === 'CREATED_AT'
|
||||
? note.createdAt : note.updatedAt
|
||||
).fromNow('D')
|
||||
|
||||
@@ -976,6 +1090,8 @@ class NoteList extends React.Component {
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
coloredTags={config.coloredTags}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1007,7 +1123,7 @@ class NoteList extends React.Component {
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='control-sortBy-select'
|
||||
title={i18n.__('Select filter mode')}
|
||||
value={config.sortBy}
|
||||
value={sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||
|
||||
@@ -48,4 +48,5 @@ body[data-theme="dark"]
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
transition 0.1s
|
||||
white-space nowrap
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
text-align center
|
||||
|
||||
|
||||
|
||||
|
||||
.top-menu-label
|
||||
margin-left 5px
|
||||
overflow ellipsis
|
||||
@@ -122,3 +122,8 @@ body[data-theme="monokai"]
|
||||
.root, .root--folded
|
||||
background-color $ui-monokai-backgroundColor
|
||||
border-right 1px solid $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--folded
|
||||
background-color $ui-dracula-backgroundColor
|
||||
border-right 1px solid $ui-dracula-borderColor
|
||||
@@ -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'
|
||||
@@ -11,9 +10,11 @@ import StorageItemChild from 'browser/components/StorageItem'
|
||||
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 { Menu, dialog } = remote
|
||||
const { dialog } = remote
|
||||
const escapeStringRegexp = require('escape-string-regexp')
|
||||
const path = require('path')
|
||||
|
||||
@@ -21,13 +22,16 @@ class StorageItem extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const { storage } = this.props
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
isOpen: !!storage.isOpen,
|
||||
draggedOver: null
|
||||
}
|
||||
}
|
||||
|
||||
handleHeaderContextMenu (e) {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Add Folder'),
|
||||
click: (e) => this.handleAddFolderButtonClick(e)
|
||||
@@ -35,13 +39,27 @@ class StorageItem extends React.Component {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export Storage'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'md')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Unlink Storage'),
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
}
|
||||
])
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleUnlinkStorageClick (e) {
|
||||
@@ -67,9 +85,43 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleExportStorageClick (e, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||
(paths) => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.exportStorage(storage.key, fileType, paths[0])
|
||||
.then(data => {
|
||||
dispatch({
|
||||
type: 'EXPORT_STORAGE',
|
||||
storage: data.storage,
|
||||
fileType: data.fileType
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { storage, dispatch } = this.props
|
||||
const isOpen = !this.state.isOpen
|
||||
dataApi.toggleStorage(storage.key, isOpen)
|
||||
.then((storage) => {
|
||||
dispatch({
|
||||
type: 'EXPAND_STORAGE',
|
||||
storage,
|
||||
isOpen
|
||||
})
|
||||
})
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
isOpen: isOpen
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,19 +134,19 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
handleFolderButtonContextMenu (e, folder) {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Rename Folder'),
|
||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||
@@ -123,8 +175,6 @@ class StorageItem extends React.Component {
|
||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||
}
|
||||
])
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleRenameFolderClick (e, folder) {
|
||||
@@ -155,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
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -182,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) {
|
||||
@@ -214,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)
|
||||
}
|
||||
@@ -225,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)
|
||||
|
||||
@@ -242,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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
white-space nowrap
|
||||
|
||||
body[data-theme="white"]
|
||||
.non-active-button
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { push } from 'connected-react-router'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
const { remote } = require('electron')
|
||||
const { Menu } = remote
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
@@ -19,9 +18,33 @@ import ListButton from './ListButton'
|
||||
import TagButton from './TagButton'
|
||||
import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import ColorPicker from 'browser/components/ColorPicker'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -31,18 +54,130 @@ class SideNav extends React.Component {
|
||||
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
||||
}
|
||||
|
||||
deleteTag (tag) {
|
||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
ype: '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, match: { params } } = this.props
|
||||
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
.filter(note => note.tags.indexOf(tag) !== -1)
|
||||
.map(note => {
|
||||
note = Object.assign({}, note)
|
||||
note.tags = note.tags.slice()
|
||||
|
||||
note.tags.splice(note.tags.indexOf(tag), 1)
|
||||
|
||||
return note
|
||||
})
|
||||
|
||||
Promise
|
||||
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
||||
.then(updatedNotes => {
|
||||
updatedNotes.forEach(note => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
|
||||
if (location.pathname.match('/tags')) {
|
||||
const tags = params.tagname.split(' ')
|
||||
const index = tags.indexOf(tag)
|
||||
if (index !== -1) {
|
||||
tags.splice(index, 1)
|
||||
|
||||
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuButtonClick (e) {
|
||||
openModal(PreferencesModal)
|
||||
}
|
||||
|
||||
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) {
|
||||
const menu = []
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Delete Tag'),
|
||||
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) {
|
||||
@@ -56,18 +191,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) {
|
||||
@@ -145,12 +280,21 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location, config } = this.props
|
||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||
const { colorPicker } = this.state
|
||||
const activeTags = this.getActiveTags(location.pathname)
|
||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||
), ['name']).filter(
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
)
|
||||
), ['name'])
|
||||
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
||||
const notesTags = data.noteMap.map(note => note.tags)
|
||||
tagList = tagList.map(tag => {
|
||||
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
||||
return tag
|
||||
})
|
||||
}
|
||||
if (config.sortTagsBy === 'COUNTER') {
|
||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||
}
|
||||
@@ -166,10 +310,12 @@ class SideNav extends React.Component {
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||
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]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -185,7 +331,7 @@ class SideNav extends React.Component {
|
||||
).filter(
|
||||
note => activeTags.every(tag => note.tags.includes(tag))
|
||||
)
|
||||
let relatedTags = new Set()
|
||||
const relatedTags = new Set()
|
||||
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
||||
return relatedTags
|
||||
}
|
||||
@@ -199,12 +345,12 @@ class SideNav extends React.Component {
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ')
|
||||
: decodeURIComponent(tags).split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${name}`)
|
||||
const { dispatch } = this.props
|
||||
dispatch(push(`/tags/${encodeURIComponent(name)}`))
|
||||
}
|
||||
|
||||
handleSortTagsByChange (e) {
|
||||
@@ -222,16 +368,15 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleClickNarrowToTag (tag) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let listOfTags = this.getActiveTags(location.pathname)
|
||||
const { dispatch, location } = this.props
|
||||
const listOfTags = this.getActiveTags(location.pathname)
|
||||
const indexOfTag = listOfTags.indexOf(tag)
|
||||
if (indexOfTag > -1) {
|
||||
listOfTags.splice(indexOfTag, 1)
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
@@ -239,6 +384,8 @@ class SideNav extends React.Component {
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
@@ -248,20 +395,19 @@ class SideNav extends React.Component {
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Trash emptied')
|
||||
}
|
||||
|
||||
handleFilterButtonContextMenu (event) {
|
||||
const { data } = this.props
|
||||
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
const menu = Menu.buildFromTemplate([
|
||||
context.popup([
|
||||
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
||||
])
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location, config, dispatch } = this.props
|
||||
const { colorPicker: colorPickerState } = this.state
|
||||
|
||||
const isFolded = config.isSideNavFolded
|
||||
|
||||
@@ -278,6 +424,20 @@ class SideNav extends React.Component {
|
||||
useDragHandle
|
||||
/>
|
||||
})
|
||||
|
||||
let colorPicker
|
||||
if (colorPickerState.show) {
|
||||
colorPicker = (
|
||||
<ColorPicker
|
||||
color={colorPickerState.color}
|
||||
targetRect={colorPickerState.targetRect}
|
||||
onConfirm={this.handleColorPickerConfirm}
|
||||
onCancel={this.dismissColorPicker}
|
||||
onReset={this.handleColorPickerReset}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const style = {}
|
||||
if (!isFolded) style.width = this.props.width
|
||||
const isTagActive = location.pathname.match(/tag/)
|
||||
@@ -297,6 +457,7 @@ class SideNav extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
{this.SideNavComponent(isFolded, storageList)}
|
||||
{colorPicker}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
color $ui-active-color
|
||||
span
|
||||
margin-left 5px
|
||||
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
height 24px
|
||||
@@ -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
|
||||
@@ -80,3 +88,14 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-active-color
|
||||
&:active
|
||||
color $ui-monokai-active-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
navButtonColor()
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-dracula-active-color
|
||||
&:active
|
||||
color $ui-dracula-active-color
|
||||
@@ -4,14 +4,36 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
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
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
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',
|
||||
@@ -26,16 +48,16 @@ class StatusBar extends React.Component {
|
||||
}
|
||||
|
||||
handleZoomButtonClick (e) {
|
||||
const menu = new Menu()
|
||||
const templates = []
|
||||
|
||||
zoomOptions.forEach((zoom) => {
|
||||
menu.append(new MenuItem({
|
||||
templates.push({
|
||||
label: Math.floor(zoom * 100) + '%',
|
||||
click: () => this.handleZoomMenuItemClick(zoom)
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
context.popup(templates)
|
||||
}
|
||||
|
||||
handleZoomMenuItemClick (zoomFactor) {
|
||||
@@ -47,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
|
||||
|
||||
|
||||
@@ -256,3 +256,25 @@ body[data-theme="monokai"]
|
||||
input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dracula-borderColor
|
||||
.control-search
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-dracula-inactive-text-color
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -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,21 @@ 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()
|
||||
}
|
||||
|
||||
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 +95,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 +107,7 @@ class TopBar extends React.Component {
|
||||
isSearching: true
|
||||
})
|
||||
}
|
||||
|
||||
handleSearchBlur (e) {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -156,13 +132,12 @@ class TopBar extends React.Component {
|
||||
if (this.state.isSearching) {
|
||||
el.blur()
|
||||
} else {
|
||||
el.focus()
|
||||
el.setSelectionRange(0, el.value.length)
|
||||
el.select()
|
||||
}
|
||||
}
|
||||
|
||||
handleCodeInit () {
|
||||
ee.emit('top:search', this.refs.searchInput.value)
|
||||
ee.emit('top:search', this.refs.searchInput.value || '')
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -175,24 +150,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>
|
||||
@@ -207,8 +181,8 @@ class TopBar extends React.Component {
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
'location',
|
||||
'match'
|
||||
])}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,15 @@ body
|
||||
font-weight 200
|
||||
-webkit-font-smoothing antialiased
|
||||
|
||||
::-webkit-scrollbar
|
||||
width 12px
|
||||
|
||||
::-webkit-scrollbar-corner
|
||||
background-color: transparent;
|
||||
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.15)
|
||||
|
||||
button, input, select, textarea
|
||||
font-family DEFAULT_FONTS
|
||||
|
||||
@@ -85,9 +94,12 @@ modalBackColor = white
|
||||
absolute top left bottom right
|
||||
background-color modalBackColor
|
||||
z-index modalZIndex + 1
|
||||
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
background-color $ui-dark-backgroundColor
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color $ui-dark-backgroundColor
|
||||
@@ -124,10 +136,22 @@ body[data-theme="dark"]
|
||||
.CodeMirror-foldgutter-folded:after
|
||||
content: "\25B8"
|
||||
|
||||
.CodeMirror-hover
|
||||
padding 2px 4px 0 4px
|
||||
position absolute
|
||||
z-index 99
|
||||
|
||||
.CodeMirror-hyperlink
|
||||
cursor pointer
|
||||
|
||||
|
||||
.sortableItemHelper
|
||||
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
|
||||
.modalBack
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
@@ -135,9 +159,27 @@ 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
|
||||
.modalBack
|
||||
background-color $ui-monokai-backgroundColor
|
||||
.sortableItemHelper
|
||||
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
|
||||
.modalBack
|
||||
background-color $ui-dracula-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-dracula-text-color
|
||||
|
||||
body[data-theme="default"]
|
||||
.SideNav ::-webkit-scrollbar-thumb
|
||||
background-color rgba(255, 255, 255, 0.3)
|
||||
|
||||
@import '../styles/Detail/TagSelect.styl'
|
||||
@@ -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')
|
||||
|
||||
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
|
||||
if (getSendEventCond()) return
|
||||
AWS.config.credentials.get((err) => {
|
||||
if (!err) {
|
||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
||||
recordDynamicCustomEvent('APP_STARTED')
|
||||
recordStaticCustomEvent()
|
||||
}
|
||||
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
|
||||
mobileAnalyticsClient.recordEvent(type, options)
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
|
||||
})
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,40 +11,61 @@ const consts = require('browser/lib/consts')
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||
"default": true
|
||||
}`
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
zoom: 1,
|
||||
isSideNavFolded: false,
|
||||
listWidth: 280,
|
||||
navWidth: 200,
|
||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
sortBy: {
|
||||
default: 'UPDATED_AT' // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
},
|
||||
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||
amaEnabled: true,
|
||||
hotkey: {
|
||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E',
|
||||
toggleMode: OSX ? 'Cmd + M' : 'Ctrl + M'
|
||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
|
||||
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
|
||||
toggleMenuBar: 'Alt'
|
||||
},
|
||||
ui: {
|
||||
language: 'en',
|
||||
theme: 'default',
|
||||
showCopyNotification: true,
|
||||
disableDirectWrite: false,
|
||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
showMenuBar: false
|
||||
},
|
||||
editor: {
|
||||
theme: 'base16-light',
|
||||
keyMap: 'sublime',
|
||||
fontSize: '14',
|
||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
||||
fontFamily: win ? 'Consolas' : 'Monaco',
|
||||
indentType: 'space',
|
||||
indentSize: '2',
|
||||
enableRulers: false,
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||
matchingPairs: '()[]{}\'\'""$$**``~~__',
|
||||
matchingTriples: '```"""\'\'\'',
|
||||
explodingPairs: '[]{}``$$',
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||
scrollPastEnd: false,
|
||||
type: 'SPLIT',
|
||||
fetchUrlTitle: true
|
||||
type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
|
||||
fetchUrlTitle: true,
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false,
|
||||
enableSmartPaste: false,
|
||||
enableMarkdownLint: false,
|
||||
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -57,9 +78,14 @@ export const DEFAULT_CONFIG = {
|
||||
latexBlockClose: '$$',
|
||||
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
||||
scrollPastEnd: false,
|
||||
scrollSync: true,
|
||||
smartQuotes: true,
|
||||
breaks: true,
|
||||
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
smartArrows: false,
|
||||
allowCustomCSS: false,
|
||||
customCSS: '',
|
||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
lineThroughCheckbox: true
|
||||
},
|
||||
blog: {
|
||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||
@@ -68,7 +94,8 @@ export const DEFAULT_CONFIG = {
|
||||
token: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
coloredTags: {}
|
||||
}
|
||||
|
||||
function validate (config) {
|
||||
@@ -111,16 +138,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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +164,8 @@ function set (updates) {
|
||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||
} else if (newConfig.ui.theme === 'monokai') {
|
||||
document.body.setAttribute('data-theme', 'monokai')
|
||||
} else if (newConfig.ui.theme === 'dracula') {
|
||||
document.body.setAttribute('data-theme', 'dracula')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
@@ -154,16 +179,11 @@ 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}`)
|
||||
}
|
||||
|
||||
ipcRenderer.send('config-renew', {
|
||||
@@ -179,6 +199,18 @@ function assignConfigValues (originalConfig, rcConfig) {
|
||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||
|
||||
rewriteHotkey(config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function rewriteHotkey (config) {
|
||||
const keys = [...Object.keys(config.hotkey)]
|
||||
keys.forEach(key => {
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ function addStorage (input) {
|
||||
key,
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
path: input.path
|
||||
path: input.path,
|
||||
isOpen: false
|
||||
}
|
||||
|
||||
return Promise.resolve(newStorage)
|
||||
@@ -48,7 +49,8 @@ function addStorage (input) {
|
||||
key: newStorage.key,
|
||||
type: newStorage.type,
|
||||
name: newStorage.name,
|
||||
path: newStorage.path
|
||||
path: newStorage.path,
|
||||
isOpen: false
|
||||
})
|
||||
|
||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
|
||||
@@ -6,9 +6,132 @@ const mdurl = require('mdurl')
|
||||
const fse = require('fs-extra')
|
||||
const escapeStringRegexp = require('escape-string-regexp')
|
||||
const sander = require('sander')
|
||||
const url = require('url')
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||
const DESTINATION_FOLDER = 'attachments'
|
||||
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
||||
/**
|
||||
* @description
|
||||
* Create a Image element to get the real size of image.
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Image>} Image element created
|
||||
*/
|
||||
function getImage (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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Get the orientation info from iamges's EXIF data.
|
||||
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
|
||||
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
|
||||
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
|
||||
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
|
||||
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
|
||||
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
|
||||
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
|
||||
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
|
||||
* Other: reserved
|
||||
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Number>} Orientation info
|
||||
*/
|
||||
function getOrientation (file) {
|
||||
const getData = arrayBuffer => {
|
||||
const view = new DataView(arrayBuffer)
|
||||
|
||||
// Not start with SOI(Start of image) Marker return fail value
|
||||
if (view.getUint16(0, false) !== 0xFFD8) return -2
|
||||
const length = view.byteLength
|
||||
let offset = 2
|
||||
while (offset < length) {
|
||||
const marker = view.getUint16(offset, false)
|
||||
offset += 2
|
||||
// Loop and seed for APP1 Marker
|
||||
if (marker === 0xFFE1) {
|
||||
// return fail value if it isn't EXIF data
|
||||
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
||||
return -1
|
||||
}
|
||||
// Read TIFF header,
|
||||
// First 2bytes defines byte align of TIFF data.
|
||||
// If it is 0x4949="II", it means "Intel" type byte align.
|
||||
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
||||
const little = view.getUint16(offset += 6, false) === 0x4949
|
||||
offset += view.getUint32(offset + 4, little)
|
||||
const tags = view.getUint16(offset, little) // Get TAG number
|
||||
offset += 2
|
||||
for (let i = 0; i < tags; i++) {
|
||||
// Loop to find Orientation TAG and return the value
|
||||
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
||||
return view.getUint16(offset + (i * 12) + 8, little)
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
|
||||
break
|
||||
} else {
|
||||
offset += view.getUint16(offset, false)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = event => resolve(getData(event.target.result))
|
||||
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @description
|
||||
* Rotate image file to correct direction.
|
||||
* Create a canvas and draw the image with correct direction, then export to base64 format.
|
||||
* @param {*} file the File object dropped.
|
||||
* @return {String} Base64 encoded image.
|
||||
*/
|
||||
function fixRotate (file) {
|
||||
return Promise.all([getImage(file), getOrientation(file)])
|
||||
.then(([img, orientation]) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (orientation > 4 && orientation < 9) {
|
||||
canvas.width = img.height
|
||||
canvas.height = img.width
|
||||
} else {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
}
|
||||
switch (orientation) {
|
||||
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
|
||||
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
|
||||
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
|
||||
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
||||
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
|
||||
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
|
||||
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
|
||||
default: break
|
||||
}
|
||||
ctx.drawImage(img, 0, 0)
|
||||
return canvas.toDataURL()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
@@ -36,26 +159,39 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(sourceFilePath)) {
|
||||
reject('source file does not exist')
|
||||
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
|
||||
return reject('source file does not exist')
|
||||
}
|
||||
|
||||
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
|
||||
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
|
||||
|
||||
let destinationName
|
||||
if (useRandomName) {
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
|
||||
} else {
|
||||
destinationName = path.basename(sourceURL.pathname)
|
||||
}
|
||||
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
|
||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||
let destinationName
|
||||
if (useRandomName) {
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
|
||||
} else {
|
||||
destinationName = path.basename(sourceFilePath)
|
||||
}
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||
inputFileStream.pipe(outputFile)
|
||||
inputFileStream.on('end', () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
|
||||
if (isBase64) {
|
||||
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
||||
const dataBuffer = Buffer.from(base64Data, 'base64')
|
||||
outputFile.write(dataBuffer, () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
} else {
|
||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||
inputFileStream.pipe(outputFile)
|
||||
inputFileStream.on('end', () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
@@ -73,6 +209,31 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey)
|
||||
* @param markdownContent of the current note
|
||||
* @param storagePath Storage path of the current note
|
||||
* @param noteKey Key of the current note
|
||||
*/
|
||||
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
||||
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||
if (attachments.length) {
|
||||
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
const attachmentBaseName = path.basename(attachment)
|
||||
const possibleLegacyPath = path.join(storagePath, 'images', attachmentBaseName)
|
||||
if (sander.existsSync(possibleLegacyPath)) {
|
||||
const destinationPath = path.join(storagePath, DESTINATION_FOLDER, attachmentBaseName)
|
||||
if (!sander.existsSync(destinationPath)) {
|
||||
sander.copyFileSync(possibleLegacyPath).to(destinationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files.
|
||||
* @param {String} renderedHTML HTML in that the links should be fixed
|
||||
@@ -80,7 +241,18 @@ function createAttachmentDestinationFolder (destinationStoragePath, 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(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||
/*
|
||||
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
|
||||
|
||||
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
|
||||
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
|
||||
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
|
||||
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
|
||||
*/
|
||||
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
|
||||
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
|
||||
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,15 +275,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']
|
||||
let promise
|
||||
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 {
|
||||
let imageURL = dropEvent.dataTransfer.getData('text/plain')
|
||||
|
||||
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
||||
const showPreview = fileType.startsWith('image')
|
||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
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(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'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,7 +366,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')
|
||||
}
|
||||
@@ -152,20 +396,59 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
fs.writeFileSync(imagePath, binaryData, 'binary')
|
||||
const imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
|
||||
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
|
||||
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns all attachment paths of the given markdown
|
||||
* @param {String} markdownContent content in which the attachment paths should be found
|
||||
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
|
||||
* @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 getAttachmentsInContent (markdownContent) {
|
||||
const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep)
|
||||
const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g')
|
||||
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
|
||||
* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
|
||||
*/
|
||||
function getAttachmentsInMarkdownContent (markdownContent) {
|
||||
const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
||||
const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
|
||||
return preparedInput.match(regexp)
|
||||
}
|
||||
|
||||
@@ -176,7 +459,7 @@ function getAttachmentsInContent (markdownContent) {
|
||||
* @returns {String[]} Absolute paths of the referenced attachments
|
||||
*/
|
||||
function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
||||
const temp = getAttachmentsInContent(markdownContent) || []
|
||||
const temp = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||
const result = []
|
||||
for (const relativePath of temp) {
|
||||
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
|
||||
@@ -184,6 +467,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.
|
||||
@@ -212,7 +543,8 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
|
||||
*/
|
||||
function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
|
||||
if (noteContent) {
|
||||
return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
|
||||
const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
|
||||
return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
|
||||
}
|
||||
return noteContent
|
||||
}
|
||||
@@ -224,7 +556,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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,20 +584,22 @@ function deleteAttachmentFolder (storageKey, noteKey) {
|
||||
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
|
||||
*/
|
||||
function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) {
|
||||
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||
return
|
||||
}
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
const attachmentsInNote = getAttachmentsInContent(markdownContent)
|
||||
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)) {
|
||||
fs.readdir(attachmentFolder, (err, files) => {
|
||||
if (err) {
|
||||
console.error("Error reading directory '" + attachmentFolder + "'. Error:")
|
||||
console.error('Error reading directory "' + attachmentFolder + '". Error:')
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
@@ -267,17 +608,17 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
|
||||
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
|
||||
fs.unlink(absolutePathOfFile, (err) => {
|
||||
if (err) {
|
||||
console.error("Could not delete '%s'", absolutePathOfFile)
|
||||
console.error('Could not delete "%s"', absolutePathOfFile)
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
console.info("File '" + absolutePathOfFile + "' deleted because it was not included in the content of the note")
|
||||
console.info('File "' + absolutePathOfFile + '" deleted because it was not included in the content of the note')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.info("Attachment folder ('" + attachmentFolder + "') did not exist..")
|
||||
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,19 +649,89 @@ function cloneAttachments (oldNote, newNote) {
|
||||
}
|
||||
}
|
||||
|
||||
function generateFileNotFoundMarkdown () {
|
||||
return '**' + i18n.__('⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠') + '**'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given text is a link to an boostnote attachment
|
||||
* @param text Text that might contain a attachment link
|
||||
* @return {Boolean} Result of the test
|
||||
*/
|
||||
function isAttachmentLink (text) {
|
||||
if (text) {
|
||||
return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Handles the paste of an attachment link. Copies the referenced attachment to the location belonging to the new note.
|
||||
* Returns a modified version of the pasted text so that it matches the copied attachment (resp. the new location)
|
||||
* @param storageKey StorageKey of the current note
|
||||
* @param noteKey NoteKey of the currentNote
|
||||
* @param linkText Text that was pasted
|
||||
* @return {Promise<String>} Promise returning the modified text
|
||||
*/
|
||||
function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
||||
if (storageKey != null && noteKey != null && linkText != null) {
|
||||
const storagePath = findStorage.findStorage(storageKey).path
|
||||
const attachments = getAttachmentsInMarkdownContent(linkText) || []
|
||||
const replaceInstructions = []
|
||||
const copies = []
|
||||
for (const attachment of attachments) {
|
||||
const absPathOfAttachment = attachment.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))
|
||||
copies.push(
|
||||
sander.exists(absPathOfAttachment)
|
||||
.then((fileExists) => {
|
||||
if (!fileExists) {
|
||||
const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
|
||||
replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
|
||||
return Promise.resolve()
|
||||
}
|
||||
return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
|
||||
.then((fileName) => {
|
||||
const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
|
||||
replaceInstructions.push({
|
||||
regexp: replaceLinkRegExp,
|
||||
replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
|
||||
})
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
return Promise.all(copies).then(() => {
|
||||
let modifiedLinkText = linkText
|
||||
for (const replaceInstruction of replaceInstructions) {
|
||||
modifiedLinkText = modifiedLinkText.replace(replaceInstruction.regexp, replaceInstruction.replacement)
|
||||
}
|
||||
return modifiedLinkText
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve(linkText)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
copyAttachment,
|
||||
fixLocalURLS,
|
||||
generateAttachmentMarkdown,
|
||||
handleAttachmentDrop,
|
||||
handlePastImageEvent,
|
||||
getAttachmentsInContent,
|
||||
handlePasteImageEvent,
|
||||
handlePasteNativeImage,
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
importAttachments,
|
||||
removeStorageAndNoteReferences,
|
||||
deleteAttachmentFolder,
|
||||
deleteAttachmentsNotPresentInNote,
|
||||
moveAttachments,
|
||||
cloneAttachments,
|
||||
isAttachmentLink,
|
||||
handleAttachmentLinkPaste,
|
||||
generateFileNotFoundMarkdown,
|
||||
migrateAttachments,
|
||||
STORAGE_FOLDER_PLACEHOLDER,
|
||||
DESTINATION_FOLDER
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
27
browser/main/lib/dataApi/createSnippet.js
Normal file
27
browser/main/lib/dataApi/createSnippet.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import fs from 'fs'
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
|
||||
|
||||
function createSnippet (snippetFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const newSnippet = {
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Unnamed snippet',
|
||||
prefix: [],
|
||||
content: '',
|
||||
linesHighlighted: []
|
||||
}
|
||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||
snippets.push(newSnippet)
|
||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||
if (err) reject(err)
|
||||
resolve(newSnippet)
|
||||
})
|
||||
}).catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createSnippet
|
||||
17
browser/main/lib/dataApi/deleteSnippet.js
Normal file
17
browser/main/lib/dataApi/deleteSnippet.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import fs from 'fs'
|
||||
import consts from 'browser/lib/consts'
|
||||
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
|
||||
|
||||
function deleteSnippet (snippet, snippetFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchSnippet(null, snippetFile).then((snippets) => {
|
||||
snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id)
|
||||
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
|
||||
if (err) reject(err)
|
||||
resolve(snippet)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = deleteSnippet
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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, path.dirname(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
|
||||
|
||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object,
|
||||
* fileType: String,
|
||||
* exportDir: String
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportStorage (storageKey, fileType, exportDir) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage => (
|
||||
resolveStorageNotes(storage).then(notes => ({storage, notes}))
|
||||
))
|
||||
.then(function exportNotes (data) {
|
||||
const { storage, notes } = data
|
||||
const folderNamesMapping = {}
|
||||
storage.folders.forEach(folder => {
|
||||
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
||||
folderNamesMapping[folder.key] = folderExportedDir
|
||||
// make sure directory exists
|
||||
try {
|
||||
fs.mkdirSync(folderExportedDir)
|
||||
} catch (e) {}
|
||||
})
|
||||
notes
|
||||
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(markdownNote => {
|
||||
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
|
||||
const notePath = path.join(folderExportedDir, snippetName)
|
||||
fs.writeFileSync(notePath, markdownNote.content)
|
||||
})
|
||||
|
||||
return {
|
||||
storage,
|
||||
fileType,
|
||||
exportDir
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = exportStorage
|
||||
20
browser/main/lib/dataApi/fetchSnippet.js
Normal file
20
browser/main/lib/dataApi/fetchSnippet.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import fs from 'fs'
|
||||
import consts from 'browser/lib/consts'
|
||||
|
||||
function fetchSnippet (id, snippetFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
const snippets = JSON.parse(data)
|
||||
if (id) {
|
||||
const snippet = snippets.find(snippet => { return snippet.id === id })
|
||||
resolve(snippet)
|
||||
}
|
||||
resolve(snippets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = fetchSnippet
|
||||
@@ -1,5 +1,6 @@
|
||||
const dataApi = {
|
||||
init: require('./init'),
|
||||
toggleStorage: require('./toggleStorage'),
|
||||
addStorage: require('./addStorage'),
|
||||
renameStorage: require('./renameStorage'),
|
||||
removeStorage: require('./removeStorage'),
|
||||
@@ -8,12 +9,17 @@ const dataApi = {
|
||||
deleteFolder: require('./deleteFolder'),
|
||||
reorderFolder: require('./reorderFolder'),
|
||||
exportFolder: require('./exportFolder'),
|
||||
exportStorage: require('./exportStorage'),
|
||||
createNote: require('./createNote'),
|
||||
createNoteFromUrl: require('./createNoteFromUrl'),
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
moveNote: require('./moveNote'),
|
||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||
createSnippet: require('./createSnippet'),
|
||||
deleteSnippet: require('./deleteSnippet'),
|
||||
updateSnippet: require('./updateSnippet'),
|
||||
fetchSnippet: require('./fetchSnippet'),
|
||||
|
||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||
_resolveStorageData: require('./resolveStorageData'),
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
|
||||
key: storageCache.key,
|
||||
name: storageCache.name,
|
||||
type: storageCache.type,
|
||||
path: storageCache.path
|
||||
path: storageCache.path,
|
||||
isOpen: storageCache.isOpen
|
||||
}
|
||||
|
||||
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
|
||||
@@ -30,13 +31,9 @@ function resolveStorageData (storageCache) {
|
||||
|
||||
const version = parseInt(storage.version, 10)
|
||||
if (version >= 1) {
|
||||
if (version > 1) {
|
||||
console.log('The repository version is newer than one of current app.')
|
||||
}
|
||||
return Promise.resolve(storage)
|
||||
}
|
||||
|
||||
console.log('Transform Legacy storage', storage.path)
|
||||
return migrateFromV6Storage(storage.path)
|
||||
.then(() => storage)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
|
||||
notePathList = sander.readdirSync(notesDirPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(notesDirPath, ' doesn\'t exist.')
|
||||
console.error(notesDirPath, ' doesn\'t exist.')
|
||||
sander.mkdirSync(notesDirPath)
|
||||
} else {
|
||||
console.warn('Failed to find note dir', notesDirPath, err)
|
||||
|
||||
27
browser/main/lib/dataApi/toggleStorage.js
Normal file
27
browser/main/lib/dataApi/toggleStorage.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {Boolean} isOpen
|
||||
* @return {Object} Storage meta data
|
||||
*/
|
||||
function toggleStorage (key, isOpen) {
|
||||
let cachedStorageList
|
||||
try {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
const targetStorage = _.find(cachedStorageList, {key: key})
|
||||
if (targetStorage == null) return Promise.reject('Storage')
|
||||
|
||||
targetStorage.isOpen = isOpen
|
||||
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
}
|
||||
|
||||
module.exports = toggleStorage
|
||||
@@ -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.')
|
||||
|
||||
35
browser/main/lib/dataApi/updateSnippet.js
Normal file
35
browser/main/lib/dataApi/updateSnippet.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import fs from 'fs'
|
||||
import consts from 'browser/lib/consts'
|
||||
|
||||
function updateSnippet (snippet, snippetFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8'))
|
||||
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
const currentSnippet = snippets[i]
|
||||
|
||||
if (currentSnippet.id === snippet.id) {
|
||||
if (
|
||||
currentSnippet.name === snippet.name &&
|
||||
currentSnippet.prefix === snippet.prefix &&
|
||||
currentSnippet.content === snippet.content &&
|
||||
currentSnippet.linesHighlighted === snippet.linesHighlighted
|
||||
) {
|
||||
// if everything is the same then don't write to disk
|
||||
resolve(snippets)
|
||||
} else {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = updateSnippet
|
||||
@@ -14,7 +14,6 @@ function once (name, listener) {
|
||||
}
|
||||
|
||||
function emit (name, ...args) {
|
||||
console.log(name)
|
||||
remote.getCurrentWindow().webContents.send(name, ...args)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,13 @@ nodeIpc.connectTo(
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Connected successfully')
|
||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
return
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,5 +3,11 @@ import ee from 'browser/main/lib/eventEmitter'
|
||||
module.exports = {
|
||||
'toggleMode': () => {
|
||||
ee.emit('topbar:togglemodebutton')
|
||||
},
|
||||
'deleteNote': () => {
|
||||
ee.emit('hotkey:deletenote')
|
||||
},
|
||||
'toggleMenuBar': () => {
|
||||
ee.emit('menubar:togglemenubar')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import CM from 'browser/main/lib/ConfigManager'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import { isObjectEqual } from 'browser/lib/utils'
|
||||
require('mousetrap-global-bind')
|
||||
const functions = require('./shortcut')
|
||||
import functions from './shortcut'
|
||||
|
||||
let shortcuts = CM.get().hotkey
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -128,3 +128,29 @@ body[data-theme="monokai"]
|
||||
|
||||
.control-confirmButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
modalDracula()
|
||||
width 500px
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
border-color $ui-dark-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()
|
||||
@@ -1,21 +1,18 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NewNoteModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { hashHistory } from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
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.state = {
|
||||
}
|
||||
this.lock = false
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -26,7 +23,7 @@ class NewNoteModal extends React.Component {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handleCreateMarkdownFromUrlClick(e) {
|
||||
handleCreateMarkdownFromUrlClick (e) {
|
||||
this.props.close()
|
||||
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
@@ -35,34 +32,18 @@ class NewNoteModal extends React.Component {
|
||||
folder: folder,
|
||||
dispatch,
|
||||
location
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
.then((note) => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: noteHash}
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
this.props.close()
|
||||
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) {
|
||||
@@ -73,36 +54,14 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
})
|
||||
.then((note) => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: noteHash}
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
this.props.close()
|
||||
const { storage, folder, dispatch, location, config } = this.props
|
||||
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) {
|
||||
@@ -120,50 +79,63 @@ class NewNoteModal extends React.Component {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'
|
||||
<div
|
||||
styleName='root'
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
onKeyDown={e => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='header'>
|
||||
<div styleName='title'>{i18n.__('Make a note')}</div>
|
||||
</div>
|
||||
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
|
||||
<ModalEscButton
|
||||
handleEscButtonClick={e => this.handleCloseButtonClick(e)}
|
||||
/>
|
||||
<div styleName='control'>
|
||||
<button styleName='control-button'
|
||||
onClick={(e) => this.handleMarkdownNoteButtonClick(e)}
|
||||
onKeyDown={(e) => this.handleMarkdownNoteButtonKeyDown(e)}
|
||||
<button
|
||||
styleName='control-button'
|
||||
onClick={e => this.handleMarkdownNoteButtonClick(e)}
|
||||
onKeyDown={e => this.handleMarkdownNoteButtonKeyDown(e)}
|
||||
ref='markdownButton'
|
||||
>
|
||||
<i styleName='control-button-icon'
|
||||
className='fa fa-file-text-o'
|
||||
/><br />
|
||||
<span styleName='control-button-label'>{i18n.__('Markdown Note')}</span><br />
|
||||
<span styleName='control-button-description'>{i18n.__('This format is for creating text documents. Checklists, code blocks and Latex blocks are available.')}</span>
|
||||
<i styleName='control-button-icon' className='fa fa-file-text-o' />
|
||||
<br />
|
||||
<span styleName='control-button-label'>
|
||||
{i18n.__('Markdown Note')}
|
||||
</span>
|
||||
<br />
|
||||
<span styleName='control-button-description'>
|
||||
{i18n.__(
|
||||
'This format is for creating text documents. Checklists, code blocks and Latex blocks are available.'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button styleName='control-button'
|
||||
onClick={(e) => this.handleSnippetNoteButtonClick(e)}
|
||||
onKeyDown={(e) => this.handleSnippetNoteButtonKeyDown(e)}
|
||||
<button
|
||||
styleName='control-button'
|
||||
onClick={e => this.handleSnippetNoteButtonClick(e)}
|
||||
onKeyDown={e => this.handleSnippetNoteButtonKeyDown(e)}
|
||||
ref='snippetButton'
|
||||
>
|
||||
<i styleName='control-button-icon'
|
||||
className='fa fa-code'
|
||||
/><br />
|
||||
<span styleName='control-button-label'>{i18n.__('Snippet Note')}</span><br />
|
||||
<span styleName='control-button-description'>{i18n.__('This format is for creating code snippets. Multiple snippets can be grouped into a single note.')}
|
||||
<i styleName='control-button-icon' className='fa fa-code' /><br />
|
||||
<span styleName='control-button-label'>
|
||||
{i18n.__('Snippet Note')}
|
||||
</span>
|
||||
<br />
|
||||
<span styleName='control-button-description'>
|
||||
{i18n.__(
|
||||
'This format is for creating code snippets. Multiple snippets can be grouped into a single note.'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewNoteModal.propTypes = {
|
||||
}
|
||||
NewNoteModal.propTypes = {}
|
||||
|
||||
export default CSSModules(NewNoteModal, styles)
|
||||
|
||||
@@ -103,3 +103,20 @@ body[data-theme="monokai"]
|
||||
|
||||
.description, .from-url
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
|
||||
.header
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button
|
||||
border-color $ui-dracula-borderColor
|
||||
color $ui-dracula-text-color
|
||||
background-color transparent
|
||||
&:focus
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.description
|
||||
color $ui-dracula-text-color
|
||||
@@ -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'
|
||||
@@ -43,7 +43,7 @@ class Blog extends React.Component {
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({BlogAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
this.oldBlog = this.state.config.blog
|
||||
@@ -70,7 +70,7 @@ class Blog extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Blog',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,21 @@
|
||||
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
|
||||
line-height 30px
|
||||
|
||||
.group-section-label
|
||||
width 150px
|
||||
width 200px
|
||||
text-align left
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
@@ -68,9 +76,9 @@
|
||||
:global
|
||||
.alert
|
||||
display inline-block
|
||||
position absolute
|
||||
top 60px
|
||||
right 15px
|
||||
position fixed
|
||||
top 130px
|
||||
right 100px
|
||||
font-size 14px
|
||||
.success
|
||||
color #1EC38B
|
||||
@@ -127,7 +135,7 @@ colorDarkControl()
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
colorSolarizedDarkControl()
|
||||
border none
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
@@ -138,16 +146,22 @@ colorMonokaiControl()
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
colorDraculaControl()
|
||||
border none
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
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
|
||||
@@ -165,27 +179,29 @@ body[data-theme="dark"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDarkControl()
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-rightButton
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
.group-hint
|
||||
@@ -199,20 +215,22 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-header
|
||||
.group-header--sub
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-header2
|
||||
.group-header2--sub
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-rightButton
|
||||
colorMonokaiPrimaryButton()
|
||||
.group-hint
|
||||
@@ -220,3 +238,32 @@ body[data-theme="monokai"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorMonokaiControl()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
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
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-rightButton
|
||||
colorDraculaPrimaryButton()
|
||||
.group-hint
|
||||
colorDraculaControl()
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDraculaControl()
|
||||
@@ -22,22 +22,28 @@ class Crowdfunding extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
|
||||
<p>{i18n.__('Dear everyone,')}</p>
|
||||
<br />
|
||||
<div styleName='group-header'>{i18n.__('Crowdfunding')}</div>
|
||||
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
||||
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
|
||||
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
|
||||
<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>
|
||||
<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. We’ve 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>
|
||||
<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.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
|
||||
<p>{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thanks,')}</p>
|
||||
<p>{i18n.__('Boostnote maintainers')}</p>
|
||||
<p>{i18n.__('Thank you,')}</p>
|
||||
<p>{i18n.__('The Boostnote Team')}</p>
|
||||
<br />
|
||||
<button styleName='cf-link'>
|
||||
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
|
||||
<a href='http://bit.ly/issuehunt-from-boostnote-app' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}</a>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
@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
|
||||
width 250px
|
||||
height 35px
|
||||
border-radius 2px
|
||||
border none
|
||||
background-color alpha(#1EC38B, 90%)
|
||||
padding-left 20px
|
||||
padding-right 20px
|
||||
&:hover
|
||||
background-color #1EC38B
|
||||
transition 0.2s
|
||||
@@ -28,7 +23,7 @@ p
|
||||
body[data-theme="dark"]
|
||||
p
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
@@ -40,3 +35,9 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
@@ -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'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
height 35px
|
||||
box-sizing border-box
|
||||
padding 2.5px 15px
|
||||
display flex
|
||||
&:hover
|
||||
background-color darken(white, 3%)
|
||||
|
||||
@@ -18,7 +19,10 @@
|
||||
border-left solid 2px transparent
|
||||
padding 0 10px
|
||||
line-height 30px
|
||||
float left
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
font-weight bold
|
||||
@@ -52,12 +56,13 @@
|
||||
outline none
|
||||
|
||||
.folderItem-right
|
||||
float right
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
|
||||
.folderItem-right-button
|
||||
vertical-align middle
|
||||
height 25px
|
||||
margin-top 2.5px
|
||||
margin-top 2px
|
||||
colorDefaultButton()
|
||||
border-radius 2px
|
||||
border $ui-border
|
||||
@@ -149,3 +154,26 @@ body[data-theme="monokai"]
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.folderItem
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
|
||||
.folderItem-left-key
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.folderItem-left-colorButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-button
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-confirmButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
|
||||
}})
|
||||
}
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
}})
|
||||
if (
|
||||
this.state.config.hotkey.toggleMain === '' ||
|
||||
this.state.config.hotkey.toggleMode === ''
|
||||
) {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'success',
|
||||
message: i18n.__('Successfully applied!')
|
||||
}})
|
||||
} else {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
}
|
||||
this.oldHotkey = this.state.config.hotkey
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
@@ -68,7 +78,10 @@ class HotkeyTab extends React.Component {
|
||||
const { config } = this.state
|
||||
config.hotkey = {
|
||||
toggleMain: this.refs.toggleMain.value,
|
||||
toggleMode: this.refs.toggleMode.value
|
||||
toggleMode: this.refs.toggleMode.value,
|
||||
deleteNote: this.refs.deleteNote.value,
|
||||
pasteSmartly: this.refs.pasteSmartly.value,
|
||||
toggleMenuBar: this.refs.toggleMenuBar.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
@@ -79,7 +92,7 @@ class HotkeyTab extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Hotkey',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +130,18 @@ class HotkeyTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
|
||||
<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'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
@@ -127,6 +151,28 @@ class HotkeyTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='deleteNote'
|
||||
value={config.hotkey.deleteNote}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Paste HTML')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='pasteSmartly'
|
||||
value={config.hotkey.pasteSmartly}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-leftButton'
|
||||
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||
|
||||
@@ -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'
|
||||
@@ -69,10 +69,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)}
|
||||
@@ -84,7 +88,7 @@ class InfoTab extends React.Component {
|
||||
>{i18n.__('GitHub')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://boostlog.io/@junp1234'
|
||||
<a href='https://medium.com/boostnote'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>{i18n.__('Blog')}</a>
|
||||
</li>
|
||||
@@ -103,7 +107,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,7 +133,7 @@ class InfoTab extends React.Component {
|
||||
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
{i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
|
||||
{i18n.__('Copyright (C) 2017 - 2019 BoostIO')}
|
||||
</li>
|
||||
<li styleName='cc'>
|
||||
{i18n.__('License: GPL v3')}
|
||||
@@ -138,7 +142,7 @@ class InfoTab extends React.Component {
|
||||
|
||||
<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 />
|
||||
|
||||
@@ -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,17 @@ 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
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
.appId
|
||||
color $ui-dracula-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-dracula-active-color
|
||||
@@ -90,7 +90,7 @@ body[data-theme="dark"]
|
||||
background-color $dark-primary-button-background--active
|
||||
&:hover
|
||||
color white
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
@@ -139,3 +139,27 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
&:hover
|
||||
color white
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
.top-bar
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
.nav
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
.nav-button
|
||||
background-color transparent
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.nav-button--active
|
||||
@extend .nav-button
|
||||
color $ui-dracula-button--active-color
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
98
browser/main/modals/PreferencesModal/SnippetEditor.js
Normal file
98
browser/main/modals/PreferencesModal/SnippetEditor.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import CodeMirror from 'codemirror'
|
||||
import React from 'react'
|
||||
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) =>
|
||||
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
|
||||
|
||||
class SnippetEditor extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.onRef(this)
|
||||
const { rulers, enableRulers } = this.props
|
||||
this.cm = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
lineNumbers: this.props.displayLineNumbers,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
indentWithTabs: this.props.indentType !== 'space',
|
||||
keyMap: this.props.keyMap,
|
||||
scrollPastEnd: this.props.scrollPastEnd,
|
||||
dragDrop: false,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
},
|
||||
mode: 'null'
|
||||
})
|
||||
this.cm.setSize('100%', '100%')
|
||||
let changeDelay = null
|
||||
|
||||
this.cm.on('change', () => {
|
||||
this.snippet.content = this.cm.getValue()
|
||||
|
||||
clearTimeout(changeDelay)
|
||||
changeDelay = setTimeout(() => {
|
||||
this.saveSnippet()
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.onRef(undefined)
|
||||
}
|
||||
|
||||
onSnippetChanged (newSnippet) {
|
||||
this.snippet = newSnippet
|
||||
this.cm.setValue(this.snippet.content)
|
||||
}
|
||||
|
||||
onSnippetNameOrPrefixChanged (newSnippet) {
|
||||
this.snippet.name = newSnippet.name
|
||||
this.snippet.prefix = newSnippet.prefix.toString().replace(/\s/g, '').split(',')
|
||||
this.saveSnippet()
|
||||
}
|
||||
|
||||
saveSnippet () {
|
||||
dataApi.updateSnippet(this.snippet)
|
||||
.then(snippets => snippetManager.assignSnippets(snippets))
|
||||
.catch((err) => { throw err })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { fontSize } = this.props
|
||||
let fontFamily = this.props.fontFamily
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
return (
|
||||
<div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
}} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetEditor.defaultProps = {
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
keyMap: 'sublime',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetEditor, styles)
|
||||
99
browser/main/modals/PreferencesModal/SnippetList.js
Normal file
99
browser/main/modals/PreferencesModal/SnippetList.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react'
|
||||
import styles from './SnippetTab.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import context from 'browser/lib/context'
|
||||
|
||||
class SnippetList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
snippets: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.reloadSnippetList()
|
||||
eventEmitter.on('snippetList:reload', this.reloadSnippetList.bind(this))
|
||||
}
|
||||
|
||||
reloadSnippetList () {
|
||||
dataApi.fetchSnippet().then(snippets => {
|
||||
this.setState({snippets})
|
||||
this.props.onSnippetSelect(this.props.currentSnippet)
|
||||
})
|
||||
}
|
||||
|
||||
handleSnippetContextMenu (snippet) {
|
||||
context.popup([{
|
||||
label: i18n.__('Delete snippet'),
|
||||
click: () => this.deleteSnippet(snippet)
|
||||
}])
|
||||
}
|
||||
|
||||
deleteSnippet (snippet) {
|
||||
dataApi.deleteSnippet(snippet).then(() => {
|
||||
this.reloadSnippetList()
|
||||
this.props.onSnippetDeleted(snippet)
|
||||
}).catch(err => { throw err })
|
||||
}
|
||||
|
||||
handleSnippetClick (snippet) {
|
||||
this.props.onSnippetSelect(snippet)
|
||||
}
|
||||
|
||||
createSnippet () {
|
||||
dataApi.createSnippet().then(() => {
|
||||
this.reloadSnippetList()
|
||||
// scroll to end of list when added new snippet
|
||||
const snippetList = document.getElementById('snippets')
|
||||
snippetList.scrollTop = snippetList.scrollHeight
|
||||
}).catch(err => { throw err })
|
||||
}
|
||||
|
||||
defineSnippetStyleName (snippet) {
|
||||
const { currentSnippet } = this.props
|
||||
|
||||
if (currentSnippet == null) {
|
||||
return 'snippet-item'
|
||||
}
|
||||
|
||||
if (currentSnippet.id === snippet.id) {
|
||||
return 'snippet-item-selected'
|
||||
} else {
|
||||
return 'snippet-item'
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { snippets } = this.state
|
||||
return (
|
||||
<div styleName='snippet-list'>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-control'>
|
||||
<button styleName='group-control-button' onClick={() => this.createSnippet()}>
|
||||
<i className='fa fa-plus' /> {i18n.__('New Snippet')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id='snippets' styleName='snippets'>
|
||||
{
|
||||
snippets.map((snippet) => (
|
||||
<li
|
||||
styleName={this.defineSnippetStyleName(snippet)}
|
||||
key={snippet.id}
|
||||
onContextMenu={() => this.handleSnippetContextMenu(snippet)}
|
||||
onClick={() => this.handleSnippetClick(snippet)}>
|
||||
{snippet.name}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetList, styles)
|
||||
154
browser/main/modals/PreferencesModal/SnippetTab.js
Normal file
154
browser/main/modals/PreferencesModal/SnippetTab.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import SnippetEditor from './SnippetEditor'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import SnippetList from './SnippetList'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
currentSnippet: null
|
||||
}
|
||||
this.changeDelay = null
|
||||
}
|
||||
|
||||
notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join(
|
||||
'file://',
|
||||
global.__dirname,
|
||||
'../../resources/app.png'
|
||||
)
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handleSnippetNameOrPrefixChange () {
|
||||
clearTimeout(this.changeDelay)
|
||||
this.changeDelay = setTimeout(() => {
|
||||
// notify the snippet editor that the name or prefix of snippet has been changed
|
||||
this.snippetEditor.onSnippetNameOrPrefixChanged(this.state.currentSnippet)
|
||||
eventEmitter.emit('snippetList:reload')
|
||||
}, 500)
|
||||
}
|
||||
|
||||
handleSnippetSelect (snippet) {
|
||||
const { currentSnippet } = this.state
|
||||
if (snippet !== null) {
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSnippetNameOrPrefixChanged (e, type) {
|
||||
const newSnippet = Object.assign({}, this.state.currentSnippet)
|
||||
if (type === 'name') {
|
||||
newSnippet.name = e.target.value
|
||||
} else {
|
||||
newSnippet.prefix = e.target.value
|
||||
}
|
||||
this.setState({ currentSnippet: newSnippet })
|
||||
this.handleSnippetNameOrPrefixChange()
|
||||
}
|
||||
|
||||
handleDeleteSnippet (snippet) {
|
||||
// prevent old snippet still display when deleted
|
||||
if (snippet.id === this.state.currentSnippet.id) {
|
||||
this.setState({currentSnippet: null})
|
||||
}
|
||||
}
|
||||
|
||||
handleCopySnippet (e) {
|
||||
const showCopyNotification = this.props.config.ui.showCopyNotification
|
||||
copy(this.state.currentSnippet.content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, storageKey } = this.props
|
||||
const { currentSnippet } = this.state
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group-header'>{i18n.__('Snippets')}</div>
|
||||
<SnippetList
|
||||
onSnippetSelect={this.handleSnippetSelect.bind(this)}
|
||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
||||
currentSnippet={currentSnippet} />
|
||||
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
onClick={e => this.handleCopySnippet(e)}>{i18n.__('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={currentSnippet ? currentSnippet.name : ''}
|
||||
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'name') }}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet prefix')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={currentSnippet ? currentSnippet.prefix : ''}
|
||||
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'prefix') }}
|
||||
type='text' />
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='snippet-editor-section'>
|
||||
<SnippetEditor
|
||||
storageKey={storageKey}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetTab.PropTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetTab, styles)
|
||||
205
browser/main/modals/PreferencesModal/SnippetTab.styl
Normal file
205
browser/main/modals/PreferencesModal/SnippetTab.styl
Normal file
@@ -0,0 +1,205 @@
|
||||
@import('./ConfigTab')
|
||||
|
||||
.group
|
||||
margin-bottom 45px
|
||||
|
||||
.group-header
|
||||
@extend .header
|
||||
color $ui-text-color
|
||||
|
||||
.group-header2
|
||||
font-size 20px
|
||||
color $ui-text-color
|
||||
margin-bottom 15px
|
||||
margin-top 30px
|
||||
|
||||
.group-section
|
||||
margin-bottom 20px
|
||||
display flex
|
||||
line-height 30px
|
||||
|
||||
.group-section-label
|
||||
width 150px
|
||||
text-align left
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
|
||||
.group-section-control
|
||||
flex 1
|
||||
margin-left 5px
|
||||
|
||||
.group-section-control select
|
||||
outline none
|
||||
border 1px solid $ui-borderColor
|
||||
font-size 16px
|
||||
height 30px
|
||||
width 250px
|
||||
margin-bottom 5px
|
||||
background-color transparent
|
||||
|
||||
.group-section-control-input
|
||||
height 30px
|
||||
vertical-align middle
|
||||
width 400px
|
||||
font-size $tab--button-font-size
|
||||
border solid 1px $border-color
|
||||
border-radius 2px
|
||||
padding 0 5px
|
||||
outline none
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor
|
||||
|
||||
.group-control-button
|
||||
height 30px
|
||||
border none
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
colorPrimaryButton()
|
||||
vertical-align middle
|
||||
padding 0 20px
|
||||
|
||||
.group-checkBoxSection
|
||||
margin-bottom 15px
|
||||
display flex
|
||||
line-height 30px
|
||||
padding-left 15px
|
||||
|
||||
.group-control
|
||||
padding-top 10px
|
||||
box-sizing border-box
|
||||
height 40px
|
||||
text-align right
|
||||
:global
|
||||
.alert
|
||||
display inline-block
|
||||
position absolute
|
||||
top 60px
|
||||
right 15px
|
||||
font-size 14px
|
||||
.success
|
||||
color #1EC38B
|
||||
.error
|
||||
color red
|
||||
.warning
|
||||
color #FFA500
|
||||
|
||||
.snippet-list
|
||||
width 30%
|
||||
height calc(100% - 200px)
|
||||
position absolute
|
||||
|
||||
.snippets
|
||||
height calc(100% - 8px)
|
||||
overflow scroll
|
||||
background: #f5f5f5
|
||||
|
||||
.snippet-item
|
||||
height 50px
|
||||
font-size 15px
|
||||
line-height 50px
|
||||
padding 0 5%
|
||||
cursor pointer
|
||||
position relative
|
||||
|
||||
&::after
|
||||
width 90%
|
||||
height 1px
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
position absolute
|
||||
top 100%
|
||||
left 5%
|
||||
content ''
|
||||
|
||||
&:hover
|
||||
background darken(#f5f5f5, 5)
|
||||
|
||||
.snippet-item-selected
|
||||
@extend .snippet-list .snippet-item
|
||||
background darken(#f5f5f5, 5)
|
||||
|
||||
.snippet-detail
|
||||
width 67%
|
||||
height calc(100% - 200px)
|
||||
position absolute
|
||||
left 33%
|
||||
|
||||
.SnippetEditor
|
||||
position absolute
|
||||
width 100%
|
||||
height 90%
|
||||
|
||||
body[data-theme="default"], body[data-theme="white"]
|
||||
.snippets
|
||||
background $ui-backgroundColor
|
||||
.snippet-item
|
||||
color black
|
||||
&::after
|
||||
background $ui-borderColor
|
||||
&:hover
|
||||
background darken($ui-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-backgroundColor, 5)
|
||||
|
||||
body[data-theme="dark"]
|
||||
.snippets
|
||||
background $ui-dark-backgroundColor
|
||||
.snippet-item
|
||||
color white
|
||||
&::after
|
||||
background $ui-dark-borderColor
|
||||
&:hover
|
||||
background darken($ui-dark-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-dark-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
.group-control-button
|
||||
colorDarkPrimaryButton()
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.snippets
|
||||
background $ui-solarized-dark-backgroundColor
|
||||
.snippet-item
|
||||
color white
|
||||
&::after
|
||||
background $ui-solarized-dark-borderColor
|
||||
&:hover
|
||||
background darken($ui-solarized-dark-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-solarized-dark-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
.group-control-button
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.snippets
|
||||
background $ui-monokai-backgroundColor
|
||||
.snippet-item
|
||||
color White
|
||||
&::after
|
||||
background $ui-monokai-borderColor
|
||||
&:hover
|
||||
background darken($ui-monokai-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-monokai-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color white
|
||||
.group-control-button
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.snippets
|
||||
background $ui-dracula-backgroundColor
|
||||
.snippet-item
|
||||
color #f8f8f2
|
||||
&::after
|
||||
background $ui-dracula-borderColor
|
||||
&:hover
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color #f8f8f2
|
||||
.group-control-button
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
box-sizing border-box
|
||||
border-bottom $default-border
|
||||
margin-bottom 5px
|
||||
display flex
|
||||
|
||||
.header-label
|
||||
float left
|
||||
cursor pointer
|
||||
&:hover
|
||||
.header-label-editButton
|
||||
opacity 1
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
.header-label-path
|
||||
color $ui-inactive-text-color
|
||||
@@ -38,8 +42,8 @@
|
||||
outline none
|
||||
|
||||
.header-control
|
||||
float right
|
||||
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
.header-control-button
|
||||
width 30px
|
||||
height 25px
|
||||
|
||||
@@ -70,7 +70,7 @@ class StoragesTab extends React.Component {
|
||||
})
|
||||
return (
|
||||
<div styleName='list'>
|
||||
<div styleName='header'>{i18n.__('Storages')}</div>
|
||||
<div styleName='header'>{i18n.__('Storage Locations')}</div>
|
||||
{storageList.length > 0
|
||||
? storageList
|
||||
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>
|
||||
@@ -182,7 +182,7 @@ class StoragesTab extends React.Component {
|
||||
<div styleName='addStorage-body-section-path'>
|
||||
<input styleName='addStorage-body-section-path-input'
|
||||
ref='addStoragePath'
|
||||
placeholder='Select Folder'
|
||||
placeholder={i18n.__('Select Folder')}
|
||||
value={this.state.newStorage.path}
|
||||
onChange={(e) => this.handleAddStorageChange(e)}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
@import('./Tab')
|
||||
|
||||
.root
|
||||
padding 15px
|
||||
color $ui-text-color
|
||||
@import('./ConfigTab')
|
||||
|
||||
.list
|
||||
margin-bottom 15px
|
||||
@@ -158,7 +154,7 @@ body[data-theme="dark"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
@@ -236,3 +232,41 @@ body[data-theme="monokai"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.folderList-item
|
||||
border-bottom $ui-dracula-borderColor
|
||||
|
||||
.folderList-empty
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.list-empty
|
||||
color $ui-dracula-text-color
|
||||
.list-control-addStorageButton
|
||||
border-color $ui-dracula-button-backgroundColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-header
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-name-input
|
||||
border-color $$ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-type-description
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-body-section-path-button
|
||||
colorPrimaryButton()
|
||||
.addStorage-body-control
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-control-createButton
|
||||
colorDarkPrimaryButton()
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
@@ -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'
|
||||
@@ -11,6 +11,7 @@ import 'codemirror-mode-elixir'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLanguages } from 'browser/lib/Languages'
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
@@ -28,6 +29,10 @@ class UiTab extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
|
||||
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
|
||||
CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript')
|
||||
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
|
||||
this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px')
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({UiAlert: {
|
||||
type: 'success',
|
||||
@@ -37,7 +42,7 @@ class UiTab extends React.Component {
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({UiAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
@@ -64,9 +69,15 @@ class UiTab extends React.Component {
|
||||
ui: {
|
||||
theme: this.refs.uiTheme.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
|
||||
@@ -82,8 +93,19 @@ class UiTab extends React.Component {
|
||||
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
|
||||
switchPreview: this.refs.editorSwitchPreview.value,
|
||||
keyMap: this.refs.editorKeyMap.value,
|
||||
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
|
||||
scrollPastEnd: this.refs.scrollPastEnd.checked,
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
|
||||
enableTableEditor: this.refs.enableTableEditor.checked,
|
||||
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
|
||||
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()
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -96,17 +118,27 @@ class UiTab extends React.Component {
|
||||
latexBlockClose: this.refs.previewLatexBlockClose.value,
|
||||
plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value,
|
||||
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
|
||||
scrollSync: this.refs.previewScrollSync.checked,
|
||||
smartQuotes: this.refs.previewSmartQuotes.checked,
|
||||
breaks: this.refs.previewBreaks.checked,
|
||||
sanitize: this.refs.previewSanitize.value
|
||||
smartArrows: this.refs.previewSmartArrows.checked,
|
||||
sanitize: this.refs.previewSanitize.value,
|
||||
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
|
||||
lineThroughCheckbox: this.refs.lineThroughCheckbox.checked,
|
||||
customCSS: this.customCSSCM.getCodeMirror().getValue()
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
@@ -116,7 +148,7 @@ class UiTab extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'UI',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -159,13 +191,16 @@ class UiTab extends React.Component {
|
||||
const { config, codemirrorTheme } = this.state
|
||||
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
|
||||
const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
|
||||
const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily)
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group'>
|
||||
<div styleName='group-header'>{i18n.__('Interface')}</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
{i18n.__('Interface Theme')}
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Interface Theme')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.theme}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -175,13 +210,16 @@ class UiTab extends React.Component {
|
||||
<option value='white'>{i18n.__('White')}</option>
|
||||
<option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
|
||||
<option value='monokai'>{i18n.__('Monokai')}</option>
|
||||
<option value='dracula'>{i18n.__('Dracula')}</option>
|
||||
<option value='dark'>{i18n.__('Dark')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
{i18n.__('Language')}
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Language')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.language}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -194,6 +232,32 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Default New Note')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.defaultNote}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
ref='defaultNote'
|
||||
>
|
||||
<option value='ALWAYS_ASK'>{i18n.__('Always Ask')}</option>
|
||||
<option value='MARKDOWN_NOTE'>{i18n.__('Markdown Note')}</option>
|
||||
<option value='SNIPPET_NOTE'>{i18n.__('Snippet Note')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showMenuBar}
|
||||
ref='showMenuBar'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show menu bar')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -214,16 +278,6 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Show a confirmation dialog when deleting notes')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showOnlyRelatedTags}
|
||||
ref='showOnlyRelatedTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show only related tags')}
|
||||
</label>
|
||||
</div>
|
||||
{
|
||||
global.process.platform === 'win32'
|
||||
? <div styleName='group-checkBoxSection'>
|
||||
@@ -234,11 +288,69 @@ class UiTab extends React.Component {
|
||||
disabled={OSX}
|
||||
type='checkbox'
|
||||
/>
|
||||
Disable Direct Write(It will be applied after restarting)
|
||||
{i18n.__('Disable Direct Write (It will be applied after restarting)')}
|
||||
</label>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<div styleName='group-header2'>Tags</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.saveTagsAlphabetically}
|
||||
ref='saveTagsAlphabetically'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Save tags of a note in alphabetical order')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showTagsAlphabetically}
|
||||
ref='showTagsAlphabetically'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show tags of a note in alphabetical order')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showOnlyRelatedTags}
|
||||
ref='showOnlyRelatedTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show only related tags')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.enableLiveNoteCounts}
|
||||
ref='enableLiveNoteCounts'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable live count of notes')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.tagNewNoteWithFilteringTags}
|
||||
ref='tagNewNoteWithFilteringTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('New notes are tagged with the filtering tags')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>Editor</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
@@ -252,12 +364,20 @@ 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>
|
||||
<div styleName='code-mirror'>
|
||||
<ReactCodeMirror ref={e => (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} />
|
||||
<div styleName='code-mirror' style={{fontFamily}}>
|
||||
<ReactCodeMirror
|
||||
ref={e => (this.codeMirrorInstance = e)}
|
||||
value={codemirrorSampleCode}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
readOnly: true,
|
||||
mode: 'javascript',
|
||||
theme: codemirrorTheme
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -372,6 +492,48 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Snippet Default Language')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.editor.snippetDefaultLanguage}
|
||||
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>))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Front matter title field')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
ref='frontMatterTitleField'
|
||||
value={config.editor.frontMatterTitleField}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableFrontMatterTitle}
|
||||
ref='enableFrontMatterTitle'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Extract title from front matter')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -405,6 +567,109 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableTableEditor}
|
||||
ref='enableTableEditor'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable smart table editor')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableSmartPaste}
|
||||
ref='enableSmartPaste'
|
||||
type='checkbox'
|
||||
/>
|
||||
{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'
|
||||
/>
|
||||
{i18n.__('Enable spellcheck - Experimental feature!! :)')}
|
||||
</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'
|
||||
/>
|
||||
{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'>
|
||||
@@ -432,8 +697,9 @@ 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-label'>{i18n.__('Code Block Theme')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.preview.codeBlockTheme}
|
||||
ref='previewCodeBlockTheme'
|
||||
@@ -441,12 +707,22 @@ 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>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.lineThroughCheckbox}
|
||||
ref='lineThroughCheckbox'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Allow line through checkbox')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -457,6 +733,16 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Allow preview to scroll past the last line')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.scrollSync}
|
||||
ref='previewScrollSync'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('When scrolling, synchronize preview with editor')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -474,7 +760,7 @@ class UiTab extends React.Component {
|
||||
ref='previewSmartQuotes'
|
||||
type='checkbox'
|
||||
/>
|
||||
Enable smart quotes
|
||||
{i18n.__('Enable smart quotes')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
@@ -484,7 +770,17 @@ class UiTab extends React.Component {
|
||||
ref='previewBreaks'
|
||||
type='checkbox'
|
||||
/>
|
||||
Render newlines in Markdown paragraphs as <br>
|
||||
{i18n.__('Render newlines in Markdown paragraphs as <br>')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.smartArrows}
|
||||
ref='previewSmartArrows'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -569,6 +865,33 @@ class UiTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Custom CSS')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={config.preview.allowCustomCSS}
|
||||
ref='previewAllowCustomCSS'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Allow custom CSS for preview')}
|
||||
<div style={{fontFamily}}>
|
||||
<ReactCodeMirror
|
||||
width='400px'
|
||||
height='400px'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
ref={e => (this.customCSSCM = e)}
|
||||
value={config.preview.customCSS}
|
||||
defaultValue={'/* Drop Your Custom CSS Code Here */\n'}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
mode: 'css',
|
||||
theme: codemirrorTheme
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
|
||||
@@ -6,6 +6,7 @@ import UiTab from './UiTab'
|
||||
import InfoTab from './InfoTab'
|
||||
import Crowdfunding from './Crowdfunding'
|
||||
import StoragesTab from './StoragesTab'
|
||||
import SnippetTab from './SnippetTab'
|
||||
import Blog from './Blog'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
@@ -86,6 +87,14 @@ class Preferences extends React.Component {
|
||||
haveToSave={alert => this.setState({BlogAlert: alert})}
|
||||
/>
|
||||
)
|
||||
case 'SNIPPET':
|
||||
return (
|
||||
<SnippetTab
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
case 'STORAGES':
|
||||
default:
|
||||
return (
|
||||
@@ -123,7 +132,8 @@ class Preferences extends React.Component {
|
||||
{target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert},
|
||||
{target: 'INFO', label: i18n.__('About')},
|
||||
{target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')},
|
||||
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert}
|
||||
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert},
|
||||
{target: 'SNIPPET', label: i18n.__('Snippets')}
|
||||
]
|
||||
|
||||
const navButtons = tabs.map((tab) => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -38,29 +40,15 @@ function data (state = defaultDataMap(), action) {
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
let storageNoteList = state.storageNoteMap.get(note.storage)
|
||||
if (storageNoteList == null) {
|
||||
storageNoteList = new Set(storageNoteList)
|
||||
state.storageNoteMap.set(note.storage, storageNoteList)
|
||||
}
|
||||
const storageNoteList = getOrInitItem(state.storageNoteMap, note.storage)
|
||||
storageNoteList.add(uniqueKey)
|
||||
|
||||
let folderNoteSet = state.folderNoteMap.get(folderKey)
|
||||
if (folderNoteSet == null) {
|
||||
folderNoteSet = new Set(folderNoteSet)
|
||||
state.folderNoteMap.set(folderKey, folderNoteSet)
|
||||
}
|
||||
const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey)
|
||||
folderNoteSet.add(uniqueKey)
|
||||
|
||||
note.tags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList == null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
tagNoteList.add(uniqueKey)
|
||||
})
|
||||
if (!note.isTrashed) {
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
}
|
||||
})
|
||||
return state
|
||||
case 'UPDATE_NOTE':
|
||||
@@ -74,40 +62,18 @@ function data (state = defaultDataMap(), action) {
|
||||
state.noteMap = new Map(state.noteMap)
|
||||
state.noteMap.set(uniqueKey, note)
|
||||
|
||||
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
if (note.isStarred) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
} else {
|
||||
state.starredSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
updateStarredChange(oldNote, note, state, uniqueKey)
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
state.starredSet.delete(uniqueKey)
|
||||
|
||||
note.tags.forEach(tag => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList != null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.delete(uniqueKey)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
})
|
||||
removeFromTags(note.tags, state, uniqueKey)
|
||||
} else {
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
|
||||
note.tags.forEach(tag => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList != null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.add(uniqueKey)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
})
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
|
||||
if (note.isStarred) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
@@ -125,54 +91,12 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
|
||||
// Update foldermap if folder changed or post created
|
||||
if (oldNote == null || oldNote.folder !== note.folder) {
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
let folderNoteSet = state.folderNoteMap.get(folderKey)
|
||||
folderNoteSet = new Set(folderNoteSet)
|
||||
folderNoteSet.add(uniqueKey)
|
||||
state.folderNoteMap.set(folderKey, folderNoteSet)
|
||||
|
||||
if (oldNote != null) {
|
||||
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
|
||||
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
|
||||
oldFolderNoteList = new Set(oldFolderNoteList)
|
||||
oldFolderNoteList.delete(uniqueKey)
|
||||
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
|
||||
}
|
||||
}
|
||||
updateFolderChange(oldNote, note, state, folderKey, uniqueKey)
|
||||
|
||||
if (oldNote != null) {
|
||||
const discardedTags = _.difference(oldNote.tags, note.tags)
|
||||
const addedTags = _.difference(note.tags, oldNote.tags)
|
||||
if (discardedTags.length + addedTags.length > 0) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
|
||||
discardedTags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList != null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.delete(uniqueKey)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
})
|
||||
addedTags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.add(uniqueKey)
|
||||
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
})
|
||||
}
|
||||
updateTagChanges(oldNote, note, state, uniqueKey)
|
||||
} else {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
note.tags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList == null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
tagNoteList.add(uniqueKey)
|
||||
})
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
}
|
||||
|
||||
return state
|
||||
@@ -193,7 +117,6 @@ function data (state = defaultDataMap(), action) {
|
||||
|
||||
// If storage chanced, origin key must be discarded
|
||||
if (originKey !== uniqueKey) {
|
||||
console.log('diffrent storage')
|
||||
// From isStarred
|
||||
if (originNote.isStarred) {
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
@@ -220,26 +143,10 @@ function data (state = defaultDataMap(), action) {
|
||||
originFolderList.delete(originKey)
|
||||
state.folderNoteMap.set(originFolderKey, originFolderList)
|
||||
|
||||
// From tagMap
|
||||
if (originNote.tags.length > 0) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
originNote.tags.forEach((tag) => {
|
||||
let noteSet = state.tagNoteMap.get(tag)
|
||||
noteSet = new Set(noteSet)
|
||||
noteSet.delete(originKey)
|
||||
state.tagNoteMap.set(tag, noteSet)
|
||||
})
|
||||
}
|
||||
removeFromTags(originNote.tags, state, originKey)
|
||||
}
|
||||
|
||||
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
if (note.isStarred) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
} else {
|
||||
state.starredSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
updateStarredChange(oldNote, note, state, uniqueKey)
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
@@ -260,55 +167,13 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
|
||||
// Update foldermap if folder changed or post created
|
||||
if (oldNote == null || oldNote.folder !== note.folder) {
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
let folderNoteList = state.folderNoteMap.get(folderKey)
|
||||
folderNoteList = new Set(folderNoteList)
|
||||
folderNoteList.add(uniqueKey)
|
||||
state.folderNoteMap.set(folderKey, folderNoteList)
|
||||
|
||||
if (oldNote != null) {
|
||||
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
|
||||
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
|
||||
oldFolderNoteList = new Set(oldFolderNoteList)
|
||||
oldFolderNoteList.delete(uniqueKey)
|
||||
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
|
||||
}
|
||||
}
|
||||
updateFolderChange(oldNote, note, state, folderKey, uniqueKey)
|
||||
|
||||
// Remove from old folder map
|
||||
if (oldNote != null) {
|
||||
const discardedTags = _.difference(oldNote.tags, note.tags)
|
||||
const addedTags = _.difference(note.tags, oldNote.tags)
|
||||
if (discardedTags.length + addedTags.length > 0) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
|
||||
discardedTags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList != null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.delete(uniqueKey)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
})
|
||||
addedTags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.add(uniqueKey)
|
||||
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
})
|
||||
}
|
||||
updateTagChanges(oldNote, note, state, uniqueKey)
|
||||
} else {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
note.tags.forEach((tag) => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList == null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
tagNoteList.add(uniqueKey)
|
||||
})
|
||||
assignToTags(note.tags, state, uniqueKey)
|
||||
}
|
||||
|
||||
return state
|
||||
@@ -347,32 +212,17 @@ function data (state = defaultDataMap(), action) {
|
||||
folderSet.delete(uniqueKey)
|
||||
state.folderNoteMap.set(folderKey, folderSet)
|
||||
|
||||
// From tagMap
|
||||
if (targetNote.tags.length > 0) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
targetNote.tags.forEach((tag) => {
|
||||
let noteSet = state.tagNoteMap.get(tag)
|
||||
noteSet = new Set(noteSet)
|
||||
noteSet.delete(uniqueKey)
|
||||
state.tagNoteMap.set(tag, noteSet)
|
||||
})
|
||||
}
|
||||
removeFromTags(targetNote.tags, state, uniqueKey)
|
||||
}
|
||||
state.noteMap = new Map(state.noteMap)
|
||||
state.noteMap.delete(uniqueKey)
|
||||
return state
|
||||
}
|
||||
case 'UPDATE_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'REORDER_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'EXPORT_FOLDER':
|
||||
case 'RENAME_STORAGE':
|
||||
case 'EXPORT_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
@@ -420,9 +270,7 @@ function data (state = defaultDataMap(), action) {
|
||||
// Delete key from tag map
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
note.tags.forEach((tag) => {
|
||||
let tagNoteSet = state.tagNoteMap.get(tag)
|
||||
tagNoteSet = new Set(tagNoteSet)
|
||||
state.tagNoteMap.set(tag, tagNoteSet)
|
||||
const tagNoteSet = getOrInitItem(state.tagNoteMap, tag)
|
||||
tagNoteSet.delete(noteKey)
|
||||
})
|
||||
}
|
||||
@@ -449,11 +297,7 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
let storageNoteList = state.storageNoteMap.get(note.storage)
|
||||
if (storageNoteList == null) {
|
||||
storageNoteList = new Set(storageNoteList)
|
||||
state.storageNoteMap.set(note.storage, storageNoteList)
|
||||
}
|
||||
const storageNoteList = getOrInitItem(state.tagNoteMap, note.storage)
|
||||
storageNoteList.add(uniqueKey)
|
||||
|
||||
let folderNoteSet = state.folderNoteMap.get(folderKey)
|
||||
@@ -464,11 +308,7 @@ function data (state = defaultDataMap(), action) {
|
||||
folderNoteSet.add(uniqueKey)
|
||||
|
||||
note.tags.forEach((tag) => {
|
||||
let tagNoteSet = state.tagNoteMap.get(tag)
|
||||
if (tagNoteSet == null) {
|
||||
tagNoteSet = new Set(tagNoteSet)
|
||||
state.tagNoteMap.set(tag, tagNoteSet)
|
||||
}
|
||||
const tagNoteSet = getOrInitItem(state.tagNoteMap, tag)
|
||||
tagNoteSet.add(uniqueKey)
|
||||
})
|
||||
})
|
||||
@@ -512,9 +352,10 @@ function data (state = defaultDataMap(), action) {
|
||||
})
|
||||
}
|
||||
return state
|
||||
case 'RENAME_STORAGE':
|
||||
case 'EXPAND_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
action.storage.isOpen = action.isOpen
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
}
|
||||
@@ -559,13 +400,83 @@ function status (state = defaultStatus, action) {
|
||||
return state
|
||||
}
|
||||
|
||||
function updateStarredChange (oldNote, note, state, uniqueKey) {
|
||||
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
if (note.isStarred) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
} else {
|
||||
state.starredSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFolderChange (oldNote, note, state, folderKey, uniqueKey) {
|
||||
if (oldNote == null || oldNote.folder !== note.folder) {
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
let folderNoteList = state.folderNoteMap.get(folderKey)
|
||||
folderNoteList = new Set(folderNoteList)
|
||||
folderNoteList.add(uniqueKey)
|
||||
state.folderNoteMap.set(folderKey, folderNoteList)
|
||||
|
||||
if (oldNote != null) {
|
||||
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
|
||||
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
|
||||
oldFolderNoteList = new Set(oldFolderNoteList)
|
||||
oldFolderNoteList.delete(uniqueKey)
|
||||
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTagChanges (oldNote, note, state, uniqueKey) {
|
||||
const discardedTags = _.difference(oldNote.tags, note.tags)
|
||||
const addedTags = _.difference(note.tags, oldNote.tags)
|
||||
if (discardedTags.length + addedTags.length > 0) {
|
||||
removeFromTags(discardedTags, state, uniqueKey)
|
||||
assignToTags(addedTags, state, uniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
function assignToTags (tags, state, uniqueKey) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
tags.forEach((tag) => {
|
||||
const tagNoteList = getOrInitItem(state.tagNoteMap, tag)
|
||||
tagNoteList.add(uniqueKey)
|
||||
})
|
||||
}
|
||||
|
||||
function removeFromTags (tags, state, uniqueKey) {
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
tags.forEach(tag => {
|
||||
let tagNoteList = state.tagNoteMap.get(tag)
|
||||
if (tagNoteList != null) {
|
||||
tagNoteList = new Set(tagNoteList)
|
||||
tagNoteList.delete(uniqueKey)
|
||||
state.tagNoteMap.set(tag, tagNoteList)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getOrInitItem (target, key) {
|
||||
let results = target.get(key)
|
||||
if (results == null) {
|
||||
results = new Set()
|
||||
target.set(key, results)
|
||||
}
|
||||
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, compose(
|
||||
applyMiddleware(routerMiddleware(history)), DevTools.instrument()))
|
||||
|
||||
export default store
|
||||
export { store, history }
|
||||
|
||||
Reference in New Issue
Block a user