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

Merge branch 'master' into feature-editor-line-lines

This commit is contained in:
David Pavlík
2017-12-23 23:06:26 +01:00
committed by GitHub
222 changed files with 6346 additions and 2127 deletions

View File

@@ -1,5 +1,8 @@
.root
absolute top bottom right
display flex
align-items center
justify-content center
.empty
height 320px
@@ -8,8 +11,9 @@
.empty-message
width 100%
font-size 42px
line-height 72px
font-size 36px
font-weight 600
line-height 56px
text-align center
color $ui-inactive-text-color
@@ -18,3 +22,9 @@ body[data-theme="dark"]
background-color $ui-dark-backgroundColor
.empty-message
color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
.empty-message
color $ui-solarized-dark-text-color

View File

@@ -3,7 +3,9 @@
*/
// Margin on the left side and the right side for NoteDetail component.
$note-detail-left-margin = 25px
$note-detail-right-margin = 25px
$note-detail-left-margin = 100px
$note-detail-right-margin = 120px
$snippet-note-detail-left-margin = 60px
$snippet-note-detail-right-margin = 80px
$note-detail-box-shadow = 2px 0 15px -8px #b1b1b1 inset

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl'
import _ from 'lodash'
@@ -73,8 +74,8 @@ class FolderSelect extends React.Component {
case 9:
if (e.shiftKey) {
e.preventDefault()
let tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
let 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()
}
}
@@ -89,9 +90,9 @@ class FolderSelect extends React.Component {
}
handleSearchInputChange (e) {
let { folders } = this.props
let search = this.refs.search.value
let optionIndex = search.length > 0
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'))
})
@@ -129,7 +130,7 @@ class FolderSelect extends React.Component {
nextOption () {
let { optionIndex } = this.state
let { folders } = this.props
const { folders } = this.props
optionIndex++
if (optionIndex >= folders.length) optionIndex = 0
@@ -140,7 +141,7 @@ class FolderSelect extends React.Component {
}
previousOption () {
let { folders } = this.props
const { folders } = this.props
let { optionIndex } = this.state
optionIndex--
@@ -152,10 +153,10 @@ class FolderSelect extends React.Component {
}
selectOption () {
let { folders } = this.props
let optionIndex = this.state.optionIndex
const { folders } = this.props
const optionIndex = this.state.optionIndex
let folder = folders[optionIndex]
const folder = folders[optionIndex]
if (folder != null) {
this.setState({
status: 'FOCUS'
@@ -184,10 +185,10 @@ class FolderSelect extends React.Component {
}
render () {
let { className, data, value } = this.props
let splitted = value.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
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) => {
@@ -198,14 +199,14 @@ class FolderSelect extends React.Component {
})
})
let 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) {
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
options = options.filter((option) => filter.test(option.folder.name))
}
let optionList = options
const optionList = options
.map((option, index) => {
return (
<div styleName={index === this.state.optionIndex
@@ -259,12 +260,11 @@ class FolderSelect extends React.Component {
{optionList}
</div>
</div>
: <div styleName='idle'>
: <div styleName='idle' style={{color: currentOption.folder.color}}>
<div styleName='idle-label'>
<span styleName='idle-label-name'
style={{color: currentOption.folder.color}}
>
{currentOption.folder.name} /
<i className='fa fa-folder' />
<span styleName='idle-label-name'>
{currentOption.folder.name}
</span>
</div>
</div>

View File

@@ -1,21 +1,22 @@
.root
position relative
border solid 1px transparent
line-height 26px
vertical-align middle
border-radius 2px
transition 0.15s
user-select none
margin-right 10px
&:hover
background-color $ui-button--hover-backgroundColor
.root--search, .root--focus
@extend .root
background-color $ui-noteDetail-backgroundColor = #F4F4F4
background-color $ui-noteDetail-backgroundColor = #fff
border-color $ui-input--focus-borderColor
width 100px
width 154px
height 30px
&:hover
border-color $ui-input--focus-borderColor
border-color $ui-input--focus-borderColor = #fff
.idle
position relative
@@ -24,13 +25,16 @@
.idle-label
right 20px
overflow ellipsis
display flex
align-items center
.idle-label-name
font-size 14px
padding 2px
font-size 13px
font-weight 600
margin-left 4px
.idle-label-name-surfix
font-size 10px
font-size 15px
color $ui-inactive-text-color
margin-left 5px
.idle-caret
@@ -38,40 +42,42 @@
height 34px
width 20px
line-height 34px
.search
absolute top left right bottom
line-height 34px
.search-input
vertical-align middle
position relative
top -2px
top 0
font-size 14px
outline none
border none
height 20px
line-height 20px
width 100%
background-color transparent
padding 0 10px
.search-optionList
position fixed
position absolute
top 30px
max-height 450px
min-width 150px
overflow auto
z-index 200
border $ui-border
background-color white
border-radius 2px
padding 10px 6px
.search-optionList-item
width 140px
height 34px
width 250px
display flex
align-items center
box-sizing border-box
padding 0 5px
padding 0
margin-bottom 10px
overflow ellipsis
cursor pointer
&:hover
background-color $ui-button--hover-backgroundColor
background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
@@ -81,13 +87,17 @@
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
.search-optionList-item-name
border-left solid 2px transparent
padding 2px 5px
border-left solid 3px transparent
padding 6px
.search-optionList-item-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
body[data-theme="dark"]
.root
color $ui-dark-text-color

View File

@@ -0,0 +1,19 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FullscreenButton.styl'
const FullscreenButton = ({
onClick
}) => (
<button styleName='control-fullScreenButton' title='Fullscreen' onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>Fullscreen</span>
</button>
)
FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(FullscreenButton, styles)

View File

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

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl'
@@ -6,9 +7,10 @@ const InfoButton = ({
onClick
}) => (
<button styleName='control-infoButton'
onClick={onClick}
onClick={(e) => onClick(e)}
>
<i className='fa fa-info infoButton' styleName='info-button' />
<img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>Info</span>
</button>
)

View File

@@ -1,12 +1,17 @@
.control-infoButton
float right
topBarButtonLight()
top 10px
topBarButtonRight()
&:hover .tooltip
opacity 1
.control-infoPanel
position fixed
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
top 26px
right 0
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
@@ -14,7 +19,6 @@
.infoButton
padding 0px
margin 15px 0
body[data-theme="dark"]
.control-infoButton

View File

@@ -1,73 +1,60 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
const InfoPanel = ({
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
}) => (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div styleName='group-section'>
<div styleName='group-section-label'>
Storage
</div>
<div styleName='group-section-control'>
{storageName}
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Folder
</div>
<div styleName='group-section-control'>
{folderName}
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Created
</div>
<div styleName='group-section-control'>
{createdAt}
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Updated
</div>
<div styleName='group-section-control'>
{updatedAt}
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Note Link
</div>
<div styleName='group-section-control'>
<input value={noteLink} onClick={(e) => { e.target.select() }} />
</div>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
</div>
<hr />
{type === 'SNIPPET_NOTE'
? ''
: <div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Words
</div>
<div styleName='group-section-control'>
{wordCount}
</div>
: <div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>Words</p>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Letters
</div>
<div styleName='group-section-control'>
{letterCount}
</div>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>Letters</p>
</div>
</div>
}
{type === 'SNIPPET_NOTE'
? ''
: <hr />
}
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
</div>
<div>
<p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
</div>
<div>
<input styleName='infoPanel-noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<p styleName='infoPanel-sub'>NOTE LINK</p>
</div>
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o fa-fw' />
@@ -79,9 +66,9 @@ const InfoPanel = ({
<p>.txt</p>
</button>
<button styleName='export--unable'>
<i className='fa fa-file-pdf-o fa-fw' />
<p>.pdf</p>
<button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print fa-fw' />
<p>Print</p>
</button>
</div>
</div>
@@ -97,7 +84,8 @@ InfoPanel.propTypes = {
exportAsTxt: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired
type: PropTypes.string.isRequired,
print: PropTypes.func.isRequired
}
export default CSSModules(InfoPanel, styles)

View File

@@ -10,57 +10,86 @@
.control-infoButton-panel
z-index 200
margin-top 45px
margin-left -210px
margin-top 0px
right 0
position absolute
padding 20px 20px 0 20px
width 340px
padding 20px 25px 0 25px
width 300px
height 350px
overflow auto
background-color $ui-noteList-backgroundColor
border 1px solid $border-color
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
border-radius 2px
.modification-date
font-size 18px
line-height 30px
color $ui-text-color
.modification-date-desc
font-size 18px
color $ui-inactive-text-color
.control-infoButton-panel-trash
z-index 200
margin-top 45px
margin-left -230px
margin-top 0px
right 0px
position absolute
padding 20px 20px 0 20px
width 320px
padding 20px 25px 0 25px
width 300px
background-color $ui-noteList-backgroundColor
border 1px solid $border-color
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
border-radius 2px
.group-section
display flex
.count-wrap
display flex
position relative
width 100%
.count-number
position relative
display block
width 50%
overflow hidden
margin 0
padding 0
.infoPanel-defaul-count
font-size 16px
line-height 30px
font-size 13px
.group-section-label
width 70px
height 20px
text-align left
margin-right 50px
font-size 13px
color $ui-inactive-text-color
.group-section-control
flex 1
font-size 13px
color $ui-text-color
.group-section-control input
width 160px
height 25px
.infoPanel-sub-count
font-size 16px
color $ui-inactive-text-color
padding-bottom 8px
.group-section-control text
color #EA4447
font-weight 600
.infoPanel-default
font-size 14px
line-height 30px
color $ui-text-color
.infoPanel-sub
font-size 14px
color $ui-inactive-text-color
padding-bottom 8px
.infoPanel-noteLink
padding-right 5px
width 200px
height 25px
margin-bottom 6px
.infoPanel-trash
color #EA4447
font-weight 600
font-size 14px
width 70px
height 25px
background-color rgba(226,33,113,0.1)
border none
border none
outline none
border-radius 2px
margin-right 5px
border-radius 2px
margin-right 5px
padding 2px 5px
[id=export-wrap]
@@ -96,20 +125,30 @@
body[data-theme="dark"]
.control-infoButton-panel
background-color $ui-dark-noteList-backgroundColor
border 1px solid $ui-dark-borderColor
.control-infoButton-panel-trash
background-color $ui-dark-noteList-backgroundColor
border 1px solid $ui-dark-borderColor
.group-section-label
color $ui-inactive-text-color
.group-section-control
.modification-date
color $ui-dark-text-color
.group-section-control input
background-color alpha($ui-dark-borderColor, 80%)
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-dark-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-dark-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-dark-borderColor, 60%)
color $ui-dark-text-color
[id=export-wrap]

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
@@ -6,37 +7,26 @@ const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
}) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div styleName='group-section'>
<div styleName='group-section-label'>
Storage
</div>
<div styleName='group-section-control'>
{storageName}
</div>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Folder
</div>
<div styleName='group-section-control'>
<text>Trash</text>{folderName}
</div>
<hr />
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Created
</div>
<div styleName='group-section-control'>
{createdAt}
</div>
<div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Updated
</div>
<div styleName='group-section-control'>
{updatedAt}
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
</div>
<div id='export-wrap'>

View File

@@ -1,7 +1,9 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownNoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor'
import TodoListPercentage from 'browser/components/TodoListPercentage'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
@@ -9,21 +11,26 @@ 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/markdown'
import markdown from 'browser/lib/markdownTextHelper'
import StatusBar from '../StatusBar'
import _ from 'lodash'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import ConfigManager from 'browser/main/lib/ConfigManager'
import TrashButton from './TrashButton'
import FullscreenButton from './FullscreenButton'
import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton'
import ToggleModeButton from './ToggleModeButton'
import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
const electron = require('electron')
const { remote } = electron
const { Menu, MenuItem, dialog } = remote
const { dialog } = remote
class MarkdownNoteDetail extends React.Component {
constructor (props) {
@@ -36,7 +43,8 @@ class MarkdownNoteDetail extends React.Component {
content: ''
}, props.note),
isLockButtonShown: false,
isLocked: false
isLocked: false,
editorType: props.config.editor.type
}
this.dispatchTimer = null
@@ -72,11 +80,11 @@ class MarkdownNoteDetail extends React.Component {
}
handleChange (e) {
let { note } = this.state
const { note } = this.state
note.content = this.refs.content.value
if (this.refs.tags) note.tags = this.refs.tags.value
note.title = markdown.strip(findNoteTitle(note.content))
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
note.updatedAt = new Date()
this.setState({
@@ -94,7 +102,7 @@ class MarkdownNoteDetail extends React.Component {
}
saveNow () {
let { note, dispatch } = this.props
const { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
@@ -110,11 +118,11 @@ class MarkdownNoteDetail extends React.Component {
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
const { note } = this.state
const value = this.refs.folder.value
const splitted = value.split('-')
const newStorageKey = splitted.shift()
const newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
@@ -123,7 +131,7 @@ class MarkdownNoteDetail extends React.Component {
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
@@ -143,7 +151,7 @@ class MarkdownNoteDetail extends React.Component {
}
handleStarButtonClick (e) {
let { note } = this.state
const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred
@@ -168,22 +176,22 @@ class MarkdownNoteDetail extends React.Component {
}
handleTrashButtonClick (e) {
let { note } = this.state
const { note } = this.state
const { isTrashed } = note
if (isTrashed) {
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
let { note, dispatch } = this.props
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
@@ -205,7 +213,7 @@ class MarkdownNoteDetail extends React.Component {
}
handleUndoButtonClick (e) {
let { note } = this.state
const { note } = this.state
note.isTrashed = false
@@ -230,7 +238,7 @@ class MarkdownNoteDetail extends React.Component {
}
getToggleLockButton () {
return this.state.isLocked ? 'fa-lock' : 'fa-unlock'
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
}
handleDeleteKeyDown (e) {
@@ -255,13 +263,50 @@ class MarkdownNoteDetail extends React.Component {
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
}
render () {
let { data, config, location } = this.props
let { note } = this.state
let storageKey = note.storage
let folderKey = note.folder
print (e) {
ee.emit('print')
}
let options = []
handleSwitchMode (type) {
this.setState({ editorType: type }, () => {
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
})
}
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}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
return <MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
}
}
render () {
const { data, location } = 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) => {
options.push({
@@ -270,7 +315,7 @@ class MarkdownNoteDetail extends React.Component {
})
})
})
let 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]
const trashTopBar = <div styleName='info'>
<div styleName='info-left'>
@@ -280,7 +325,7 @@ class MarkdownNoteDetail extends React.Component {
/>
</div>
<div styleName='info-right'>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
@@ -297,10 +342,6 @@ class MarkdownNoteDetail extends React.Component {
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
@@ -315,36 +356,40 @@ class MarkdownNoteDetail extends React.Component {
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
<TodoListPercentage
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
{(() => {
const faClassName = `fa ${this.getToggleLockButton()}`
const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<i className={faClassName} styleName='lock-button' />
<span styleName='control-lockButton-tooltip'>
{this.state.isLocked ? 'Unlock' : 'Lock'}
</span>
<img styleName='iconInfo' src={imgSrc} />
</button>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-expand' styleName='fullScreen-button' />
</button>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
@@ -354,8 +399,9 @@ class MarkdownNoteDetail extends React.Component {
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
wordCount={note.content.split(' ').length}
letterCount={note.content.length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
/>
</div>
</div>
@@ -369,15 +415,7 @@ class MarkdownNoteDetail extends React.Component {
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={this.state.note.content}
storageKey={this.state.note.storage}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
{this.renderEditor()}
</div>
<StatusBar

View File

@@ -3,50 +3,42 @@
.root
absolute top right bottom
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
border-left 1px solid alpha(#DEDEDE, 60%)
background-color $ui-noteDetail-backgroundColor
box-shadow $note-detail-box-shadow
box-shadow none
padding 20px 40px
.lock-button
padding-bottom 3px
.control-lockButton
topBarButtonLight()
top 150px
topBarButtonRight()
.control-lockButton-tooltip
tooltip()
position fixed
pointer-events none
top 50px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-fullScreenButton
float right
padding 0 0 2px 0
topBarButtonLight()
.trashed-infopanel
top 40px
position relative
.body
absolute left right
left $note-detail-left-margin
right $note-detail-right-margin
left 0
right 0
top $info-height + $info-margin-under-border
bottom $statusBar-height
margin 0 45px
.body-noteEditor
absolute top bottom left right
body[data-theme="white"]
.root
box-shadow $note-detail-box-shadow
border none
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
border-left 1px solid $ui-dark-borderColor
.control-lockButton
topBarButtonDark()
@@ -56,3 +48,9 @@ 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

View File

@@ -1,33 +1,31 @@
@import('DetailVars')
$info-height = 60px
$info-margin-under-border = 27px
$info-height = 50px
$info-margin-under-border = 30px
.info
absolute top left right
left $note-detail-left-margin
right $note-detail-right-margin
left 0
right 0
height $info-height
border-bottom $ui-border
border-bottom 1px solid #eee
background-color $ui-noteDetail-backgroundColor
width 100%
display flex
align-items center
.info-left
float left
padding 0 5px
margin 0px 2px
.info-left-top
display inline-block
height $info-height
line-height $info-height
padding 0 10px
width 100%
display flex
align-items center
.info-left-top-folderSelect
padding 0 3px
height 34px
line-height 26px
display flex
align-items center
justify-content center
border-radius 3px
.info-left-button
width 34px
height 34px
@@ -48,18 +46,18 @@ $info-margin-under-border = 27px
.info-right
position absolute
right 0
top 0
background $ui-noteDetail-backgroundColor
right 40px
top 60px
bottom 1px
padding-left 30px
z-index 101
.undo-button
width 34px
height 34px
border-radius 17px
font-size 14px
margin 15px 7px
margin 5px 0px
border none
color $ui-button-color
display flex
@@ -72,6 +70,7 @@ $info-margin-under-border = 27px
border-color $ui-button--active-backgroundColor
&:hover
background-color alpha($ui-button--hover-backgroundColor, 60%)
transition 0.2s
.control-lockButton-tooltip
opacity 1
@@ -96,4 +95,10 @@ body[data-theme="dark"]
background-color $ui-dark-noteDetail-backgroundColor
.undo-button
topBarButtonDark()
topBarButtonDark()
body[data-theme="solarized-dark"]
.info
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor

View File

@@ -0,0 +1,21 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
const PermanentDeleteButton = ({
onClick
}) => (
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Permanent Delete</span>
</button>
)
PermanentDeleteButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(PermanentDeleteButton, styles)

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetNoteDetail.styl'
import CodeEditor from 'browser/components/CodeEditor'
@@ -18,6 +19,7 @@ import _ from 'lodash'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton'
import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
@@ -60,7 +62,7 @@ class SnippetNoteDetail extends React.Component {
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
if (this.saveQueue != null) this.saveNow()
let nextNote = Object.assign({
const nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
@@ -69,7 +71,7 @@ class SnippetNoteDetail extends React.Component {
snippetIndex: 0,
note: nextNote
}, () => {
let { snippets } = this.state.note
const { snippets } = this.state.note
snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
@@ -83,7 +85,7 @@ class SnippetNoteDetail extends React.Component {
}
handleChange (e) {
let { note } = this.state
const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value
note.description = this.refs.description.value
@@ -105,7 +107,7 @@ class SnippetNoteDetail extends React.Component {
}
saveNow () {
let { note, dispatch } = this.props
const { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
@@ -121,11 +123,11 @@ class SnippetNoteDetail extends React.Component {
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
const { note } = this.state
const value = this.refs.folder.value
const splitted = value.split('-')
const newStorageKey = splitted.shift()
const newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
@@ -134,7 +136,7 @@ class SnippetNoteDetail extends React.Component {
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
const { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
@@ -154,7 +156,7 @@ class SnippetNoteDetail extends React.Component {
}
handleStarButtonClick (e) {
let { note } = this.state
const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred
@@ -171,22 +173,22 @@ class SnippetNoteDetail extends React.Component {
}
handleTrashButtonClick (e) {
let { note } = this.state
const { note } = this.state
const { isTrashed } = note
if (isTrashed) {
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
let { note, dispatch } = this.props
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
@@ -208,7 +210,7 @@ class SnippetNoteDetail extends React.Component {
}
handleUndoButtonClick (e) {
let { note } = this.state
const { note } = this.state
note.isTrashed = false
@@ -234,10 +236,31 @@ class SnippetNoteDetail extends React.Component {
})
}
handleTabDragStart (e, index) {
e.dataTransfer.setData('text/plain', index)
}
handleTabDrop (e, index) {
const oldIndex = parseInt(e.dataTransfer.getData('text'))
const snippets = this.state.note.snippets.slice()
const draggedSnippet = snippets[oldIndex]
snippets[oldIndex] = snippets[index]
snippets[index] = draggedSnippet
const snippetIndex = index
const note = Object.assign({}, this.state.note, {snippets})
this.setState({ note, snippetIndex }, () => {
this.save()
this.refs['code-' + index].reload()
this.refs['code-' + oldIndex].reload()
})
}
handleTabDeleteButtonClick (e, index) {
if (this.state.note.snippets.length > 1) {
if (this.state.note.snippets[index].content.trim().length > 0) {
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a snippet',
detail: 'This work cannot be undone.',
@@ -266,11 +289,16 @@ class SnippetNoteDetail extends React.Component {
}
renameSnippetByIndex (index, name) {
let snippets = this.state.note.snippets.slice()
const snippets = this.state.note.snippets.slice()
snippets[index].name = name
let syntax = CodeMirror.findModeByFileName(name.trim())
let mode = syntax != null ? syntax.name : null
if (mode != null) snippets[index].mode = mode
const syntax = CodeMirror.findModeByFileName(name.trim())
const mode = syntax != null ? syntax.name : null
if (mode != null) {
snippets[index].mode = mode
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SNIPPET_LANG', {
name: mode
})
}
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
this.setState({
@@ -282,7 +310,7 @@ class SnippetNoteDetail extends React.Component {
handleModeOptionClick (index, name) {
return (e) => {
let snippets = this.state.note.snippets.slice()
const snippets = this.state.note.snippets.slice()
snippets[index].mode = name
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
@@ -291,12 +319,16 @@ class SnippetNoteDetail extends React.Component {
}, () => {
this.save()
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('SELECT_LANG', {
name
})
}
}
handleCodeChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
this.setState({
@@ -323,7 +355,7 @@ class SnippetNoteDetail extends React.Component {
break
case 76:
{
let isSuper = global.process.platform === 'darwin'
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
@@ -334,7 +366,7 @@ class SnippetNoteDetail extends React.Component {
break
case 84:
{
let isSuper = global.process.platform === 'darwin'
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
@@ -347,7 +379,7 @@ class SnippetNoteDetail extends React.Component {
}
handleModeButtonClick (e, index) {
let menu = new Menu()
const menu = new Menu()
CodeMirror.modeInfo.forEach((mode) => {
menu.append(new MenuItem({
label: mode.name,
@@ -388,8 +420,8 @@ class SnippetNoteDetail extends React.Component {
}
handleIndentSizeItemClick (e, indentSize) {
let { config, dispatch } = this.props
let editor = Object.assign({}, config.editor, {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
indentSize
})
ConfigManager.set({
@@ -404,8 +436,8 @@ class SnippetNoteDetail extends React.Component {
}
handleIndentTypeItemClick (e, indentType) {
let { config, dispatch } = this.props
let editor = Object.assign({}, config.editor, {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
indentType
})
ConfigManager.set({
@@ -424,14 +456,14 @@ class SnippetNoteDetail extends React.Component {
}
addSnippet () {
let { note } = this.state
const { note } = this.state
note.snippets = note.snippets.concat([{
name: '',
mode: 'Plain Text',
content: ''
}])
let snippetIndex = note.snippets.length - 1
const snippetIndex = note.snippets.length - 1
this.setState({
note,
@@ -477,19 +509,19 @@ class SnippetNoteDetail extends React.Component {
}
render () {
let { data, config, location } = this.props
let { note } = this.state
const { data, config, location } = this.props
const { note } = this.state
let storageKey = note.storage
let folderKey = note.folder
const storageKey = note.storage
const folderKey = note.folder
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
const tabList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
return <SnippetTab
key={index}
@@ -500,11 +532,13 @@ class SnippetNoteDetail extends React.Component {
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
onRename={(name) => this.renameSnippetByIndex(index, name)}
isDeletable={note.snippets.length > 1}
onDragStart={(e) => this.handleTabDragStart(e, index)}
onDrop={(e) => this.handleTabDrop(e, index)}
/>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
@@ -532,6 +566,7 @@ class SnippetNoteDetail extends React.Component {
indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers}
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
@@ -539,7 +574,7 @@ class SnippetNoteDetail extends React.Component {
</div>
})
let options = []
const options = []
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
options.push({
@@ -548,7 +583,7 @@ class SnippetNoteDetail extends React.Component {
})
})
})
let 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]
const trashTopBar = <div styleName='info'>
<div styleName='info-left'>
@@ -558,7 +593,7 @@ class SnippetNoteDetail extends React.Component {
/>
</div>
<div styleName='info-right'>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
@@ -575,10 +610,6 @@ class SnippetNoteDetail extends React.Component {
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
@@ -595,15 +626,21 @@ class SnippetNoteDetail extends React.Component {
/>
</div>
<div styleName='info-right'>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<i className='fa fa-expand' styleName='fullScreen-button' />
</button>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='control-fullScreenButton' title='Fullscreen'
onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
</button>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}

View File

@@ -3,23 +3,21 @@
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
border-left 1px solid alpha(#DEDEDE, 60%)
background-color $ui-noteDetail-backgroundColor
box-shadow $note-detail-box-shadow
box-shadow none
.body
absolute left right
left $note-detail-left-margin
right $note-detail-right-margin
left $snippet-note-detail-left-margin
right $snippet-note-detail-right-margin
top $info-height + $info-margin-under-border
bottom $statusBar-height
background-color $ui-noteDetail-backgroundColor
.body .description
absolute top left right
height 80px
height 50px
.body .description textarea
outline none
@@ -27,14 +25,14 @@
height 100%
width 100%
resize none
border none
padding 10px
border 1px solid $ui-borderColor
padding 2px 5px
line-height 1.6
background-color $ui-noteDetail-backgroundColor
.tabList
absolute left right
top 80px
top 55px
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
@@ -45,35 +43,42 @@
overflow hidden
.tabList .plusButton
navButtonColor()
navWhiteButtonColor()
width 30px
.tabView
absolute left right bottom
top 130px
top 100px
.tabView-content
absolute top left right bottom
.override
absolute bottom left
bottom 1px
left 60px
height 23px
z-index 1
z-index 101
button
navButtonColor()
height 24px
padding 0 6px
&:hover
color $ui-active-color
&:active .update-icon
color white
color $ui-active-color
.control-fullScreenButton
float right
padding 0 0 2px 0
topBarButtonLight()
top 80px
margin-bottom 10px
topBarButtonRight()
body[data-theme="white"]
.root
box-shadow $note-detail-box-shadow
border none
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
@@ -83,6 +88,7 @@ body[data-theme="dark"]
.body .description textarea
background-color $ui-dark-noteDetail-backgroundColor
color $ui-dark-text-color
border 1px solid $ui-dark-borderColor
.tabList
background-color $ui-button--active-backgroundColor
@@ -103,3 +109,20 @@ 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
.body
background-color $ui-solarized-dark-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
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl'
import _ from 'lodash'
@@ -31,7 +32,7 @@ class StarButton extends React.Component {
}
render () {
let { className } = this.props
const { className } = this.props
return (
<button className={_.isString(className)
@@ -45,14 +46,14 @@ class StarButton extends React.Component {
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<i styleName='icon'
className={this.state.isActive || this.props.isActive
? 'fa fa-star'
: 'fa fa-star-o'
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'>Star</span>
</button>
)
}

View File

@@ -1,47 +1,40 @@
.root
left 7px
top 0
padding 0
color alpha($ui-favorite-star-button-color, 60%)
top 45px
topBarButtonRight()
&:hover
transition 0.15s
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-favorite-star-button-color
&:active
transition 0.15s
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-favorite-star-button-color
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
right 0
width 100%
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.15s
color $ui-favorite-star-button-color
background-color alpha($ui-button--active-backgroundColor, 40%)
&:active
transition 0.15s
color $ui-favorite-star-button-color
background-color alpha($ui-button--active-backgroundColor, 40%)
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
.icon
transition transform 0.15s
body[data-theme="dark"]
.root
topBarButtonDark()
&:hover
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-favorite-star-button-color
&:active
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-favorite-star-button-color
.root--active
@extend .root
color $ui-favorite-star-button-color
&:hover
transition 0.15s
color $ui-favorite-star-button-color
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
@@ -37,6 +38,10 @@ class TagSelect extends React.Component {
}
}
handleNewTagBlur (e) {
this.submitTag()
}
removeLastTag () {
let { value } = this.props
@@ -59,7 +64,7 @@ class TagSelect extends React.Component {
submitTag () {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
let { value } = this.props
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
const newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
if (newTag.length <= 0) {
this.setState({
@@ -101,9 +106,9 @@ class TagSelect extends React.Component {
}
render () {
let { value, className } = this.props
const { value, className } = this.props
let tagList = _.isArray(value)
const tagList = _.isArray(value)
? value.map((tag) => {
return (
<span styleName='tag'
@@ -113,7 +118,7 @@ class TagSelect extends React.Component {
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
>
<i className='fa fa-times fa-fw tag-removeButton-icon' />
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
</button>
</span>
)
@@ -134,6 +139,7 @@ class TagSelect extends React.Component {
placeholder='Add tag...'
onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
onBlur={(e) => this.handleNewTagBlur(e)}
/>
</div>
)

View File

@@ -1,61 +1,53 @@
.root
display inline-block
display flex
align-items center
user-select none
height 23px
vertical-align middle
width 300px
width 100%
overflow-x scroll
white-space nowrap
margin-right 10px
.root::-webkit-scrollbar
display none
.tag
display inline-block
margin 1px 3px
padding 0
height 20px
background-color alpha($ui-tag-backgroundColor, 10%)
border-radius 3px
overflow hidden
display flex
align-items center
margin 0px 2px
padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px
position relative
clearfix()
.tag-removeButton
float right
height 20px
width 18px
margin 0
padding 0
border-style solid
border-width 0
border-radius 20px
line-height 18px
background-color transparent
color $ui-button-color
position absolute
right 6px
.tag-removeButton-icon
width 5px
padding-right 4px
.tag-label
font-size 11px
font-weight 600
color: alpha($ui-text-color, 80%)
float left
height 20px
line-height 20px
padding 0 6px
font-size 13px
color: $ui-text-color
padding 4px 16px 4px 8px
.newTag
display inline-block
margin 2px 0 15px 2px
vertical-align middle
height 18px
box-sizing borde-box
box-sizing border-box
border none
background-color transparent
outline none
padding 0 4px
font-size 13px
body[data-theme="dark"]
.tag
@@ -72,4 +64,20 @@ body[data-theme="dark"]
.newTag
border-color none
background-color transparent
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.tag
background-color $ui-solarized-dark-tag-backgroundColor
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-solarized-dark-text-color
.newTag
border-color none
background-color transparent
color $ui-solarized-dark-text-color

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
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-split-on.svg' : '../resources/icon/icon-mode-split-on-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-markdown-off-active.svg' : '../resources/icon/icon-mode-markdown-off.svg'} />
</div>
<span styleName='tooltip'>Toggle Mode</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
}
export default CSSModules(ToggleModeButton, styles)

View File

@@ -0,0 +1,61 @@
.control-toggleModeButton
border 1px solid #eee
height 34px
display flex
align-items center
div
width 40px
height 100%
background-color #f9f9f9
display flex
align-items center
justify-content center
cursor pointer
&:first-child
border-right 1px solid #eee
.active
background-color #fff
box-shadow 2px 0px 7px #eee
z-index 1
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 47px
right 11px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
border 1px solid #444444
div
background-color $ui-dark-noteDetail-backgroundColor
&:first-child
border-right 1px solid #444444
.active
background-color #3A404C
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
border 1px solid #586E75
div
background-color $ui-solarized-dark-noteDetail-backgroundColor
&:first-child
border-right 1px solid #586E75
.active
background-color #002B36
box-shadow 2px 0px 7px #222222

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
@@ -8,7 +9,8 @@ const TrashButton = ({
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
<i className='fa fa-trash trashButton' styleName='info-button' />
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Trash</span>
</button>
)

View File

@@ -1,10 +1,28 @@
.control-trashButton
float right
topBarButtonLight()
top 115px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
right 0
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-trashButton--in-trash
top 60px
topBarButtonRight()
.trashButton
padding 0px
margin 15px 0
body[data-theme="dark"]
.control-trashButton

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Detail.styl'
import _ from 'lodash'
@@ -32,12 +33,12 @@ class Detail extends React.Component {
}
render () {
let { location, data, config } = this.props
const { location, data, config } = this.props
let note = null
if (location.query.key != null) {
let splitted = location.query.key.split('-')
let storageKey = splitted.shift()
let noteKey = splitted.shift()
const splitted = location.query.key.split('-')
const storageKey = splitted.shift()
const noteKey = splitted.shift()
note = data.noteMap.get(storageKey + '-' + noteKey)
}

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Main.styl'
import { connect } from 'react-redux'
@@ -11,15 +12,11 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
import mixpanel from 'browser/main/lib/mixpanel'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
function focused () {
mixpanel.track('MAIN_FOCUSED')
}
class Main extends React.Component {
constructor (props) {
super(props)
@@ -27,7 +24,7 @@ class Main extends React.Component {
mobileAnalytics.initAwsMobileAnalytics()
}
let { config } = props
const { config } = props
this.state = {
isRightSliderFocused: false,
@@ -43,7 +40,7 @@ class Main extends React.Component {
}
getChildContext () {
let { status, config } = this.props
const { status, config } = this.props
return {
status,
@@ -52,10 +49,14 @@ class Main extends React.Component {
}
componentDidMount () {
let { dispatch, config } = this.props
const { dispatch, config } = this.props
if (config.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else if (config.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else if (config.ui.theme === 'solarized-dark') {
document.body.setAttribute('data-theme', 'solarized-dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
@@ -75,11 +76,9 @@ class Main extends React.Component {
})
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
window.addEventListener('focus', focused)
}
componentWillUnmount () {
window.removeEventListener('focus', focused)
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
}
@@ -103,8 +102,8 @@ class Main extends React.Component {
this.setState({
isRightSliderFocused: false
}, () => {
let { dispatch } = this.props
let newListWidth = this.state.listWidth
const { dispatch } = this.props
const newListWidth = this.state.listWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({listWidth: newListWidth})
dispatch({
@@ -119,8 +118,8 @@ class Main extends React.Component {
this.setState({
isLeftSliderFocused: false
}, () => {
let { dispatch } = this.props
let navWidth = this.state.navWidth
const { dispatch } = this.props
const navWidth = this.state.navWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({ navWidth })
dispatch({
@@ -133,7 +132,7 @@ class Main extends React.Component {
handleMouseMove (e) {
if (this.state.isRightSliderFocused) {
let offset = this.refs.body.getBoundingClientRect().left
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
@@ -186,7 +185,10 @@ class Main extends React.Component {
}
render () {
let { config } = this.props
const { config } = this.props
// the width of the navigation bar when it is folded/collapsed
const foldedNavigationWidth = 44
return (
<div
@@ -216,7 +218,7 @@ class Main extends React.Component {
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body'
ref='body'
style={{left: config.isSideNavFolded ? 44 : this.state.navWidth}}
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
>
<TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [

View File

@@ -4,7 +4,6 @@
height $topBar-height - 1
margin-left: auto;
width: 64px;
margin-right: -15px;
.root--expanded
@extend .root
@@ -14,10 +13,8 @@ $control-height = 34px
.control
position absolute
top 13px
left 8px
right 8px
right 7px
height $control-height
overflow hidden
display flex
.control-newNoteButton
@@ -35,10 +32,11 @@ $control-height = 34px
.control-newNoteButton-tooltip
tooltip()
position fixed
position absolute
pointer-events none
top 50px
left 433px
top 26px
right -43px
width 124px
z-index 200
padding 5px
line-height normal
@@ -46,6 +44,13 @@ $control-height = 34px
opacity 0
transition 0.1s
body[data-theme="white"]
.root, .root--expanded
background-color $ui-white-noteList-backgroundColor
.control-newNoteButton
background-color $ui-white-noteList-backgroundColor
body[data-theme="dark"]
.root, .root--expanded
background-color $ui-dark-noteList-backgroundColor
@@ -66,3 +71,7 @@ body[data-theme="dark"]
.control-newNoteButton-tooltip
darkTooltip()
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor

View File

@@ -1,12 +1,11 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteButton.styl'
import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import eventEmitter from 'browser/main/lib/eventEmitter'
import dataApi from 'browser/main/lib/dataApi'
const { remote } = require('electron')
const { dialog } = remote
@@ -34,7 +33,7 @@ class NewNoteButton extends React.Component {
}
handleNewNoteButtonClick (e) {
const { config, location, dispatch } = this.props
const { location, dispatch } = this.props
const { storage, folder } = this.resolveTargetFolder()
modal.open(NewNoteModal, {
@@ -51,7 +50,7 @@ class NewNoteButton extends React.Component {
// Find first storage
if (storage == null) {
for (let kv of data.storageMap) {
for (const kv of data.storageMap) {
storage = kv[1]
break
}
@@ -85,7 +84,7 @@ class NewNoteButton extends React.Component {
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<i className='fa fa-pencil-square-o' />
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
Make a Note {OSX ? '⌘' : '^'} + n
</span>

View File

@@ -21,14 +21,14 @@ $control-height = 30px
.control-sortBy-select
appearance: none;
margin-left 3px
margin-left 5px
color $ui-inactive-text-color
padding 0
border none
background-color transparent
outline none
cursor pointer
font-size 11px
font-size 12px
&:hover
transition 0.2s
color $ui-text-color
@@ -59,6 +59,13 @@ $control-height = 30px
top $control-height
overflow auto
body[data-theme="white"]
.root
background-color $ui-white-noteList-backgroundColor
.control
background-color $ui-white-noteList-backgroundColor
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
@@ -81,4 +88,29 @@ body[data-theme="dark"]
.control-button--active
color $ui-dark-text-color
&:active
color $ui-dark-text-color
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

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteList.styl'
import moment from 'moment'
@@ -11,8 +12,10 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
import searchFromNotes from 'browser/lib/search'
import fs from 'fs'
import { hashHistory } from 'react-router'
import markdown from 'browser/lib/markdown'
import markdown from 'browser/lib/markdownTextHelper'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import store from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
@@ -29,6 +32,18 @@ function sortByUpdatedAt (a, b) {
return new Date(b.updatedAt) - new Date(a.updatedAt)
}
function findNoteByKey (notes, noteKey) {
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
}
function findNotesByKeys (notes, noteKeys) {
return notes.filter((note) => noteKeys.includes(getNoteKey(note)))
}
function getNoteKey (note) {
return `${note.storage}-${note.key}`
}
class NoteList extends React.Component {
constructor (props) {
super(props)
@@ -48,9 +63,19 @@ class NoteList extends React.Component {
}
this.importFromFileHandler = this.importFromFile.bind(this)
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
shiftKeyDown: false,
selectedNoteKeys: []
}
this.contextNotes = []
}
componentDidMount () {
@@ -85,10 +110,11 @@ class NoteList extends React.Component {
}
componentDidUpdate (prevProps) {
let { location } = this.props
const { location } = this.props
if (this.notes.length > 0 && location.query.key == null) {
let { router } = this.context
const { router } = this.context
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
router.replace({
pathname: location.pathname,
query: {
@@ -100,20 +126,18 @@ class NoteList extends React.Component {
// Auto scroll
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
let targetIndex = _.findIndex(this.notes, (note) => {
return note != null && note.storage + '-' + note.key === location.query.key
})
const targetIndex = this.getTargetIndex()
if (targetIndex > -1) {
let list = this.refs.list
let item = list.childNodes[targetIndex]
const list = this.refs.list
const item = list.childNodes[targetIndex]
if (item == null) return false
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
}
let overflowAbove = list.scrollTop > item.offsetTop
const overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
@@ -121,29 +145,54 @@ class NoteList extends React.Component {
}
}
focusNote (selectedNoteKeys, noteKey) {
const { router } = this.context
const { location } = this.props
this.setState({
selectedNoteKeys
})
router.push({
pathname: location.pathname,
query: {
key: noteKey
}
})
}
getNoteKeyFromTargetIndex (targetIndex) {
const note = Object.assign({}, this.notes[targetIndex])
const noteKey = getNoteKey(note)
return noteKey
}
selectPriorNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let { selectedNoteKeys, shiftKeyDown } = this.state
let targetIndex = _.findIndex(this.notes, (note) => {
return note.storage + '-' + note.key === location.query.key
})
let targetIndex = this.getTargetIndex()
if (targetIndex === 0) {
return
}
targetIndex--
if (targetIndex < 0) targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
if (!shiftKeyDown) { selectedNoteKeys = [] }
const priorNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
if (selectedNoteKeys.includes(priorNoteKey)) {
selectedNoteKeys.pop()
} else {
selectedNoteKeys.push(priorNoteKey)
}
this.focusNote(selectedNoteKeys, priorNoteKey)
ee.emit('list:moved')
}
selectNextNote () {
@@ -152,25 +201,31 @@ class NoteList extends React.Component {
}
let { router } = this.context
let { location } = this.props
let { selectedNoteKeys, shiftKeyDown } = this.state
let targetIndex = _.findIndex(this.notes, (note) => {
return note.storage + '-' + note.key === location.query.key
})
let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1
if (targetIndex === this.notes.length - 1) {
if (isTargetLastNote && shiftKeyDown) {
return
} else if (isTargetLastNote) {
targetIndex = 0
} else {
targetIndex++
if (targetIndex < 0) targetIndex = 0
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
else if (targetIndex > this.notes.length - 1) targetIndex = this.notes.length - 1
}
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
if (!shiftKeyDown) { selectedNoteKeys = [] }
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
if (selectedNoteKeys.includes(nextNoteKey)) {
selectedNoteKeys.pop()
} else {
selectedNoteKeys.push(nextNoteKey)
}
this.focusNote(selectedNoteKeys, nextNoteKey)
ee.emit('list:moved')
}
@@ -183,23 +238,21 @@ class NoteList extends React.Component {
const { router } = this.context
const { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.storage + '-' + note.key === noteHash
})
let targetIndex = this.getTargetIndex()
if (targetIndex < 0) targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
const selectedNoteKeys = []
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
selectedNoteKeys.push(nextNoteKey)
this.focusNote(selectedNoteKeys, nextNoteKey)
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
const { shiftKeyDown } = this.state
if (e.metaKey || e.ctrlKey) return true
if (e.keyCode === 65 && !e.shiftKey) {
@@ -209,7 +262,7 @@ class NoteList extends React.Component {
if (e.keyCode === 68) {
e.preventDefault()
ee.emit('detail:delete')
this.deleteNote()
}
if (e.keyCode === 69) {
@@ -226,60 +279,110 @@ class NoteList extends React.Component {
e.preventDefault()
this.selectNextNote()
}
if (e.shiftKey) {
this.setState({ shiftKeyDown: true })
}
}
handleNoteListKeyUp (e) {
if (!e.shiftKey) {
this.setState({ shiftKeyDown: false })
}
}
getNotes () {
let { data, params, location } = this.props
let { router } = this.context
const { data, params, location } = this.props
if (location.pathname.match(/\/home/)) {
return data.noteMap.map((note) => note)
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) {
const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
return allNotes
}
if (location.pathname.match(/\/starred/)) {
return data.starredSet.toJS()
.map((uniqueKey) => data.noteMap.get(uniqueKey))
const starredNotes = data.starredSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
this.contextNotes = starredNotes
return starredNotes
}
if (location.pathname.match(/\/searched/)) {
const searchInputText = document.getElementsByClassName('searchInput')[0].value
if (searchInputText === '') {
router.push('/home')
return this.sortByPin(this.contextNotes)
}
return searchFromNotes(this.notes, searchInputText)
return searchFromNotes(this.contextNotes, searchInputText)
}
if (location.pathname.match(/\/trashed/)) {
return data.trashedSet.toJS()
.map((uniqueKey) => data.noteMap.get(uniqueKey))
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
this.contextNotes = trashedNotes
return trashedNotes
}
let storageKey = params.storageKey
let folderKey = params.folderKey
let storage = data.storageMap.get(storageKey)
if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) {
let storageNoteSet = data.storageNoteMap
.get(storage.key)
if (storageNoteSet == null) storageNoteSet = []
return storageNoteSet
.map((uniqueKey) => data.noteMap.get(uniqueKey))
if (location.pathname.match(/\/tags/)) {
return data.noteMap.map(note => {
return note
}).filter(note => {
return note.tags.includes(params.tagname)
})
}
let folderNoteKeyList = data.folderNoteMap
.get(storage.key + '-' + folder.key)
return this.getContextNotes()
}
return folderNoteKeyList != null
? folderNoteKeyList
.map((uniqueKey) => data.noteMap.get(uniqueKey))
: []
// get notes in the current folder
getContextNotes () {
const { data, params } = this.props
const storageKey = params.storageKey
const folderKey = params.folderKey
const storage = data.storageMap.get(storageKey)
if (storage === undefined) return []
const folder = _.find(storage.folders, {key: folderKey})
if (folder === undefined) {
const storageNoteSet = data.storageNoteMap.get(storage.key) || []
return storageNoteSet.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
const folderNoteKeyList = data.folderNoteMap.get(`${storage.key}-${folder.key}`) || []
return folderNoteKeyList.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
sortByPin (unorderedNotes) {
const pinnedNotes = []
const unpinnedNotes = []
unorderedNotes.forEach((note) => {
if (note.isPinned) {
pinnedNotes.push(note)
} else {
unpinnedNotes.push(note)
}
})
return pinnedNotes.concat(unpinnedNotes)
}
handleNoteClick (e, uniqueKey) {
let { router } = this.context
let { location } = this.props
let { shiftKeyDown, selectedNoteKeys } = this.state
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
this.setState({
selectedNoteKeys: newSelectedNoteKeys
})
return
}
if (!shiftKeyDown) {
selectedNoteKeys = []
}
selectedNoteKeys.push(uniqueKey)
this.setState({
selectedNoteKeys
})
router.push({
pathname: location.pathname,
@@ -290,9 +393,9 @@ class NoteList extends React.Component {
}
handleSortByChange (e) {
let { dispatch } = this.props
const { dispatch } = this.props
let config = {
const config = {
sortBy: e.target.value
}
@@ -304,9 +407,9 @@ class NoteList extends React.Component {
}
handleListStyleButtonClick (e, style) {
let { dispatch } = this.props
const { dispatch } = this.props
let config = {
const config = {
listStyle: style
}
@@ -318,10 +421,7 @@ class NoteList extends React.Component {
}
alertIfSnippet () {
let { location } = this.props
const targetIndex = _.findIndex(this.notes, (note) => {
return `${note.storage}-${note.key}` === location.query.key
})
const targetIndex = this.getTargetIndex()
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
@@ -333,13 +433,129 @@ class NoteList extends React.Component {
}
handleDragStart (e, note) {
const noteData = JSON.stringify(note)
const { selectedNoteKeys } = this.state
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const noteData = JSON.stringify(selectedNotes)
e.dataTransfer.setData('note', noteData)
this.setState({ selectedNoteKeys: [] })
}
handleNoteContextMenu (e, uniqueKey) {
const { location } = this.props
const { selectedNoteKeys } = this.state
const note = findNoteByKey(this.notes, uniqueKey)
const noteKey = getNoteKey(note)
if (selectedNoteKeys.length === 0 || !selectedNoteKeys.includes(noteKey)) {
this.handleNoteClick(e, uniqueKey)
}
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
const deleteLabel = 'Delete Note'
const menu = new Menu()
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
menu.append(new MenuItem({
label: pinLabel,
click: this.pinToTop
}))
}
menu.append(new MenuItem({
label: deleteLabel,
click: this.deleteNote
}))
menu.popup()
}
pinToTop () {
const { selectedNoteKeys } = this.state
const { dispatch } = this.props
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
Promise.all(
selectedNotes.map((note) => {
note.isPinned = !note.isPinned
return dataApi
.updateNote(note.storage, note.key, note)
})
)
.then((updatedNotes) => {
updatedNotes.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note
})
})
})
this.setState({ selectedNoteKeys: [] })
}
deleteNote () {
const { dispatch } = this.props
const { selectedNoteKeys } = this.state
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
if (firstNote.isTrashed) {
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
Promise.all(
selectedNoteKeys.map((uniqueKey) => {
const storageKey = uniqueKey.split('-')[0]
const noteKey = uniqueKey.split('-')[1]
return dataApi
.deleteNote(storageKey, noteKey)
})
)
.then((data) => {
data.forEach((item) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: item.storageKey,
noteKey: item.noteKey
})
})
})
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
console.log('Notes were all deleted')
} else {
Promise.all(
selectedNotes.map((note) => {
note.isTrashed = true
return dataApi
.updateNote(note.storage, note.key, note)
})
)
.then((newNotes) => {
newNotes.forEach((newNote) => {
dispatch({
type: 'UPDATE_NOTE',
note: newNote
})
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
console.log('Notes went to trash')
})
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
})
}
this.setState({ selectedNoteKeys: [] })
}
importFromFile () {
const { dispatch, location } = this.props
const options = {
filters: [
{ name: 'Documents', extensions: ['md', 'txt'] }
@@ -347,26 +563,41 @@ class NoteList extends React.Component {
properties: ['openFile', 'multiSelections']
}
const targetIndex = _.findIndex(this.notes, (note) => {
return note !== null && `${note.storage}-${note.key}` === location.query.key
})
const storageKey = this.notes[targetIndex].storage
const folderKey = this.notes[targetIndex].folder
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => {
if (filepaths === undefined) return
filepaths.forEach((filepath) => {
fs.readFile(filepath, (err, data) => {
if (err) throw Error('File reading error: ', err)
this.addNotesFromFiles(filepaths)
})
}
handleDrop (e) {
e.preventDefault()
const { location } = this.props
const filepaths = Array.from(e.dataTransfer.files).map(file => { return file.path })
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
}
// Add notes to the current folder
addNotesFromFiles (filepaths) {
const { dispatch, location } = this.props
const { storage, folder } = this.resolveTargetFolder()
if (filepaths === undefined) return
filepaths.forEach((filepath) => {
fs.readFile(filepath, (err, data) => {
if (err) throw Error('File reading error: ', err)
fs.stat(filepath, (err, {mtime, birthtime}) => {
if (err) throw Error('File stat reading error: ', err)
const content = data.toString()
const newNote = {
content: content,
folder: folderKey,
folder: folder.key,
title: markdown.strip(findNoteTitle(content)),
type: 'MARKDOWN_NOTE'
type: 'MARKDOWN_NOTE',
createdAt: birthtime,
updatedAt: mtime
}
dataApi.createNote(storageKey, newNote)
dataApi.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
@@ -374,7 +605,7 @@ class NoteList extends React.Component {
})
hashHistory.push({
pathname: location.pathname,
query: {key: `${note.storage}-${note.key}`}
query: {key: getNoteKey(note)}
})
})
})
@@ -382,32 +613,92 @@ class NoteList extends React.Component {
})
}
getTargetIndex () {
const { location } = this.props
const targetIndex = _.findIndex(this.notes, (note) => {
return getNoteKey(note) === location.query.key
})
return targetIndex
}
resolveTargetFolder () {
const { data, params } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (const kv of data.storageMap) {
storage = kv[1]
break
}
}
if (storage == null) this.showMessageBox('No storage for importing note(s)')
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
if (folder == null) this.showMessageBox('No folder for importing note(s)')
return {
storage,
folder
}
}
showMessageBox (message) {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: message,
buttons: ['OK']
})
}
render () {
let { location, notes, config, dispatch } = this.props
let { selectedNoteKeys } = this.state
let sortFunc = config.sortBy === 'CREATED_AT'
? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL'
? sortByAlphabetical
: sortByUpdatedAt
this.notes = notes = this.getNotes()
.sort(sortFunc)
.filter((note) => {
// this is for the trash box
if (note.isTrashed !== true || location.pathname === '/trashed') return true
})
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
? this.getNotes().sort(sortFunc)
: this.sortByPin(this.getNotes().sort(sortFunc))
this.notes = notes = sortedNotes.filter((note) => {
// this is for the trash box
if (note.isTrashed !== true || location.pathname === '/trashed') return true
})
let noteList = notes
moment.locale('en', {
relativeTime: {
future: 'in %s',
past: '%s ago',
s: '%ds',
ss: '%ss',
m: '1m',
mm: '%dm',
h: 'an hour',
hh: '%dh',
d: '1d',
dd: '%dd',
M: '1M',
MM: '%dM',
y: '1Y',
yy: '%dY'
}
})
const noteList = notes
.map(note => {
if (note == null) {
return null
}
const isDefault = config.listStyle === 'DEFAULT'
const isActive = location.query.key === note.storage + '-' + note.key
const uniqueKey = getNoteKey(note)
const isActive = selectedNoteKeys.includes(uniqueKey)
const dateDisplay = moment(
config.sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt
).fromNow()
).fromNow('D')
const key = `${note.storage}-${note.key}`
if (isDefault) {
@@ -416,9 +707,11 @@ class NoteList extends React.Component {
isActive={isActive}
note={note}
dateDisplay={dateDisplay}
key={key}
key={uniqueKey}
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname}
/>
)
}
@@ -427,9 +720,11 @@ class NoteList extends React.Component {
<NoteItemSimple
isActive={isActive}
note={note}
key={key}
key={uniqueKey}
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname}
/>
)
})
@@ -438,16 +733,17 @@ class NoteList extends React.Component {
<div className='NoteList'
styleName='root'
style={this.props.style}
onDrop={(e) => this.handleDrop(e)}
>
<div styleName='control'>
<div styleName='control-sortBy'>
<i className='fa fa-bolt' />
<i className='fa fa-angle-down' />
<select styleName='control-sortBy-select'
value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)}
>
<option value='UPDATED_AT'>Last Updated</option>
<option value='CREATED_AT'>Creation Time</option>
<option value='UPDATED_AT'>Updated</option>
<option value='CREATED_AT'>Created</option>
<option value='ALPHABETICAL'>Alphabetically</option>
</select>
</div>
@@ -458,7 +754,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
>
<i className='fa fa-th-large' />
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
</button>
<button styleName={config.listStyle === 'SMALL'
? 'control-button--active'
@@ -466,7 +762,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
>
<i className='fa fa-list-ul' />
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' />
</button>
</div>
</div>
@@ -474,6 +770,7 @@ class NoteList extends React.Component {
ref='list'
tabIndex='-1'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
onKeyUp={this.handleNoteListKeyUp}
>
{noteList}
</div>

View File

@@ -0,0 +1,24 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
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'>Notes</span>
</button>
)
ListButton.propTypes = {
onClick: PropTypes.func.isRequired,
isTagActive: PropTypes.bool.isRequired
}
export default CSSModules(ListButton, styles)

View File

@@ -0,0 +1,19 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl'
const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>Preferences</span>
</button>
)
PreferenceButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(PreferenceButton, styles)

View File

@@ -0,0 +1,51 @@
.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

View File

@@ -1,62 +1,57 @@
.root
absolute top left bottom
width $sideNav-width
background-color #f9f9f9
background-color #2E3235
user-select none
color $ui-text-color
height: 100vh
display: flex
flex-direction column
.top
height $topBar-height
padding-bottom 15px
.top-menu
navButtonColor()
height $topBar-height
padding 0 15px
font-size 12px
width 100%
text-align left
&:hover
color $ui-text-color
&:active, &:active:hover
color $ui-text-color
background-color alpha($ui-button--active-backgroundColor, 20%)
.switch-buttons
background-color transparent
border 0
margin 24px auto 4px 14px
display flex
text-align center
.top-menu-label
margin-left 5px
overflow ellipsis
opacity 0
.storageList
absolute left right
bottom 37px
top 160px
.tabBody
flex 1
display flex
flex-direction column
.tag-title
padding-left 15px
padding-bottom 13px
p
color $ui-button-default-color
.tagList
overflow-y auto
.storageList-empty
padding 0 10px
margin-top 15px
line-height 24px
color $ui-inactive-text-color
.navToggle
navButtonColor()
display block
position absolute
right 5px
bottom 5px
border-radius 16.5px
height 34px
width 34px
line-height 32px
padding 0
flex: 1
.root--folded
@extend .root
width 44px
.storageList-empty
white-space nowrap
transform rotate(90deg)
height 100vh
width $sideNav--folded-width
background-color #2E3235
.switch-buttons
display none
.top
height 60px
.top-menu
width 44px - 1
position static
width $sideNav--folded-width
height 60px
text-align center
&:hover .top-menu-label
transition opacity 0.15s
@@ -65,66 +60,38 @@
position fixed
display inline-block
height 30px
left 32px
left $sideNav--folded-width
padding 0 10px
margin-top -8px
opacity 0
margin-left 0
overflow hidden
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 30px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
font-size 12px
.menu-button, .menu-button--active
text-align center
&:hover .menu-button-label
transition opacity 0.15s
opacity 1
font-size 13px
.top-menu-preference
position absolute
left 7px
.menu-button-label
position fixed
display inline-block
height 32px
left 44px
padding 0 10px
margin-top -8px
margin-left 0
overflow ellipsis
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 32px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
opacity 0
font-size 12px
body[data-theme="white"]
.root, .root--folded
background-color #f9f9f9
color $ui-text-color
body[data-theme="dark"]
.root, .root--folded
border-color $ui-dark-borderColor
border-right 1px solid $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
.top
border-color $ui-dark-borderColor
.top-menu
navDarkButtonColor()
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.storageList-empty
color $ui-dark-inactive-text-color
.navToggle
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root, .root--folded
background-color $ui-solarized-dark-backgroundColor
border-right 1px solid $ui-solarized-dark-borderColor

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
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'
@@ -8,6 +9,7 @@ import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem'
import eventEmitter from 'browser/main/lib/eventEmitter'
import _ from 'lodash'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
@@ -22,7 +24,7 @@ class StorageItem extends React.Component {
}
handleHeaderContextMenu (e) {
let menu = new Menu()
const menu = new Menu()
menu.append(new MenuItem({
label: 'Add Folder',
click: (e) => this.handleAddFolderButtonClick(e)
@@ -38,7 +40,7 @@ class StorageItem extends React.Component {
}
handleUnlinkStorageClick (e) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
@@ -46,7 +48,7 @@ class StorageItem extends React.Component {
})
if (index === 0) {
let { storage, dispatch } = this.props
const { storage, dispatch } = this.props
dataApi.removeStorage(storage.key)
.then(() => {
dispatch({
@@ -67,7 +69,7 @@ class StorageItem extends React.Component {
}
handleAddFolderButtonClick (e) {
let { storage } = this.props
const { storage } = this.props
modal.open(CreateFolderModal, {
storage
@@ -75,19 +77,19 @@ class StorageItem extends React.Component {
}
handleHeaderInfoClick (e) {
let { storage } = this.props
const { storage } = this.props
hashHistory.push('/storages/' + storage.key)
}
handleFolderButtonClick (folderKey) {
return (e) => {
let { storage } = this.props
const { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
}
}
handleFolderButtonContextMenu (e, folder) {
let menu = new Menu()
const menu = new Menu()
menu.append(new MenuItem({
label: 'Rename Folder',
click: (e) => this.handleRenameFolderClick(e, folder)
@@ -103,7 +105,7 @@ class StorageItem extends React.Component {
}
handleRenameFolderClick (e, folder) {
let { storage } = this.props
const { storage } = this.props
modal.open(RenameFolderModal, {
storage,
folder
@@ -111,7 +113,7 @@ class StorageItem extends React.Component {
}
handleFolderDeleteClick (e, folder) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete Folder',
detail: 'This will delete all notes in the folder and can not be undone.',
@@ -119,7 +121,7 @@ class StorageItem extends React.Component {
})
if (index === 0) {
let { storage, dispatch } = this.props
const { storage, dispatch } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
@@ -142,48 +144,57 @@ class StorageItem extends React.Component {
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
}
dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder)
if (noteData.length === 0) return
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
Promise.all(
newNoteData.map((note) => dataApi.createNote(storage.key, note))
)
.then((createdNoteData) => {
createdNoteData.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
})
.catch((err) => {
console.error(`error on create notes: ${err}`)
})
.then(() => {
return Promise.all(
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
)
})
.then((deletedNoteData) => {
deletedNoteData.forEach((note) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: note.storageKey,
noteKey: note.noteKey
})
})
})
.catch((err) => {
console.error(`error on delete notes: ${err}`)
})
}
handleDrop (e, storage, folder, dispatch, location) {
e.target.style.opacity = '1'
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
const noteData = JSON.parse(e.dataTransfer.getData('note'))
const newNoteData = Object.assign({}, noteData, {storage: storage, folder: folder.key})
if (folder.key === noteData.folder) return
dataApi
.createNote(storage.key, newNoteData)
.then((note) => {
dataApi
.deleteNote(noteData.storage, noteData.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
eventEmitter.once('list:moved', dispatchHandler)
eventEmitter.emit('list:next')
})
.catch((err) => {
console.error(err)
})
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: `${note.storage}-${note.key}`}
})
})
this.dropNote(storage, folder, dispatch, location, noteData)
}
render () {
let { storage, location, isFolded, data, dispatch } = this.props
let { folderNoteMap, trashedSet } = data
let folderList = storage.folders.map((folder) => {
let isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
let noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const folderList = storage.folders.map((folder) => {
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0
if (noteSet) {
@@ -211,7 +222,7 @@ class StorageItem extends React.Component {
)
})
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
const isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
return (
<div styleName={isFolded ? 'root--folded' : 'root'}
@@ -226,9 +237,9 @@ class StorageItem extends React.Component {
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
<img src={this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
/>
</button>
@@ -237,7 +248,7 @@ class StorageItem extends React.Component {
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
>
<i className='fa fa-plus' />
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
</button>
}
@@ -245,7 +256,7 @@ class StorageItem extends React.Component {
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
{isFolded ? storage.name.substring(0, 1) : storage.name}
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
</span>
{isFolded &&
<span styleName='header-info--folded-tooltip'>

View File

@@ -5,27 +5,23 @@
.header
position relative
height 25px
height 36px
width 100%
margin-bottom 5px
transition 0.15s
display flex
align-items center
.header--active
margin-bottom 5px
background-color $ui-button--active-backgroundColor
background-color alpha($ui-button-default--active-backgroundColor, 20%)
transition color background-color 0.15s
.header--active
display flex
align-items center
.header-toggleButton
color $ui-text-color
.header--active
.header-info
color $ui-text-color
.header--active
.header-addFolderButton
color $ui-text-color
color #1EC38B
.header-toggleButton
navButtonColor()
@@ -38,23 +34,31 @@
border-radius 50%
&:hover
transition 0.2s
background-color alpha($ui-button--active-backgroundColor, 40%)
background-color alpha($ui-button-default--hover-backgroundColor, 40%)
color $ui-text-color
.header-info
navButtonColor()
display block
width 100%
height 25px
padding-left 23px
padding-right 10px
height 36px
padding-left 25px
padding-right 15px
line-height 22px
cursor pointer
font-size 13px
font-size 14px
border none
overflow ellipsis
text-align left
background-color alpha($ui-button--active-backgroundColor, 20%)
font-weight 600;
background-color transparent
&:hover
color #1EC38B
background-color alpha($ui-button-default--active-backgroundColor, 20%)
transition background-color 0.15s
&:active, &:active:hover
color #1EC38B
background-color alpha($ui-button-default--active-backgroundColor, 20%)
.header-info-path
font-size 10px
@@ -63,22 +67,20 @@
.header-addFolderButton
navButtonColor()
position absolute
right 0
right 7px
width 25px
height 25px
padding 0
border none
margin-right 5px
border-radius 50%
&:hover
transition 0.2s
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-text-color
.root--folded
@extend .root
.header
width 100%
padding-left 5px
.header-info
overflow ellipsis
padding 0 0 0 18px
@@ -88,6 +90,7 @@
display none
.header-toggleButton
width 15px
padding-left 9px
.header-info--folded-tooltip
tooltip()
position fixed
@@ -102,6 +105,33 @@
font-size 10px
margin 0 5px
body[data-theme="white"]
.header--active
background-color $ui-button--active-backgroundColor
transition color background-color 0.15s
.header-toggleButton
color $ui-text-color
.header-info
color $ui-text-color
.header-addFolderButton
color $ui-text-color
.header-toggleButton
navWhiteButtonColor()
&:hover
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-text-color
.header-info
navWhiteButtonColor()
background-color alpha($ui-button--active-backgroundColor, 20%)
.header-addFolderButton
navWhiteButtonColor()
&:hover
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
@@ -149,4 +179,8 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
background-color $ui-dark-button--active-backgroundColor

View File

@@ -0,0 +1,59 @@
.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
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

@@ -0,0 +1,24 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
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'>Tags</span>
</button>
)
TagButton.propTypes = {
onClick: PropTypes.func.isRequired,
isTagActive: PropTypes.bool.isRequired
}
export default CSSModules(TagButton, styles)

View File

@@ -1,30 +1,47 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal'
import ConfigManager from 'browser/main/lib/ConfigManager'
import StorageItem from './StorageItem'
import TagListItem from 'browser/components/TagListItem'
import SideNavFilter from 'browser/components/SideNavFilter'
import StorageList from 'browser/components/StorageList'
import NavToggleButton from 'browser/components/NavToggleButton'
import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton'
import TagButton from './TagButton'
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
componentDidMount () {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
}
componentWillUnmount () {
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
}
handleMenuButtonClick (e) {
openModal(PreferencesModal)
}
handleHomeButtonClick (e) {
let { router } = this.context
const { router } = this.context
router.push('/home')
}
handleStarredButtonClick (e) {
let { router } = this.context
const { router } = this.context
router.push('/starred')
}
handleToggleButtonClick (e) {
let { dispatch, config } = this.props
const { dispatch, config } = this.props
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
dispatch({
@@ -34,19 +51,100 @@ class SideNav extends React.Component {
}
handleTrashedButtonClick (e) {
let { router } = this.context
const { router } = this.context
router.push('/trashed')
}
handleSwitchFoldersButtonClick () {
const { router } = this.context
router.push('/home')
}
handleSwitchTagsButtonClick () {
const { router } = this.context
router.push('/alltags')
}
SideNavComponent (isFolded, storageList) {
const { location, data } = this.props
const isHomeActive = !!location.pathname.match(/^\/home$/)
const isStarredActive = !!location.pathname.match(/^\/starred$/)
const isTrashedActive = !!location.pathname.match(/^\/trashed$/)
let component
// TagsMode is not selected
if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) {
component = (
<div>
<SideNavFilter
isFolded={isFolded}
isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive}
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
counterTotalNote={data.noteMap._map.size}
counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size}
/>
<StorageList storageList={storageList} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div>
)
} else {
component = (
<div styleName='tabBody'>
<div styleName='tag-title'>
<p>Tags</p>
</div>
<div styleName='tagList'>
{this.tagListComponent(data)}
</div>
</div>
)
}
return component
}
tagListComponent () {
const { data, location } = this.props
const tagList = data.tagNoteMap.map((tag, key) => {
return key
})
return (
tagList.map(tag => (
<TagListItem
name={tag}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
isActive={this.getTagActive(location.pathname, tag)}
key={tag}
/>
))
)
}
getTagActive (path, tag) {
const pathSegments = path.split('/')
const pathTag = pathSegments[pathSegments.length - 1]
return pathTag === tag
}
handleClickTagListItem (name) {
const { router } = this.context
router.push(`/tags/${name}`)
}
render () {
let { data, location, config, dispatch } = this.props
const { data, location, config, dispatch } = this.props
let isFolded = config.isSideNavFolded
let isHomeActive = !!location.pathname.match(/^\/home$/)
let isStarredActive = !!location.pathname.match(/^\/starred$/)
let isTrashedActive = !!location.pathname.match(/^\/trashed$/)
const isFolded = config.isSideNavFolded
let storageList = data.storageMap.map((storage, key) => {
const storageList = data.storageMap.map((storage, key) => {
return <StorageItem
key={storage.key}
storage={storage}
@@ -56,8 +154,9 @@ class SideNav extends React.Component {
dispatch={dispatch}
/>
})
let style = {}
const style = {}
if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/)
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
@@ -65,37 +164,15 @@ class SideNav extends React.Component {
style={style}
>
<div styleName='top'>
<button styleName='top-menu'
onClick={(e) => this.handleMenuButtonClick(e)}
>
<i className='fa fa-wrench fa-fw' />
<span styleName='top-menu-label'>Preferences</span>
</button>
<div styleName='switch-buttons'>
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
</div>
<div>
<PreferenceButton onClick={this.handleMenuButtonClick} />
</div>
</div>
<SideNavFilter
isFolded={isFolded}
isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive}
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
/>
<div styleName='storageList'>
{storageList.length > 0 ? storageList : (
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>
<button styleName='navToggle'
onClick={(e) => this.handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right' />
: <i className='fa fa-angle-double-left' />
}
</button>
{this.SideNavComponent(isFolded, storageList)}
</div>
)
}

View File

@@ -1,9 +1,10 @@
@import('../Detail/DetailVars')
.root
absolute bottom left right
height $statusBar-height
background-color $ui-noteDetail-backgroundColor
position absolute
bottom 10px
right 10px
z-index 100
display flex
.blank
@@ -21,8 +22,19 @@
.zoom
navButtonColor()
height 24px
color rgba(0,0,0,.54)
height 20px
display flex
padding 0
align-items center
background-color transparent
&:hover
color $ui-active-color
&:active
color $ui-active-color
span
margin-left 5px
.update
navButtonColor()
height 24px
@@ -37,14 +49,14 @@
body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
border-color $ui-dark-borderColor
box-shadow none
.zoom
border-color $ui-dark-borderColor
background-color transparent
color #f9f9f9
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
color $ui-dark-text-color

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
@@ -11,7 +12,7 @@ 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
class StatusBar extends React.Component {
updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
@@ -24,7 +25,7 @@ class StatusBar extends React.Component {
}
handleZoomButtonClick (e) {
let menu = new Menu()
const menu = new Menu()
zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({
@@ -37,7 +38,7 @@ class StatusBar extends React.Component {
}
handleZoomMenuItemClick (zoomFactor) {
let { dispatch } = this.props
const { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
type: 'SET_ZOOM',
@@ -46,7 +47,7 @@ class StatusBar extends React.Component {
}
render () {
let { config, status } = this.context
const { config, status } = this.context
return (
<div className='StatusBar'
@@ -55,12 +56,10 @@ class StatusBar extends React.Component {
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<i className='fa fa-search-plus' />&nbsp;
{Math.floor(config.zoom * 100)}%
<img src='../resources/icon/icon-zoom.svg' />
<span>{Math.floor(config.zoom * 100)}%</span>
</button>
<div styleName='blank' />
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!

View File

@@ -36,7 +36,7 @@ $control-height = 34px
outline none
border none
color $ui-text-color
font-size 16px
font-size 18px
padding-bottom 2px
background-color $ui-noteList-backgroundColor
@@ -112,6 +112,21 @@ $control-height = 34px
opacity 0
transition 0.1s
body[data-theme="white"]
.root, .root--expanded
background-color $ui-white-noteList-backgroundColor
.control
border-color $ui-dark-borderColor
.control-search
background-color $ui-white-noteList-backgroundColor
.control-search-input
background-color $ui-white-noteList-backgroundColor
input
background-color $ui-white-noteList-backgroundColor
body[data-theme="dark"]
.root, .root--expanded
background-color $ui-dark-noteList-backgroundColor
@@ -170,3 +185,26 @@ body[data-theme="dark"]
.control-newPostButton-tooltip
darkTooltip()
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-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 $ui-solarized-dark-inactive-text-color
background-color $ui-solarized-dark-noteList-backgroundColor
.control-search-input
background-color $ui-solarized-dark-noteList-backgroundColor
input
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color

View File

@@ -1,16 +1,11 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TopBar.styl'
import _ from 'lodash'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton'
const { remote } = require('electron')
const { dialog } = remote
const OSX = window.process.platform === 'darwin'
class TopBar extends React.Component {
constructor (props) {
super(props)
@@ -18,7 +13,10 @@ class TopBar extends React.Component {
this.state = {
search: '',
searchOptions: [],
isSearching: false
isSearching: false,
isAlphabet: false,
isIME: false,
isConfirmTranslation: false
}
this.focusSearchHandler = () => {
@@ -34,9 +32,52 @@ class TopBar extends React.Component {
ee.off('top:focus-search', this.focusSearchHandler)
}
handleKeyDown (e) {
// reset states
this.setState({
isAlphabet: false,
isIME: false
})
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
isAlphabet: true
})
// When the key is an IME input (Japanese, Chinese)
} else if (e.keyCode === 229) {
this.setState({
isIME: true
})
}
}
handleKeyUp (e) {
const { router } = this.context
// reset states
this.setState({
isConfirmTranslation: false
})
// When the key is translation confirmation (Enter, Space)
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
this.setState({
isConfirmTranslation: true
})
router.push('/searched')
this.setState({
search: this.refs.searchInput.value
})
}
}
handleSearchChange (e) {
let { router } = this.context
router.push('/searched')
const { router } = this.context
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push('/searched')
} else {
e.preventDefault()
}
this.setState({
search: this.refs.searchInput.value
})
@@ -75,7 +116,7 @@ class TopBar extends React.Component {
}
render () {
let { config, style, data, location } = this.props
const { config, style, location } = this.props
return (
<div className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
@@ -93,6 +134,8 @@ class TopBar extends React.Component {
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
placeholder='Search'
type='text'
className='searchInput'

View File

@@ -1,4 +1,5 @@
global-reset()
@import '../styles/vars.styl'
DEFAULT_FONTS = 'OpenSans', helvetica, arial, sans-serif
@@ -12,6 +13,7 @@ body
color textColor
font-size fontSize
font-weight 200
-webkit-font-smoothing antialiased
button, input, select, textarea
font-family DEFAULT_FONTS
@@ -64,7 +66,7 @@ textarea.block-input
fullsize()
modalZIndex= 1000
modalBackColor = transparentify(white, 65%)
modalBackColor = white
.ace_focus
outline-color rgb(59, 153, 252)
outline-offset 0px
@@ -83,15 +85,19 @@ modalBackColor = transparentify(white, 65%)
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
body[data-theme="dark"]
.ModalBase
.modalBack
background-color alpha(black, 60%)
background-color $ui-dark-backgroundColor
.sortableItemHelper
color: $ui-dark-text-color
.CodeMirror
font-family inherit !important
line-height 1.4em
height 100%
height 96%
.CodeMirror > div > textarea
margin-bottom -1em
.CodeMirror-focused .CodeMirror-selected
@@ -102,3 +108,15 @@ body[data-theme="dark"]
background #B1D7FE
::selection
background #B1D7FE
.sortableItemHelper
z-index modalZIndex + 5
body[data-theme="solarized-dark"]
.ModalBase
.modalBack
background-color $ui-solarized-dark-backgroundColor
.sortableItemHelper
color: $ui-solarized-dark-text-color

View File

@@ -27,13 +27,16 @@ document.addEventListener('click', function (e) {
const className = e.target.className
if (!className && typeof (className) !== 'string') return
const isInfoButton = className.includes('infoButton')
const isInfoPanel = e.target.offsetParent.className.includes('infoPanel')
const offsetParent = e.target.offsetParent
const isInfoPanel = offsetParent !== null
? offsetParent.className.includes('infoPanel')
: false
if (isInfoButton || isInfoPanel) return
const infoPanel = document.querySelector('.infoPanel')
if (infoPanel) infoPanel.style.display = 'none'
})
let el = document.getElementById('content')
const el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
function notify (...args) {
@@ -41,7 +44,7 @@ function notify (...args) {
}
function updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
@@ -62,6 +65,11 @@ ReactDOM.render((
<Route path='starred' />
<Route path='searched' />
<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'>
@@ -73,7 +81,7 @@ ReactDOM.render((
</Router>
</Provider>
), el, function () {
let loadingCover = document.getElementById('loadingCover')
const loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
ipcRenderer.on('update-ready', function () {

View File

@@ -2,19 +2,47 @@ const AWS = require('aws-sdk')
const AMA = require('aws-sdk-mobile-analytics')
const ConfigManager = require('browser/main/lib/ConfigManager')
const remote = require('electron').remote
const os = require('os')
let mobileAnalyticsClient
AWS.config.region = 'us-east-1'
if (process.env.NODE_ENV === 'production' && ConfigManager.default.get().amaEnabled) {
if (!getSendEventCond()) {
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
})
const mobileAnalyticsClient = new AMA.Manager({
const validPlatformName = convertPlatformName(os.platform())
mobileAnalyticsClient = new AMA.Manager({
appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
appTitle: 'xxxxxxxxxx'
appTitle: 'xxxxxxxxxx',
appVersionName: remote.app.getVersion().toString(),
platform: validPlatformName
})
}
function convertPlatformName (platformName) {
if (platformName === 'darwin') {
return 'MacOS'
} else if (platformName === 'win32') {
return 'Windows'
} else if (platformName === 'linux') {
return 'Linux'
} else {
return ''
}
}
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 () {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
if (!err) {
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
@@ -24,16 +52,28 @@ function initAwsMobileAnalytics () {
})
}
function recordDynamicCustomEvent (type) {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
mobileAnalyticsClient.recordEvent(type)
function recordDynamicCustomEvent (type, options = {}) {
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent(type, options)
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
console.log(analyticsError.name + ': ' + analyticsError.message)
}
}
}
function recordStaticCustomEvent () {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
uiColorTheme: ConfigManager.default.get().ui.theme
})
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
uiColorTheme: ConfigManager.default.get().ui.theme
})
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
console.log(analyticsError.name + ': ' + analyticsError.message)
}
}
}
module.exports = {

View File

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

View File

@@ -6,8 +6,6 @@ const win = global.process.platform === 'win32'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
const path = require('path')
const fs = require('fs')
let isInitialized = false
@@ -25,6 +23,7 @@ export const DEFAULT_CONFIG = {
},
ui: {
theme: 'default',
showCopyNotification: true,
disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
},
@@ -36,13 +35,19 @@ export const DEFAULT_CONFIG = {
indentType: 'space',
indentSize: '2',
displayLineNumbers: true,
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
scrollPastEnd: false,
type: 'SPLIT'
},
preview: {
fontSize: '14',
fontFamily: win ? 'Segoe UI' : 'Lato',
codeBlockTheme: 'dracula',
lineNumber: true
lineNumber: true,
latexInlineOpen: '$',
latexInlineClose: '$',
latexBlockOpen: '$$',
latexBlockClose: '$$'
}
}
@@ -91,7 +96,11 @@ function get () {
: 'default'
if (config.editor.theme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
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')
}
}
}
@@ -99,13 +108,17 @@ function get () {
}
function set (updates) {
let currentConfig = get()
let newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
const currentConfig = get()
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
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 {
document.body.setAttribute('data-theme', 'default')
}
@@ -117,12 +130,16 @@ function set (updates) {
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
let newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
? newConfig.editor.theme
: 'default'
if (newTheme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
if (newTheme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
}
}
ipcRenderer.send('config-renew', {
@@ -131,7 +148,7 @@ function set (updates) {
}
function assignConfigValues (originalConfig, rcConfig) {
let config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)

View File

@@ -19,7 +19,7 @@ function setZoom (zoomFactor, noSave = false) {
}
function getZoom () {
let config = ConfigManager.get()
const config = ConfigManager.get()
return config.zoom
}

View File

@@ -1,7 +1,5 @@
const fs = require('fs')
const path = require('path')
const _ = require('lodash')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
/**

View File

@@ -23,7 +23,6 @@ const { findStorage } = require('browser/lib/findStorage')
* ```
*/
function createFolder (storageKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
@@ -41,7 +40,7 @@ function createFolder (storageKey, input) {
while (storage.folders.some((folder) => folder.key === key)) {
key = keygen()
}
let newFolder = {
const newFolder = {
key,
color: input.color,
name: input.name

View File

@@ -66,12 +66,16 @@ function createNote (storageKey, input) {
}
}
}
let noteData = Object.assign({}, input, {
key,
createdAt: new Date(),
updatedAt: new Date(),
storage: storageKey
})
const noteData = Object.assign({},
{
createdAt: new Date(),
updatedAt: new Date()
},
input, // input may contain more accurate dates
{
key,
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))

View File

@@ -19,7 +19,6 @@ const { findStorage } = require('browser/lib/findStorage')
* ```
*/
function deleteFolder (storageKey, folderKey) {
let rawStorages
let targetStorage
try {
targetStorage = findStorage(storageKey)
@@ -38,17 +37,17 @@ function deleteFolder (storageKey, folderKey) {
})
})
.then(function deleteFolderAndNotes (data) {
let { storage, notes } = data
const { storage, notes } = data
storage.folders = storage.folders
.filter(function excludeTargetFolder (folder) {
return folder.key !== folderKey
})
let targetNotes = notes.filter(function filterTargetNotes (note) {
const targetNotes = notes.filter(function filterTargetNotes (note) {
return note.folder === folderKey
})
let deleteAllNotes = targetNotes
const deleteAllNotes = targetNotes
.map(function deleteNote (note) {
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
return sander.unlink(notePath)

View File

@@ -1,5 +1,4 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
@@ -14,7 +13,7 @@ function deleteNote (storageKey, noteKey) {
return resolveStorageData(targetStorage)
.then(function deleteNoteFile (storage) {
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
sander.unlinkSync(notePath)

View File

@@ -6,6 +6,7 @@ const dataApi = {
createFolder: require('./createFolder'),
updateFolder: require('./updateFolder'),
deleteFolder: require('./deleteFolder'),
reorderFolder: require('./reorderFolder'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),

View File

@@ -20,7 +20,7 @@ const CSON = require('@rokt33r/season')
* 3. empty directory
*/
function init () {
let fetchStorages = function () {
const fetchStorages = function () {
let rawStorages
try {
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
@@ -34,8 +34,8 @@ function init () {
.map(resolveStorageData))
}
let fetchNotes = function (storages) {
let findNotesFromEachStorage = storages
const fetchNotes = function (storages) {
const findNotesFromEachStorage = storages
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {

View File

@@ -9,7 +9,7 @@ const sander = require('sander')
function migrateFromV5Storage (storageKey, data) {
let targetStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
@@ -24,15 +24,15 @@ function migrateFromV5Storage (storageKey, data) {
}
function importAll (storage, data) {
let oldArticles = data.articles
let notes = []
const oldArticles = data.articles
const notes = []
data.folders
.forEach(function (oldFolder) {
let folderKey = keygen()
while (storage.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
const newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
@@ -40,7 +40,7 @@ function importAll (storage, data) {
storage.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
const articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
articles.forEach((article) => {
let noteKey = keygen()
let isUnique = false
@@ -59,7 +59,7 @@ function importAll (storage, data) {
}
if (article.mode === 'markdown') {
let newNote = {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
@@ -73,7 +73,7 @@ function importAll (storage, data) {
}
notes.push(newNote)
} else {
let newNote = {
const newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,

View File

@@ -20,7 +20,7 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
.then(function saveNote (_oldStorage) {
oldStorage = _oldStorage
let noteData
let notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {

View File

@@ -1,6 +1,5 @@
const _ = require('lodash')
const resolveStorageData = require('./resolveStorageData')
const { findStorage } = require('browser/lib/findStorage')
/**
* @param {String} key
@@ -19,7 +18,7 @@ function renameStorage (key, name) {
console.error(err)
return Promise.reject(err)
}
let targetStorage = _.find(cachedStorageList, {key: key})
const targetStorage = _.find(cachedStorageList, {key: key})
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.name = name

View File

@@ -0,0 +1,42 @@
const _ = require('lodash')
_.move = require('lodash-move').default
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
/**
* @param {String} storageKey
* @param {number} oldIndex
* @param {number} newIndex
*
* @return {Object}
* ```
* {
* storage: Object
* }
* ```
*/
function reorderFolder (storageKey, oldIndex, newIndex) {
let targetStorage
try {
if (!_.isNumber(oldIndex)) throw new Error('oldIndex must be a number.')
if (!_.isNumber(newIndex)) throw new Error('newIndex must be a number.')
targetStorage = findStorage(storageKey)
} catch (e) {
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 {
storage
}
})
}
module.exports = reorderFolder

View File

@@ -4,7 +4,7 @@ const CSON = require('@rokt33r/season')
const migrateFromV6Storage = require('./migrateFromV6Storage')
function resolveStorageData (storageCache) {
let storage = {
const storage = {
key: storageCache.key,
name: storageCache.name,
type: storageCache.type,
@@ -13,7 +13,7 @@ function resolveStorageData (storageCache) {
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
try {
let jsonData = CSON.readFileSync(boostnoteJSONPath)
const jsonData = CSON.readFileSync(boostnoteJSONPath)
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
storage.folders = jsonData.folders
storage.version = jsonData.version
@@ -28,7 +28,7 @@ function resolveStorageData (storageCache) {
storage.version = '1.0'
}
let version = parseInt(storage.version, 10)
const version = parseInt(storage.version, 10)
if (version >= 1) {
if (version > 1) {
console.log('The repository version is newer than one of current app.')

View File

@@ -16,13 +16,13 @@ function resolveStorageNotes (storage) {
}
notePathList = []
}
let notes = notePathList
const notes = notePathList
.filter(function filterOnlyCSONFile (notePath) {
return /\.cson$/.test(notePath)
})
.map(function parseCSONFile (notePath) {
try {
let data = CSON.readFileSync(path.join(notesDirPath, notePath))
const data = CSON.readFileSync(path.join(notesDirPath, notePath))
data.key = path.basename(notePath, '.cson')
data.storage = storage.key
return data

View File

@@ -2,6 +2,7 @@ const _ = require('lodash')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
/**
* @param {String} storageKey
@@ -22,25 +23,20 @@ const CSON = require('@rokt33r/season')
* ```
*/
function updateFolder (storageKey, folderKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
if (!_.isString(input.name)) throw new Error('Name must be a string.')
if (!_.isString(input.color)) throw new Error('Color must be a string.')
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
targetStorage = findStorage(storageKey)
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function updateFolder (storage) {
let targetFolder = _.find(storage.folders, {key: folderKey})
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

View File

@@ -5,7 +5,7 @@ const CSON = require('@rokt33r/season')
const { findStorage } = require('browser/lib/findStorage')
function validateInput (input) {
let validatedInput = {}
const validatedInput = {}
if (input.tags != null) {
if (!_.isArray(input.tags)) validatedInput.tags = []
@@ -26,6 +26,10 @@ function validateInput (input) {
validatedInput.isTrashed = !!input.isTrashed
}
if (input.isPinned !== undefined) {
validatedInput.isPinned = !!input.isPinned
}
validatedInput.type = input.type
switch (input.type) {
case 'MARKDOWN_NOTE':
@@ -77,7 +81,7 @@ function updateNote (storageKey, noteKey, input) {
return resolveStorageData(targetStorage)
.then(function saveNote (storage) {
let noteData
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
const notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
@@ -104,6 +108,7 @@ function updateNote (storageKey, noteKey, input) {
noteData.isStarred = false
noteData.isTrashed = false
noteData.tags = []
noteData.isPinned = false
}
if (noteData.type === 'SNIPPET_NOTE') {

View File

@@ -1,121 +0,0 @@
import store from 'browser/main/store'
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const Mixpanel = require('mixpanel')
const mixpanel = Mixpanel.init('7a0aca437d72dfd07cbcbf58d3b61f27', {key: 'fde4fd23f4d550f1b646bcd7d4374b1f'})
const moment = require('moment')
const electron = require('electron')
function _getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen(20)
_setClientKey(clientKey)
}
return clientKey
}
function _setClientKey (newKey) {
localStorage.setItem('clientKey', newKey)
}
function _fetch () {
let events
try {
events = JSON.parse(localStorage.getItem('events'))
if (!_.isArray(events)) throw new Error('events is not an array.')
} catch (err) {
console.warn(err)
events = []
localStorage.setItem('events', JSON.stringify(events))
console.info('Events cache initialzed')
}
return events
}
function _keep (name, properties) {
let events = _fetch()
properties.time = new Date()
events.push({
name,
properties
})
localStorage.setItem('events', JSON.stringify(events))
}
function _keepUnique (name, properties) {
let events = _fetch()
properties.time = new Date()
events = events.filter((event) => event.name !== name)
events.push({
name,
properties
})
localStorage.setItem('events', JSON.stringify(events))
}
function _flush () {
let events = _fetch()
let spliced = events.splice(0, 50)
localStorage.setItem('events', JSON.stringify(events))
if (spliced.length > 0) {
let parsedEvents = spliced
.filter((event) => {
if (!_.isObject(event)) return false
if (!_.isString(event.name)) return false
if (!_.isObject(event.properties)) return false
if (!moment(event.properties.time).isValid()) return false
if (new Date() - moment(event.properties.time).toDate() > 1000 * 3600 * 24 * 3) return false
return true
})
.map((event) => {
return {
event: event.name,
properties: event.properties
}
})
mixpanel.import_batch(parsedEvents, {}, (errs) => {
if (errs.length > 0) {
let events = _fetch()
events = events.concat(spliced)
localStorage.setItem('events', JSON.stringify(events))
} else {
_flush()
}
})
let state = store.getState()
mixpanel.people.set(_getClientKey(), {
storage_count: state.data.storageMap.size,
note_count: state.data.noteMap.size,
version: electron.remote.app.getVersion()
})
}
}
setInterval(_flush, 1000 * 60 * 60)
function track (name, properties) {
switch (name) {
case 'MAIN_FOCUSED':
properties = Object.assign({}, properties, {
distinct_id: _getClientKey()
})
_keepUnique(name, properties)
break
default:
properties = Object.assign({}, properties, {
distinct_id: _getClientKey()
})
_keep(name, properties)
}
}
module.exports = {
_mp: mixpanel,
track
}

View File

@@ -16,7 +16,7 @@ class ModalBase extends React.Component {
close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
// Toggle overflow style on NoteList
let list = document.querySelector('.NoteList__list___browser-main-NoteList-')
const list = document.querySelector('.NoteList__list___browser-main-NoteList-')
list.style.overflow = 'auto'
}
@@ -34,14 +34,14 @@ class ModalBase extends React.Component {
}
}
let el = document.createElement('div')
const el = document.createElement('div')
document.body.appendChild(el)
let modalBase = ReactDOM.render(<ModalBase />, el)
const modalBase = ReactDOM.render(<ModalBase />, el)
export function openModal (component, props) {
if (modalBase == null) { return }
// Hide scrollbar by removing overflow when modal opens
let 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})

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './CreateFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
@@ -51,8 +52,8 @@ class CreateFolderModal extends React.Component {
confirm () {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_FOLDER')
if (this.state.name.trim().length > 0) {
let { storage } = this.props
let input = {
const { storage } = this.props
const input = {
name: this.state.name.trim(),
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}

View File

@@ -4,10 +4,9 @@
height 270px
overflow hidden
position relative
padding 0 40px
.header
height 70px
height 80px
margin-bottom 10px
margin-top 20px
font-size 18px
@@ -15,23 +14,27 @@
background-color $ui-backgroundColor
color $ui-text-color
.title
font-size 36px
font-weight 600
.control-folder-label
text-align left
font-size 12px
font-size 14px
color $ui-text-color
.control-folder-input
display block
height 30px
width 420px
height 40px
width 490px
padding 0 5px
margin 10px auto 15px
margin 10px 0
border 1px solid #C9C9C9 // TODO: use variable.
border-radius 2px
background-color transparent
outline none
vertical-align middle
font-size 14px
font-size 16px
&:disabled
background-color $ui-input--disabled-backgroundColor
&:focus, &:active
@@ -39,14 +42,13 @@
.control-confirmButton
display block
float right
height 30px
width 100px
height 35px
width 140px
border none
border-radius 2px
padding 0 25px
margin 20px auto
font-size 12px
font-size 14px
colorPrimaryButton()
body[data-theme="dark"]
@@ -56,7 +58,6 @@ body[data-theme="dark"]
height 270px
overflow hidden
position relative
padding 0 40px
.header
background-color transparent

View File

@@ -5,7 +5,6 @@ import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { hashHistory } from 'react-router'
import _ from 'lodash'
import ModalEscButton from 'browser/components/ModalEscButton'
const CSON = require('@rokt33r/season')
const path = require('path')
@@ -13,9 +12,9 @@ const electron = require('electron')
const { remote } = electron
function browseFolder () {
let dialog = remote.dialog
const dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
const defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
@@ -55,7 +54,7 @@ class InitModal extends React.Component {
} catch (err) {
console.error(err)
}
let newState = {
const newState = {
isLoading: false
}
if (data != null) {
@@ -122,7 +121,7 @@ class InitModal extends React.Component {
notes: data.notes
})
let defaultSnippetNote = dataApi
const defaultSnippetNote = dataApi
.createNote(data.storage.key, {
type: 'SNIPPET_NOTE',
folder: data.storage.folders[0].key,
@@ -147,12 +146,12 @@ class InitModal extends React.Component {
note: note
})
})
let defaultMarkdownNote = dataApi
const defaultMarkdownNote = dataApi
.createNote(data.storage.key, {
type: 'MARKDOWN_NOTE',
folder: data.storage.folders[0].key,
title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote! \n### _Click to edit this note._\n\n---\n\nBoostnote is an *open source* note-taking app. \nRepository is published on [GitHub](https://github.com/BoostIO/Boostnote), and tweeting everyday on [@Boostnoteapp](https://twitter.com/boostnoteapp)!\n\n## Features \n- [x] No Internet and Registration Required. \n- [ ] Quick search and copy the content of note. `macOS: Cmd + Alt + S / windows: Ctrl + Alt + S` \n- [ ] Markdown & Snippet note. \n- [ ] Available for `vim` and `emacs` mode. \n- [ ] Choose your favorite theme on UI, Editor and Code Block! \n--- \n\n- Copy Codeblock on Markdown Preview.\n```javascript\nvar boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)\n```'
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({
@@ -184,6 +183,12 @@ class InitModal extends React.Component {
})
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
if (this.state.isLoading) {
return <div styleName='root--loading'>
@@ -196,17 +201,12 @@ class InitModal extends React.Component {
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='header-title'>Initialize Storage</div>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='body'>
<div styleName='body-welcome'>
Welcome!
Welcome to Boostnote!
</div>
<div styleName='body-description'>
Please select a directory for Boostnote storage.
Please select a directory for data storage.
</div>
<div styleName='body-path'>
<input styleName='body-path-input'
@@ -237,7 +237,7 @@ class InitModal extends React.Component {
? <span>
<i className='fa fa-spin fa-spinner' /> Loading...
</span>
: 'Let\'s Go!'
: 'CREATE'
}
</button>
</div>

View File

@@ -1,7 +1,11 @@
.root
modal()
max-width 540px
background-color #fff
max-width 100vw
max-height 100vh
overflow hidden
margin 0
padding 150px 0
position relative
.root--loading
@extend .root
@@ -13,14 +17,6 @@
.loadingMessage
color $ui-text-color
margin 15px auto 35px
.header
height 50px
font-size 18px
line-height 50px
padding 0 15px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
color $ui-text-color
.body
padding 30px
@@ -32,20 +28,20 @@
color $ui-text-color
.body-description
font-size 14px
font-size 16px
color $ui-text-color
text-align center
margin-bottom 25px
.body-path
margin 0 auto 25px
width 280px
width 330px
.body-path-input
height 30px
height 40px
vertical-align middle
width 250px
font-size 12px
width 300px
font-size 14px
border-style solid
border-width 1px 0 1px 1px
border-color $border-color
@@ -54,7 +50,10 @@
padding 0 5px
.body-path-button
height 30px
height 42px
width 30px
font-size 16px
font-weight 600
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
@@ -69,6 +68,8 @@
.body-control-createButton
colorPrimaryButton()
font-size 14px
font-weight 600
border none
border-radius 2px
height 40px

View File

@@ -26,7 +26,7 @@ class NewNoteModal extends React.Component {
handleMarkdownNoteButtonClick (e) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let { storage, folder, dispatch, location } = this.props
const { storage, folder, dispatch, location } = this.props
dataApi
.createNote(storage, {
type: 'MARKDOWN_NOTE',
@@ -58,7 +58,7 @@ class NewNoteModal extends React.Component {
handleSnippetNoteButtonClick (e) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let { storage, folder, dispatch, location } = this.props
const { storage, folder, dispatch, location } = this.props
dataApi
.createNote(storage, {

View File

@@ -9,16 +9,19 @@
font-size 18px
line-height 50px
padding 0 15px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
color $ui-text-color
margin-bottom 20px
.title
font-size 36px
font-weight 600
.control
padding 25px 15px 15px
padding 25px 0px
text-align center
.control-button
width 220px
width 240px
height 220px
margin 0 15px
border $ui-border
@@ -30,8 +33,8 @@
colorPrimaryButton()
.control-button-icon
font-size 50px
margin-bottom 15px
font-size 48px
margin-bottom 25px
.control-button-label
font-size 18px
@@ -49,8 +52,6 @@ body[data-theme="dark"]
modalDark()
.header
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-dark-borderColor
color $ui-dark-text-color
.control-button
@@ -63,3 +64,20 @@ body[data-theme="dark"]
.description
color $ui-inactive-text-color
body[data-theme="solarized-dark"]
.root
background-color transparent
.header
color $ui-solarized-dark-text-color
.control-button
border-color $ui-solarized-dark-borderColor
color $ui-solarized-dark-text-color
background-color transparent
&:focus
colorDarkPrimaryButton()
.description
color $ui-solarized-dark-text-color

View File

@@ -31,10 +31,15 @@
.group-section-control
flex 1
margin-left 5px
.group-section-control select
outline none
border 1px solid $ui-borderColor
font-size 16px
height 30px
width 250px
margin-bottom 5px
background-color transparent
.group-section-control-input
@@ -62,10 +67,17 @@
text-align right
:global
.alert
font-size 12px
line-height 30px
padding 0 5px
float right
display inline-block
position absolute
top 60px
right 15px
font-size 14px
.success
color #1EC38B
.error
color red
.group-control-leftButton
colorDefaultButton()
@@ -77,14 +89,16 @@
margin-right 10px
.group-control-rightButton
float right
position absolute
top 10px
right 20px
colorPrimaryButton()
border none
border-radius 2px
font-size $tab--button-font-size
height 35px
width 100px
margin-right 10px
height 40px
width 120px
padding 0 15px
.group-hint
border $ui-border
@@ -101,7 +115,6 @@
line-height 1.2
.note-for-keymap
margin-left: 10px
font-size: 12px
.code-mirror
@@ -114,6 +127,12 @@ colorDarkControl()
border-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
colorSolarizedDarkControl()
border none
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
body[data-theme="dark"]
.root
@@ -141,3 +160,33 @@ body[data-theme="dark"]
.group-section-control
select, .group-section-control-input
colorDarkControl()
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.group-header
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
.group-header2
color $ui-solarized-dark-text-color
.group-section-control-input
border-color $ui-solarized-dark-borderColor
.group-control
border-color $ui-solarized-dark-borderColor
.group-control-leftButton
colorDarkDefaultButton()
border-color $ui-solarized-dark-borderColor
.group-control-rightButton
colorSolarizedDarkPrimaryButton()
.group-hint
colorSolarizedDarkControl()
.group-section-control
select, .group-section-control-input
colorSolarizedDarkControl()

View File

@@ -0,0 +1,49 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Crowdfunding.styl'
const electron = require('electron')
const { shell } = electron
class Crowdfunding extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
render () {
return (
<div styleName='root'>
<div styleName='header'>Crowdfunding</div>
<p>Dear all,</p>
<br />
<p>Thanks for your using!</p>
<p>Boostnote is used in about 200 countries and regions, it is a awesome developer community.</p>
<br />
<p>To continue supporting this growth, and to satisfy community expectations,</p>
<p>we would like to invest more time in this project.</p>
<br />
<p>If you like this project and see its potential, you can help!</p>
<br />
<p>Thanks,</p>
<p>Boostnote maintainers.</p>
<br />
<button styleName='cf-link'>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>Support via OpenCollective</a>
</button>
</div>
)
}
}
Crowdfunding.propTypes = {
}
export default CSSModules(Crowdfunding, styles)

View File

@@ -0,0 +1,36 @@
@import('./Tab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
p
font-size 16px
.cf-link
width 250px
height 35px
border-radius 2px
border none
background-color alpha(#1EC38B, 90%)
&:hover
background-color #1EC38B
transition 0.2s
a
text-decoration none
color white
font-weight 600
font-size 16px
body[data-theme="dark"]
p
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
p
color $ui-solarized-dark-text-color

View File

@@ -0,0 +1,304 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import ReactDOM from 'react-dom'
import styles from './FolderItem.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { SketchPicker } from 'react-color'
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
class FolderItem extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
folder: {
showColumnPicker: false,
colorPickerPos: { left: 0, top: 0 },
color: props.color,
name: props.name
}
}
}
handleEditChange (e) {
const { folder } = this.state
folder.name = this.refs.nameInput.value
this.setState({
folder
})
}
handleConfirmButtonClick (e) {
this.confirm()
}
confirm () {
const { storage, folder } = this.props
dataApi
.updateFolder(storage.key, folder.key, {
color: this.state.folder.color,
name: this.state.folder.name
})
.then((data) => {
store.dispatch({
type: 'UPDATE_FOLDER',
storage: data.storage
})
this.setState({
status: 'IDLE'
})
})
}
handleColorButtonClick (e) {
const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } })
this.setState({ folder }, function () {
// After the color picker has been painted, re-calculate its position
// by comparing its dimensions to the host dimensions.
const { hostBoundingBox } = this.props
const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker)
const colorPickerBox = colorPickerNode.getBoundingClientRect()
const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom
const folder = Object.assign({}, this.state.folder, {
colorPickerPos: {
left: 25,
top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics
}
})
this.setState({ folder })
})
}
handleColorChange (color) {
const folder = Object.assign({}, this.state.folder, { color: color.hex })
this.setState({ folder })
}
handleColorPickerClose (event) {
const folder = Object.assign({}, this.state.folder, { showColumnPicker: false })
this.setState({ folder })
}
handleCancelButtonClick (e) {
this.setState({
status: 'IDLE'
})
}
handleFolderItemBlur (e) {
let el = e.relatedTarget
while (el != null) {
if (el === this.refs.root) {
return false
}
el = el.parentNode
}
this.confirm()
}
renderEdit (e) {
const popover = { position: 'absolute', zIndex: 2 }
const cover = {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
}
const pickerStyle = Object.assign({}, {
position: 'absolute'
}, this.state.folder.colorPickerPos)
return (
<div styleName='folderItem'
onBlur={(e) => this.handleFolderItemBlur(e)}
tabIndex='-1'
ref='root'
>
<div styleName='folderItem-left'>
<button styleName='folderItem-left-colorButton' style={{color: this.state.folder.color}}
onClick={(e) => !this.state.folder.showColumnPicker && this.handleColorButtonClick(e)}
>
{this.state.folder.showColumnPicker
? <div style={popover}>
<div style={cover}
onClick={() => this.handleColorPickerClose()}
/>
<div style={pickerStyle}>
<SketchPicker
ref='colorPicker'
color={this.state.folder.color}
onChange={(color) => this.handleColorChange(color)}
onChangeComplete={(color) => this.handleColorChange(color)}
/>
</div>
</div>
: null
}
<i className='fa fa-square' />
</button>
<input styleName='folderItem-left-nameInput'
value={this.state.folder.name}
ref='nameInput'
onChange={(e) => this.handleEditChange(e)}
/>
</div>
<div styleName='folderItem-right'>
<button styleName='folderItem-right-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleDeleteConfirmButtonClick (e) {
const { storage, folder } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
store.dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
renderDelete () {
return (
<div styleName='folderItem'>
<div styleName='folderItem-left'>
Are you sure to <span styleName='folderItem-left-danger'>delete</span> this folder?
</div>
<div styleName='folderItem-right'>
<button styleName='folderItem-right-dangerButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleEditButtonClick (e) {
const { folder: propsFolder } = this.props
const { folder: stateFolder } = this.state
const folder = Object.assign({}, stateFolder, propsFolder)
this.setState({
status: 'EDIT',
folder
}, () => {
this.refs.nameInput.select()
})
}
handleDeleteButtonClick (e) {
this.setState({
status: 'DELETE'
})
}
renderIdle () {
const { folder } = this.props
return (
<div styleName='folderItem'
onDoubleClick={(e) => this.handleEditButtonClick(e)}
>
<div styleName='folderItem-left'
style={{borderColor: folder.color}}
>
<span styleName='folderItem-left-name'>{folder.name}</span>
<span styleName='folderItem-left-key'>({folder.key})</span>
</div>
<div styleName='folderItem-right'>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleEditButtonClick(e)}
>
Edit
</button>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
Delete
</button>
</div>
</div>
)
}
render () {
switch (this.state.status) {
case 'DELETE':
return this.renderDelete()
case 'EDIT':
return this.renderEdit()
case 'IDLE':
default:
return this.renderIdle()
}
}
}
FolderItem.propTypes = {
hostBoundingBox: PropTypes.shape({
bottom: PropTypes.number,
height: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
width: PropTypes.number
}),
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.string,
color: PropTypes.string,
name: PropTypes.string
})
}
class Handle extends React.Component {
render () {
return (
<div styleName='folderItem-drag-handle'>
<i className='fa fa-reorder' />
</div>
)
}
}
class SortableFolderItemComponent extends React.Component {
render () {
const StyledHandle = CSSModules(Handle, this.props.styles)
const DragHandle = SortableHandle(StyledHandle)
const StyledFolderItem = CSSModules(FolderItem, this.props.styles)
return (
<div>
<DragHandle />
<StyledFolderItem {...this.props} />
</div>
)
}
}
export default CSSModules(SortableElement(SortableFolderItemComponent), styles)

View File

@@ -0,0 +1,128 @@
.folderItem
height 35px
box-sizing border-box
padding 2.5px 15px
&:hover
background-color darken(white, 3%)
.folderItem-drag-handle
height 35px
border none
padding 0 10px
line-height 35px
float left
cursor row-resize
.folderItem-left
height 30px
border-left solid 2px transparent
padding 0 10px
line-height 30px
float left
.folderItem-left-danger
color $danger-color
font-weight bold
.folderItem-left-key
color $ui-inactive-text-color
font-size 13px
margin 0 5px
border none
.folderItem-left-colorButton
colorDefaultButton()
height 25px
width 25px
line-height 23px
padding 0
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
margin-right 5px
margin-left -15px
.folderItem-left-nameInput
height 25px
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
padding 0 5px
outline none
.folderItem-right
float right
.folderItem-right-button
vertical-align middle
height 25px
margin-top 2.5px
colorDefaultButton()
border-radius 2px
border $ui-border
margin-right 5px
padding 0 5px
&:last-child
margin-right 0
.folderItem-right-confirmButton
@extend .folderItem-right-button
border none
colorPrimaryButton()
.folderItem-right-dangerButton
@extend .folderItem-right-button
border none
colorDangerButton()
body[data-theme="dark"]
.folderItem
&:hover
background-color lighten($ui-dark-button--hover-backgroundColor, 5%)
.folderItem-left-danger
color $danger-color
font-weight bold
.folderItem-left-key
color $ui-dark-inactive-text-color
.folderItem-left-colorButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
.folderItem-right-button
colorDarkDefaultButton()
border-color $ui-dark-borderColor
.folderItem-right-confirmButton
colorDarkPrimaryButton()
.folderItem-right-dangerButton
colorDarkDangerButton()
body[data-theme="solarized-dark"]
.folderItem
&:hover
background-color $ui-solarized-dark-button-backgroundColor
.folderItem-left-danger
color $danger-color
.folderItem-left-key
color $ui-dark-inactive-text-color
.folderItem-left-colorButton
colorSolarizedDarkPrimaryButton()
.folderItem-right-button
colorSolarizedDarkPrimaryButton()
.folderItem-right-confirmButton
colorSolarizedDarkPrimaryButton()
.folderItem-right-dangerButton
colorSolarizedDarkPrimaryButton()

View File

@@ -0,0 +1,85 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './FolderList.styl'
import store from 'browser/main/store'
import FolderItem from './FolderItem'
import { SortableContainer } from 'react-sortable-hoc'
class FolderList extends React.Component {
render () {
const { storage, hostBoundingBox } = this.props
const folderList = storage.folders.map((folder, index) => {
return <FolderItem key={folder.key}
folder={folder}
storage={storage}
index={index}
hostBoundingBox={hostBoundingBox}
/>
})
return (
<div styleName='folderList'>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>No Folders</div>
}
</div>
)
}
}
FolderList.propTypes = {
hostBoundingBox: PropTypes.shape({
bottom: PropTypes.number,
height: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
width: PropTypes.number
}),
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.number,
color: PropTypes.string,
name: PropTypes.string
})
}
class SortableFolderListComponent extends React.Component {
constructor (props) {
super(props)
this.onSortEnd = ({oldIndex, newIndex}) => {
const { storage } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
store.dispatch({
type: 'REORDER_FOLDER',
storage: data.storage
})
this.setState()
})
}
}
render () {
const StyledFolderList = CSSModules(FolderList, this.props.styles)
const SortableFolderList = SortableContainer(StyledFolderList)
return (
<SortableFolderList
helperClass='sortableItemHelper'
onSortEnd={this.onSortEnd}
useDragHandle
{...this.props}
/>
)
}
}
export default CSSModules(SortableFolderListComponent, styles)

View File

@@ -1,8 +1,10 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import _ from 'lodash'
const electron = require('electron')
const ipc = electron.ipcRenderer
@@ -40,7 +42,7 @@ class HotkeyTab extends React.Component {
}
handleSaveButtonClick (e) {
let newConfig = {
const newConfig = {
hotkey: this.state.config.hotkey
}
@@ -50,6 +52,7 @@ class HotkeyTab extends React.Component {
type: 'SET_UI',
config: newConfig
})
this.clearMessage()
}
handleHintToggleButtonClick (e) {
@@ -59,7 +62,7 @@ class HotkeyTab extends React.Component {
}
handleHotkeyChange (e) {
let { config } = this.state
const { config } = this.state
config.hotkey = {
toggleFinder: this.refs.toggleFinder.value,
toggleMain: this.refs.toggleMain.value
@@ -69,14 +72,22 @@ class HotkeyTab extends React.Component {
})
}
clearMessage () {
_.debounce(() => {
this.setState({
keymapAlert: null
})
}, 2000)()
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
const keymapAlert = this.state.keymapAlert
const keymapAlertElement = keymapAlert != null
? <p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
: null
let { config } = this.state
const { config } = this.state
return (
<div styleName='root'>
@@ -94,7 +105,7 @@ class HotkeyTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Toggle Finder(popup)</div>
<div styleName='group-section-label'>Toggle Finder (Quick search)</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}

View File

@@ -4,6 +4,7 @@ import styles from './InfoTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import _ from 'lodash'
const electron = require('electron')
const { shell, remote } = electron
@@ -36,8 +37,21 @@ class InfoTab extends React.Component {
if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
this.setState({
amaMessage: 'We hope we will gain your trust'
})
} else {
this.setState({
amaMessage: 'Thank\'s for trust us'
})
}
_.debounce(() => {
this.setState({
amaMessage: ''
})
}, 3000)()
ConfigManager.set(newConfig)
store.dispatch({
@@ -46,10 +60,49 @@ class InfoTab extends React.Component {
})
}
infoMessage () {
const { amaMessage } = this.state
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
}
render () {
return (
<div styleName='root'>
<div styleName='header'>Info</div>
<div styleName='header--sub'>Community</div>
<div styleName='top'>
<ul styleName='list'>
<li>
<a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)}
>Subscribe to Newsletter</a>
</li>
<li>
<a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)}
>GitHub</a>
</li>
<li>
<a href='https://medium.com/boostnote'
onClick={(e) => this.handleLinkClick(e)}
>Blog</a>
</li>
<li>
<a href='https://www.facebook.com/groups/boostnote'
onClick={(e) => this.handleLinkClick(e)}
>Facebook Group</a>
</li>
<li>
<a href='https://twitter.com/boostnoteapp'
onClick={(e) => this.handleLinkClick(e)}
>Twitter</a>
</li>
</ul>
</div>
<hr />
<div styleName='header--sub'>Info</div>
<div styleName='top'>
<div styleName='icon-space'>
@@ -62,31 +115,17 @@ class InfoTab extends React.Component {
</div>
</div>
</div>
<ul styleName='list'>
<li>
<a href='https://boostnote.io'
onClick={(e) => this.handleLinkClick(e)}
>Website</a>
</li>
<li>
<a href='https://boostnote.paintory.com/'
onClick={(e) => this.handleLinkClick(e)}
>Boostnote Shop</a> : Products are shipped to all over the world 🌏
</li>
<li>
<a href='https://salt.bountysource.com/teams/boostnote'
onClick={(e) => this.handleLinkClick(e)}
>Donate via Bountysource</a> : Thank you for your support 🎉
</li>
<li>
<a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)}
>GitHub Issues</a> : We&apos;d love to hear your feedback 🙌
</li>
<li>
<a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md'
onClick={(e) => this.handleLinkClick(e)}
>Development</a> : Development configurations for Boostnote 🚀
>Development</a> : Development configurations for Boostnote.
</li>
<li styleName='cc'>
Copyright (C) 2017 Maisin&Co.
@@ -95,11 +134,13 @@ class InfoTab extends React.Component {
License: GPL v3
</li>
</ul>
<hr />
<hr styleName='separate-line' />
<div styleName='policy'>Data collection policy</div>
<p>We collect only the number of DAU for Boostnote and DO NOT collect any detail information</p>
<p>such as your note content. You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</p>
<p>These data are only used for Boostnote improvements.</p>
<div>We collect only the number of DAU for Boostnote and **DO NOT collect** any detail information such as your note content.</div>
<div>You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<div>This data is only used for Boostnote improvements.</div>
<input onChange={(e) => this.handleConfigChange(e)}
checked={this.state.config.amaEnabled}
ref='amaEnabled'
@@ -107,6 +148,7 @@ class InfoTab extends React.Component {
/>
Enable to send analytics to our servers<br />
<button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>Save</button>
{this.infoMessage()}
</div>
)
}

View File

@@ -42,13 +42,29 @@
color #4E8EC6
text-decoration none
.separate-line
margin 40px 0
.policy
width 100%
font-size 20px
margin-bottom 10px
.policy-submit
margin-top 10px
.policy-confirm
margin-top 10px
font-size 12px
body[data-theme="dark"]
.root
color alpha($tab--dark-text-color, 80%)
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.list
a
color $ui-solarized-dark-active-color

View File

@@ -4,9 +4,10 @@ top-bar--height = 50px
.root
modal()
max-width 800px
min-height 500px
height 80%
max-width 100vw
min-height 100vh
height 100vh
width 100vw
overflow hidden
position relative
@@ -24,23 +25,23 @@ top-bar--height = 50px
absolute top left right
top top-bar--height
left 0
width 140px
margin-left 30px
width 170px
margin-left 10px
margin-top 20px
background-color $ui-backgroundColor
.nav-button
font-size 14px
text-align left
width 100px
margin 4px 0
padding 5px 0
width 150px
margin 5px 0
padding 7px 0
padding-left 10px
border none
border-radius 2px
background-color transparent
color $ui-text-color
font-size 14px
font-size 16px
.nav-button--active
@extend .nav-button
@@ -55,7 +56,7 @@ top-bar--height = 50px
.content
absolute left right bottom
top top-bar--height
left 140px
left 170px
margin-top 10px
overflow-y auto
@@ -85,3 +86,29 @@ body[data-theme="dark"]
background-color $dark-primary-button-background--active
&:hover
color white
body[data-theme="solarized-dark"]
.root
background-color transparent
.top-bar
background-color transparent
border-color $ui-solarized-dark-borderColor
p
color $ui-solarized-dark-text-color
.nav
background-color transparent
border-color $ui-solarized-dark-borderColor
.nav-button
background-color transparent
color $ui-solarized-dark-text-color
&:hover
color $ui-solarized-dark-text-color
.nav-button--active
@extend .nav-button
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button--active-backgroundColor
&:hover
color white

View File

@@ -1,265 +1,14 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import FolderList from './FolderList'
const { shell, remote } = require('electron')
const { dialog } = remote
import { SketchPicker } from 'react-color'
class UnstyledFolderItem extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
folder: {
showColumnPicker: false,
colorPickerPos: { left: 0, top: 0 },
color: props.color,
name: props.name
}
}
}
handleEditChange (e) {
let { folder } = this.state
folder.name = this.refs.nameInput.value
this.setState({
folder
})
}
handleConfirmButtonClick (e) {
this.confirm()
}
confirm () {
let { storage, folder } = this.props
dataApi
.updateFolder(storage.key, folder.key, {
color: this.state.folder.color,
name: this.state.folder.name
})
.then((data) => {
store.dispatch({
type: 'UPDATE_FOLDER',
storage: data.storage
})
this.setState({
status: 'IDLE'
})
})
}
handleColorButtonClick (e) {
const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } })
this.setState({ folder }, function () {
// After the color picker has been painted, re-calculate its position
// by comparing its dimensions to the host dimensions.
const { hostBoundingBox } = this.props
const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker)
const colorPickerBox = colorPickerNode.getBoundingClientRect()
const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom
const folder = Object.assign({}, this.state.folder, {
colorPickerPos: {
left: 25,
top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics
}
})
this.setState({ folder })
})
}
handleColorChange (color) {
const folder = Object.assign({}, this.state.folder, { color: color.hex })
this.setState({ folder })
}
handleColorPickerClose (event) {
const folder = Object.assign({}, this.state.folder, { showColumnPicker: false })
this.setState({ folder })
}
handleCancelButtonClick (e) {
this.setState({
status: 'IDLE'
})
}
handleFolderItemBlur (e) {
let el = e.relatedTarget
while (el != null) {
if (el === this.refs.root) {
return false
}
el = el.parentNode
}
this.confirm()
}
renderEdit (e) {
const popover = { position: 'absolute', zIndex: 2 }
const cover = {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
}
const pickerStyle = Object.assign({}, {
position: 'absolute'
}, this.state.folder.colorPickerPos)
return (
<div styleName='folderList-item'
onBlur={(e) => this.handleFolderItemBlur(e)}
tabIndex='-1'
ref='root'
>
<div styleName='folderList-item-left'>
<button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}}
onClick={(e) => !this.state.folder.showColumnPicker && this.handleColorButtonClick(e)}
>
{this.state.folder.showColumnPicker
? <div style={popover}>
<div style={cover}
onClick={() => this.handleColorPickerClose()}
/>
<div style={pickerStyle}>
<SketchPicker
ref='colorPicker'
color={this.state.folder.color}
onChange={(color) => this.handleColorChange(color)}
onChangeComplete={(color) => this.handleColorChange(color)}
/>
</div>
</div>
: null
}
<i className='fa fa-square' />
</button>
<input styleName='folderList-item-left-nameInput'
value={this.state.folder.name}
ref='nameInput'
onChange={(e) => this.handleEditChange(e)}
/>
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleDeleteConfirmButtonClick (e) {
let { storage, folder } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
store.dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
renderDelete () {
return (
<div styleName='folderList-item'>
<div styleName='folderList-item-left'>
Are you sure to <span styleName='folderList-item-left-danger'>delete</span> this folder?
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-dangerButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleEditButtonClick (e) {
let { folder: propsFolder } = this.props
let { folder: stateFolder } = this.state
const folder = Object.assign({}, stateFolder, propsFolder)
this.setState({
status: 'EDIT',
folder
}, () => {
this.refs.nameInput.select()
})
}
handleDeleteButtonClick (e) {
this.setState({
status: 'DELETE'
})
}
renderIdle () {
let { folder } = this.props
return (
<div styleName='folderList-item'
onDoubleClick={(e) => this.handleEditButtonClick(e)}
>
<div styleName='folderList-item-left'
style={{borderColor: folder.color}}
>
<span styleName='folderList-item-left-name'>{folder.name}</span>
<span styleName='folderList-item-left-key'>({folder.key})</span>
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleEditButtonClick(e)}
>
Edit
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
Delete
</button>
</div>
</div>
)
}
render () {
switch (this.state.status) {
case 'DELETE':
return this.renderDelete()
case 'EDIT':
return this.renderEdit()
case 'IDLE':
default:
return this.renderIdle()
}
}
}
const FolderItem = CSSModules(UnstyledFolderItem, styles)
class StorageItem extends React.Component {
constructor (props) {
@@ -271,8 +20,8 @@ class StorageItem extends React.Component {
}
handleNewFolderButtonClick (e) {
let { storage } = this.props
let input = {
const { storage } = this.props
const input = {
name: 'Untitled',
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
@@ -290,12 +39,12 @@ class StorageItem extends React.Component {
}
handleExternalButtonClick () {
let { storage } = this.props
const { storage } = this.props
shell.showItemInFolder(storage.path)
}
handleUnlinkButtonClick (e) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.',
@@ -303,7 +52,7 @@ class StorageItem extends React.Component {
})
if (index === 0) {
let { storage } = this.props
const { storage } = this.props
dataApi.removeStorage(storage.key)
.then(() => {
store.dispatch({
@@ -318,7 +67,7 @@ class StorageItem extends React.Component {
}
handleLabelClick (e) {
let { storage } = this.props
const { storage } = this.props
this.setState({
isLabelEditing: true,
name: storage.name
@@ -333,7 +82,7 @@ class StorageItem extends React.Component {
}
handleLabelBlur (e) {
let { storage } = this.props
const { storage } = this.props
dataApi
.renameStorage(storage.key, this.state.name)
.then((_storage) => {
@@ -348,14 +97,8 @@ class StorageItem extends React.Component {
}
render () {
let { storage, hostBoundingBox } = this.props
let folderList = storage.folders.map((folder) => {
return <FolderItem key={folder.key}
folder={folder}
storage={storage}
hostBoundingBox={hostBoundingBox}
/>
})
const { storage, hostBoundingBox } = this.props
return (
<div styleName='root' key={storage.key}>
<div styleName='header'>
@@ -404,12 +147,9 @@ class StorageItem extends React.Component {
</button>
</div>
</div>
<div styleName='folderList'>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>No Folders</div>
}
</div>
<FolderList storage={storage}
hostBoundingBox={hostBoundingBox}
/>
</div>
)
}
@@ -426,11 +166,6 @@ StorageItem.propTypes = {
}),
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.string,
color: PropTypes.string,
name: PropTypes.string
})
}

View File

@@ -63,75 +63,6 @@
z-index 10
white-space nowrap
.folderList-item
height 35px
box-sizing border-box
padding 2.5px 15px
&:hover
background-color darken(white, 3%)
.folderList-item-left
height 30px
border-left solid 2px transparent
padding 0 10px
line-height 30px
float left
.folderList-item-left-danger
color $danger-color
font-weight bold
.folderList-item-left-key
color $ui-inactive-text-color
font-size 10px
margin 0 5px
border none
.folderList-item-left-colorButton
colorDefaultButton()
height 25px
width 25px
line-height 23px
padding 0
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
margin-right 5px
margin-left -15px
.folderList-item-left-nameInput
height 25px
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
padding 0 5px
outline none
.folderList-item-right
float right
.folderList-item-right-button
vertical-align middle
height 25px
margin-top 2.5px
colorDefaultButton()
border-radius 2px
border $ui-border
margin-right 5px
padding 0 5px
&:last-child
margin-right 0
.folderList-item-right-confirmButton
@extend .folderList-item-right-button
border none
colorPrimaryButton()
.folderList-item-right-dangerButton
@extend .folderList-item-right-button
border none
colorDangerButton()
body[data-theme="dark"]
.header
border-color $ui-dark-borderColor
@@ -153,28 +84,3 @@ body[data-theme="dark"]
top 25px
z-index 10
white-space nowrap
.folderList-item
&:hover
background-color lighten($ui-dark-button--hover-backgroundColor, 5%)
.folderList-item-left-danger
color $danger-color
font-weight bold
.folderList-item-left-key
color $ui-dark-inactive-text-color
.folderList-item-left-colorButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
.folderList-item-right-button
colorDarkDefaultButton()
border-color $ui-dark-borderColor
.folderList-item-right-confirmButton
colorDarkPrimaryButton()
.folderList-item-right-dangerButton
colorDarkDangerButton()

View File

@@ -1,16 +1,17 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi'
import StorageItem from './StorageItem'
const electron = require('electron')
const remote = electron.remote
const { shell, remote } = electron
function browseFolder () {
let dialog = remote.dialog
const dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
const defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
@@ -50,11 +51,16 @@ class StoragesTab extends React.Component {
})
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
renderList () {
let { data, boundingBox } = this.props
const { data, boundingBox } = this.props
if (!boundingBox) { return null }
let storageList = data.storageMap.map((storage) => {
const storageList = data.storageMap.map((storage) => {
return <StorageItem
key={storage.key}
storage={storage}
@@ -83,7 +89,7 @@ class StoragesTab extends React.Component {
browseFolder()
.then((targetPath) => {
if (targetPath.length > 0) {
let { newStorage } = this.state
const { newStorage } = this.state
newStorage.path = targetPath
this.setState({
newStorage
@@ -97,7 +103,7 @@ class StoragesTab extends React.Component {
}
handleAddStorageChange (e) {
let { newStorage } = this.state
const { newStorage } = this.state
newStorage.name = this.refs.addStorageName.value
newStorage.path = this.refs.addStoragePath.value
this.setState({
@@ -112,7 +118,7 @@ class StoragesTab extends React.Component {
path: this.state.newStorage.path
})
.then((data) => {
let { dispatch } = this.props
const { dispatch } = this.props
dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
@@ -161,7 +167,10 @@ class StoragesTab extends React.Component {
<option value='FILESYSTEM'>File System</option>
</select>
<div styleName='addStorage-body-section-type-description'>
3rd party cloud integration(such as Google Drive and Dropbox) will be available soon.
3rd party cloud integration:
<a href='https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup'
onClick={(e) => this.handleLinkClick(e)}
>Cloud-Syncing-and-Backup</a>
</div>
</div>
</div>

View File

@@ -158,3 +158,44 @@ body[data-theme="dark"]
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.folderList-item
border-bottom $ui-solarized-dark-borderColor
.folderList-empty
color $ui-solarized-dark-text-color
.list-empty
color $ui-solarized-dark-text-color
.list-control-addStorageButton
border-color $ui-solarized-dark-button-backgroundColor
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.addStorage-header
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
.addStorage-body-section-name-input
border-color $$ui-solarized-dark-borderColor
.addStorage-body-section-type-description
color $ui-solarized-dark-text-color
.addStorage-body-section-path-button
colorPrimaryButton()
.addStorage-body-control
border-color $ui-solarized-dark-borderColor
.addStorage-body-control-createButton
colorDarkPrimaryButton()
.addStorage-body-control-cancelButton
colorDarkDefaultButton()
border-color $ui-solarized-dark-borderColor

View File

@@ -10,8 +10,12 @@ $tab--button-font-size = 14px
$tab--dark-text-color = #E5E5E5
.header
font-size 24px
margin-bottom 30px
font-size 36px
margin-bottom 60px
.header--sub
font-size 36px
margin-bottom 20px
body[data-theme="dark"]
.header

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
@@ -9,6 +10,11 @@ import CodeMirror from 'codemirror'
const OSX = global.process.platform === 'darwin'
import _ from 'lodash'
const electron = require('electron')
const ipc = electron.ipcRenderer
class UiTab extends React.Component {
constructor (props) {
super(props)
@@ -18,8 +24,27 @@ class UiTab extends React.Component {
}
}
componentWillMount () {
CodeMirror.autoLoadMode(ReactCodeMirror, 'javascript')
componentDidMount () {
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
this.handleSettingDone = () => {
this.setState({UiAlert: {
type: 'success',
message: 'Successfully applied!'
}})
}
this.handleSettingError = (err) => {
this.setState({UiAlert: {
type: 'error',
message: err.message != null ? err.message : 'Error occurs!'
}})
}
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
componentWillUnmount () {
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
}
handleUIChange (e) {
@@ -36,6 +61,7 @@ class UiTab extends React.Component {
const newConfig = {
ui: {
theme: this.refs.uiTheme.value,
showCopyNotification: this.refs.showCopyNotification.checked,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
@@ -48,20 +74,25 @@ class UiTab extends React.Component {
indentSize: this.refs.editorIndentSize.value,
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
switchPreview: this.refs.editorSwitchPreview.value,
keyMap: this.refs.editorKeyMap.value
keyMap: this.refs.editorKeyMap.value,
scrollPastEnd: this.refs.scrollPastEnd.checked
},
preview: {
fontSize: this.refs.previewFontSize.value,
fontFamily: this.refs.previewFontFamily.value,
codeBlockTheme: this.refs.previewCodeBlockTheme.value,
lineNumber: this.refs.previewLineNumber.checked
lineNumber: this.refs.previewLineNumber.checked,
latexInlineOpen: this.refs.previewLatexInlineOpen.value,
latexInlineClose: this.refs.previewLatexInlineClose.value,
latexBlockOpen: this.refs.previewLatexBlockOpen.value,
latexBlockClose: this.refs.previewLatexBlockClose.value
}
}
const newCodemirrorTheme = this.refs.editorTheme.value
if (newCodemirrorTheme !== codemirrorTheme) {
checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme}.css`)
checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`)
}
this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme })
@@ -80,9 +111,25 @@ class UiTab extends React.Component {
type: 'SET_UI',
config: newConfig
})
this.clearMessage()
}
clearMessage () {
_.debounce(() => {
this.setState({
UiAlert: null
})
}, 2000)()
}
render () {
const UiAlert = this.state.UiAlert
const UiAlertElement = UiAlert != null
? <p className={`alert ${UiAlert.type}`}>
{UiAlert.message}
</p>
: null
const themes = consts.THEMES
const { config, codemirrorTheme } = this.state
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
@@ -91,19 +138,30 @@ class UiTab extends React.Component {
<div styleName='group'>
<div styleName='group-header'>UI</div>
<div styleName='group-header2'>Theme</div>
<div styleName='group-section'>
Color Theme
<div styleName='group-section-control'>
<select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)}
ref='uiTheme'
>
<option value='default'>Light</option>
<option value='default'>Default</option>
<option value='white'>White</option>
<option value='solarized-dark'>Solarized Dark</option>
<option value='dark'>Dark</option>
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.showCopyNotification}
ref='showCopyNotification'
type='checkbox'
/>&nbsp;
Show &quot;Saved to Clipboard&quot; notification when copying
</label>
</div>
{
global.process.platform === 'win32'
? <div styleName='group-checkBoxSection'>
@@ -137,7 +195,7 @@ class UiTab extends React.Component {
}
</select>
<div styleName='code-mirror'>
<ReactCodeMirror value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} />
<ReactCodeMirror ref={e => (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} />
</div>
</div>
</div>
@@ -219,7 +277,7 @@ class UiTab extends React.Component {
<option value='vim'>vim</option>
<option value='emacs'>emacs</option>
</select>
<span styleName='note-for-keymap'>Please restart boostnote after you change the keymap</span>
<p styleName='note-for-keymap'> Please restart boostnote after you change the keymap</p>
</div>
</div>
@@ -234,6 +292,18 @@ class UiTab extends React.Component {
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.scrollPastEnd}
ref='scrollPastEnd'
type='checkbox'
/>&nbsp;
Allow editor to scroll past the last line
</label>
</div>
<div styleName='group-header2'>Preview</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
@@ -286,13 +356,64 @@ class UiTab extends React.Component {
Show line numbers for preview code blocks
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Inline Open Delimiter
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewLatexInlineOpen'
value={config.preview.latexInlineOpen}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Inline Close Delimiter
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewLatexInlineClose'
value={config.preview.latexInlineClose}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Block Open Delimiter
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewLatexBlockOpen'
value={config.preview.latexBlockOpen}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Block Close Delimiter
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewLatexBlockClose'
value={config.preview.latexBlockClose}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div className='group-control'>
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}
>
Save
onClick={(e) => this.handleSaveUIClick(e)}>Save
</button>
{UiAlertElement}
</div>
</div>
</div>

View File

@@ -1,13 +1,16 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import ReactDOM from 'react-dom'
import { connect } from 'react-redux'
import HotkeyTab from './HotkeyTab'
import UiTab from './UiTab'
import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab'
import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferencesModal.styl'
import RealtimeNotification from 'browser/components/RealtimeNotification'
class Preferences extends React.Component {
constructor (props) {
@@ -40,7 +43,7 @@ class Preferences extends React.Component {
renderContent () {
const { boundingBox } = this.state
let { dispatch, config, data } = this.props
const { dispatch, config, data } = this.props
switch (this.state.currentTab) {
case 'INFO':
@@ -64,6 +67,10 @@ class Preferences extends React.Component {
config={config}
/>
)
case 'CROWDFUNDING':
return (
<Crowdfunding />
)
case 'STORAGES':
default:
return (
@@ -88,17 +95,18 @@ class Preferences extends React.Component {
}
render () {
let content = this.renderContent()
const content = this.renderContent()
let tabs = [
const tabs = [
{target: 'STORAGES', label: 'Storages'},
{target: 'HOTKEY', label: 'Hotkey'},
{target: 'UI', label: 'UI'},
{target: 'INFO', label: 'Info'}
{target: 'INFO', label: 'Community / Info'},
{target: 'CROWDFUNDING', label: 'Crowdfunding'}
]
let navButtons = tabs.map((tab) => {
let isActive = this.state.currentTab === tab.target
const navButtons = tabs.map((tab) => {
const isActive = this.state.currentTab === tab.target
return (
<button styleName={isActive
? 'nav-button--active'
@@ -130,6 +138,7 @@ class Preferences extends React.Component {
<div ref='content' styleName='content'>
{content}
</div>
<RealtimeNotification />
</div>
)
}

View File

@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RenameFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
@@ -48,7 +49,7 @@ class RenameFolderModal extends React.Component {
confirm () {
if (this.state.name.trim().length > 0) {
let { storage, folder } = this.props
const { storage, folder } = this.props
dataApi
.updateFolder(storage.key, folder.key, {
name: this.state.name,

View File

@@ -27,8 +27,8 @@ function data (state = defaultDataMap(), action) {
action.notes.some((note) => {
if (note === undefined) return true
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
const uniqueKey = note.storage + '-' + note.key
const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
if (note.isStarred) {
@@ -65,10 +65,10 @@ function data (state = defaultDataMap(), action) {
return state
case 'UPDATE_NOTE':
{
let note = action.note
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
let oldNote = state.noteMap.get(uniqueKey)
const note = action.note
const uniqueKey = note.storage + '-' + note.key
const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
state.noteMap = new Map(state.noteMap)
@@ -110,7 +110,7 @@ function data (state = defaultDataMap(), action) {
state.folderNoteMap.set(folderKey, folderNoteSet)
if (oldNote != null) {
let oldFolderKey = oldNote.storage + '-' + oldNote.folder
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
@@ -119,8 +119,8 @@ function data (state = defaultDataMap(), action) {
}
if (oldNote != null) {
let discardedTags = _.difference(oldNote.tags, note.tags)
let addedTags = _.difference(note.tags, oldNote.tags)
const discardedTags = _.difference(oldNote.tags, note.tags)
const addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
@@ -156,12 +156,12 @@ function data (state = defaultDataMap(), action) {
}
case 'MOVE_NOTE':
{
let originNote = action.originNote
let originKey = originNote.storage + '-' + originNote.key
let note = action.note
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
let oldNote = state.noteMap.get(uniqueKey)
const originNote = action.originNote
const originKey = originNote.storage + '-' + originNote.key
const note = action.note
const uniqueKey = note.storage + '-' + note.key
const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
state.noteMap = new Map(state.noteMap)
@@ -191,7 +191,7 @@ function data (state = defaultDataMap(), action) {
// From folderNoteMap
state.folderNoteMap = new Map(state.folderNoteMap)
let originFolderKey = originNote.storage + '-' + originNote.folder
const originFolderKey = originNote.storage + '-' + originNote.folder
let originFolderList = state.folderNoteMap.get(originFolderKey)
originFolderList = new Set(originFolderList)
originFolderList.delete(originKey)
@@ -245,7 +245,7 @@ function data (state = defaultDataMap(), action) {
state.folderNoteMap.set(folderKey, folderNoteList)
if (oldNote != null) {
let oldFolderKey = oldNote.storage + '-' + oldNote.folder
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
@@ -255,8 +255,8 @@ function data (state = defaultDataMap(), action) {
// Remove from old folder map
if (oldNote != null) {
let discardedTags = _.difference(oldNote.tags, note.tags)
let addedTags = _.difference(note.tags, oldNote.tags)
const discardedTags = _.difference(oldNote.tags, note.tags)
const addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
@@ -292,8 +292,8 @@ function data (state = defaultDataMap(), action) {
}
case 'DELETE_NOTE':
{
let uniqueKey = action.storageKey + '-' + action.noteKey
let targetNote = state.noteMap.get(uniqueKey)
const uniqueKey = action.storageKey + '-' + action.noteKey
const targetNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
@@ -317,7 +317,7 @@ function data (state = defaultDataMap(), action) {
}
// From folderNoteMap
let folderKey = targetNote.storage + '-' + targetNote.folder
const folderKey = targetNote.storage + '-' + targetNote.folder
state.folderNoteMap = new Map(state.folderNoteMap)
let folderSet = state.folderNoteMap.get(folderKey)
folderSet = new Set(folderSet)
@@ -340,11 +340,14 @@ function data (state = defaultDataMap(), action) {
return state
}
case 'UPDATE_FOLDER':
{
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
}
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
return state
case 'REORDER_FOLDER':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
return state
case 'DELETE_FOLDER':
{
@@ -354,8 +357,8 @@ function data (state = defaultDataMap(), action) {
// Get note list from folder-note map
// and delete note set from folder-note map
let folderKey = action.storage.key + '-' + action.folderKey
let noteSet = state.folderNoteMap.get(folderKey)
const folderKey = action.storage.key + '-' + action.folderKey
const noteSet = state.folderNoteMap.get(folderKey)
state.folderNoteMap = new Map(state.folderNoteMap)
state.folderNoteMap.delete(folderKey)
@@ -368,7 +371,7 @@ function data (state = defaultDataMap(), action) {
if (noteSet != null) {
noteSet.forEach(function handleNoteKey (noteKey) {
// Get note from noteMap
let note = state.noteMap.get(noteKey)
const note = state.noteMap.get(noteKey)
if (note != null) {
state.noteMap.delete(noteKey)
@@ -410,8 +413,8 @@ function data (state = defaultDataMap(), action) {
state.folderNoteMap = new Map(state.folderNoteMap)
state.tagNoteMap = new Map(state.tagNoteMap)
action.notes.forEach((note) => {
let uniqueKey = note.storage + '-' + note.key
let folderKey = note.storage + '-' + note.folder
const uniqueKey = note.storage + '-' + note.key
const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
if (note.isStarred) {
@@ -444,7 +447,7 @@ function data (state = defaultDataMap(), action) {
return state
case 'REMOVE_STORAGE':
state = Object.assign({}, state)
let storage = state.storageMap.get(action.storageKey)
const storage = state.storageMap.get(action.storageKey)
state.storageMap = new Map(state.storageMap)
state.storageMap.delete(action.storageKey)
@@ -452,17 +455,17 @@ function data (state = defaultDataMap(), action) {
if (storage != null) {
state.folderMap = new Map(state.folderMap)
storage.folders.forEach((folder) => {
let folderKey = storage.key + '-' + folder.key
const folderKey = storage.key + '-' + folder.key
state.folderMap.delete(folderKey)
})
}
// Remove notes from noteMap and tagNoteMap
let storageNoteSet = state.storageNoteMap.get(action.storageKey)
const storageNoteSet = state.storageNoteMap.get(action.storageKey)
state.storageNoteMap = new Map(state.storageNoteMap)
state.storageNoteMap.delete(action.storageKey)
if (storageNoteSet != null) {
let notes = storageNoteSet
const notes = storageNoteSet
.map((noteKey) => state.noteMap.get(noteKey))
.filter((note) => note != null)
@@ -470,7 +473,7 @@ function data (state = defaultDataMap(), action) {
state.tagNoteMap = new Map(state.tagNoteMap)
state.starredSet = new Set(state.starredSet)
notes.forEach((note) => {
let noteKey = storage.key + '-' + note.key
const noteKey = storage.key + '-' + note.key
state.noteMap.delete(noteKey)
state.starredSet.delete(noteKey)
note.tags.forEach((tag) => {
@@ -528,13 +531,13 @@ function status (state = defaultStatus, action) {
return state
}
let reducer = combineReducers({
const reducer = combineReducers({
data,
config,
status,
routing: routerReducer
})
let store = createStore(reducer)
const store = createStore(reducer)
export default store