1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-27 16:41:41 +00:00

Merge branch 'master' into export-yfm

This commit is contained in:
Baptiste Augrain
2020-06-12 15:17:02 +02:00
327 changed files with 21961 additions and 12973 deletions

View File

@@ -24,23 +24,16 @@ body[data-theme="dark"]
.empty-message
color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
border-left 1px solid $ui-solarized-dark-borderColor
.empty-message
color $ui-solarized-dark-text-color
apply-theme(theme)
body[data-theme={theme}]
.root
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
border-left 1px solid get-theme-var(theme, 'borderColor')
.empty-message
color get-theme-var(theme, 'text-color')
body[data-theme="monokai"]
.root
background-color $ui-monokai-noteDetail-backgroundColor
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
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
for theme in $themes
apply-theme(theme)

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -16,24 +16,27 @@ class FolderSelect extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
this.value = this.props.value
}
componentDidUpdate () {
componentDidUpdate() {
this.value = this.props.value
}
handleClick (e) {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
handleClick(e) {
this.setState(
{
status: 'SEARCH',
optionIndex: -1
},
() => {
this.refs.search.focus()
}
)
}
handleFocus (e) {
handleFocus(e) {
if (this.state.status === 'IDLE') {
this.setState({
status: 'FOCUS'
@@ -41,7 +44,7 @@ class FolderSelect extends React.Component {
}
}
handleBlur (e) {
handleBlur(e) {
if (this.state.status === 'FOCUS') {
this.setState({
status: 'IDLE'
@@ -49,40 +52,49 @@ class FolderSelect extends React.Component {
}
}
handleKeyDown (e) {
handleKeyDown(e) {
switch (e.keyCode) {
case 13:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
this.setState(
{
status: 'SEARCH',
optionIndex: -1
},
() => {
this.refs.search.focus()
}
)
}
break
case 40:
case 38:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: 0
}, () => {
this.refs.search.focus()
})
this.setState(
{
status: 'SEARCH',
optionIndex: 0
},
() => {
this.refs.search.focus()
}
)
}
break
case 9:
if (e.shiftKey) {
e.preventDefault()
const tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
const tabbable = document.querySelectorAll(
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'
)
const previousEl =
tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
if (previousEl != null) previousEl.focus()
}
}
}
handleSearchInputBlur (e) {
handleSearchInputBlur(e) {
if (e.relatedTarget !== this.refs.root) {
this.setState({
status: 'IDLE'
@@ -90,14 +102,17 @@ class FolderSelect extends React.Component {
}
}
handleSearchInputChange (e) {
handleSearchInputChange(e) {
const { folders } = this.props
const search = this.refs.search.value
const optionIndex = search.length > 0
? _.findIndex(folders, (folder) => {
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
})
: -1
const optionIndex =
search.length > 0
? _.findIndex(folders, folder => {
return folder.name.match(
new RegExp('^' + _.escapeRegExp(search), 'i')
)
})
: -1
this.setState({
search: this.refs.search.value,
@@ -105,7 +120,7 @@ class FolderSelect extends React.Component {
})
}
handleSearchInputKeyDown (e) {
handleSearchInputKeyDown(e) {
switch (e.keyCode) {
case 40:
e.stopPropagation()
@@ -121,15 +136,18 @@ class FolderSelect extends React.Component {
break
case 27:
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.refs.root.focus()
}
)
}
}
nextOption () {
nextOption() {
let { optionIndex } = this.state
const { folders } = this.props
@@ -141,7 +159,7 @@ class FolderSelect extends React.Component {
})
}
previousOption () {
previousOption() {
const { folders } = this.props
let { optionIndex } = this.state
@@ -153,46 +171,52 @@ class FolderSelect extends React.Component {
})
}
selectOption () {
selectOption() {
const { folders } = this.props
const optionIndex = this.state.optionIndex
const folder = folders[optionIndex]
if (folder != null) {
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(folder.key)
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.setValue(folder.key)
this.refs.root.focus()
}
)
}
}
handleOptionClick (storageKey, folderKey) {
return (e) => {
handleOptionClick(storageKey, folderKey) {
return e => {
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
})
this.setState(
{
status: 'FOCUS'
},
() => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
}
)
}
}
setValue (value) {
setValue(value) {
this.value = value
this.props.onChange()
}
render () {
render() {
const { className, data, value } = this.props
const splitted = value.split('-')
const storageKey = splitted.shift()
const folderKey = splitted.shift()
let options = []
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
storage.folders.forEach(folder => {
options.push({
storage: storage,
folder: folder
@@ -200,68 +224,78 @@ class FolderSelect extends React.Component {
})
})
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
const currentOption = options.filter(
option =>
option.storage.key === storageKey && option.folder.key === folderKey
)[0]
if (this.state.search.trim().length > 0) {
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
options = options.filter((option) => filter.test(option.folder.name))
options = options.filter(option => filter.test(option.folder.name))
}
const optionList = options
.map((option, index) => {
return (
<div styleName={index === this.state.optionIndex
const optionList = options.map((option, index) => {
return (
<div
styleName={
index === this.state.optionIndex
? 'search-optionList-item--active'
: 'search-optionList-item'
}
key={option.storage.key + '-' + option.folder.key}
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
}
key={option.storage.key + '-' + option.folder.key}
onClick={e =>
this.handleOptionClick(option.storage.key, option.folder.key)(e)
}
>
<span
styleName='search-optionList-item-name'
style={{ borderColor: option.folder.color }}
>
<span styleName='search-optionList-item-name'
style={{borderColor: option.folder.color}}
>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>
in {option.storage.name}
</span>
</div>
)
})
</span>
</div>
)
})
return (
<div className={_.isString(className)
? 'FolderSelect ' + className
: 'FolderSelect'
<div
className={
_.isString(className) ? 'FolderSelect ' + className : 'FolderSelect'
}
styleName={this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
styleName={
this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
}
ref='root'
tabIndex='0'
onClick={(e) => this.handleClick(e)}
onFocus={(e) => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onClick={e => this.handleClick(e)}
onFocus={e => this.handleFocus(e)}
onBlur={e => this.handleBlur(e)}
onKeyDown={e => this.handleKeyDown(e)}
>
{this.state.status === 'SEARCH'
? <div styleName='search'>
<input styleName='search-input'
{this.state.status === 'SEARCH' ? (
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={this.state.search}
placeholder={i18n.__('Folder...')}
onChange={(e) => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
onChange={e => this.handleSearchInputChange(e)}
onBlur={e => this.handleSearchInputBlur(e)}
onKeyDown={e => this.handleSearchInputKeyDown(e)}
/>
<div styleName='search-optionList'
ref='optionList'
>
<div styleName='search-optionList' ref='optionList'>
{optionList}
</div>
</div>
: <div styleName='idle' style={{color: currentOption.folder.color}}>
) : currentOption ? (
<div styleName='idle' style={{ color: currentOption.folder.color }}>
<div styleName='idle-label'>
<i className='fa fa-folder' />
<span styleName='idle-label-name'>
@@ -269,8 +303,7 @@ class FolderSelect extends React.Component {
</span>
</div>
</div>
}
) : null}
</div>
)
}
@@ -280,11 +313,13 @@ FolderSelect.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
}))
folders: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
})
)
}
export default CSSModules(FolderSelect, styles)

View File

@@ -134,54 +134,39 @@ body[data-theme="dark"]
.search-optionList-item-name-surfix
color $ui-dark-inactive-text-color
body[data-theme="monokai"]
.root
color $ui-dark-text-color
&:hover
color white
background-color $ui-monokai-button--hover-backgroundColor
border-color $ui-monokai-borderColor
apply-theme(theme)
body[data-theme={theme}]
.root
&:hover
background-color get-theme-var(theme, 'button--hover-backgroundColor')
border-color get-theme-var(theme, 'borderColor')
.search-optionList
color white
border-color $ui-monokai-borderColor
background-color $ui-monokai-button-backgroundColor
.search-input
color get-theme-var(theme, 'text-color')
background-color transparent
border-color get-theme-var(theme, 'borderColor')
.search-optionList-item
&:hover
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%)
.search-optionList
color get-theme-var(theme, 'text-color')
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'button-backgroundColor')
.search-optionList-item--active
background-color $ui-monokai-button--active-backgroundColor
color $ui-monokai-button--active-color
&:hover
background-color $ui-monokai-button--active-backgroundColor
color $ui-monokai-button--active-color
.search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color
.search-optionList-item
&:hover
background-color lighten(get-theme-var(theme, 'button--hover-backgroundColor'), 15%)
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-item--active
background-color get-theme-var(theme, 'button--active-backgroundColor')
color get-theme-var(theme, 'button--active-color')
&:hover
background-color get-theme-var(theme, 'button--active-backgroundColor')
color get-theme-var(theme, 'button--active-color')
.search-optionList
color #f8f8f2
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.search-optionList-item-name-surfix
color get-theme-var(theme, 'inactive-text-color')
.search-optionList-item
&:hover
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
.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
for theme in $themes
apply-theme(theme)

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,26 @@
.control-fullScreenButton
top 80px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 70px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-fullScreenButton
.control-fullScreenButton
top 80px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 70px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 35px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n'
const InfoButton = ({
onClick
}) => (
<button styleName='control-infoButton'
onClick={(e) => onClick(e)}
>
const InfoButton = ({ onClick }) => (
<button styleName='control-infoButton' onClick={e => onClick(e)}>
<img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>{i18n.__('Info')}</span>
</button>

View File

@@ -6,28 +6,47 @@ import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component {
copyNoteLink () {
const {noteLink} = this.props
copyNoteLink() {
const { noteLink } = this.props
this.refs.noteLink.select()
copy(noteLink)
}
render () {
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'}}>
<div
className='infoPanel'
styleName='control-infoButton-panel'
style={{ display: 'none' }}
>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
<p styleName='modification-date-desc'>
{i18n.__('MODIFICATION DATE')}
</p>
</div>
<hr />
{type === 'SNIPPET_NOTE'
? ''
: <div styleName='count-wrap'>
{type === 'SNIPPET_NOTE' ? (
''
) : (
<div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
@@ -37,12 +56,9 @@ class InfoPanel extends React.Component {
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div>
</div>
}
)}
{type === 'SNIPPET_NOTE'
? ''
: <hr />
}
{type === 'SNIPPET_NOTE' ? '' : <hr />}
<div>
<p styleName='infoPanel-default'>{storageName}</p>
@@ -60,8 +76,18 @@ class InfoPanel extends React.Component {
</div>
<div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<input
styleName='infoPanel-noteLink'
ref='noteLink'
defaultValue={noteLink}
onClick={e => {
e.target.select()
}}
/>
<button
onClick={() => this.copyNoteLink()}
styleName='infoPanel-copyButton'
>
<i className='fa fa-clipboard' />
</button>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
@@ -70,22 +96,39 @@ class InfoPanel extends React.Component {
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<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, 'export-txt')}>
<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, 'export-html')}>
<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, 'print')}>
<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 +147,7 @@ InfoPanel.propTypes = {
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired,

View File

@@ -15,7 +15,7 @@
right 25px
position absolute
padding 20px 25px 0 25px
width 300px
// width 300px
overflow auto
background-color $ui-noteList-backgroundColor
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
@@ -138,162 +138,49 @@
.export--unable
cursor not-allowed
body[data-theme="dark"]
.control-infoButton-panel
background-color $ui-dark-noteList-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.control-infoButton-panel
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control-infoButton-panel-trash
background-color $ui-dark-noteList-backgroundColor
.control-infoButton-panel-trash
background-color get-theme-var(theme, 'noteList-backgroundColor')
.modification-date
color $ui-dark-text-color
.modification-date
color get-theme-var(theme, 'text-color')
.modification-date-desc
color $ui-inactive-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-dark-text-color
.infoPanel-defaul-count
color get-theme-var(theme, 'text-color')
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-dark-text-color
.infoPanel-default
color get-theme-var(theme, 'text-color')
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-dark-borderColor, 60%)
color $ui-dark-text-color
.infoPanel-noteLink
background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color get-theme-var(theme, 'text-color')
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-dark-borderColor, 20%)
color $ui-dark-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color get-theme-var(theme, 'text-color')
p
color $ui-dark-inactive-text-color
&:hover
color get-theme-var(theme, 'text-color')
body[data-theme="solarized-dark"]
.control-infoButton-panel
background-color $ui-solarized-dark-noteList-backgroundColor
for theme in 'dark' 'solarized-dark' 'dracula'
apply-theme(theme)
.control-infoButton-panel-trash
background-color $ui-solarized-ark-noteList-backgroundColor
.modification-date
color $ui-solarized-ark-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-solarized-dark-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-solarized-ark-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-dark-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-ark-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-solarized-ark-text-color
body[data-theme="monokai"]
.control-infoButton-panel
background-color $ui-monokai-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-monokai-noteList-backgroundColor
.modification-date
color $ui-monokai-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-monokai-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-monokai-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
p
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
for theme in $themes
apply-theme(theme)

View File

@@ -5,9 +5,20 @@ 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
className='infoPanel'
styleName='control-infoButton-panel-trash'
style={{ display: 'none' }}
>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
@@ -21,7 +32,10 @@ const InfoPanelTrashed = ({
</div>
<div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
<p styleName='infoPanel-default'>
<text styleName='infoPanel-trash'>Trash</text>
{folderName}
</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div>
@@ -31,22 +45,34 @@ const InfoPanelTrashed = ({
</div>
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<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, 'export-txt')}>
<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, 'export-html')}>
<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 +87,8 @@ InfoPanelTrashed.propTypes = {
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired
}
export default CSSModules(InfoPanelTrashed, styles)

View File

@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
@@ -9,7 +10,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdownTextHelper'
import StatusBar from '../StatusBar'
@@ -30,109 +30,147 @@ 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'
import ToggleDirectionButton from 'browser/main/Detail/ToggleDirectionButton'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
isMovingNote: false,
note: Object.assign({
title: '',
content: '',
linesHighlighted: []
}, props.note),
isLockButtonShown: false,
note: Object.assign(
{
title: '',
content: '',
linesHighlighted: []
},
props.note
),
isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false,
editorType: props.config.editor.type
editorType: props.config.editor.type,
switchPreview: props.config.editor.switchPreview,
RTL: false
}
this.dispatchTimer = null
this.generateToc = this.handleGenerateToc.bind(this)
this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc()
this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
this.getNote = this.getNote.bind(this)
}
focus () {
focus() {
this.refs.content.focus()
}
componentDidMount () {
componentDidMount() {
ee.on('editor:orientation', this.handleSwitchStackDirection)
ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
ee.on('topbar:togglemodebutton', () => {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
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) {
UNSAFE_componentWillReceiveProps(nextProps) {
const isNewNote = nextProps.note.key !== this.props.note.key
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
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({ 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({
note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
switchPreview
})
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
console.log('setting focus', switchPreview)
this.focus()
}
}
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow()
}
handleUpdateTag () {
handleUpdateTag() {
const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value
this.updateNote(note)
}
handleUpdateContent () {
handleUpdateContent() {
const { note } = this.state
note.content = this.refs.content.value
note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
let title = findNoteTitle(
note.content,
this.props.config.editor.enableFrontMatterTitle,
this.props.config.editor.frontMatterTitleField
)
title = striptags(title)
title = markdown.strip(title)
note.title = title
this.updateNote(note)
}
updateNote (note) {
updateNote(note) {
note.updatedAt = new Date()
this.setState({note}, () => {
this.setState({ note }, () => {
this.save()
})
}
save () {
save() {
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
saveNow() {
const { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
})
}
handleFolderChange (e) {
handleFolderChange(e) {
const { note } = this.state
const value = this.refs.folder.value
const splitted = value.split('-')
@@ -141,60 +179,71 @@ class MarkdownNoteDetail extends React.Component {
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.key
}
})
this.setState({
isMovingNote: false
})
})
.then(newNote => {
this.setState(
{
isMovingNote: true,
note: Object.assign({}, newNote)
},
() => {
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
dispatch(
replace({
pathname: location.pathname,
search: queryString.stringify({
key: newNote.key
})
})
)
this.setState({
isMovingNote: false
})
}
)
})
}
handleStarButtonClick (e) {
handleStarButtonClick(e) {
const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
if (!note.isStarred)
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
this.setState(
{
note
},
() => {
this.save()
}
)
}
exportAsFile () {
exportAsFile() {}
}
exportAsMd () {
exportAsMd() {
ee.emit('export:save-md')
}
exportAsTxt () {
exportAsTxt() {
ee.emit('export:save-text')
}
exportAsHtml () {
exportAsHtml() {
ee.emit('export:save-html')
}
handleKeyDown (e) {
exportAsPdf() {
ee.emit('export:save-pdf')
}
handleKeyDown(e) {
switch (e.keyCode) {
// tab key
case 9:
@@ -204,7 +253,11 @@ class MarkdownNoteDetail extends React.Component {
} else if (e.ctrlKey && e.shiftKey) {
e.preventDefault()
this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
} else if (
!e.ctrlKey &&
!e.shiftKey &&
e.target === this.refs.description
) {
e.preventDefault()
this.focusEditor()
}
@@ -212,9 +265,8 @@ class MarkdownNoteDetail extends React.Component {
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
const isSuper =
global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
@@ -224,17 +276,17 @@ class MarkdownNoteDetail extends React.Component {
}
}
handleTrashButtonClick (e) {
handleTrashButtonClick(e) {
const { note } = this.state
const { isTrashed } = note
const { confirmDeletion } = this.props.config.ui
if (isTrashed) {
if (confirmDeleteNote(confirmDeletion, true)) {
const {note, dispatch} = this.props
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
.then(data => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
@@ -250,102 +302,139 @@ class MarkdownNoteDetail extends React.Component {
if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true
this.setState({
note
}, () => {
this.save()
})
this.setState(
{
note
},
() => {
this.save()
}
)
ee.emit('list:next')
}
}
}
handleUndoButtonClick (e) {
handleUndoButtonClick(e) {
const { note } = this.state
note.isTrashed = false
this.setState({
note
}, () => {
this.save()
this.refs.content.reload()
ee.emit('list:next')
})
this.setState(
{
note
},
() => {
this.save()
this.refs.content.reload()
ee.emit('list:next')
}
)
}
handleFullScreenButton (e) {
handleFullScreenButton(e) {
ee.emit('editor:fullscreen')
}
handleLockButtonMouseDown (e) {
handleLockButtonMouseDown(e) {
e.preventDefault()
ee.emit('editor:lock')
this.setState({ isLocked: !this.state.isLocked })
if (this.state.isLocked) this.focus()
}
getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
getToggleLockButton() {
return this.state.isLocked
? '../resources/icon/icon-lock.svg'
: '../resources/icon/icon-unlock.svg'
}
handleDeleteKeyDown (e) {
handleDeleteKeyDown(e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
}
handleToggleLockButton (event, noteStatus) {
handleToggleLockButton(event, noteStatus) {
// first argument event is not used
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
if (noteStatus === 'CODE') {
this.setState({ isLockButtonShown: true })
} else {
this.setState({isLockButtonShown: false})
this.setState({ isLockButtonShown: false })
}
}
handleGenerateToc () {
handleGenerateToc() {
const editor = this.refs.content.refs.code.editor
markdownToc.generateInEditor(editor)
}
handleFocus (e) {
handleFocus(e) {
this.focus()
}
handleInfoButtonClick (e) {
handleInfoButtonClick(e) {
const infoPanel = document.querySelector('.infoPanel')
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
if (infoPanel.style)
infoPanel.style.display =
infoPanel.style.display === 'none' ? 'inline' : 'none'
}
print (e) {
print(e) {
ee.emit('print')
}
handleSwitchMode (type) {
this.setState({ editorType: type }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
})
handleSwitchMode(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 () {
handleSwitchStackDirection() {
this.setState(
prevState => ({ isStacking: !prevState.isStacking }),
() => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.ui.isStacking = this.state.isStacking
ConfigManager.set(newConfig)
}
)
}
handleSwitchDirection() {
if (!this.props.config.editor.rtlEnabled) {
return
}
// If in split mode, hide the lock button
const direction = this.state.RTL
this.setState({ RTL: !direction })
}
handleDeleteNote() {
this.handleTrashButtonClick()
}
handleClearTodo () {
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')
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)
@@ -353,161 +442,199 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note)
}
getNote () {
getNote() {
return this.state.note
}
renderEditor () {
renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') {
return <MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
/>
return (
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
} else {
return <MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
/>
return (
<MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL}
/>
)
}
}
render () {
const { data, location, config } = this.props
render() {
const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
const folderKey = note.folder
const options = []
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
storage.folders.forEach(folder => {
options.push({
storage: storage,
folder: folder
})
})
})
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
const trashTopBar = <div styleName='info'>
<div styleName='info-left'>
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanelTrashed
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
/>
</div>
</div>
const currentOption = _.find(
options,
option =>
option.storage.key === storageKey && option.folder.key === folderKey
)
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={(e) => this.handleFolderChange(e)}
// currentOption may be undefined
const storageName = _.get(currentOption, 'storage.name') || ''
const folderName = _.get(currentOption, 'folder.name') || ''
const trashTopBar = (
<div styleName='info'>
<div styleName='info-left'>
<RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton
onClick={e => this.handleTrashButtonClick(e)}
/>
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed
storageName={storageName}
folderName={folderName}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={this.handleUpdateTag.bind(this)}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
)
{(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img styleName='iconInfo' src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
const detailTopBar = (
<div styleName='info'>
<div styleName='info-left'>
<div>
<FolderSelect
styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
data={data}
onChange={e => this.handleFolderChange(e)}
/>
</div>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<TagSelect
ref='tags'
value={this.state.note.tags}
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage
onClearCheckboxClick={e => this.handleClearTodo(e)}
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/>
</div>
<div styleName='info-right'>
<ToggleModeButton
onClick={e => this.handleSwitchMode(e)}
editorType={editorType}
/>
{this.props.config.editor.rtlEnabled && (
<ToggleDirectionButton
onClick={e => this.handleSwitchDirection(e)}
isRTL={this.state.RTL}
/>
)}
<StarButton
onClick={e => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
{(() => {
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent = (
<button
styleName='control-lockButton'
onFocus={e => this.handleFocus(e)}
onMouseDown={e => this.handleLockButtonMouseDown(e)}
>
<img src={imgSrc} />
{this.state.isLocked ? (
<span styleName='tooltip'>Unlock</span>
) : (
<span styleName='tooltip'>Lock</span>
)}
</button>
)
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
return this.state.isLockButtonShown ? lockButtonComponent : ''
})()}
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
wordCount={note.content.split(' ').length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
<TrashButton onClick={e => this.handleTrashButtonClick(e)} />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanel
storageName={storageName}
folderName={folderName}
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.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
</div>
</div>
</div>
)
return (
<div className='NoteDetail'
<div
className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyDown={e => this.handleKeyDown(e)}
>
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'>
{this.renderEditor()}
</div>
<div styleName='body'>{this.renderEditor()}</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -521,9 +648,7 @@ class MarkdownNoteDetail extends React.Component {
MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
note: PropTypes.shape({}),
style: PropTypes.shape({
left: PropTypes.number
}),

View File

@@ -15,7 +15,7 @@
.control-lockButton
topBarButtonRight()
position absolute
right 225px
right 265px
&:hover .tooltip
opacity 1
@@ -66,18 +66,14 @@ body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
apply-theme(theme)
body[data-theme={theme}]
.root
border-left 1px solid get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
body[data-theme="solarized-dark"]
.root
border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
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
for theme in $themes
apply-theme(theme)

View File

@@ -15,6 +15,14 @@ $info-margin-under-border = 30px
padding 0 20px
z-index 99
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none
.info-left
padding 0 10px
width 100%
@@ -94,17 +102,14 @@ body[data-theme="dark"]
.undo-button
topBarButtonDark()
body[data-theme="solarized-dark"]
.info
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.info
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
body[data-theme="monokai"]
.info
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
body[data-theme="dracula"]
.info
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
for theme in $themes
apply-theme(theme)

View File

@@ -4,13 +4,9 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({
onClick
}) => (
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
const PermanentDeleteButton = ({ onClick }) => (
<button styleName='control-trashButton--in-trash' onClick={e => onClick(e)}>
<img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>
)

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n'
const RestoreButton = ({
onClick
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
const RestoreButton = ({ onClick }) => (
<button styleName='control-restoreButton' onClick={onClick}>
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span>
</button>

File diff suppressed because it is too large Load Diff

View File

@@ -156,78 +156,35 @@ body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
body[data-theme="solarized-dark"]
.root
border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.root
border-left 1px solid get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body
background-color $ui-solarized-dark-noteDetail-backgroundColor
.body
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body .description textarea
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor
.body .description textarea
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color get-theme-var(theme, 'text-color')
border 1px solid get-theme-var(theme, 'borderColor')
.tabList .tabButton
border-color $ui-solarized-dark-borderColor
.tabList .tabButton
border-color get-theme-var(theme, '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
.tabButton
&:hover
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
transition 0.15s
body[data-theme="monokai"]
.root
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
.tabList
background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color get-theme-var(theme, 'text-color')
.body
background-color $ui-monokai-noteDetail-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
.body .description textarea
background-color $ui-monokai-noteDetail-backgroundColor
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
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
for theme in $themes
apply-theme(theme)

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class StarButton extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -14,47 +14,51 @@ class StarButton extends React.Component {
}
}
handleMouseDown (e) {
handleMouseDown(e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
handleMouseUp(e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
handleMouseLeave(e) {
this.setState({
isActive: false
})
}
render () {
render() {
const { className } = this.props
return (
<button className={_.isString(className)
? 'StarButton ' + className
: 'StarButton'
<button
className={
_.isString(className) ? 'StarButton ' + className : 'StarButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
styleName={
this.state.isActive || this.props.isActive ? 'root--active' : 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg'
onMouseDown={e => this.handleMouseDown(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseLeave={e => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<img
styleName='icon'
src={
this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Star')}</span>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Star')}
</span>
</button>
)
}

View File

@@ -21,6 +21,11 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 103px
width 70px
.root--active
@extend .root
transition 0.15s
@@ -37,4 +42,4 @@ body[data-theme="dark"]
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
@@ -7,9 +8,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -18,15 +20,20 @@ class TagSelect extends React.Component {
}
this.handleAddTag = this.handleAddTag.bind(this)
this.handleRenameTag = this.handleRenameTag.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.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(
this
)
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(
this
)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}
addNewTag (newTag) {
addNewTag(newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
newTag = newTag.trim().replace(/ +/g, '_')
@@ -42,9 +49,7 @@ class TagSelect extends React.Component {
}
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value = _.isArray(value) ? value.slice() : []
if (!_.includes(value, newTag)) {
value.push(newTag)
@@ -54,68 +59,87 @@ class TagSelect extends React.Component {
value = _.sortBy(value)
}
this.setState({
newTag: ''
}, () => {
this.value = value
this.props.onChange()
})
this.setState(
{
newTag: ''
},
() => {
this.value = value
this.props.onChange()
}
)
}
buildSuggestions () {
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
(tag, name) => ({
name,
nameLC: name.toLowerCase(),
size: tag.size
})
).filter(
tag => tag.size > 0
), ['name'])
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 () {
componentDidMount() {
this.value = this.props.value
this.buildSuggestions()
ee.on('editor:add-tag', this.handleAddTag)
ee.on('sidebar:rename-tag', this.handleRenameTag)
}
componentDidUpdate () {
componentDidUpdate() {
this.value = this.props.value
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('editor:add-tag', this.handleAddTag)
ee.off('sidebar:rename-tag', this.handleRenameTag)
}
handleAddTag () {
handleAddTag() {
this.refs.newTag.input.focus()
}
handleTagLabelClick (tag) {
const { router } = this.context
router.push(`/tags/${tag}`)
handleRenameTag(event, tagChange) {
const { value } = this.props
const { tag, updatedTag } = tagChange
const newTags = value.slice()
newTags[value.indexOf(tag)] = updatedTag
this.value = newTags
this.props.onChange()
}
handleTagRemoveButtonClick (tag) {
handleTagLabelClick(tag) {
const { dispatch } = this.props
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick(tag) {
this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1)
}, tag)
}
onInputBlur (e) {
onInputBlur(e) {
this.submitNewTag()
}
onInputChange (e, { newValue, method }) {
onInputChange(e, { newValue, method }) {
this.setState({
newTag: newValue
})
}
onInputKeyDown (e) {
onInputKeyDown(e) {
switch (e.keyCode) {
case 9:
e.preventDefault()
@@ -131,17 +155,18 @@ class TagSelect extends React.Component {
}
}
onSuggestionsClearRequested () {
onSuggestionsClearRequested() {
this.setState({
suggestions: []
})
}
onSuggestionsFetchRequested ({ value }) {
onSuggestionsFetchRequested({ value }) {
const valueLC = value.toLowerCase()
const suggestions = _.filter(
this.suggestions,
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
tag =>
!_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
)
this.setState({
@@ -149,22 +174,20 @@ class TagSelect extends React.Component {
})
}
onSuggestionSelected (event, { suggestion, suggestionValue }) {
onSuggestionSelected(event, { suggestion, suggestionValue }) {
this.addNewTag(suggestionValue)
}
removeLastTag () {
this.removeTagByCallback((value) => {
removeLastTag() {
this.removeTagByCallback(value => {
value.pop()
})
}
removeTagByCallback (callback, tag = null) {
removeTagByCallback(callback, tag = null) {
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value = _.isArray(value) ? value.slice() : []
callback(value, tag)
value = _.uniq(value)
@@ -172,7 +195,7 @@ class TagSelect extends React.Component {
this.props.onChange()
}
reset () {
reset() {
this.buildSuggestions()
this.setState({
@@ -180,36 +203,60 @@ class TagSelect extends React.Component {
})
}
submitNewTag () {
submitNewTag() {
this.addNewTag(this.refs.newTag.input.value)
}
render () {
const { value, className, showTagsAlphabetically } = this.props
render() {
const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
return (
<span styleName='tag'
key={tag}
>
<span styleName='tag-label' 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' />
</button>
</span>
)
})
? (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'
style={textStyle}
onClick={e => this.handleTagLabelClick(tag)}
>
#{tag}
</span>
<button
styleName='tag-removeButton'
onClick={e => this.handleTagRemoveButtonClick(tag)}
>
<img
className='tag-removeButton-icon'
src={iconRemove}
width='8px'
/>
</button>
</span>
)
})
: []
const { newTag, suggestions } = this.state
return (
<div className={_.isString(className)
? 'TagSelect ' + className
: 'TagSelect'
<div
className={
_.isString(className) ? 'TagSelect ' + className : 'TagSelect'
}
styleName='root'
>
@@ -221,11 +268,7 @@ class TagSelect extends React.Component {
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => (
<div>
{suggestion.name}
</div>
)}
renderSuggestion={suggestion => <div>{suggestion.name}</div>}
inputProps={{
placeholder: i18n.__('Add tag...'),
value: newTag,
@@ -239,14 +282,12 @@ class TagSelect extends React.Component {
}
}
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = {
dispatch: PropTypes.func,
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
onChange: PropTypes.func,
coloredTags: PropTypes.object
}
export default CSSModules(TagSelect, styles)

View File

@@ -3,19 +3,18 @@
align-items center
user-select none
vertical-align middle
width 100%
overflow-x scroll
width 96%
overflow-x auto
white-space nowrap
margin-top 31px
top 50px
position absolute
.root::-webkit-scrollbar
display none
&::-webkit-scrollbar
height 8px
.tag
display flex
align-items center
margin 0px 2px
margin 0px 2px 2px
padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px
@@ -55,35 +54,20 @@ body[data-theme="dark"]
.tag-label
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.tag
background-color $ui-solarized-dark-tag-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.tag
background-color get-theme-var(theme, 'tag-backgroundColor')
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-solarized-dark-text-color
.tag-label
color get-theme-var(theme, 'text-color')
body[data-theme="monokai"]
.tag
background-color $ui-monokai-tag-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-monokai-text-color
body[data-theme="dracula"]
.tag
background-color $ui-dracula-tag-backgroundColor
.tag-removeButton
border-color $ui-dracula-button--focus-borderColor
background-color transparent
.tag-label
color $ui-dracula-borderColor
for theme in $themes
apply-theme(theme)

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleDirectionButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleDirectionButton = ({ onClick, isRTL }) => (
<div styleName='control-toggleModeButton'>
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
</div>
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
</div>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Direction')}
</span>
</div>
)
ToggleDirectionButton.propTypes = {
onClick: PropTypes.func.isRequired,
isRTL: PropTypes.bool.isRequired
}
export default CSSModules(ToggleDirectionButton, styles)

View File

@@ -0,0 +1,85 @@
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
margin-left 5px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
apply-theme(theme)
body[data-theme={theme}]
.control-toggleModeButton
background-color get-theme-var(theme, 'borderColor')
.active
background-color get-theme-var(theme, 'active-color')
box-shadow 2px 0px 7px #222222
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -4,23 +4,41 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
}) => (
const ToggleModeButton = ({ onClick, editorType }) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
<div
styleName={editorType === 'SPLIT' ? 'active' : undefined}
onClick={() => onClick('SPLIT')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? '../resources/icon/icon-mode-markdown-off-active.svg'
: ''
}
/>
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
<div
styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined}
onClick={() => onClick('EDITOR_PREVIEW')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? ''
: '../resources/icon/icon-mode-split-on-active.svg'
}
/>
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Mode')}
</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
editorType: PropTypes.string.isRequired
}
export default CSSModules(ToggleModeButton, styles)

View File

@@ -1,72 +1,84 @@
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
body[data-theme="monokai"]
.control-toggleModeButton
background-color #373831
.active
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
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
apply-theme(theme)
body[data-theme={theme}]
.control-toggleModeButton
background-color get-theme-var(theme, 'borderColor')
.active
background-color get-theme-var(theme, 'active-color')
box-shadow 2px 0px 7px #222222
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -4,14 +4,12 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const TrashButton = ({
onClick
}) => (
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Trash')}</span>
const TrashButton = ({ onClick }) => (
<button styleName='control-trashButton' onClick={e => onClick(e)}>
<img src='../resources/icon/icon-trash.svg' />
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Trash')}
</span>
</button>
)

View File

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

View File

@@ -10,11 +10,12 @@ 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'
class Detail extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.focusHandler = () => {
@@ -25,43 +26,55 @@ class Detail extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
ee.on('detail:focus', this.focusHandler)
ee.on('detail:delete', this.deleteHandler)
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('detail:focus', this.focusHandler)
ee.off('detail:delete', this.deleteHandler)
}
render () {
const { location, data, params, config } = this.props
render() {
const {
location,
data,
match: { params },
config
} = this.props
const noteKey =
location.search !== '' && queryString.parse(location.search).key
let note = null
if (location.query.key != null) {
const noteKey = location.query.key
if (location.search !== '') {
const allNotes = data.noteMap.map(note => note)
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
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/)) {
displayedNotes =
searchStr === undefined || searchStr === ''
? allNotes
: searchFromNotes(allNotes, searchStr)
} else if (location.pathname.match(/^\/tags/)) {
const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
displayedNotes = data.noteMap
.map(note => note)
.filter(note => listOfTags.every(tag => note.tags.includes(tag)))
}
if (location.pathname.match(/\/trashed/)) {
if (location.pathname.match(/^\/trashed/)) {
displayedNotes = trashedNotes
} else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
displayedNotes = _.differenceWith(
displayedNotes,
trashedNotes,
(note, trashed) => note.key === trashed.key
)
}
const noteKeys = displayedNotes.map(note => note.key)
@@ -72,12 +85,12 @@ class Detail extends React.Component {
if (note == null) {
return (
<div styleName='root'
style={this.props.style}
tabIndex='0'
>
<div styleName='root' style={this.props.style} tabIndex='0'>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
<div styleName='empty-message'>
{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />
{i18n.__('to create a new note')}
</div>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

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

View File

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

View File

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

View File

@@ -12,17 +12,19 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager'
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
import { push } from 'connected-react-router'
const path = require('path')
const electron = require('electron')
const { remote } = electron
class Main extends React.Component {
constructor (props) {
constructor(props) {
super(props)
if (process.env.NODE_ENV === 'production') {
@@ -44,7 +46,7 @@ class Main extends React.Component {
this.toggleFullScreen = () => this.handleFullScreenButton()
}
getChildContext () {
getChildContext() {
const { status, config } = this.props
return {
@@ -53,7 +55,7 @@ class Main extends React.Component {
}
}
init () {
init() {
dataApi
.addStorage({
name: 'My Storage Location',
@@ -91,18 +93,21 @@ class Main extends React.Component {
type: 'SNIPPET_NOTE',
folder: data.storage.folders[0].key,
title: 'Snippet note example',
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
description:
'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
snippets: [
{
name: 'example.html',
mode: 'html',
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
content:
"<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
linesHighlighted: []
},
{
name: 'example.js',
mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
content:
"var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: []
}
]
@@ -118,7 +123,8 @@ class Main extends React.Component {
type: 'MARKDOWN_NOTE',
folder: data.storage.folders[0].key,
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)'
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 => {
store.dispatch({
@@ -132,23 +138,23 @@ class Main extends React.Component {
.then(() => data.storage)
})
.then(storage => {
hashHistory.push('/storages/' + storage.key)
store.dispatch(push('/storages/' + storage.key))
})
.catch(err => {
throw err
})
}
componentDidMount () {
componentDidMount() {
const { dispatch, config } = this.props
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
this.refreshTheme = setInterval(() => {
const conf = ConfigManager.get()
chooseTheme(conf)
}, 5 * 1000)
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', config.ui.theme)
} else {
document.body.setAttribute('data-theme', 'default')
}
chooseTheme(config)
applyTheme(config.ui.theme)
if (getLocales().indexOf(config.ui.language) !== -1) {
i18n.setLocale(config.ui.language)
@@ -169,30 +175,56 @@ class Main extends React.Component {
}
})
// eslint-disable-next-line no-undef
delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
eventEmitter.on(
'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this)
)
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
}
componentWillUnmount () {
componentWillUnmount() {
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
eventEmitter.off(
'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this)
)
eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this))
clearInterval(this.refreshTheme)
}
handleLeftSlideMouseDown (e) {
changeRoutePush(event, destination) {
const { dispatch } = this.props
dispatch(push(destination))
}
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) {
e.preventDefault()
this.setState({
isLeftSliderFocused: true
})
}
handleRightSlideMouseDown (e) {
handleRightSlideMouseDown(e) {
e.preventDefault()
this.setState({
isRightSliderFocused: true
})
}
handleMouseUp (e) {
handleMouseUp(e) {
// Change width of NoteList component.
if (this.state.isRightSliderFocused) {
this.setState(
@@ -232,7 +264,7 @@ class Main extends React.Component {
}
}
handleMouseMove (e) {
handleMouseMove(e) {
if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
@@ -258,7 +290,7 @@ class Main extends React.Component {
}
}
handleFullScreenButton (e) {
handleFullScreenButton(e) {
this.setState({ fullScreen: !this.state.fullScreen }, () => {
const noteDetail = document.querySelector('.NoteDetail')
const noteList = document.querySelector('.NoteList')
@@ -272,7 +304,7 @@ class Main extends React.Component {
})
}
hideLeftLists (noteDetail, noteList, mainBody) {
hideLeftLists(noteDetail, noteList, mainBody) {
this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px'
@@ -280,13 +312,13 @@ class Main extends React.Component {
noteList.style.display = 'none'
}
showLeftLists (noteDetail, noteList, mainBody) {
showLeftLists(noteDetail, noteList, mainBody) {
noteDetail.style.left = this.state.noteDetailWidth
mainBody.style.left = this.state.mainBodyWidth
noteList.style.display = 'inline'
}
render () {
render() {
const { config } = this.props
// the width of the navigation bar when it is folded/collapsed
@@ -300,10 +332,16 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'match',
'location'
])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
{!config.isSideNavFolded && (
<div
styleName={
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
@@ -313,7 +351,8 @@ class Main extends React.Component {
draggable='false'
>
<div styleName='slider-hitbox' />
</div>}
</div>
)}
<div
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body'
@@ -330,7 +369,7 @@ class Main extends React.Component {
'dispatch',
'config',
'data',
'params',
'match',
'location'
])}
/>
@@ -340,7 +379,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
/>
@@ -362,7 +401,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}

View File

@@ -72,14 +72,13 @@ body[data-theme="dark"]
.control-newNoteButton-tooltip
darkTooltip()
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.root, .root--expanded
background-color get-theme-var(theme, 'noteList-backgroundColor')
body[data-theme="monokai"]
.root, .root--expanded
background-color $ui-monokai-noteList-backgroundColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
body[data-theme="dracula"]
.root, .root--expanded
background-color $ui-dracula-noteList-backgroundColor
for theme in $themes
apply-theme(theme)

View File

@@ -15,33 +15,48 @@ const { dialog } = remote
const OSX = window.process.platform === 'darwin'
class NewNoteButton extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
}
this.state = {}
this.newNoteHandler = () => {
this.handleNewNoteButtonClick()
}
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
}
componentDidMount () {
eventEmitter.on('top:new-note', this.newNoteHandler)
componentDidMount() {
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
}
componentWillUnmount () {
eventEmitter.off('top:new-note', this.newNoteHandler)
componentWillUnmount() {
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
}
handleNewNoteButtonClick (e) {
const { location, params, dispatch, config } = this.props
handleNewNoteButtonClick(e) {
const {
location,
dispatch,
match: { params },
config
} = this.props
const { storage, folder } = this.resolveTargetFolder()
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
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)
createSnippetNote(
storage.key,
folder.key,
dispatch,
location,
params,
config
)
} else {
modal.open(NewNoteModal, {
storage: storage.key,
@@ -54,10 +69,12 @@ class NewNoteButton extends React.Component {
}
}
resolveTargetFolder () {
const { data, params } = this.props
resolveTargetFolder() {
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) {
@@ -66,9 +83,12 @@ class NewNoteButton extends React.Component {
}
}
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
if (storage == null)
this.showMessageBox(i18n.__('No storage to create a note'))
const folder =
_.find(storage.folders, { key: params.folderKey }) || storage.folders[0]
if (folder == null)
this.showMessageBox(i18n.__('No folder to create a note'))
return {
storage,
@@ -76,7 +96,7 @@ class NewNoteButton extends React.Component {
}
}
showMessageBox (message) {
showMessageBox(message) {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: message,
@@ -84,17 +104,20 @@ class NewNoteButton extends React.Component {
})
}
render () {
render() {
const { config, style } = this.props
return (
<div className='NewNoteButton'
<div
className='NewNoteButton'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<button
styleName='control-newNoteButton'
onClick={this.handleNewNoteButtonClick}
>
<img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span>

View File

@@ -59,106 +59,34 @@ $control-height = 30px
top $control-height
overflow auto
body[data-theme="white"]
.root
background-color $ui-white-noteList-backgroundColor
apply-theme(theme)
body[data-theme={theme}]
.root
border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control
background-color $ui-white-noteList-backgroundColor
.control
background-color get-theme-var(theme, 'noteList-backgroundColor')
border-color get-theme-var(theme, 'borderColor')
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
.control-sortBy-select
&:hover
transition 0.2s
color get-theme-var(theme, 'text-color')
background-color: get-theme-var(theme, 'noteList-backgroundColor')
.control
background-color $ui-dark-noteList-backgroundColor
border-color $ui-dark-borderColor
.control-button
color get-theme-var(theme, 'inactive-text-color')
&:hover
color get-theme-var(theme, 'text-color')
.control-sortBy-select
&:hover
transition 0.2s
color $ui-dark-text-color
.control-button--active
color get-theme-var(theme, 'text-color')
&:active
color get-theme-var(theme, 'text-color')
.control-button
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color
for theme in 'white' 'dark' 'solarized-dark' 'dracula'
apply-theme(theme)
.control-button--active
color $ui-dark-text-color
&:active
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
.control
background-color $ui-solarized-dark-noteList-backgroundColor
border-color $ui-solarized-dark-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-solarized-dark-text-color
.control-button
color $ui-solarized-dark-inactive-text-color
&:hover
color $ui-solarized-dark-text-color
.control-button--active
color $ui-solarized-dark-text-color
&:active
color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
.control
background-color $ui-monokai-noteList-backgroundColor
border-color $ui-monokai-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-monokai-text-color
.control-button
color $ui-monokai-inactive-text-color
&:hover
color $ui-monokai-text-color
.control-button--active
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
for theme in $themes
apply-theme(theme)

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
const ListButton = ({ onClick, isTagActive }) => (
<button
styleName={isTagActive ? 'non-active-button' : 'active-button'}
onClick={onClick}
>
<img
src={
isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Notes')}</span>
</button>

View File

@@ -4,11 +4,9 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
const PreferenceButton = ({ onClick }) => (
<button styleName='top-menu-preference' onClick={e => onClick(e)}>
<img src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>
)

View File

@@ -1,52 +1,47 @@
.top-menu-preference
navButtonColor()
position absolute
top 22px
right 10px
width 2em
background-color transparent
&:hover
color $ui-button-default--active-backgroundColor
background-color transparent
.tooltip
opacity 1
&:active, &:active:hover
color $ui-button-default--active-backgroundColor
body[data-theme="white"]
.top-menu-preference
navWhiteButtonColor()
background-color transparent
&:hover
color #0B99F1
background-color transparent
&:active, &:active:hover
color #0B99F1
background-color transparent
body[data-theme="dark"]
.top-menu-preference
navDarkButtonColor()
background-color transparent
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
left -20px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
white-space nowrap
.top-menu-preference
navButtonColor()
width 2em
background-color transparent
&:hover
color $ui-button-default--active-backgroundColor
background-color transparent
.tooltip
opacity 1
&:active, &:active:hover
color $ui-button-default--active-backgroundColor
body[data-theme="white"]
.top-menu-preference
navWhiteButtonColor()
background-color transparent
&:hover
color #0B99F1
background-color transparent
&:active, &:active:hover
color #0B99F1
background-color transparent
body[data-theme="dark"]
.top-menu-preference
navDarkButtonColor()
background-color transparent
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
left -20px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
white-space nowrap

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SearchButton.styl'
import i18n from 'browser/lib/i18n'
const SearchButton = ({ onClick, isActive }) => (
<button styleName='top-menu-search' onClick={e => onClick(e)}>
<img
styleName='icon-search'
src={
isActive
? '../resources/icon/icon-search-active.svg'
: '../resources/icon/icon-search.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Search')}</span>
</button>
)
SearchButton.propTypes = {
onClick: PropTypes.func.isRequired,
isActive: PropTypes.bool
}
export default CSSModules(SearchButton, styles)

View File

@@ -0,0 +1,55 @@
.top-menu-search
navButtonColor()
position relative
margin-right 6px
top 3px
width 2em
background-color transparent
&:hover
color $ui-button-default--active-backgroundColor
background-color transparent
.tooltip
opacity 1
&:active, &:active:hover
color $ui-button-default--active-backgroundColor
.icon-search
width 16px
body[data-theme="white"]
.top-menu-search
navWhiteButtonColor()
background-color transparent
&:hover
color #0B99F1
background-color transparent
&:active, &:active:hover
color #0B99F1
background-color transparent
body[data-theme="dark"]
.top-menu-search
navDarkButtonColor()
background-color transparent
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
left -20px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
white-space nowrap

View File

@@ -9,16 +9,47 @@
flex-direction column
.top
padding-bottom 15px
display flex
align-items top
justify-content space-between
padding-bottom 10px
margin 14px 14px 4px
.switch-buttons
background-color transparent
border 0
margin 24px auto 4px 14px
display flex
align-items center
text-align center
.extra-buttons
position relative
display flex
align-items center
.search
position relative
flex 1
display flex
max-height 0
overflow hidden
transition max-height .4s
margin -5px 10px 0
.search-input
flex 1
height 2em
vertical-align middle
font-size 14px
border solid 1px $border-color
border-radius 2px
padding 2px 6px
outline none
.search-clear
width 10px
position absolute
right 8px
top 9px
cursor pointer
.top-menu-label
margin-left 5px
@@ -68,8 +99,15 @@
background-color #2E3235
.switch-buttons
display none
.extra-buttons > button:first-of-type // hide search icon
display none
.top
height 60px
align-items center
margin 0
justify-content center
position relative
left -4px
.top-menu
position static
width $sideNav--folded-width
@@ -98,32 +136,52 @@
.top-menu-preference
position absolute
left 7px
.search
height 28px
.search-input
display none
.search-clear
display none
.search-folded
width 16px
padding-left 4px
margin-bottom 8px
cursor pointer
body[data-theme="white"]
.root, .root--folded
background-color #f9f9f9
color $ui-text-color
.search .search-input
background-color #f9f9f9
color $ui-text-color
body[data-theme="dark"]
.root, .root--folded
border-right 1px solid $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
.search .search-input
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
border-color $ui-dark-borderColor
.top
border-color $ui-dark-borderColor
body[data-theme="solarized-dark"]
.root, .root--folded
background-color $ui-solarized-dark-backgroundColor
border-right 1px solid $ui-solarized-dark-borderColor
apply-theme(theme)
body[data-theme={theme}]
.root, .root--folded
background-color get-theme-var(theme, 'backgroundColor')
border-right 1px solid get-theme-var(theme, 'borderColor')
body[data-theme="monokai"]
.root, .root--folded
background-color $ui-monokai-backgroundColor
border-right 1px solid $ui-monokai-borderColor
.search .search-input
background-color get-theme-var(theme, 'backgroundColor')
color get-theme-var(theme, 'text-color')
border-color get-theme-var(theme, 'borderColor')
body[data-theme="dracula"]
.root, .root--folded
background-color $ui-dracula-backgroundColor
border-right 1px solid $ui-dracula-borderColor
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
import modal from 'browser/main/lib/modal'
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
@@ -12,6 +11,7 @@ import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { push } from 'connected-react-router'
const { remote } = require('electron')
const { dialog } = remote
@@ -19,7 +19,7 @@ const escapeStringRegexp = require('escape-string-regexp')
const path = require('path')
class StorageItem extends React.Component {
constructor (props) {
constructor(props) {
super(props)
const { storage } = this.props
@@ -30,11 +30,11 @@ class StorageItem extends React.Component {
}
}
handleHeaderContextMenu (e) {
handleHeaderContextMenu(e) {
context.popup([
{
label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e)
click: e => this.handleAddFolderButtonClick(e)
},
{
type: 'separator'
@@ -44,15 +44,19 @@ class StorageItem extends React.Component {
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportStorageClick(e, 'txt')
click: e => this.handleExportStorageClick(e, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportStorageClick(e, 'md')
click: e => this.handleExportStorageClick(e, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: (e) => this.handleExportStorageClick(e, 'html')
click: e => this.handleExportStorageClick(e, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportStorageClick(e, 'pdf')
}
]
},
@@ -61,87 +65,88 @@ class StorageItem extends React.Component {
},
{
label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e)
click: e => this.handleUnlinkStorageClick(e)
}
])
}
handleUnlinkStorageClick (e) {
handleUnlinkStorageClick(e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Unlink Storage'),
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
detail: i18n.__(
"This work will just detatches a storage from Boostnote. (Any data won't be deleted.)"
),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi.removeStorage(storage.key)
dataApi
.removeStorage(storage.key)
.then(() => {
dispatch({
type: 'REMOVE_STORAGE',
storageKey: storage.key
})
})
.catch((err) => {
.catch(err => {
throw err
})
}
}
handleExportStorageClick (e, fileType) {
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, config } = this.props
dataApi
.exportStorage(storage.key, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { storage, dispatch, config } = this.props
dataApi
.exportStorage(storage.key, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
})
}
handleToggleButtonClick (e) {
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
})
dataApi.toggleStorage(storage.key, isOpen).then(storage => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({
isOpen: isOpen
})
}
handleAddFolderButtonClick (e) {
handleAddFolderButtonClick(e) {
const { storage } = this.props
modal.open(CreateFolderModal, {
@@ -149,23 +154,32 @@ class StorageItem extends React.Component {
})
}
handleHeaderInfoClick (e) {
const { storage } = this.props
hashHistory.push('/storages/' + storage.key)
handleHeaderInfoClick(e) {
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)
handleFolderButtonClick(folderKey) {
return e => {
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
}
}
handleFolderButtonContextMenu (e, folder) {
handleFolderMouseEnter(e, tooltipRef, isFolded) {
if (isFolded) {
const buttonEl = e.currentTarget
const tooltipEl = tooltipRef.current
tooltipEl.style.top = buttonEl.getBoundingClientRect().y + 'px'
}
}
handleFolderButtonContextMenu(e, folder) {
context.popup([
{
label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder)
click: e => this.handleRenameFolderClick(e, folder)
},
{
type: 'separator'
@@ -175,15 +189,19 @@ class StorageItem extends React.Component {
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
click: e => this.handleExportFolderClick(e, folder, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: (e) => this.handleExportFolderClick(e, folder, 'md')
click: e => this.handleExportFolderClick(e, folder, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: (e) => this.handleExportFolderClick(e, folder, 'html')
click: e => this.handleExportFolderClick(e, folder, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportFolderClick(e, folder, 'pdf')
}
]
},
@@ -192,12 +210,12 @@ class StorageItem extends React.Component {
},
{
label: i18n.__('Delete Folder'),
click: (e) => this.handleFolderDeleteClick(e, folder)
click: e => this.handleFolderDeleteClick(e, folder)
}
])
}
handleRenameFolderClick (e, folder) {
handleRenameFolderClick(e, folder) {
const { storage } = this.props
modal.open(RenameFolderModal, {
storage,
@@ -205,15 +223,14 @@ class StorageItem extends React.Component {
})
}
handleExportFolderClick (e, folder, fileType) {
handleExportFolderClick(e, folder, 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) => {
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { storage, dispatch, config } = this.props
dataApi
@@ -242,66 +259,74 @@ class StorageItem extends React.Component {
})
}
handleFolderDeleteClick (e, folder) {
handleFolderDeleteClick(e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Delete Folder'),
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
detail: i18n.__(
'This will delete all notes in the folder and can not be undone.'
),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
dataApi.deleteFolder(storage.key, folder.key).then(data => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
}
handleDragEnter (e, key) {
handleDragEnter(e, key) {
e.preventDefault()
if (this.state.draggedOver === key) { return }
if (this.state.draggedOver === key) {
return
}
this.setState({
draggedOver: key
})
}
handleDragLeave (e) {
handleDragLeave(e) {
e.preventDefault()
if (this.state.draggedOver === null) { return }
if (this.state.draggedOver === null) {
return
}
this.setState({
draggedOver: null
})
}
dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder)
dropNote(storage, folder, dispatch, location, noteData) {
noteData = noteData.filter(note => folder.key !== note.folder)
if (noteData.length === 0) return
Promise.all(
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
noteData.map(note =>
dataApi.moveNote(note.storage, note.key, storage.key, folder.key)
)
)
.then((createdNoteData) => {
createdNoteData.forEach((newNote) => {
dispatch({
type: 'MOVE_NOTE',
originNote: noteData.find((note) => note.content === newNote.oldContent),
note: newNote
.then(createdNoteData => {
createdNoteData.forEach(newNote => {
dispatch({
type: 'MOVE_NOTE',
originNote: noteData.find(
note => note.content === newNote.oldContent
),
note: newNote
})
})
})
})
.catch((err) => {
console.error(`error on delete notes: ${err}`)
})
.catch(err => {
console.error(`error on delete notes: ${err}`)
})
}
handleDrop (e, storage, folder, dispatch, location) {
handleDrop(e, storage, folder, dispatch, location) {
e.preventDefault()
if (this.state.draggedOver !== null) {
this.setState({
@@ -312,21 +337,38 @@ class StorageItem extends React.Component {
this.dropNote(storage, folder, dispatch, location, noteData)
}
render () {
render() {
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
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 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 tooltipRef = React.createRef(null)
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0
if (noteSet) {
let trashedNoteCount = 0
const noteKeys = noteSet.map(noteKey => { return noteKey })
const noteKeys = noteSet.map(noteKey => {
return noteKey
})
trashedSet.toJS().forEach(trashedKey => {
if (noteKeys.some(noteKey => { return noteKey === trashedKey })) trashedNoteCount++
if (
noteKeys.some(noteKey => {
return noteKey === trashedKey
})
)
trashedNoteCount++
})
noteCount = noteSet.size - trashedNoteCount
}
@@ -335,73 +377,84 @@ class StorageItem extends React.Component {
key={folder.key}
index={index}
isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
tooltipRef={tooltipRef}
handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)}
handleMouseEnter={e =>
this.handleFolderMouseEnter(e, tooltipRef, isFolded)
}
handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
handleDrop={(e) => {
handleDrop={e => {
this.handleDrop(e, storage, folder, dispatch, location)
}}
handleDragEnter={(e) => {
handleDragEnter={e => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={(e) => {
handleDragLeave={e => {
this.handleDragLeave(e, folder)
}}
/>
)
})
const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
const isActive = location.pathname.match(
new RegExp(
escapeStringRegexp(path.sep) +
'storages' +
escapeStringRegexp(path.sep) +
storage.key +
'$'
)
)
return (
<div styleName={isFolded ? 'root--folded' : 'root'}
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
<div styleName={isFolded ? 'root--folded' : 'root'} key={storage.key}>
<div
styleName={isActive ? 'header--active' : 'header'}
onContextMenu={e => this.handleHeaderContextMenu(e)}
>
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
<button
styleName='header-toggleButton'
onMouseDown={e => this.handleToggleButtonClick(e)}
>
<img src={this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
<img
src={
this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
/>
</button>
{!isFolded &&
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
{!isFolded && (
<button
styleName='header-addFolderButton'
onClick={e => this.handleAddFolderButtonClick(e)}
>
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
<img src='../resources/icon/icon-plus.svg' />
</button>
}
)}
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
<button
styleName='header-info'
onClick={e => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
<span>
{isFolded
? _.truncate(storage.name, { length: 1, omission: '' })
: storage.name}
</span>
{isFolded &&
{isFolded && (
<span styleName='header-info--folded-tooltip'>
{storage.name}
</span>
}
)}
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
{folderList}
</div>
}
{this.state.isOpen && <div>{folderList}</div>}
</div>
)
}

View File

@@ -132,55 +132,57 @@ body[data-theme="white"]
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-text-color
body[data-theme="dark"]
.header--active
background-color $ui-dark-button--active-backgroundColor
transition color background-color 0.15s
apply-theme(theme)
body[data-theme={theme}]
.header--active
background-color get-theme-var(theme, 'button--active-backgroundColor')
transition color background-color 0.15s
.header--active
.header-toggleButton
color get-theme-var(theme, 'text-color')
.header--active
.header-info
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
&:active
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
.header--active
.header-addFolderButton
color get-theme-var(theme, 'text-color')
.header--active
.header-toggleButton
color $ui-dark-text-color
&:hover
transition 0.2s
color get-theme-var(theme, 'text-color')
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
&:active, &:active:hover
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
.header--active
.header-info
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
&:hover
transition 0.2s
color get-theme-var(theme, 'text-color')
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
&:active, &:active:hover
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
.header--active
.header-addFolderButton
color $ui-dark-text-color
.header-toggleButton
&:hover
transition 0.2s
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.header-info
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
&:hover
transition 0.2s
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.header-addFolderButton
&:hover
transition 0.2s
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
&:hover
transition 0.2s
color get-theme-var(theme, 'text-color')
background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
&:active, &:active:hover
color get-theme-var(theme, 'text-color')
background-color get-theme-var(theme, 'button--active-backgroundColor')
apply-theme('dark')
for theme in $themes
apply-theme(theme)

View File

@@ -1,60 +1,60 @@
.non-active-button
color $ui-inactive-text-color
font-size 16px
border 0
background-color transparent
transition 0.2s
display flex
text-align center
margin-right 4px
position relative
&:hover
color alpha(#239F86, 60%)
.tooltip
opacity 1
.active-button
@extend .non-active-button
color $ui-button-default--active-backgroundColor
.tooltip
tooltip()
position absolute
pointer-events none
top 22px
left -2px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
white-space nowrap
body[data-theme="white"]
.non-active-button
color $ui-inactive-text-color
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color $ui-text-color
.non-active-button
&:hover
color alpha(#0B99F1, 60%)
.active-button
@extend .non-active-button
color #0B99F1
body[data-theme="dark"]
.non-active-button
color alpha($ui-dark-text-color, 60%)
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
.non-active-button
color $ui-inactive-text-color
font-size 16px
border 0
background-color transparent
transition 0.2s
display flex
text-align center
margin-right 4px
position relative
&:hover
color alpha(#239F86, 60%)
.tooltip
opacity 1
.active-button
@extend .non-active-button
color $ui-button-default--active-backgroundColor
.tooltip
tooltip()
position absolute
pointer-events none
top 22px
left -2px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
white-space nowrap
body[data-theme="white"]
.non-active-button
color $ui-inactive-text-color
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color $ui-text-color
.non-active-button
&:hover
color alpha(#0B99F1, 60%)
.active-button
@extend .non-active-button
color #0B99F1
body[data-theme="dark"]
.non-active-button
color alpha($ui-dark-text-color, 60%)
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color alpha($ui-dark-text-color, 60%)

View File

@@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
const TagButton = ({ onClick, isTagActive }) => (
<button
styleName={isTagActive ? 'active-button' : 'non-active-button'}
onClick={onClick}
>
<img
src={
isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Tags')}</span>
</button>

View File

@@ -1,10 +1,12 @@
import PropTypes from 'prop-types'
import React from 'react'
import { push } from 'connected-react-router'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal'
import RenameTagModal from 'browser/main/modals/RenameTagModal'
import ConfigManager from 'browser/main/lib/ConfigManager'
import StorageItem from './StorageItem'
import TagListItem from 'browser/components/TagListItem'
@@ -13,39 +15,71 @@ import StorageList from 'browser/components/StorageList'
import NavToggleButton from 'browser/components/NavToggleButton'
import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton'
import SearchButton from './SearchButton'
import ListButton from './ListButton'
import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
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'
import { every, sortBy } from 'lodash'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
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)
componentDidMount () {
this.state = {
colorPicker: {
show: false,
color: null,
tagName: null,
targetRect: null,
showSearch: false,
searchText: ''
}
}
this.dismissColorPicker = this.dismissColorPicker.bind(this)
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
this.handleSearchButtonClick = this.handleSearchButtonClick.bind(this)
this.handleSearchInputChange = this.handleSearchInputChange.bind(this)
this.handleSearchInputClear = this.handleSearchInputClear.bind(this)
}
componentDidMount() {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
}
componentWillUnmount () {
componentWillUnmount() {
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')]
})
deleteTag(tag) {
const selectedButton = remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
type: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
)
if (selectedButton === 0) {
const { data, dispatch, location, params } = this.props
const {
data,
dispatch,
location,
match: { params }
} = this.props
const notes = data.noteMap
.map(note => note)
@@ -59,44 +93,68 @@ class SideNav extends React.Component {
return note
})
Promise
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
.then(updatedNotes => {
updatedNotes.forEach(note => {
dispatch({
type: 'UPDATE_NOTE',
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)
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
}
}
})
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) {
handleMenuButtonClick(e) {
openModal(PreferencesModal)
}
handleHomeButtonClick (e) {
const { router } = this.context
router.push('/home')
handleSearchButtonClick(e) {
const { showSearch } = this.state
this.setState({
showSearch: !showSearch,
searchText: ''
})
}
handleStarredButtonClick (e) {
const { router } = this.context
router.push('/starred')
handleSearchInputClear(e) {
this.setState({
searchText: ''
})
}
handleTagContextMenu (e, tag) {
handleSearchInputChange(e) {
this.setState({
searchText: e.target.value
})
}
handleHomeButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/home'))
}
handleStarredButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/starred'))
}
handleTagContextMenu(e, tag) {
const menu = []
menu.push({
@@ -104,47 +162,139 @@ class SideNav extends React.Component {
click: this.deleteTag.bind(this, tag)
})
menu.push({
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
})
menu.push({
label: i18n.__('Rename Tag'),
click: this.handleRenameTagClick.bind(this, tag)
})
context.popup(menu)
}
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
dismissColorPicker() {
this.setState({
colorPicker: {
show: false
}
})
}
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
displayColorPicker(tagName, rect) {
const { config } = this.props
this.setState({
colorPicker: {
show: true,
color: config.coloredTags[tagName],
tagName,
targetRect: rect
}
})
}
handleRenameTagClick(tagName) {
const { data, dispatch } = this.props
openModal(RenameTagModal, {
tagName,
data,
dispatch
})
}
handleColorPickerConfirm(color) {
const {
dispatch,
config: { coloredTags }
} = this.props
const {
colorPicker: { tagName }
} = this.state
const newColoredTags = Object.assign({}, coloredTags, {
[tagName]: color.hex
})
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleColorPickerReset() {
const {
dispatch,
config: { coloredTags }
} = this.props
const {
colorPicker: { tagName }
} = this.state
const newColoredTags = Object.assign({}, coloredTags)
delete newColoredTags[tagName]
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleToggleButtonClick(e) {
const { dispatch, config } = this.props
const { showSearch, searchText } = this.state
ConfigManager.set({ isSideNavFolded: !config.isSideNavFolded })
dispatch({
type: 'SET_IS_SIDENAV_FOLDED',
isFolded: !config.isSideNavFolded
})
}
handleTrashedButtonClick (e) {
const { router } = this.context
router.push('/trashed')
}
handleSwitchFoldersButtonClick () {
const { router } = this.context
router.push('/home')
}
handleSwitchTagsButtonClick () {
const { router } = this.context
router.push('/alltags')
}
onSortEnd (storage) {
return ({oldIndex, newIndex}) => {
const { dispatch } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
if (showSearch && searchText.length === 0) {
this.setState({
showSearch: false
})
}
}
SideNavComponent (isFolded, storageList) {
const { location, data, config } = this.props
handleTrashedButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/trashed'))
}
handleSwitchFoldersButtonClick() {
const { dispatch } = this.props
dispatch(push('/home'))
}
handleSwitchTagsButtonClick() {
const { dispatch } = this.props
dispatch(push('/alltags'))
}
onSortEnd(storage) {
return ({ oldIndex, newIndex }) => {
const { dispatch } = this.props
dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
}
}
SideNavComponent(isFolded) {
const { location, data, config, dispatch } = this.props
const { showSearch, searchText } = this.state
const isHomeActive = !!location.pathname.match(/^\/home$/)
const isStarredActive = !!location.pathname.match(/^\/starred$/)
@@ -153,25 +303,63 @@ class SideNav extends React.Component {
let component
// TagsMode is not selected
if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) {
if (
!location.pathname.match('/tags') &&
!location.pathname.match('/alltags')
) {
let storageMap = data.storageMap
if (showSearch && searchText.length > 0) {
storageMap = storageMap.map(storage => {
const folders = storage.folders.filter(
folder =>
folder.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1
)
return Object.assign({}, storage, { folders })
})
}
const storageList = storageMap.map((storage, key) => {
const SortableStorageItem = SortableContainer(StorageItem)
return (
<SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
config={config}
/>
)
})
component = (
<div>
<SideNavFilter
isFolded={isFolded}
isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
handleAllNotesButtonClick={e => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive}
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
handleStarredButtonClick={e => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={e => this.handleTrashedButtonClick(e)}
counterTotalNote={
data.noteMap._map.size - data.trashedSet._set.size
}
counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(
this
)}
/>
<StorageList storageList={storageList} isFolded={isFolded} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
<NavToggleButton
isFolded={isFolded}
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
/>
</div>
)
} else {
@@ -183,21 +371,26 @@ class SideNav extends React.Component {
</div>
<div styleName='tag-control-sortTagsBy'>
<i className='fa fa-angle-down' />
<select styleName='tag-control-sortTagsBy-select'
<select
styleName='tag-control-sortTagsBy-select'
title={i18n.__('Select filter mode')}
value={config.sortTagsBy}
onChange={(e) => this.handleSortTagsByChange(e)}
onChange={e => this.handleSortTagsByChange(e)}
>
<option title='Sort alphabetically'
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
<option title='Sort by update time'
value='COUNTER'>{i18n.__('Counter')}</option>
<option title='Sort alphabetically' value='ALPHABETICAL'>
{i18n.__('Alphabetically')}
</option>
<option title='Sort by update time' value='COUNTER'>
{i18n.__('Counter')}
</option>
</select>
</div>
</div>
<div styleName='tagList'>
{this.tagListComponent(data)}
</div>
<div styleName='tagList'>{this.tagListComponent(data)}</div>
<NavToggleButton
isFolded={isFolded}
handleToggleButtonClick={this.handleToggleButtonClick.bind(this)}
/>
</div>
)
}
@@ -205,80 +398,89 @@ class SideNav extends React.Component {
return component
}
tagListComponent () {
tagListComponent() {
const { data, location, config } = this.props
const { colorPicker, showSearch, searchText } = 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) })
).filter(
tag => tag.size > 0
), ['name'])
let tagList = sortBy(
data.tagNoteMap
.map((tag, name) => ({
name,
size: tag.size,
related: relatedTags.has(name)
}))
.filter(tag => tag.size > 0),
['name']
)
if (showSearch && searchText.length > 0) {
tagList = tagList.filter(
tag => tag.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1
)
}
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
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))
tagList = sortBy(tagList, item => 0 - item.size)
}
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
tagList = tagList.filter(
tag => tag.related
if (config.ui.showOnlyRelatedTags && relatedTags.size > 0) {
tagList = tagList.filter(tag => tag.related)
}
return tagList.map(tag => {
return (
<TagListItem
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
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]}
/>
)
}
return (
tagList.map(tag => {
return (
<TagListItem
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name)}
isRelated={tag.related}
key={tag.name}
count={tag.size}
/>
)
})
)
})
}
getRelatedTags (activeTags, noteMap) {
getRelatedTags(activeTags, noteMap) {
if (activeTags.length === 0) {
return new Set()
}
const relatedNotes = noteMap.map(
note => ({key: note.key, tags: note.tags})
).filter(
note => activeTags.every(tag => note.tags.includes(tag))
)
const relatedNotes = noteMap
.map(note => ({ key: note.key, tags: note.tags }))
.filter(note => activeTags.every(tag => note.tags.includes(tag)))
const relatedTags = new Set()
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
return relatedTags
}
getTagActive (path, tag) {
getTagActive(path, tag) {
return this.getActiveTags(path).includes(tag)
}
getActiveTags (path) {
getActiveTags(path) {
const pathSegments = path.split('/')
const tags = pathSegments[pathSegments.length - 1]
return (tags === 'alltags')
? []
: decodeURIComponent(tags).split(' ')
return tags === 'alltags' ? [] : decodeURIComponent(tags).split(' ')
}
handleClickTagListItem (name) {
const { router } = this.context
router.push(`/tags/${encodeURIComponent(name)}`)
handleClickTagListItem(name) {
const { dispatch } = this.props
dispatch(push(`/tags/${encodeURIComponent(name)}`))
}
handleSortTagsByChange (e) {
handleSortTagsByChange(e) {
const { dispatch } = this.props
const config = {
@@ -292,9 +494,8 @@ class SideNav extends React.Component {
})
}
handleClickNarrowToTag (tag) {
const { router } = this.context
const { location } = this.props
handleClickNarrowToTag(tag) {
const { dispatch, location } = this.props
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) {
@@ -302,73 +503,115 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
}
emptyTrash (entries) {
emptyTrash(entries) {
const { dispatch } = this.props
const deletionPromises = entries.map((note) => {
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 }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
.then(arrayOfStorageAndNoteKeys => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
})
})
.catch(err => {
console.error('Cannot Delete note: ' + err)
})
})
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
}
handleFilterButtonContextMenu (event) {
handleFilterButtonContextMenu(event) {
const { data } = this.props
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
const trashedNotes = data.trashedSet
.toJS()
.map(uniqueKey => data.noteMap.get(uniqueKey))
context.popup([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
{
label: i18n.__('Empty Trash'),
click: () => this.emptyTrash(trashedNotes)
}
])
}
render () {
const { data, location, config, dispatch } = this.props
render() {
const { location, config } = this.props
const { showSearch, searchText, colorPicker: colorPickerState } = this.state
let colorPicker
if (colorPickerState.show) {
colorPicker = (
<ColorPicker
color={colorPickerState.color}
targetRect={colorPickerState.targetRect}
onConfirm={this.handleColorPickerConfirm}
onCancel={this.dismissColorPicker}
onReset={this.handleColorPickerReset}
/>
)
}
const isFolded = config.isSideNavFolded
const storageList = data.storageMap.map((storage, key) => {
const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
config={config}
/>
})
const style = {}
if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/)
const isTagActive = /tag/.test(location.pathname)
const navSearch = (
<div styleName='search' style={{ maxHeight: showSearch ? '3em' : '0' }}>
<input
styleName='search-input'
type='text'
onChange={this.handleSearchInputChange}
value={searchText}
placeholder={i18n.__('Filter tags/folders...')}
/>
<img
styleName='search-clear'
src='../resources/icon/icon-x.svg'
onClick={this.handleSearchInputClear}
/>
{isFolded && (
<img
styleName='search-folded'
src='../resources/icon/icon-search-active.svg'
onClick={this.handleSearchButtonClick}
/>
)}
</div>
)
return (
<div className='SideNav'
<div
className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
tabIndex='1'
style={style}
>
<div styleName='top'>
<div styleName='switch-buttons'>
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
<ListButton
onClick={this.handleSwitchFoldersButtonClick.bind(this)}
isTagActive={isTagActive}
/>
<TagButton
onClick={this.handleSwitchTagsButtonClick.bind(this)}
isTagActive={isTagActive}
/>
</div>
<div>
<div styleName='extra-buttons'>
<SearchButton
onClick={this.handleSearchButtonClick}
isActive={showSearch}
/>
<PreferenceButton onClick={this.handleMenuButtonClick} />
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}
{navSearch}
{this.SideNavComponent(isFolded)}
{colorPicker}
</div>
)
}

View File

@@ -78,24 +78,19 @@ body[data-theme="dark"]
border-color $ui-dark-borderColor
border-left 1px solid $ui-dark-borderColor
body[data-theme="monokai"]
navButtonColor()
.zoom
border-color $ui-dark-borderColor
color $ui-monokai-text-color
&:hover
transition 0.15s
color $ui-monokai-active-color
&:active
color $ui-monokai-active-color
apply-theme(theme)
body[data-theme={theme}]
.zoom
border-color $ui-dark-borderColor
color get-theme-var(theme, 'text-color')
&:hover
transition 0.15s
color get-theme-var(theme, 'active-color')
&:active
color get-theme-var(theme, '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
for theme in 'dracula' 'solarized-dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -11,30 +11,43 @@ const electron = require('electron')
const { remote, ipcRenderer } = electron
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]
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) {
constructor(props) {
super(props)
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
}
componentDidMount () {
componentDidMount() {
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
}
componentWillUnmount () {
componentWillUnmount() {
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
}
updateApp () {
updateApp() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
@@ -47,10 +60,10 @@ class StatusBar extends React.Component {
}
}
handleZoomButtonClick (e) {
handleZoomButtonClick(e) {
const templates = []
zoomOptions.forEach((zoom) => {
zoomOptions.forEach(zoom => {
templates.push({
label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom)
@@ -60,7 +73,7 @@ class StatusBar extends React.Component {
context.popup(templates)
}
handleZoomMenuItemClick (zoomFactor) {
handleZoomMenuItemClick(zoomFactor) {
const { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
@@ -69,40 +82,36 @@ class StatusBar extends React.Component {
})
}
handleZoomInMenuItem () {
handleZoomInMenuItem() {
const zoomFactor = ZoomManager.getZoom() + 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomOutMenuItem () {
handleZoomOutMenuItem() {
const zoomFactor = ZoomManager.getZoom() - 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomResetMenuItem () {
handleZoomResetMenuItem() {
this.handleZoomMenuItemClick(1.0)
}
render () {
render() {
const { config, status } = this.context
return (
<div className='StatusBar'
styleName='root'
>
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<div className='StatusBar' styleName='root'>
<button styleName='zoom' onClick={e => this.handleZoomButtonClick(e)}>
<img src='../resources/icon/icon-zoom.svg' />
<span>{Math.floor(config.zoom * 100)}%</span>
</button>
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
{status.updateReady ? (
<button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' />{' '}
{i18n.__('Ready to Update!')}
</button>
: null
}
) : null}
</div>
)
}

View File

@@ -212,69 +212,31 @@ body[data-theme="dark"]
.control-newPostButton-tooltip
darkTooltip()
apply-theme(theme)
body[data-theme={theme}]
.root, .root--expanded
background-color get-theme-var(theme, 'noteList-backgroundColor')
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor
.control
border-color get-theme-var(theme, 'borderColor')
.control-search
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control
border-color $ui-solarized-dark-borderColor
.control-search
background-color $ui-solarized-dark-noteList-backgroundColor
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color get-theme-var(theme, 'inactive-text-color')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-solarized-dark-inactive-text-color
background-color $ui-solarized-dark-noteList-backgroundColor
.control-search-input
background-color get-theme-var(theme, 'noteList-backgroundColor')
input
background-color get-theme-var(theme, 'noteList-backgroundColor')
color get-theme-var(theme, 'text-color')
.control-search-input
background-color $ui-solarized-dark-noteList-backgroundColor
input
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
for theme in 'solarized-dark' 'dracula'
apply-theme(theme)
body[data-theme="monokai"]
.root, .root--expanded
background-color $ui-monokai-noteList-backgroundColor
.control
border-color $ui-monokai-borderColor
.control-search
background-color $ui-monokai-noteList-backgroundColor
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-monokai-inactive-text-color
background-color $ui-monokai-noteList-backgroundColor
.control-search-input
background-color $ui-monokai-noteList-backgroundColor
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
for theme in $themes
apply-theme(theme)

View File

@@ -7,34 +7,52 @@ 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) {
constructor(props) {
super(props)
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.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
maxWait: 1000 / 8
})
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
componentDidMount() {
const {
match: { params }
} = this.props
const searchWord = params && params.searchword
if (searchWord !== undefined) {
this.setState({
search: searchWord,
@@ -45,28 +63,28 @@ class TopBar extends React.Component {
ee.on('code:init', this.codeInitHandler)
}
componentWillUnmount () {
componentWillUnmount() {
ee.off('top:focus-search', this.focusSearchHandler)
ee.off('code:init', this.codeInitHandler)
}
handleSearchClearButton (e) {
const { router } = this.context
handleSearchClearButton(e) {
const { dispatch } = this.props
this.setState({
search: '',
isSearching: false
})
this.refs.search.childNodes[0].blur
router.push('/searched')
dispatch(push('/searched'))
e.preventDefault()
this.debouncedUpdateKeyword('')
}
handleKeyDown (e) {
// reset states
this.setState({
isAlphabet: false,
isIME: false
})
handleKeyDown(e) {
// Re-apply search field on ENTER key
if (e.keyCode === 13) {
this.debouncedUpdateKeyword(e.target.value)
}
// Clear search on ESC
if (e.keyCode === 27) {
@@ -84,59 +102,20 @@ 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) {
// 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
this.updateKeyword(keyword)
}
handleSearchChange(e) {
const keyword = e.target.value
this.debouncedUpdateKeyword(keyword)
}
handleSearchChange (e) {
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
const keyword = this.refs.searchInput.value
this.updateKeyword(keyword)
} else {
e.preventDefault()
}
}
updateKeyword (keyword) {
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
}
handleSearchFocus (e) {
handleSearchFocus(e) {
this.setState({
isSearching: true
})
}
handleSearchBlur (e) {
handleSearchBlur(e) {
e.stopPropagation()
let el = e.relatedTarget
@@ -155,7 +134,7 @@ class TopBar extends React.Component {
}
}
handleOnSearchFocus () {
handleOnSearchFocus() {
const el = this.refs.search.childNodes[0]
if (this.state.isSearching) {
el.blur()
@@ -164,56 +143,63 @@ class TopBar extends React.Component {
}
}
handleCodeInit () {
ee.emit('top:search', this.refs.searchInput.value)
handleCodeInit() {
ee.emit('top:search', this.refs.searchInput.value || '')
}
render () {
render() {
const { config, style, location } = this.props
return (
<div className='TopBar'
<div
className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<div styleName='control-search'>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
<div
styleName='control-search-input'
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)}
{this.state.search !== '' && (
<button
styleName='control-search-input-clear'
onClick={this.handleSearchClearButton}
>
<i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
<span styleName='control-search-input-clear-tooltip'>
{i18n.__('Clear Search')}
</span>
</button>
}
)}
</div>
</div>
</div>
{location.pathname === '/trashed' ? ''
: <NewNoteButton
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'params',
'location'
])}
/>}
{location.pathname === '/trashed' ? (
''
) : (
<NewNoteButton
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'location',
'match'
])}
/>
)}
</div>
)
}

View File

@@ -96,15 +96,6 @@ modalBackColor = white
z-index modalZIndex + 1
body[data-theme="dark"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color $ui-dark-backgroundColor
.sortableItemHelper
color: $ui-dark-text-color
.CodeMirror
font-family inherit !important
line-height 1.4em
@@ -147,35 +138,25 @@ body[data-theme="dark"]
.sortableItemHelper
z-index modalZIndex + 5
body[data-theme="solarized-dark"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color $ui-solarized-dark-backgroundColor
.sortableItemHelper
color: $ui-solarized-dark-text-color
apply-theme(theme)
body[data-theme={theme}]
background-color get-theme-var(theme, 'backgroundColor')
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color get-theme-var(theme, 'backgroundColor')
.sortableItemHelper
color get-theme-var(theme, 'text-color')
body[data-theme="monokai"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color $ui-monokai-backgroundColor
.sortableItemHelper
color: $ui-monokai-text-color
for theme in 'dark' 'solarized-dark' 'dracula'
apply-theme(theme)
body[data-theme="dracula"]
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
.modalBack
background-color $ui-dracula-backgroundColor
.sortableItemHelper
color: $ui-dracula-text-color
for theme in $themes
apply-theme(theme)
body[data-theme="default"]
.SideNav ::-webkit-scrollbar-thumb
background-color rgba(255, 255, 255, 0.3)
@import '../styles/Detail/TagSelect.styl'
@import '../styles/Detail/TagSelect.styl'

View File

@@ -1,11 +1,14 @@
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 config from 'browser/main/lib/ConfigManager'
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'
@@ -15,11 +18,11 @@ const electron = require('electron')
const { remote, ipcRenderer } = electron
const { dialog } = remote
document.addEventListener('drop', function (e) {
document.addEventListener('drop', function(e) {
e.preventDefault()
e.stopPropagation()
})
document.addEventListener('dragover', function (e) {
document.addEventListener('dragover', function(e) {
e.preventDefault()
e.stopPropagation()
})
@@ -31,7 +34,7 @@ let isAltWithMouse = false
let isAltWithOtherKey = false
let isOtherKey = false
document.addEventListener('keydown', function (e) {
document.addEventListener('keydown', function(e) {
if (e.key === 'Alt') {
isAltPressing = true
if (isOtherKey) {
@@ -45,13 +48,13 @@ document.addEventListener('keydown', function (e) {
}
})
document.addEventListener('mousedown', function (e) {
document.addEventListener('mousedown', function(e) {
if (isAltPressing) {
isAltWithMouse = true
}
})
document.addEventListener('keyup', function (e) {
document.addEventListener('keyup', function(e) {
if (e.key === 'Alt') {
if (isAltWithMouse || isAltWithOtherKey) {
e.preventDefault()
@@ -63,27 +66,35 @@ document.addEventListener('keyup', function (e) {
}
})
document.addEventListener('click', function (e) {
document.addEventListener('click', function(e) {
const className = e.target.className
if (!className && typeof (className) !== 'string') return
if (!className && typeof className !== 'string') return
const isInfoButton = className.includes('infoButton')
const offsetParent = e.target.offsetParent
const isInfoPanel = offsetParent !== null
? offsetParent.className.includes('infoPanel')
: false
const isInfoPanel =
offsetParent !== null ? offsetParent.className.includes('infoPanel') : false
if (isInfoButton || isInfoPanel) return
const infoPanel = document.querySelector('.infoPanel')
if (infoPanel) infoPanel.style.display = 'none'
})
const el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
if (!config.get().ui.showScrollBar) {
document.styleSheets[54].insertRule('::-webkit-scrollbar {display: none}')
document.styleSheets[54].insertRule(
'::-webkit-scrollbar-corner {display: none}'
)
document.styleSheets[54].insertRule(
'::-webkit-scrollbar-thumb {display: none}'
)
}
function notify (...args) {
const el = document.getElementById('content')
function notify(...args) {
return new window.Notification(...args)
}
function updateApp () {
function updateApp() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
@@ -96,56 +107,56 @@ function updateApp () {
}
}
ReactDOM.render((
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>
</Provider>
), el, function () {
const loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
<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} />
ipcRenderer.on('update-ready', function () {
store.dispatch({
type: 'UPDATE_AVAILABLE'
})
notify('Update ready!', {
body: 'New Boostnote is ready to be installed.'
})
updateApp()
})
{/* 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')
loadingCover.parentNode.removeChild(loadingCover)
ipcRenderer.on('update-found', function () {
notify('Update found!', {
body: 'Preparing to update...'
ipcRenderer.on('update-ready', function() {
store.dispatch({
type: 'UPDATE_AVAILABLE'
})
notify('Update ready!', {
body: 'New Boostnote is ready to be installed.'
})
updateApp()
})
})
ipcRenderer.send('update-check', 'check-update')
window.addEventListener('online', function () {
if (!store.getState().status.updateReady) {
ipcRenderer.send('update-check', 'check-update')
}
})
})
ipcRenderer.on('update-found', function() {
notify('Update found!', {
body: 'Preparing to update...'
})
})
ipcRenderer.send('update-check', 'check-update')
window.addEventListener('online', function() {
if (!store.getState().status.updateReady) {
ipcRenderer.send('update-check', 'check-update')
}
})
}
)

View File

@@ -22,7 +22,7 @@ if (!getSendEventCond()) {
})
}
function convertPlatformName (platformName) {
function convertPlatformName(platformName) {
if (platformName === 'darwin') {
return 'MacOS'
} else if (platformName === 'win32') {
@@ -34,16 +34,16 @@ function convertPlatformName (platformName) {
}
}
function getSendEventCond () {
function getSendEventCond() {
const isDev = process.env.NODE_ENV !== 'production'
const isDisable = !ConfigManager.default.get().amaEnabled
const isOffline = !window.navigator.onLine
return isDev || isDisable || isOffline
}
function initAwsMobileAnalytics () {
function initAwsMobileAnalytics() {
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
AWS.config.credentials.get(err => {
if (!err) {
recordDynamicCustomEvent('APP_STARTED')
recordStaticCustomEvent()
@@ -51,7 +51,7 @@ function initAwsMobileAnalytics () {
})
}
function recordDynamicCustomEvent (type, options = {}) {
function recordDynamicCustomEvent(type, options = {}) {
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent(type, options)
@@ -62,7 +62,7 @@ function recordDynamicCustomEvent (type, options = {}) {
}
}
function recordStaticCustomEvent () {
function recordStaticCustomEvent() {
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {

View File

@@ -1,25 +1,24 @@
let callees = []
function bind (name, el) {
function bind(name, el) {
callees.push({
name: name,
element: el
})
}
function release (el) {
callees = callees.filter((callee) => callee.element !== el)
function release(el) {
callees = callees.filter(callee => callee.element !== el)
}
function fire (command) {
function fire(command) {
console.info('COMMAND >>', command)
const splitted = command.split(':')
const target = splitted[0]
const targetCommand = splitted[1]
const targetCallees = callees
.filter((callee) => callee.name === target)
const targetCallees = callees.filter(callee => callee.name === target)
targetCallees.forEach((callee) => {
targetCallees.forEach(callee => {
callee.element.fire(targetCommand)
})
}

View File

@@ -8,9 +8,30 @@ const win = global.process.platform === 'win32'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
const electronConfig = new (require('electron-config'))()
let isInitialized = false
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
"default": true
}`
const DEFAULT_CSS_CONFIG = `
/* Drop Your Custom CSS Code Here */
[data-theme="default"] p code,
[data-theme="default"] li code,
[data-theme="default"] td code
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
background-color: #F4F4F4;
border-color: #d9d9d9;
color: #03C588;
}
`
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
@@ -21,31 +42,50 @@ export const DEFAULT_CONFIG = {
},
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
listDirection: 'ASCENDING', // 'ASCENDING', 'DESCENDING'
amaEnabled: true,
autoUpdateEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Alt + Right',
deleteNote: OSX
? 'Command + Shift + Backspace'
: 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
toggleMenuBar: 'Alt'
},
ui: {
language: 'en',
theme: 'default',
defaultTheme: 'default',
enableScheduleTheme: false,
scheduledTheme: 'monokai',
scheduleStart: 1200,
scheduleEnd: 360,
showCopyNotification: true,
disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
showScrollBar: true,
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
showMenuBar: false,
isStacking: false
},
editor: {
theme: 'base16-light',
keyMap: 'sublime',
fontSize: '14',
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
fontFamily: win ? 'Consolas' : 'Monaco',
indentType: 'space',
indentSize: '2',
lineWrapping: true,
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``',
matchingPairs: '()[]{}\'\'""$$**``~~__',
matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
@@ -57,7 +97,17 @@ export const DEFAULT_CONFIG = {
enableFrontMatterTitle: true,
frontMatterTitleField: 'title',
spellcheck: false,
enableSmartPaste: false
enableSmartPaste: false,
enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
prettierConfig: `{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}`,
deleteUnusedAttachments: true,
rtlEnabled: false
},
preview: {
fontSize: '14',
@@ -75,8 +125,9 @@ export const DEFAULT_CONFIG = {
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
customCSS: DEFAULT_CSS_CONFIG,
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
mermaidHTMLLabel: false,
lineThroughCheckbox: true
},
blog: {
@@ -91,10 +142,11 @@ export const DEFAULT_CONFIG = {
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
variable: 'boostnote',
prefixAttachmentFolder: false
}
},
coloredTags: {}
}
function validate (config) {
function validate(config) {
if (!_.isObject(config)) return false
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
if (!_.isBoolean(config.isSideNavFolded)) return false
@@ -103,14 +155,17 @@ function validate (config) {
return true
}
function _save (config) {
console.log(config)
function _save(config) {
window.localStorage.setItem('config', JSON.stringify(config))
}
function get () {
function get() {
const rawStoredConfig = window.localStorage.getItem('config')
const storedConfig = Object.assign({}, DEFAULT_CONFIG, JSON.parse(rawStoredConfig))
const storedConfig = Object.assign(
{},
DEFAULT_CONFIG,
JSON.parse(rawStoredConfig)
)
let config = storedConfig
try {
@@ -124,6 +179,11 @@ function get () {
_save(config)
}
config.autoUpdateEnabled = electronConfig.get(
'autoUpdateEnabled',
config.autoUpdateEnabled
)
if (!isInitialized) {
isInitialized = true
let editorTheme = document.getElementById('editorTheme')
@@ -134,42 +194,37 @@ 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'
}
}
return config
}
function set (updates) {
function set(updates) {
const currentConfig = get()
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
const arrangedUpdates = updates
if (updates.preview !== undefined && updates.preview.customCSS === '') {
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
}
const newConfig = Object.assign(
{},
DEFAULT_CONFIG,
currentConfig,
arrangedUpdates
)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
if (newConfig.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else if (newConfig.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else if (newConfig.ui.theme === 'solarized-dark') {
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')
}
i18n.setLocale(newConfig.ui.language)
let editorTheme = document.getElementById('editorTheme')
@@ -179,41 +234,65 @@ function set (updates) {
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
? newConfig.editor.theme
: 'default'
if (newTheme !== 'default') {
if (newTheme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
}
const newTheme = consts.THEMES.find(
theme => theme.name === newConfig.editor.theme
)
if (newTheme) {
editorTheme.setAttribute('href', newTheme.path)
}
electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled)
ipcRenderer.send('config-renew', {
config: get()
})
ee.emit('config-renew')
}
function assignConfigValues (originalConfig, rcConfig) {
function assignConfigValues(originalConfig, rcConfig) {
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
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)
config.hotkey = Object.assign(
{},
DEFAULT_CONFIG.hotkey,
originalConfig.hotkey,
rcConfig.hotkey
)
config.blog = Object.assign(
{},
DEFAULT_CONFIG.blog,
originalConfig.blog,
rcConfig.blog
)
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) {
function rewriteHotkey(config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
})
return config

View File

@@ -0,0 +1,65 @@
import ConfigManager from 'browser/main/lib/ConfigManager'
const saveChanges = newConfig => {
ConfigManager.set(newConfig)
}
const chooseTheme = config => {
const { ui } = config
if (!ui.enableScheduleTheme) {
return
}
const start = parseInt(ui.scheduleStart)
const end = parseInt(ui.scheduleEnd)
const now = new Date()
const minutes = now.getHours() * 60 + now.getMinutes()
const isEndAfterStart = end > start
const isBetweenStartAndEnd = minutes >= start && minutes < end
const isBetweenEndAndStart = minutes >= start || minutes < end
if (
(isEndAfterStart && isBetweenStartAndEnd) ||
(!isEndAfterStart && isBetweenEndAndStart)
) {
if (ui.theme !== ui.scheduledTheme) {
ui.defaultTheme = ui.theme
ui.theme = ui.scheduledTheme
applyTheme(ui.theme)
saveChanges(config)
}
} else {
if (ui.theme !== ui.defaultTheme) {
ui.theme = ui.defaultTheme
applyTheme(ui.theme)
saveChanges(config)
}
}
}
const applyTheme = theme => {
const supportedThemes = [
'dark',
'white',
'solarized-dark',
'monokai',
'dracula'
]
if (supportedThemes.indexOf(theme) !== -1) {
document.body.setAttribute('data-theme', theme)
if (document.body.querySelector('.MarkdownPreview')) {
document.body
.querySelector('.MarkdownPreview')
.contentDocument.body.setAttribute('data-theme', theme)
}
} else {
document.body.setAttribute('data-theme', 'default')
}
}
module.exports = {
chooseTheme,
applyTheme
}

View File

@@ -5,20 +5,20 @@ const { remote } = electron
_init()
function _init () {
function _init() {
setZoom(getZoom(), true)
}
function _saveZoom (zoomFactor) {
ConfigManager.set({zoom: zoomFactor})
function _saveZoom(zoomFactor) {
ConfigManager.set({ zoom: zoomFactor })
}
function setZoom (zoomFactor, noSave = false) {
function setZoom(zoomFactor, noSave = false) {
if (!noSave) _saveZoom(zoomFactor)
remote.getCurrentWebContents().setZoomFactor(zoomFactor)
}
function getZoom () {
function getZoom() {
const config = ConfigManager.get()
return config.zoom

View File

@@ -16,7 +16,7 @@ const CSON = require('@rokt33r/season')
* 3. fetch notes & folders
* 4. return `{storage: {...} folders: [folder]}`
*/
function addStorage (input) {
function addStorage(input) {
if (!_.isString(input.path)) {
return Promise.reject(new Error('Path must be a string.'))
}
@@ -29,7 +29,7 @@ function addStorage (input) {
rawStorages = []
}
let key = keygen()
while (rawStorages.some((storage) => storage.key === key)) {
while (rawStorages.some(storage => storage.key === key)) {
key = keygen()
}
@@ -43,7 +43,7 @@ function addStorage (input) {
return Promise.resolve(newStorage)
.then(resolveStorageData)
.then(function saveMetadataToLocalStorage (resolvedStorage) {
.then(function saveMetadataToLocalStorage(resolvedStorage) {
newStorage = resolvedStorage
rawStorages.push({
key: newStorage.key,
@@ -56,27 +56,29 @@ function addStorage (input) {
localStorage.setItem('storages', JSON.stringify(rawStorages))
return newStorage
})
.then(function (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (!storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
.then(function(storage) {
return resolveStorageNotes(storage).then(notes => {
let unknownCount = 0
notes.forEach(note => {
if (!storage.folders.some(folder => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
return notes
})
if (unknownCount > 0) {
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
}
return notes
})
})
.then(function returnValue (notes) {
.then(function returnValue(notes) {
return {
storage: newStorage,
notes

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ import path from 'path'
* @param {String} dstPath
* @return {Promise} an image path
*/
function copyFile (srcPath, dstPath) {
function copyFile(srcPath, dstPath) {
if (!path.extname(dstPath)) {
dstPath = path.join(dstPath, path.basename(srcPath))
}

View File

@@ -22,7 +22,7 @@ const { findStorage } = require('browser/lib/findStorage')
* }
* ```
*/
function createFolder (storageKey, input) {
function createFolder(storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -34,26 +34,28 @@ function createFolder (storageKey, input) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function createFolder (storage) {
let key = keygen()
while (storage.folders.some((folder) => folder.key === key)) {
key = keygen()
}
const newFolder = {
key,
color: input.color,
name: input.name
}
return resolveStorageData(targetStorage).then(function createFolder(storage) {
let key = keygen()
while (storage.folders.some(folder => folder.key === key)) {
key = keygen()
}
const newFolder = {
key,
color: input.color,
name: input.name
}
storage.folders.push(newFolder)
storage.folders.push(newFolder)
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
return {
storage
}
})
return {
storage
}
})
}
module.exports = createFolder

View File

@@ -6,9 +6,11 @@ const path = require('path')
const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
function validateInput (input) {
function validateInput(input) {
if (!_.isArray(input.tags)) input.tags = []
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
input.tags = input.tags.filter(
tag => _.isString(tag) && tag.trim().length > 0
)
if (!_.isString(input.title)) input.title = ''
input.isStarred = !!input.isStarred
input.isTrashed = !!input.isTrashed
@@ -21,20 +23,24 @@ function validateInput (input) {
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
if (!_.isArray(input.snippets)) {
input.snippets = [{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}]
input.snippets = [
{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}
]
}
break
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
throw new Error(
'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.'
)
}
}
function createNote (storageKey, input) {
function createNote(storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -47,13 +53,13 @@ function createNote (storageKey, input) {
}
return resolveStorageData(targetStorage)
.then(function checkFolderExists (storage) {
if (_.find(storage.folders, {key: input.folder}) == null) {
throw new Error('Target folder doesn\'t exist.')
.then(function checkFolderExists(storage) {
if (_.find(storage.folders, { key: input.folder }) == null) {
throw new Error("Target folder doesn't exist.")
}
return storage
})
.then(function saveNote (storage) {
.then(function saveNote(storage) {
let key = keygen(true)
let isUnique = false
while (!isUnique) {
@@ -68,7 +74,8 @@ function createNote (storageKey, input) {
}
}
}
const noteData = Object.assign({},
const noteData = Object.assign(
{},
{
createdAt: new Date(),
updatedAt: new Date()
@@ -77,9 +84,13 @@ function createNote (storageKey, input) {
{
key,
storage: storageKey
})
}
)
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
CSON.writeFileSync(
path.join(storage.path, 'notes', key + '.cson'),
_.omit(noteData, ['key', 'storage'])
)
return noteData
})

View File

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

View File

@@ -3,7 +3,7 @@ import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function createSnippet (snippetFile) {
function createSnippet(snippetFile) {
return new Promise((resolve, reject) => {
const newSnippet = {
id: crypto.randomBytes(16).toString('hex'),
@@ -12,15 +12,21 @@ function createSnippet (snippetFile) {
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)
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)
})
}).catch((err) => {
reject(err)
})
})
}

View File

@@ -3,7 +3,6 @@ const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const deleteSingleNote = require('./deleteNote')
@@ -19,7 +18,7 @@ const deleteSingleNote = require('./deleteNote')
* }
* ```
*/
function deleteFolder (storageKey, folderKey) {
function deleteFolder(storageKey, folderKey) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -28,35 +27,36 @@ function deleteFolder (storageKey, folderKey) {
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
.then(function assignNotes(storage) {
return resolveStorageNotes(storage).then(notes => {
return {
storage,
notes
}
})
})
.then(function deleteFolderAndNotes (data) {
.then(function deleteFolderAndNotes(data) {
const { storage, notes } = data
storage.folders = storage.folders
.filter(function excludeTargetFolder (folder) {
return folder.key !== folderKey
})
storage.folders = storage.folders.filter(function excludeTargetFolder(
folder
) {
return folder.key !== folderKey
})
const targetNotes = notes.filter(function filterTargetNotes (note) {
const targetNotes = notes.filter(function filterTargetNotes(note) {
return note.folder === folderKey
})
const deleteAllNotes = targetNotes
.map(function deleteNote (note) {
return deleteSingleNote(storageKey, note.key)
})
return Promise.all(deleteAllNotes)
.then(() => storage)
const deleteAllNotes = targetNotes.map(function deleteNote(note) {
return deleteSingleNote(storageKey, note.key)
})
return Promise.all(deleteAllNotes).then(() => storage)
})
.then(function (storage) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
.then(function(storage) {
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
return {
storage,

View File

@@ -4,7 +4,7 @@ const sander = require('sander')
const attachmentManagement = require('./attachmentManagement')
const { findStorage } = require('browser/lib/findStorage')
function deleteNote (storageKey, noteKey) {
function deleteNote(storageKey, noteKey) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -13,7 +13,7 @@ function deleteNote (storageKey, noteKey) {
}
return resolveStorageData(targetStorage)
.then(function deleteNoteFile (storage) {
.then(function deleteNoteFile(storage) {
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
@@ -26,8 +26,11 @@ function deleteNote (storageKey, noteKey) {
storageKey
}
})
.then(function deleteAttachments (storageInfo) {
attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey)
.then(function deleteAttachments(storageInfo) {
attachmentManagement.deleteAttachmentFolder(
storageInfo.storageKey,
storageInfo.noteKey
)
return storageInfo
})
}

View File

@@ -2,14 +2,20 @@ import fs from 'fs'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function deleteSnippet (snippet, snippetFile) {
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)
})
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)
}
)
})
})
}

View File

@@ -4,8 +4,7 @@ import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
import path from 'path'
import exportNote from './exportNote'
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
import getContentFormatter from './getContentFormatter'
/**
* @param {String} storageKey
@@ -25,7 +24,7 @@ import formatHTML from './formatHTML'
* ```
*/
function exportFolder (storageKey, folderKey, fileType, exportDir, config) {
function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -37,48 +36,32 @@ function exportFolder (storageKey, folderKey, fileType, exportDir, config) {
.then(storage => {
return resolveStorageNotes(storage).then(notes => ({
storage,
notes: notes.filter(note => note.folder === folderKey && !note.isTrashed && note.type === 'MARKDOWN_NOTE')
notes: notes.filter(
note =>
note.folder === folderKey &&
!note.isTrashed &&
note.type === 'MARKDOWN_NOTE'
)
}))
})
.then(({ storage, notes }) => {
let contentFormatter = null
if (fileType === 'md') {
contentFormatter = formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
contentFormatter = formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export
})
}
const contentFormatter = getContentFormatter(storage, fileType, config)
return Promise
.all(notes.map(note => {
const targetPath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return Promise.all(
notes.map(note => {
const targetPath = path.join(
exportDir,
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
)
return exportNote(storage.key, note, targetPath, contentFormatter)
}))
.then(() => ({
storage,
folderKey,
fileType,
exportDir
}))
})
).then(() => ({
storage,
folderKey,
fileType,
exportDir
}))
})
}

View File

@@ -4,8 +4,6 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs')
const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/**
* Export note together with attachments
*
@@ -18,31 +16,38 @@ const attachmentManagement = require('./attachmentManagement')
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, note, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
function exportNote(storageKey, note, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey)
? storageKey
: findStorage(storageKey).path
const exportTasks = []
if (!storagePath) {
throw new Error('Storage path is not found')
}
const exportedData = outputFormatter ? outputFormatter(note, targetPath, exportTasks) : note.content
const exportedData = Promise.resolve(
outputFormatter
? outputFormatter(note, targetPath, exportTasks)
: note.content
)
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise
.all(tasks.map(task => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
})
.catch(error => {
rollbackExport(tasks)
throw error
})
return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
.then(() => exportedData)
.then(data => {
return saveToFile(data, targetPath)
})
.catch(error => {
rollbackExport(tasks)
throw error
})
}
function prepareTasks (tasks, storagePath, targetPath) {
return tasks.map((task) => {
function prepareTasks(tasks, storagePath, targetPath) {
return tasks.map(task => {
if (!path.isAbsolute(task.src)) {
task.src = path.join(storagePath, task.src)
}
@@ -55,14 +60,12 @@ function prepareTasks (tasks, storagePath, targetPath) {
})
}
function saveToFile (data, filename) {
function saveToFile(data, filename) {
return new Promise((resolve, reject) => {
fs.writeFile(filename, data, error => {
if (error) {
reject(error)
} else {
resolve(filename)
}
fs.writeFile(filename, data, err => {
if (err) return reject(err)
resolve(filename)
})
})
}
@@ -71,9 +74,9 @@ function saveToFile (data, filename) {
* Remove exported files
* @param tasks Array of copy task objects. Object consists of two mandatory fields `src` and `dst`
*/
function rollbackExport (tasks) {
function rollbackExport(tasks) {
const folders = new Set()
tasks.forEach((task) => {
tasks.forEach(task => {
let fullpath = task.dst
if (!path.extname(task.dst)) {
@@ -86,7 +89,7 @@ function rollbackExport (tasks) {
}
})
folders.forEach((folder) => {
folders.forEach(folder => {
if (fs.readdirSync(folder).length === 0) {
fs.rmdirSync(folder)
}

View File

@@ -1,7 +1,6 @@
import { findStorage } from 'browser/lib/findStorage'
import exportNote from './exportNote'
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
import getContentFormatter from './getContentFormatter'
/**
* @param {Object} note
@@ -10,34 +9,9 @@ import formatHTML from './formatHTML'
* @param {Object} config
*/
function exportNoteAs (note, filename, fileType, config) {
function exportNoteAs(note, filename, fileType, config) {
const storage = findStorage(note.storage)
let contentFormatter = null
if (fileType === 'md') {
contentFormatter = formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
contentFormatter = formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export
})
}
const contentFormatter = getContentFormatter(storage, fileType, config)
return exportNote(storage.key, note, filename, contentFormatter)
}

View File

@@ -24,7 +24,7 @@ import formatHTML from './formatHTML'
* ```
*/
function exportStorage (storageKey, fileType, exportDir, config) {
function exportStorage(storageKey, fileType, exportDir, config) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -36,7 +36,9 @@ function exportStorage (storageKey, fileType, exportDir, config) {
.then(storage => {
return resolveStorageNotes(storage).then(notes => ({
storage,
notes: notes.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
notes: notes.filter(
note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
)
}))
})
.then(({ storage, notes }) => {
@@ -68,7 +70,10 @@ function exportStorage (storageKey, fileType, exportDir, config) {
const folderNamesMapping = {}
storage.folders.forEach(folder => {
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
const folderExportedDir = path.join(
exportDir,
filenamify(folder.name, { replacement: '_' })
)
folderNamesMapping[folder.key] = folderExportedDir
@@ -78,17 +83,20 @@ function exportStorage (storageKey, fileType, exportDir, config) {
} catch (e) {}
})
return Promise
.all(notes.map(note => {
const targetPath = path.join(folderNamesMapping[note.folder], `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return Promise.all(
notes.map(note => {
const targetPath = path.join(
folderNamesMapping[note.folder],
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
)
return exportNote(storage.key, note, targetPath, contentFormatter)
}))
.then(() => ({
storage,
fileType,
exportDir
}))
})
).then(() => ({
storage,
fileType,
exportDir
}))
})
}

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
function fetchSnippet (id, snippetFile) {
function fetchSnippet(id, snippetFile) {
return new Promise((resolve, reject) => {
fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => {
if (err) {
@@ -9,7 +9,9 @@ function fetchSnippet (id, snippetFile) {
}
const snippets = JSON.parse(data)
if (id) {
const snippet = snippets.find(snippet => { return snippet.id === id })
const snippet = snippets.find(snippet => {
return snippet.id === id
})
resolve(snippet)
}
resolve(snippets)

View File

@@ -5,9 +5,12 @@ import { remote } from 'electron'
import consts from 'browser/lib/consts'
import Markdown from 'browser/lib/markdown'
import attachmentManagement from './attachmentManagement'
import { version as codemirrorVersion } from 'codemirror/package.json'
const { app } = remote
const appPath = fileUrl(process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve())
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
let markdownStyle = ''
try {
@@ -16,11 +19,14 @@ try {
export const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
]
const macos = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (global.process.platform !== 'darwin') {
if (!macos) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
@@ -34,7 +40,7 @@ const defaultCodeBlockFontFamily = [
'monospace'
]
function unprefix (file) {
function unprefix(file) {
if (global.process.platform === 'win32') {
return file.replace('file:///', '')
} else {
@@ -63,7 +69,7 @@ function unprefix (file) {
* }
* ```
*/
export default function formatHTML (props) {
export default function formatHTML(props) {
const {
fontFamily,
fontSize,
@@ -96,14 +102,16 @@ export default function formatHTML (props) {
const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
return function (note, targetPath, exportTasks) {
let styles = files.map(file => `<link rel="stylesheet" href="css/${path.basename(file)}">`).join('\n')
return function(note, targetPath, exportTasks) {
const styles = files
.map(file => `<link rel="stylesheet" href="css/${path.basename(file)}">`)
.join('\n')
let inlineScripts = ''
let scripts = ''
let decodeEntities = false
function addDecodeEntities () {
function addDecodeEntities() {
if (decodeEntities) {
return
}
@@ -130,7 +138,7 @@ function decodeEntities (text) {
}
let lodash = false
function addLodash () {
function addLodash() {
if (lodash) {
return
}
@@ -146,7 +154,7 @@ function decodeEntities (text) {
}
let raphael = false
function addRaphael () {
function addRaphael() {
if (raphael) {
return
}
@@ -162,7 +170,7 @@ function decodeEntities (text) {
}
let yaml = false
function addYAML () {
function addYAML() {
if (yaml) {
return
}
@@ -178,7 +186,7 @@ function decodeEntities (text) {
}
let chart = false
function addChart () {
function addChart() {
if (chart) {
return
}
@@ -195,7 +203,7 @@ function decodeEntities (text) {
scripts += `<script src="js/Chart.min.js"></script>`
inlineScripts += `
function displayCharts () {
function displayCharts() {
_.forEach(
document.querySelectorAll('.chart'),
el => {
@@ -227,7 +235,7 @@ document.addEventListener('DOMContentLoaded', displayCharts);
}
let codemirror = false
function addCodeMirror () {
function addCodeMirror() {
if (codemirror) {
return
}
@@ -237,19 +245,28 @@ document.addEventListener('DOMContentLoaded', displayCharts);
addDecodeEntities()
addLodash()
exportTasks.push({
src: unprefix(`${appPath}/node_modules/codemirror/lib/codemirror.js`),
dst: 'js/codemirror'
}, {
src: unprefix(`${appPath}/node_modules/codemirror/mode/meta.js`),
dst: 'js/codemirror/mode'
}, {
src: unprefix(`${appPath}/node_modules/codemirror/addon/mode/loadmode.js`),
dst: 'js/codemirror/addon/mode'
}, {
src: unprefix(`${appPath}/node_modules/codemirror/addon/runmode/runmode.js`),
dst: 'js/codemirror/addon/runmode'
})
exportTasks.push(
{
src: unprefix(`${appPath}/node_modules/codemirror/lib/codemirror.js`),
dst: 'js/codemirror'
},
{
src: unprefix(`${appPath}/node_modules/codemirror/mode/meta.js`),
dst: 'js/codemirror/mode'
},
{
src: unprefix(
`${appPath}/node_modules/codemirror/addon/mode/loadmode.js`
),
dst: 'js/codemirror/addon/mode'
},
{
src: unprefix(
`${appPath}/node_modules/codemirror/addon/runmode/runmode.js`
),
dst: 'js/codemirror/addon/runmode'
}
)
scripts += `
<script src="js/codemirror/codemirror.js"></script>
@@ -265,18 +282,18 @@ document.addEventListener('DOMContentLoaded', displayCharts);
}
inlineScripts += `
CodeMirror.modeURL = 'js/codemirror/mode/%N/%N.js';
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@${codemirrorVersion}/mode/%N/%N.js';
function displayCodeBlocks () {
function displayCodeBlocks() {
_.forEach(
document.querySelectorAll('.code code'),
el => {
el.parentNode.className += ' ${className}'
let syntax = CodeMirror.findModeByName(el.className)
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
CodeMirror.requireMode(syntax.mode, () => {
const content = decodeEntities(el.innerHTML)
el.innerHTML = ''
el.parentNode.className += ' ${className}'
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: ${indentSize}
})
@@ -290,7 +307,7 @@ document.addEventListener('DOMContentLoaded', displayCodeBlocks);
}
let flowchart = false
function addFlowchart () {
function addFlowchart() {
if (flowchart) {
return
}
@@ -302,14 +319,16 @@ document.addEventListener('DOMContentLoaded', displayCodeBlocks);
addRaphael()
exportTasks.push({
src: unprefix(`${appPath}/node_modules/flowchart.js/release/flowchart.min.js`),
src: unprefix(
`${appPath}/node_modules/flowchart.js/release/flowchart.min.js`
),
dst: 'js'
})
scripts += `<script src="js/flowchart.min.js"></script>`
inlineScripts += `
function displayFlowcharts () {
function displayFlowcharts() {
_.forEach(
document.querySelectorAll('.flowchart'),
el => {
@@ -332,7 +351,7 @@ document.addEventListener('DOMContentLoaded', displayFlowcharts);
}
let mermaid = false
function addMermaid () {
function addMermaid() {
if (mermaid) {
return
}
@@ -348,10 +367,8 @@ document.addEventListener('DOMContentLoaded', displayFlowcharts);
scripts += `<script src="js/mermaid.min.js"></script>`
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
inlineScripts += `
function displayMermaids () {
function displayMermaids() {
_.forEach(
document.querySelectorAll('.mermaid'),
el => {
@@ -368,7 +385,7 @@ document.addEventListener('DOMContentLoaded', displayMermaids);
}
let sequence = false
function addSequence () {
function addSequence() {
if (sequence) {
return
}
@@ -380,14 +397,16 @@ document.addEventListener('DOMContentLoaded', displayMermaids);
addRaphael()
exportTasks.push({
src: unprefix(`${appPath}/node_modules/js-sequence-diagrams/fucknpm/sequence-diagram-min.js`),
src: unprefix(
`${appPath}/node_modules/@rokt33r/js-sequence-diagrams/dist/sequence-diagram-min.js`
),
dst: 'js'
})
scripts += `<script src="js/sequence-diagram-min.js"></script>`
inlineScripts += `
function displaySequences () {
function displaySequences() {
_.forEach(
document.querySelectorAll('.sequence'),
el => {
@@ -414,7 +433,7 @@ document.addEventListener('DOMContentLoaded', displaySequences);
typographer: smartQuotes,
sanitize,
breaks,
onFence (type, mode) {
onFence(type, mode) {
if (type === 'chart') {
addChart()
@@ -425,7 +444,9 @@ document.addEventListener('DOMContentLoaded', displaySequences);
addCodeMirror()
if (mode && modes[mode] !== true) {
const file = unprefix(`${appPath}/node_modules/codemirror/mode/${mode}/${mode}.js`)
const file = unprefix(
`${appPath}/node_modules/codemirror/mode/${mode}/${mode}.js`
)
if (fs.existsSync(file)) {
exportTasks.push({
@@ -448,7 +469,10 @@ document.addEventListener('DOMContentLoaded', displaySequences);
let body = markdown.render(note.content)
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(note.content, props.storagePath)
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
note.content,
props.storagePath
)
files.forEach(file => {
exportTasks.push({
@@ -457,7 +481,11 @@ document.addEventListener('DOMContentLoaded', displaySequences);
})
})
const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
const destinationFolder = props.export.prefixAttachmentFolder
? `${path.parse(targetPath).name} - ${
attachmentManagement.DESTINATION_FOLDER
}`
: attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
@@ -466,7 +494,11 @@ document.addEventListener('DOMContentLoaded', displaySequences);
})
})
body = attachmentManagement.replaceStorageReferences(body, note.key, destinationFolder)
body = attachmentManagement.replaceStorageReferences(
body,
note.key,
destinationFolder
)
return `
<html>
@@ -478,7 +510,7 @@ document.addEventListener('DOMContentLoaded', displaySequences);
${scripts}
<script>${inlineScripts}</script>
</head>
<body>
<body data-theme="${theme}">
${body}
</body>
</html>
@@ -486,7 +518,7 @@ ${body}
}
}
export function getStyleParams (props) {
export function getStyleParams(props) {
const {
fontSize,
lineNumber,
@@ -494,25 +526,27 @@ export function getStyleParams (props) {
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
customCSS,
RTL
} = props
let { fontFamily, codeBlockFontFamily } = props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
codeBlockFontFamily =
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
@@ -523,21 +557,20 @@ export function getStyleParams (props) {
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
customCSS,
RTL
}
}
export function getCodeThemeLink (theme) {
if (consts.THEMES.some(_theme => _theme === theme)) {
theme = theme !== 'default' ? theme : 'elegant'
}
export function getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
return theme != null
? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css`
}
export function buildStyle (
export function buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
@@ -545,7 +578,8 @@ export function buildStyle (
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
customCSS,
RTL
) {
return `
@font-face {
@@ -581,17 +615,96 @@ ${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
${scrollPastEnd && 'padding-bottom: 90vh;box-sizing: border-box;'}
${RTL && 'direction: rtl;text-align: right;'}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
text-align: left;
direction: ltr;
}
p code,
li code,
td code
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
[data-theme="default"] p code,
[data-theme="default"] li code,
[data-theme="default"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="white"] p code,
[data-theme="white"] li code,
[data-theme="white"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="dark"] p code,
[data-theme="dark"] li code,
[data-theme="dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="dracula"] p code,
[data-theme="dracula"] li code,
[data-theme="dracula"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="monokai"] p code,
[data-theme="monokai"] li code,
[data-theme="monokai"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="nord"] p code,
[data-theme="nord"] li code,
[data-theme="nord"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="solarized-dark"] p code,
[data-theme="solarized-dark"] li code,
[data-theme="solarized-dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="vulcan"] p code,
[data-theme="vulcan"] li code,
[data-theme="vulcan"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';

View File

@@ -12,14 +12,21 @@ const delimiterRegExp = /^\-{3}/
* }
* ```
*/
export default function formatMarkdown (props) {
return function (note, targetPath, exportTasks) {
export default function formatMarkdown(props) {
return function(note, targetPath, exportTasks) {
let result = note.content
if (props.storagePath && note.key) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(result, props.storagePath)
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
result,
props.storagePath
)
const destinationFolder = props.export.prefixAttachmentFolder ? `${path.parse(targetPath).name} - ${attachmentManagement.DESTINATION_FOLDER}` : attachmentManagement.DESTINATION_FOLDER
const destinationFolder = props.export.prefixAttachmentFolder
? `${path.parse(targetPath).name} - ${
attachmentManagement.DESTINATION_FOLDER
}`
: attachmentManagement.DESTINATION_FOLDER
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
@@ -28,7 +35,11 @@ export default function formatMarkdown (props) {
})
})
result = attachmentManagement.replaceStorageReferences(result, note.key, destinationFolder)
result = attachmentManagement.replaceStorageReferences(
result,
note.key,
destinationFolder
)
}
if (props.export.metadata === 'MERGE_HEADER') {
@@ -65,13 +76,12 @@ export default function formatMarkdown (props) {
}
}
function getFrontMatter (markdown) {
function getFrontMatter(markdown) {
const lines = markdown.split('\n')
if (delimiterRegExp.test(lines[0])) {
let line = 0
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {
}
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
return yaml.load(lines.slice(1, line).join('\n')) || {}
} else {
@@ -79,13 +89,12 @@ function getFrontMatter (markdown) {
}
}
function replaceFrontMatter (markdown, metadata) {
function replaceFrontMatter(markdown, metadata) {
const lines = markdown.split('\n')
if (delimiterRegExp.test(lines[0])) {
let line = 0
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {
}
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
} else {

View File

@@ -0,0 +1,26 @@
import formatHTML from './formatHTML'
import { remote } from 'electron'
export default function formatPDF(props) {
return function(note, targetPath, exportTasks) {
const printout = new remote.BrowserWindow({
show: false,
webPreferences: { webSecurity: false, javascript: false }
})
printout.loadURL(
'data:text/html;charset=UTF-8,' +
formatHTML(props)(note, targetPath, exportTasks)
)
return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
if (err) reject(err)
else resolve(data)
printout.destroy()
})
})
})
}
}

View File

@@ -0,0 +1,58 @@
import formatMarkdown from './formatMarkdown'
import formatHTML from './formatHTML'
import formatPDF from './formatPDF'
/**
* @param {Object} storage
* @param {String} fileType
* @param {Object} config
*/
export default function getContentFormatterr(storage, fileType, config) {
if (fileType === 'md') {
return formatMarkdown({
storagePath: storage.path,
export: config.export
})
} else if (fileType === 'html') {
return formatHTML({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export,
RTL: config.editor.rtlEnabled /* && this.state.RTL */
})
} else if (fileType === 'pdf') {
return formatPDF({
theme: config.ui.theme,
fontSize: config.preview.fontSize,
fontFamily: config.preview.fontFamily,
codeBlockTheme: config.preview.codeBlockTheme,
codeBlockFontFamily: config.editor.fontFamily,
lineNumber: config.preview.lineNumber,
indentSize: config.editor.indentSize,
scrollPastEnd: config.preview.scrollPastEnd,
smartQuotes: config.preview.smartQuotes,
breaks: config.preview.breaks,
sanitize: config.preview.sanitize,
customCSS: config.preview.customCSS,
allowCustomCSS: config.preview.allowCustomCSS,
storagePath: storage.path,
export: config.export,
RTL: config.editor.rtlEnabled /* && this.state.RTL */
})
}
return null
}

View File

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

View File

@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const consts = require('browser/lib/consts')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
/**
* @return {Object} all storages and notes
@@ -19,50 +20,64 @@ const CSON = require('@rokt33r/season')
* 2. legacy
* 3. empty directory
*/
function init () {
const fetchStorages = function () {
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)
rawStorages = []
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
}
return Promise.all(rawStorages
.map(resolveStorageData))
return Promise.all(rawStorages.map(resolveStorageData))
}
const fetchNotes = function (storages) {
const fetchNotes = function(storages) {
const findNotesFromEachStorage = storages
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (note && !storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
.filter(storage => fs.existsSync(storage.path))
.map(storage => {
return resolveStorageNotes(storage).then(notes => {
let unknownCount = 0
notes.forEach(note => {
if (
note &&
!storage.folders.some(folder => note.folder === folder.key)
) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
return notes
})
if (unknownCount > 0) {
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
})
})
return Promise.all(findNotesFromEachStorage)
.then(function concatNoteGroup (noteGroups) {
return noteGroups.reduce(function (sum, group) {
.then(function concatNoteGroup(noteGroups) {
return noteGroups.reduce(function(sum, group) {
return sum.concat(group)
}, [])
})
.then(function returnData (notes) {
.then(function returnData(notes) {
return {
storages,
notes
@@ -71,12 +86,11 @@ function init () {
}
return Promise.resolve(fetchStorages())
.then((storages) => {
return storages
.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
.then(storages => {
return storages.filter(storage => {
if (!_.isObject(storage)) return false
return true
})
})
.then(fetchNotes)
}

View File

@@ -6,102 +6,111 @@ const CSON = require('@rokt33r/season')
const path = require('path')
const sander = require('sander')
function migrateFromV5Storage (storageKey, data) {
function migrateFromV5Storage(storageKey, data) {
let targetStorage
try {
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
if (!_.isArray(cachedStorageList))
throw new Error("Target storage doesn't exist.")
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, { key: storageKey })
if (targetStorage == null) throw new Error("Target storage doesn't exist.")
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function (storage) {
return importAll(storage, data)
})
return resolveStorageData(targetStorage).then(function(storage) {
return importAll(storage, data)
})
}
function importAll (storage, data) {
function importAll(storage, data) {
const oldArticles = data.articles
const notes = []
data.folders
.forEach(function (oldFolder) {
let folderKey = keygen()
while (storage.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
const newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
data.folders.forEach(function(oldFolder) {
let folderKey = keygen()
while (storage.folders.some(folder => folder.key === folderKey)) {
folderKey = keygen()
}
const newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.folders.push(newFolder)
storage.folders.push(newFolder)
const articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
articles.forEach((article) => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
const articles = oldArticles.filter(
article => article.FolderKey === oldFolder.key
)
articles.forEach(article => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
}
}
if (article.mode === 'markdown') {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
if (article.mode === 'markdown') {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [
{
name: article.mode,
mode: article.mode,
content: article.content,
linesHighlighted: article.linesHighlighted
}]
}
notes.push(newNote)
}
]
}
})
notes.push(newNote)
}
})
notes.forEach(function (note) {
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
})
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
notes.forEach(function(note) {
CSON.writeFileSync(
path.join(storage.path, 'notes', note.key + '.cson'),
_.omit(note, ['storage', 'key'])
)
})
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['version', 'folders'])
)
return {
storage,

View File

@@ -4,86 +4,91 @@ const keygen = require('browser/lib/keygen')
const _ = require('lodash')
const CSON = require('@rokt33r/season')
function migrateFromV5Storage (storagePath) {
function migrateFromV5Storage(storagePath) {
var boostnoteJSONPath = path.join(storagePath, 'boostnote.json')
return Promise.resolve()
.then(function readBoostnoteJSON () {
.then(function readBoostnoteJSON() {
return sander.readFile(boostnoteJSONPath, {
encoding: 'utf-8'
})
})
.then(function verifyVersion (rawData) {
.then(function verifyVersion(rawData) {
var boostnoteJSONData = JSON.parse(rawData)
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
if (boostnoteJSONData.version === '1.0')
throw new Error('Target storage seems to be transformed already.')
if (!_.isArray(boostnoteJSONData.folders))
throw new Error('the value of folders is not an array.')
return boostnoteJSONData
})
.then(function setVersion (boostnoteJSONData) {
.then(function setVersion(boostnoteJSONData) {
boostnoteJSONData.version = '1.0'
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
return sander
.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
.then(() => boostnoteJSONData)
})
.then(function fetchNotes (boostnoteJSONData) {
var fetchNotesFromEachFolder = boostnoteJSONData.folders
.map(function (folder) {
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
return sander
.readFile(folderDataJSONPath, {
encoding: 'utf-8'
.then(function fetchNotes(boostnoteJSONData) {
var fetchNotesFromEachFolder = boostnoteJSONData.folders.map(function(
folder
) {
const folderDataJSONPath = path.join(
storagePath,
folder.key,
'data.json'
)
return sander
.readFile(folderDataJSONPath, {
encoding: 'utf-8'
})
.then(function(rawData) {
var data = JSON.parse(rawData)
if (!_.isArray(data.notes))
throw new Error('value of notes is not an array.')
return data.notes.map(function setFolderToNote(note) {
note.folder = folder.key
return note
})
.then(function (rawData) {
var data = JSON.parse(rawData)
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
return data.notes
.map(function setFolderToNote (note) {
note.folder = folder.key
return note
})
})
.catch(function failedToReadDataJSON (err) {
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
return []
})
})
})
.catch(function failedToReadDataJSON(err) {
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
return []
})
})
return Promise.all(fetchNotesFromEachFolder)
.then(function flatten (folderNotes) {
return folderNotes
.reduce(function concatNotes (sum, notes) {
return sum.concat(notes)
}, [])
.then(function flatten(folderNotes) {
return folderNotes.reduce(function concatNotes(sum, notes) {
return sum.concat(notes)
}, [])
})
.then(function saveNotes (notes) {
notes.forEach(function renewKey (note) {
.then(function saveNotes(notes) {
notes.forEach(function renewKey(note) {
var newKey = keygen()
while (notes.some((_note) => _note.key === newKey)) {
while (notes.some(_note => _note.key === newKey)) {
newKey = keygen()
}
note.key = newKey
})
const noteDirPath = path.join(storagePath, 'notes')
notes
.map(function saveNote (note) {
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
})
notes.map(function saveNote(note) {
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
})
return true
})
.then(function deleteFolderDir (check) {
.then(function deleteFolderDir(check) {
if (check) {
boostnoteJSONData.folders.forEach((folder) => {
boostnoteJSONData.folders.forEach(folder => {
sander.rimrafSync(path.join(storagePath, folder.key))
})
}
return check
})
})
.catch(function handleError (err) {
.catch(function handleError(err) {
console.warn(err)
return false
})
}
module.exports = migrateFromV5Storage

View File

@@ -7,90 +7,104 @@ const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const attachmentManagement = require('./attachmentManagement')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
function moveNote(storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage
try {
oldStorage = findStorage(storageKey)
newStorage = findStorage(newStorageKey)
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
if (newStorage == null) throw new Error("Target storage doesn't exist.")
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(oldStorage)
.then(function saveNote (_oldStorage) {
oldStorage = _oldStorage
let noteData
const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
throw err
}
let newNoteKey
return Promise.resolve()
.then(function resolveNewStorage () {
if (storageKey === newStorageKey) {
newNoteKey = noteKey
return oldStorage
}
return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage
newNoteKey = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen(true)
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
return newStorage
})
})
.then(function checkFolderExistsAndPrepareNoteData (newStorage) {
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
noteData.folder = newFolderKey
noteData.key = newNoteKey
noteData.storage = newStorageKey
noteData.updatedAt = new Date()
noteData.oldContent = noteData.content
return noteData
})
.then(function moveAttachments (noteData) {
if (oldStorage.path === newStorage.path) {
return noteData
}
noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content)
return noteData
})
.then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent']))
return noteData
})
.then(function deleteOldNote (data) {
if (storageKey !== newStorageKey) {
return resolveStorageData(oldStorage).then(function saveNote(_oldStorage) {
oldStorage = _oldStorage
let noteData
const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
throw err
}
let newNoteKey
return Promise.resolve()
.then(function resolveNewStorage() {
if (storageKey === newStorageKey) {
newNoteKey = noteKey
return oldStorage
}
return resolveStorageData(newStorage).then(function findNewNoteKey(
_newStorage
) {
newStorage = _newStorage
newNoteKey = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
sander.statSync(
path.join(newStorage.path, 'notes', newNoteKey + '.cson')
)
newNoteKey = keygen(true)
} catch (err) {
console.warn(err)
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
return data
return newStorage
})
})
})
.then(function checkFolderExistsAndPrepareNoteData(newStorage) {
if (_.find(newStorage.folders, { key: newFolderKey }) == null)
throw new Error("Target folder doesn't exist.")
noteData.folder = newFolderKey
noteData.key = newNoteKey
noteData.storage = newStorageKey
noteData.updatedAt = new Date()
noteData.oldContent = noteData.content
return noteData
})
.then(function moveAttachments(noteData) {
if (oldStorage.path === newStorage.path) {
return noteData
}
noteData.content = attachmentManagement.moveAttachments(
oldStorage.path,
newStorage.path,
noteKey,
newNoteKey,
noteData.content
)
return noteData
})
.then(function writeAndReturn(noteData) {
CSON.writeFileSync(
path.join(newStorage.path, 'notes', noteData.key + '.cson'),
_.omit(noteData, ['key', 'storage', 'oldContent'])
)
return noteData
})
.then(function deleteOldNote(data) {
if (storageKey !== newStorageKey) {
try {
sander.unlinkSync(
path.join(oldStorage.path, 'notes', noteKey + '.cson')
)
} catch (err) {
console.warn(err)
}
}
return data
})
})
}
module.exports = moveNote

View File

@@ -4,7 +4,7 @@ const _ = require('lodash')
* @param {String} key
* @return {key}
*/
function removeStorage (key) {
function removeStorage(key) {
let rawStorages
try {
@@ -15,10 +15,9 @@ function removeStorage (key) {
rawStorages = []
}
rawStorages = rawStorages
.filter(function excludeTargetStorage (rawStorage) {
return rawStorage.key !== key
})
rawStorages = rawStorages.filter(function excludeTargetStorage(rawStorage) {
return rawStorage.key !== key
})
localStorage.setItem('storages', JSON.stringify(rawStorages))

View File

@@ -6,8 +6,9 @@ const resolveStorageData = require('./resolveStorageData')
* @param {String} name
* @return {Object} Storage meta data
*/
function renameStorage (key, name) {
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
function renameStorage(key, name) {
if (!_.isString(name))
return Promise.reject(new Error('Name must be a string.'))
let cachedStorageList
try {
@@ -17,7 +18,7 @@ function renameStorage (key, name) {
console.error(err)
return Promise.reject(err)
}
const targetStorage = _.find(cachedStorageList, {key: key})
const targetStorage = _.find(cachedStorageList, { key: key })
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.name = name

View File

@@ -17,7 +17,7 @@ const { findStorage } = require('browser/lib/findStorage')
* }
* ```
*/
function reorderFolder (storageKey, oldIndex, newIndex) {
function reorderFolder(storageKey, oldIndex, newIndex) {
let targetStorage
try {
if (!_.isNumber(oldIndex)) throw new Error('oldIndex must be a number.')
@@ -28,15 +28,19 @@ function reorderFolder (storageKey, oldIndex, newIndex) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function reorderFolder (storage) {
storage.folders = _.move(storage.folders, oldIndex, newIndex)
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return resolveStorageData(targetStorage).then(function reorderFolder(
storage
) {
storage.folders = _.move(storage.folders, oldIndex, newIndex)
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
return {
storage
}
})
return {
storage
}
})
}
module.exports = reorderFolder

View File

@@ -3,7 +3,7 @@ const path = require('path')
const CSON = require('@rokt33r/season')
const migrateFromV6Storage = require('./migrateFromV6Storage')
function resolveStorageData (storageCache) {
function resolveStorageData(storageCache) {
const storage = {
key: storageCache.key,
name: storageCache.name,
@@ -15,13 +15,14 @@ function resolveStorageData (storageCache) {
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
try {
const jsonData = CSON.readFileSync(boostnoteJSONPath)
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
if (!_.isArray(jsonData.folders))
throw new Error('folders should be an array.')
storage.folders = jsonData.folders
storage.version = jsonData.version
} catch (err) {
if (err.code === 'ENOENT') {
console.warn('boostnote.json file doesn\'t exist the given path')
CSON.writeFileSync(boostnoteJSONPath, {folders: [], version: '1.0'})
console.warn("boostnote.json file doesn't exist the given path")
CSON.writeFileSync(boostnoteJSONPath, { folders: [], version: '1.0' })
} else {
console.error(err)
}
@@ -34,8 +35,7 @@ function resolveStorageData (storageCache) {
return Promise.resolve(storage)
}
return migrateFromV6Storage(storage.path)
.then(() => storage)
return migrateFromV6Storage(storage.path).then(() => storage)
}
module.exports = resolveStorageData

View File

@@ -2,14 +2,14 @@ const sander = require('sander')
const path = require('path')
const CSON = require('@rokt33r/season')
function resolveStorageNotes (storage) {
function resolveStorageNotes(storage) {
const notesDirPath = path.join(storage.path, 'notes')
let notePathList
try {
notePathList = sander.readdirSync(notesDirPath)
} catch (err) {
if (err.code === 'ENOENT') {
console.error(notesDirPath, ' doesn\'t exist.')
console.error(notesDirPath, " doesn't exist.")
sander.mkdirSync(notesDirPath)
} else {
console.warn('Failed to find note dir', notesDirPath, err)
@@ -17,10 +17,10 @@ function resolveStorageNotes (storage) {
notePathList = []
}
const notes = notePathList
.filter(function filterOnlyCSONFile (notePath) {
.filter(function filterOnlyCSONFile(notePath) {
return /\.cson$/.test(notePath)
})
.map(function parseCSONFile (notePath) {
.map(function parseCSONFile(notePath) {
try {
const data = CSON.readFileSync(path.join(notesDirPath, notePath))
data.key = path.basename(notePath, '.cson')
@@ -30,7 +30,7 @@ function resolveStorageNotes (storage) {
console.error(`error on note path: ${notePath}, error: ${err}`)
}
})
.filter(function filterOnlyNoteObject (noteObj) {
.filter(function filterOnlyNoteObject(noteObj) {
return typeof noteObj === 'object'
})

View File

@@ -6,7 +6,7 @@ const resolveStorageData = require('./resolveStorageData')
* @param {Boolean} isOpen
* @return {Object} Storage meta data
*/
function toggleStorage (key, isOpen) {
function toggleStorage(key, isOpen) {
let cachedStorageList
try {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
@@ -15,7 +15,7 @@ function toggleStorage (key, isOpen) {
console.error(err)
return Promise.reject(err)
}
const targetStorage = _.find(cachedStorageList, {key: key})
const targetStorage = _.find(cachedStorageList, { key: key })
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.isOpen = isOpen

View File

@@ -22,7 +22,7 @@ const { findStorage } = require('browser/lib/findStorage')
* }
* ```
*/
function updateFolder (storageKey, folderKey, input) {
function updateFolder(storageKey, folderKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -34,19 +34,21 @@ function updateFolder (storageKey, folderKey, input) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function updateFolder (storage) {
const targetFolder = _.find(storage.folders, {key: folderKey})
if (targetFolder == null) throw new Error('Target folder doesn\'t exist.')
targetFolder.name = input.name
targetFolder.color = input.color
return resolveStorageData(targetStorage).then(function updateFolder(storage) {
const targetFolder = _.find(storage.folders, { key: folderKey })
if (targetFolder == null) throw new Error("Target folder doesn't exist.")
targetFolder.name = input.name
targetFolder.color = input.color
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
CSON.writeFileSync(
path.join(storage.path, 'boostnote.json'),
_.pick(storage, ['folders', 'version'])
)
return {
storage
}
})
return {
storage
}
})
}
module.exports = updateFolder

View File

@@ -4,13 +4,14 @@ const path = require('path')
const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
function validateInput (input) {
function validateInput(input) {
const validatedInput = {}
if (input.tags != null) {
if (!_.isArray(input.tags)) validatedInput.tags = []
validatedInput.tags = input.tags
.filter((tag) => _.isString(tag) && tag.trim().length > 0)
validatedInput.tags = input.tags.filter(
tag => _.isString(tag) && tag.trim().length > 0
)
}
if (input.title != null) {
@@ -40,7 +41,8 @@ function validateInput (input) {
if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
if (!_.isArray(input.linesHighlighted))
validatedInput.linesHighlighted = []
else validatedInput.linesHighlighted = input.linesHighlighted
}
return validatedInput
@@ -51,30 +53,33 @@ function validateInput (input) {
}
if (input.snippets != null) {
if (!_.isArray(input.snippets)) {
validatedInput.snippets = [{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}]
validatedInput.snippets = [
{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}
]
} else {
validatedInput.snippets = input.snippets
}
validatedInput.snippets
.filter((snippet) => {
if (!_.isString(snippet.name)) return false
if (!_.isString(snippet.mode)) return false
if (!_.isString(snippet.content)) return false
return true
})
validatedInput.snippets.filter(snippet => {
if (!_.isString(snippet.name)) return false
if (!_.isString(snippet.mode)) return false
if (!_.isString(snippet.content)) return false
return true
})
}
return validatedInput
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
throw new Error(
'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.'
)
}
}
function updateNote (storageKey, noteKey, input) {
function updateNote(storageKey, noteKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -85,55 +90,61 @@ function updateNote (storageKey, noteKey, input) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function saveNote (storage) {
let noteData
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
noteData = input.type === 'SNIPPET_NOTE'
return resolveStorageData(targetStorage).then(function saveNote(storage) {
let noteData
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
noteData =
input.type === 'SNIPPET_NOTE'
? {
type: 'SNIPPET_NOTE',
description: [],
snippets: [{
name: '',
mode: 'text',
type: 'SNIPPET_NOTE',
description: [],
snippets: [
{
name: '',
mode: 'text',
content: '',
linesHighlighted: []
}
]
}
: {
type: 'MARKDOWN_NOTE',
content: '',
linesHighlighted: []
}]
}
: {
type: 'MARKDOWN_NOTE',
content: '',
linesHighlighted: []
}
noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
noteData.folder = storage.folders[0].key
noteData.createdAt = new Date()
noteData.updatedAt = new Date()
noteData.isStarred = false
noteData.isTrashed = false
noteData.tags = []
noteData.isPinned = false
}
}
noteData.title = ''
if (storage.folders.length === 0)
throw new Error('Failed to restore note: No folder exists.')
noteData.folder = storage.folders[0].key
noteData.createdAt = new Date()
noteData.updatedAt = new Date()
noteData.isStarred = false
noteData.isTrashed = false
noteData.tags = []
noteData.isPinned = false
}
if (noteData.type === 'SNIPPET_NOTE') {
noteData.title
}
if (noteData.type === 'SNIPPET_NOTE') {
noteData.title
}
Object.assign(noteData, input, {
key: noteKey,
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
Object.assign(noteData, input, {
key: noteKey,
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(
path.join(storage.path, 'notes', noteKey + '.cson'),
_.omit(noteData, ['key', 'storage'])
)
return noteData
})
}
module.exports = updateNote

View File

@@ -1,9 +1,11 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
function updateSnippet (snippet, snippetFile) {
function updateSnippet(snippet, snippetFile) {
return new Promise((resolve, reject) => {
const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8'))
const snippets = JSON.parse(
fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8')
)
for (let i = 0; i < snippets.length; i++) {
const currentSnippet = snippets[i]
@@ -21,11 +23,15 @@ function updateSnippet (snippet, snippetFile) {
currentSnippet.name = snippet.name
currentSnippet.prefix = snippet.prefix
currentSnippet.content = snippet.content
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippets)
})
currentSnippet.linesHighlighted = snippet.linesHighlighted
fs.writeFile(
snippetFile || consts.SNIPPET_FILE,
JSON.stringify(snippets, null, 4),
err => {
if (err) reject(err)
resolve(snippets)
}
)
}
}
}

View File

@@ -1,19 +1,19 @@
const electron = require('electron')
const { ipcRenderer, remote } = electron
function on (name, listener) {
function on(name, listener) {
ipcRenderer.on(name, listener)
}
function off (name, listener) {
function off(name, listener) {
ipcRenderer.removeListener(name, listener)
}
function once (name, listener) {
function once(name, listener) {
ipcRenderer.once(name, listener)
}
function emit (name, ...args) {
function emit(name, ...args) {
remote.getCurrentWindow().webContents.send(name, ...args)
}

View File

@@ -12,14 +12,14 @@ nodeIpc.config.silent = true
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
function() {
nodeIpc.of.node.on('error', function(err) {
console.error(err)
})
nodeIpc.of.node.on('connect', function () {
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
nodeIpc.of.node.on('connect', function() {
ipcRenderer.send('config-renew', { config: ConfigManager.get() })
})
nodeIpc.of.node.on('disconnect', function () {
nodeIpc.of.node.on('disconnect', function() {
return
})
}

View File

@@ -1,10 +1,10 @@
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) {
constructor(props) {
super(props)
this.state = {
component: null,
@@ -13,20 +13,30 @@ class ModalBase extends React.Component {
}
}
close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
close() {
if (modalBase != null)
modalBase.setState({
component: null,
componentProps: null,
isHidden: true
})
// Toggle overflow style on NoteList
const list = document.querySelector('.NoteList__list___browser-main-NoteList-')
const list = document.querySelector(
'.NoteList__list___browser-main-NoteList-'
)
list.style.overflow = 'auto'
}
render () {
render() {
return (
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
<div onClick={(e) => this.close(e)} className='modalBack' />
<div onClick={e => this.close(e)} className='modalBack' />
{this.state.component == null ? null : (
<Provider store={store}>
<this.state.component {...this.state.componentProps} close={this.close} />
<this.state.component
{...this.state.componentProps}
close={this.close}
/>
</Provider>
)}
</div>
@@ -38,21 +48,31 @@ const el = document.createElement('div')
document.body.appendChild(el)
const modalBase = ReactDOM.render(<ModalBase />, el)
export function openModal (component, props) {
if (modalBase == null) { return }
export function openModal(component, props) {
if (modalBase == null) {
return
}
// Hide scrollbar by removing overflow when modal opens
const list = document.querySelector('.NoteList__list___browser-main-NoteList-')
const list = document.querySelector(
'.NoteList__list___browser-main-NoteList-'
)
list.style.overflow = 'hidden'
document.body.setAttribute('data-modal', 'open')
modalBase.setState({component: component, componentProps: props, isHidden: false})
modalBase.setState({
component: component,
componentProps: props,
isHidden: false
})
}
export function closeModal () {
if (modalBase == null) { return }
export function closeModal() {
if (modalBase == null) {
return
}
modalBase.close()
}
export function isModalOpen () {
export function isModalOpen() {
return !modalBase.state.isHidden
}

View File

@@ -1,8 +1,12 @@
const path = require('path')
function notify (title, options) {
function notify(title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.icon = path.join(
'file://',
global.__dirname,
'../../resources/app.png'
)
options.silent = false
}
return new window.Notification(title, options)

View File

@@ -1,10 +1,16 @@
import ee from 'browser/main/lib/eventEmitter'
module.exports = {
'toggleMode': () => {
toggleMode: () => {
ee.emit('topbar:togglemodebutton')
},
'deleteNote': () => {
toggleDirection: () => {
ee.emit('topbar:toggledirectionbutton')
},
deleteNote: () => {
ee.emit('hotkey:deletenote')
},
toggleMenuBar: () => {
ee.emit('menubar:togglemenubar')
}
}

View File

@@ -7,7 +7,7 @@ import functions from './shortcut'
let shortcuts = CM.get().hotkey
ee.on('config-renew', function () {
ee.on('config-renew', function() {
// only update if hotkey changed !
const newHotkey = CM.get().hotkey
if (!isObjectEqual(newHotkey, shortcuts)) {
@@ -15,17 +15,17 @@ ee.on('config-renew', function () {
}
})
function updateShortcut (newHotkey) {
function updateShortcut(newHotkey) {
Mousetrap.reset()
shortcuts = newHotkey
applyShortcuts(newHotkey)
}
function formatShortcut (shortcut) {
function formatShortcut(shortcut) {
return shortcut.toLowerCase().replace(/ /g, '')
}
function applyShortcuts (shortcuts) {
function applyShortcuts(shortcuts) {
for (const shortcut in shortcuts) {
const toggler = formatShortcut(shortcuts[shortcut])
// only bind if the function for that shortcut exists

View File

@@ -3,14 +3,14 @@ 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'
import i18n from 'browser/lib/i18n'
class CreateFolderModal extends React.Component {
constructor (props) {
constructor(props) {
super(props)
this.state = {
@@ -18,39 +18,39 @@ class CreateFolderModal extends React.Component {
}
}
componentDidMount () {
componentDidMount() {
this.refs.name.focus()
this.refs.name.select()
}
handleCloseButtonClick (e) {
handleCloseButtonClick(e) {
this.props.close()
}
handleChange (e) {
handleChange(e) {
this.setState({
name: this.refs.name.value
})
}
handleKeyDown (e) {
handleKeyDown(e) {
if (e.keyCode === 27) {
this.props.close()
}
}
handleInputKeyDown (e) {
handleInputKeyDown(e) {
switch (e.keyCode) {
case 13:
this.confirm()
}
}
handleConfirmButtonClick (e) {
handleConfirmButtonClick(e) {
this.confirm()
}
confirm () {
confirm() {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_FOLDER')
if (this.state.name.trim().length > 0) {
const { storage } = this.props
@@ -59,42 +59,48 @@ class CreateFolderModal extends React.Component {
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
dataApi.createFolder(storage.key, input)
.then((data) => {
dataApi
.createFolder(storage.key, input)
.then(data => {
store.dispatch({
type: 'UPDATE_FOLDER',
storage: data.storage
})
this.props.close()
})
.catch((err) => {
.catch(err => {
console.error(err)
})
}
}
render () {
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.__('Create new folder')}</div>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<ModalEscButton
handleEscButtonClick={e => this.handleCloseButtonClick(e)}
/>
<div styleName='control'>
<div styleName='control-folder'>
<div styleName='control-folder-label'>{i18n.__('Folder name')}</div>
<input styleName='control-folder-input'
<input
styleName='control-folder-input'
ref='name'
value={this.state.name}
onChange={(e) => this.handleChange(e)}
onKeyDown={(e) => this.handleInputKeyDown(e)}
onChange={e => this.handleChange(e)}
onKeyDown={e => this.handleInputKeyDown(e)}
/>
</div>
<button styleName='control-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
<button
styleName='control-confirmButton'
onClick={e => this.handleConfirmButtonClick(e)}
>
{i18n.__('Create')}
</button>

View File

@@ -51,106 +51,40 @@
font-size 14px
colorPrimaryButton()
body[data-theme="dark"]
.root
modalDark()
width 500px
height 270px
overflow hidden
position relative
apply-theme(theme)
body[data-theme={theme}]
.root
width 500px
height 270px
overflow hidden
position relative
position relative
z-index $modal-z-index
width 100%
background-color get-theme-var(theme, 'backgroundColor')
overflow hidden
border-radius $modal-border-radius
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-dark-text-color
.header
background-color transparent
border-color $ui-dark-borderColor
color get-theme-var(theme, 'text-color')
.control-folder-label
color $ui-dark-text-color
.control-folder-label
color get-theme-var(theme, 'text-color')
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.description
color $ui-inactive-text-color
.control-confirmButton
colorDarkPrimaryButton()
.control-confirmButton
colorThemedPrimaryButton(theme)
body[data-theme="solarized-dark"]
.root
modalSolarizedDark()
width 500px
height 270px
overflow hidden
position relative
for theme in 'dark' 'solarized-dark' 'dracula'
apply-theme(theme)
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-solarized-dark-text-color
.control-folder-label
color $ui-solarized-dark-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorSolarizedDarkPrimaryButton()
body[data-theme="monokai"]
.root
modalMonokai()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-monokai-text-color
.control-folder-label
color $ui-monokai-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.root
modalDracula()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-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()
for theme in $themes
apply-theme(theme)

Some files were not shown because too many files have changed in this diff Show More