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

Merge branch 'master' into sean/improve-english

This commit is contained in:
Sean Baines
2017-10-05 07:46:02 +01:00
committed by GitHub
73 changed files with 1467 additions and 635 deletions

33
.boostnoterc.sample Normal file
View File

@@ -0,0 +1,33 @@
{
"amaEnabled": true,
"editor": {
"fontFamily": "Monaco, Consolas",
"fontSize": "14",
"indentSize": "2",
"indentType": "space",
"keyMap": "vim",
"switchPreview": "BLUR",
"theme": "monokai"
},
"hotkey": {
"toggleFinder": "Cmd + Alt + S",
"toggleMain": "Cmd + Alt + L"
},
"isSideNavFolded": false,
"listStyle": "DEFAULT",
"listWidth": 174,
"navWidth": 200,
"preview": {
"codeBlockTheme": "dracula",
"fontFamily": "Lato",
"fontSize": "14",
"lineNumber": true
},
"sortBy": "UPDATED_AT",
"ui": {
"defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false,
"theme": "default"
},
"zoom": 1
}

View File

@@ -1,10 +1,16 @@
{ {
"extends": ["standard", "standard-jsx"], "extends": ["standard", "standard-jsx", "plugin:react/recommended"],
"plugins": ["react"],
"rules": { "rules": {
"no-useless-escape": 0, "no-useless-escape": 0,
"prefer-const": "warn", "prefer-const": "warn",
"no-unused-vars": "warn", "no-unused-vars": "warn",
"no-undef": "warn", "no-undef": "warn",
"no-lone-blocks": "warn" "no-lone-blocks": "warn",
"react/prop-types": 0,
"react/no-string-refs": 0,
"react/no-find-dom-node": "warn",
"react/no-render-return-value": "warn",
"react/no-deprecated": "warn"
} }
} }

View File

@@ -3,3 +3,7 @@ You can support Boostnote from $ 5 a month!
# Backers # Backers
[Kazu Yokomizo](https://twitter.com/kazup_bot) [Kazu Yokomizo](https://twitter.com/kazup_bot)
kolchan11
RonWalker22

View File

@@ -3,6 +3,7 @@ import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import path from 'path' import path from 'path'
import copyImage from 'browser/main/lib/dataApi/copyImage' import copyImage from 'browser/main/lib/dataApi/copyImage'
import { findStorage } from 'browser/lib/findStorage'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -39,6 +40,7 @@ export default class CodeEditor extends React.Component {
} }
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
} }
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
this.loadStyleHandler = (e) => { this.loadStyleHandler = (e) => {
this.editor.refresh() this.editor.refresh()
} }
@@ -98,14 +100,25 @@ export default class CodeEditor extends React.Component {
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
let editorTheme = document.getElementById('editorTheme') let editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler) editorTheme.addEventListener('load', this.loadStyleHandler)
CodeMirror.Vim.defineEx('quit', 'q', this.quitEditor)
CodeMirror.Vim.defineEx('q!', 'q!', this.quitEditor)
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
}
quitEditor () {
document.querySelector('textarea').blur()
} }
componentWillUnmount () { componentWillUnmount () {
this.editor.off('blur', this.blurHandler) this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler) this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler)
let editorTheme = document.getElementById('editorTheme') let editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
} }
@@ -201,7 +214,30 @@ export default class CodeEditor extends React.Component {
insertImageMd (imageMd) { insertImageMd (imageMd) {
const textarea = this.editor.getInputField() const textarea = this.editor.getInputField()
const cm = this.editor const cm = this.editor
textarea.value = `${textarea.value.substr(0, textarea.selectionStart)}${imageMd}${textarea.value.substr(textarea.selectionEnd)}` cm.replaceSelection(`${textarea.value.substr(0, textarea.selectionStart)}${imageMd}${textarea.value.substr(textarea.selectionEnd)}`)
}
handlePaste (editor, e) {
const dataTransferItem = e.clipboardData.items[0]
if (!dataTransferItem.type.match('image')) return
const blob = dataTransferItem.getAsFile()
let reader = new FileReader()
let base64data
reader.readAsDataURL(blob)
reader.onloadend = () => {
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary')
const imageName = Math.random().toString(36).slice(-16)
const storagePath = findStorage(this.props.storageKey).path
const imagePath = path.join(`${storagePath}`, 'images', `${imageName}.png`)
require('fs').writeFile(imagePath, binaryData, 'binary')
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})`
this.insertImageMd(imageMd)
}
} }
render () { render () {

View File

@@ -266,6 +266,7 @@ class MarkdownEditor extends React.Component {
onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)} onMouseDown={(e) => this.handlePreviewMouseDown(e)}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
/> />
</div> </div>

View File

@@ -40,7 +40,6 @@ body {
code { code {
font-family: ${codeBlockFontFamily.join(', ')}; font-family: ${codeBlockFontFamily.join(', ')};
background-color: rgba(0,0,0,0.04); background-color: rgba(0,0,0,0.04);
color: #CC305F;
} }
.lineNumber { .lineNumber {
${lineNumber && 'display: block !important;'} ${lineNumber && 'display: block !important;'}
@@ -51,12 +50,12 @@ code {
color: rgba(147,147,149,0.8);; color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);; fill: rgba(147,147,149,1);;
border-radius: 50%; border-radius: 50%;
margin: 7px; margin: 0px 10px;
border: none; border: none;
background-color: transparent; background-color: transparent;
outline: none; outline: none;
height: 32px; height: 15px;
width: 32px; width: 15px;
cursor: pointer; cursor: pointer;
} }
@@ -224,6 +223,7 @@ export default class MarkdownPreview extends React.Component {
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme || prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber || prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
prevProps.theme !== this.props.theme) { prevProps.theme !== this.props.theme) {
this.applyStyle() this.applyStyle()
this.rewriteIframe() this.rewriteIframe()
@@ -262,7 +262,7 @@ export default class MarkdownPreview extends React.Component {
el.removeEventListener('click', this.linkClickHandler) el.removeEventListener('click', this.linkClickHandler)
}) })
let { value, theme, indentSize, codeBlockTheme, storagePath } = this.props let { value, theme, indentSize, codeBlockTheme, showCopyNotification, storagePath } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
@@ -291,8 +291,9 @@ export default class MarkdownPreview extends React.Component {
}) })
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
el.src = markdown.normalizeLinkText(el.src)
if (!/\/:storage/.test(el.src)) return if (!/\/:storage/.test(el.src)) return
el.src = `file:///${path.join(storagePath, 'images', path.basename(el.src))}` el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
}) })
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
@@ -308,10 +309,12 @@ export default class MarkdownPreview extends React.Component {
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>' copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
copyIcon.onclick = (e) => { copyIcon.onclick = (e) => {
copy(content) copy(content)
this.notify('Saved to Clipboard!', { if (showCopyNotification) {
body: 'Paste it wherever you want!', this.notify('Saved to Clipboard!', {
silent: true body: 'Paste it wherever you want!',
}) silent: true
})
}
} }
el.parentNode.appendChild(copyIcon) el.parentNode.appendChild(copyIcon)
el.innerHTML = '' el.innerHTML = ''
@@ -425,5 +428,6 @@ MarkdownPreview.propTypes = {
onMouseDown: PropTypes.func, onMouseDown: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string storagePath: PropTypes.string
} }

View File

@@ -6,7 +6,7 @@ const ModalEscButton = ({
handleEscButtonClick handleEscButtonClick
}) => ( }) => (
<button styleName='escButton' onClick={handleEscButtonClick}> <button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>x</div> <div styleName='esc-mark'>×</div>
<div styleName='esc-text'>esc</div> <div styleName='esc-text'>esc</div>
</button> </button>
) )

View File

@@ -11,4 +11,6 @@
height top-bar-height height top-bar-height
.esc-mark .esc-mark
font-size 15px font-size 28px
margin-top -5px
margin-bottom -7px

View File

@@ -4,7 +4,9 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { isArray } from 'lodash' import { isArray } from 'lodash'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl' import styles from './NoteItem.styl'
import TodoProcess from './TodoProcess'
/** /**
* @description Tag element component. * @description Tag element component.
@@ -68,6 +70,10 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleDragStar
{note.isStarred {note.isStarred
? <i styleName='item-star' className='fa fa-star' /> : '' ? <i styleName='item-star' className='fa fa-star' /> : ''
} }
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''
}
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0

View File

@@ -39,6 +39,7 @@ $control-height = 30px
.item-wrapper .item-wrapper
padding 15px 0 padding 15px 0
border-bottom $ui-border border-bottom $ui-border
position relative
.item--active .item--active
@extend .item @extend .item
@@ -116,8 +117,8 @@ $control-height = 30px
.item-star .item-star
position absolute position absolute
right 5px right -20px
bottom 0px bottom 2px
width 34px width 34px
height 34px height 34px
color alpha($ui-favorite-star-button-color, 60%) color alpha($ui-favorite-star-button-color, 60%)

View File

@@ -48,6 +48,7 @@ $control-height = 30px
overflow ellipsis overflow ellipsis
color $ui-inactive-text-color color $ui-inactive-text-color
border-bottom $ui-border border-bottom $ui-border
position relative
.item-simple-title-icon .item-simple-title-icon
font-size 12px font-size 12px

View File

@@ -0,0 +1,55 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RealtimeNotification.styl'
const electron = require('electron')
const { shell } = electron
class RealtimeNotification extends React.Component {
constructor (props) {
super(props)
this.state = {
notifications: []
}
}
componentDidMount () {
this.fetchNotifications()
}
fetchNotifications () {
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
fetch(notificationsUrl)
.then(response => {
return response.json()
})
.then(json => {
this.setState({notifications: json.notifications})
})
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
render () {
const { notifications } = this.state
const link = notifications.length > 0
? <a styleName='notification-link' href={notifications[0].linkUrl}
onClick={(e) => this.handleLinkClick(e)}
>
{notifications[0].text}
</a>
: ''
return (
<div styleName='notification-area' style={this.props.style}>{link}</div>
)
}
}
RealtimeNotification.propTypes = {}
export default CSSModules(RealtimeNotification, styles)

View File

@@ -0,0 +1,34 @@
.notification-area
z-index 1000
font-size 12px
position absolute
bottom 0px
right 0px
background-color #EBEBEB
height 30px
.notification-link
position absolute
right 5px
top 5px
text-decoration none
color #282A36
border 1px solid #6FA8E6
background-color alpha(#6FA8E6, 0.2)
padding 3px 9px
border-radius 2px
transition 0.2s
&:hover
color #1378BD
body[data-theme="dark"]
.notification-area
background-color #1E2124
.notification-link
color #fff
border 1px solid alpha(#5CB85C, 0.6)
background-color alpha(#5CB85C, 0.2)
transition 0.2s
&:hover
color #5CB85C

View File

@@ -0,0 +1,33 @@
/**
* @fileoverview Percentage of todo achievement.
*/
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TodoProcess.styl'
const TodoProcess = ({
todoStatus: {
total: totalTodo,
completed: completedTodo
}
}) => (
<div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}>
<div styleName='todo-process-text'>
<i className='fa fa-fw fa-check-square-o' />
{completedTodo} of {totalTodo}
</div>
<div styleName='todo-process-bar'>
<div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} />
</div>
</div>
)
TodoProcess.propTypes = {
todoStatus: {
total: PropTypes.number.isRequired,
completed: PropTypes.number.isRequired
}
}
export default CSSModules(TodoProcess, styles)

View File

@@ -0,0 +1,45 @@
.todo-process
font-size 12px
display flex
padding-top 15px
width 85%
.todo-process-text
display inline-block
padding-right 10px
white-space nowrap
text-overflow ellipsis
color $ui-inactive-text-color
i
color $ui-inactive-text-color
padding-right 5px
.todo-process-bar
display inline-block
margin auto
height 4px
border-radius 10px
background-color #DADFE1
border-radius 2px
flex-grow 1
border 1px solid alpha(#6C7A89, 10%)
.todo-process-bar--inner
height 100%
border-radius 5px
background-color #6C7A89
transition 0.3s
body[data-theme="dark"]
.todo-process
color $ui-dark-text-color
.todo-process-bar
background-color #363A3D
.todo-process-text
color $ui-inactive-text-color
.todo-process-bar--inner
background-color: alpha(#939395, 50%)

View File

@@ -193,6 +193,7 @@ ol
&>li>ul, &>li>ol &>li>ul, &>li>ol
margin 0 margin 0
code code
color #CC305F
padding 0.2em 0.4em padding 0.2em 0.4em
background-color #f7f7f7 background-color #f7f7f7
border-radius 3px border-radius 3px

View File

@@ -64,7 +64,7 @@ $list-width = 250px
.result-nav-storageList .result-nav-storageList
absolute bottom left right absolute bottom left right
top 80px + 32px + 10px + 10px top 110px + 32px + 10px + 10px
overflow-y auto overflow-y auto
.result-list .result-list

View File

@@ -146,6 +146,7 @@ class NoteDetail extends React.Component {
config={config} config={config}
value={snippet.content} value={snippet.content}
ref={'code-' + index} ref={'code-' + index}
storageKey={note.storage}
/> />
: <CodeEditor styleName='tabView-content' : <CodeEditor styleName='tabView-content'
mode={snippet.mode} mode={snippet.mode}
@@ -195,6 +196,7 @@ class NoteDetail extends React.Component {
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize} indentSize={editorIndentSize}
value={note.content} value={note.content}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
/> />
) )

View File

@@ -2,6 +2,7 @@
.root .root
absolute top bottom left right absolute top bottom left right
bottom 30px
left $note-detail-left-margin left $note-detail-left-margin
right $note-detail-right-margin right $note-detail-right-margin
height 100% height 100%

21
browser/lib/RcParser.js Normal file
View File

@@ -0,0 +1,21 @@
import path from 'path'
import sander from 'sander'
const BOOSTNOTERC = '.boostnoterc'
const homePath = global.process.env.HOME || global.process.env.USERPROFILE
const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
export function parse (boostnotercPath = _boostnotercPath) {
if (!sander.existsSync(boostnotercPath)) return {}
try {
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
} catch (e) {
console.warn(e)
console.warn('Your .boostnoterc is broken so it\'s not used.')
return {}
}
}
export default {
parse
}

View File

@@ -0,0 +1,25 @@
export function getTodoStatus (content) {
let splitted = content.split('\n')
let numberOfTodo = 0
let numberOfCompletedTodo = 0
splitted.forEach((line) => {
let trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
numberOfTodo++
}
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
numberOfCompletedTodo++
}
})
return {
total: numberOfTodo,
completed: numberOfCompletedTodo
}
}
export function getTodoPercentageOfCompleted (content) {
const state = getTodoStatus(content)
return Math.floor(state.completed / state.total * 100)
}

View File

@@ -59,6 +59,15 @@ md.use(math, {
}) })
md.use(require('markdown-it-imsize')) md.use(require('markdown-it-imsize'))
md.use(require('markdown-it-footnote')) md.use(require('markdown-it-footnote'))
md.use(require('markdown-it-multimd-table'))
md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
// Override task item // Override task item
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token let content, terminate, i, l, token
@@ -156,12 +165,17 @@ function strip (input) {
return output return output
} }
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
}
const markdown = { const markdown = {
render: function markdown (content) { render: function markdown (content) {
if (!_.isString(content)) content = '' if (!_.isString(content)) content = ''
const renderedContent = md.render(content) const renderedContent = md.render(content)
return md.normalizeLinkText(renderedContent) return renderedContent
}, },
strip strip,
normalizeLinkText
} }
export default markdown export default markdown

View File

@@ -3,7 +3,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
const InfoPanel = ({ const InfoPanel = ({
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type
}) => ( }) => (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}> <div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div styleName='group-section'> <div styleName='group-section'>
@@ -24,7 +24,7 @@ const InfoPanel = ({
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Created at Created
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
{createdAt} {createdAt}
@@ -32,7 +32,7 @@ const InfoPanel = ({
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Updated at Updated
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
{updatedAt} {updatedAt}
@@ -46,6 +46,27 @@ const InfoPanel = ({
<input value={noteLink} onClick={(e) => { e.target.select() }} /> <input value={noteLink} onClick={(e) => { e.target.select() }} />
</div> </div>
</div> </div>
{type === 'SNIPPET_NOTE'
? ''
: <div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Words
</div>
<div styleName='group-section-control'>
{wordCount}
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Letters
</div>
<div styleName='group-section-control'>
{letterCount}
</div>
</div>
</div>
}
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}> <button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
@@ -73,7 +94,10 @@ InfoPanel.propTypes = {
updatedAt: PropTypes.string.isRequired, updatedAt: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired, exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired exportAsTxt: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired
} }
export default CSSModules(InfoPanel, styles) export default CSSModules(InfoPanel, styles)

View File

@@ -18,6 +18,16 @@
background-color $ui-noteList-backgroundColor background-color $ui-noteList-backgroundColor
border 1px solid $border-color border 1px solid $border-color
.control-infoButton-panel-trash
z-index 200
margin-top 45px
margin-left -230px
position absolute
padding 20px 20px 0 20px
width 320px
background-color $ui-noteList-backgroundColor
border 1px solid $border-color
.group-section .group-section
display flex display flex
line-height 30px line-height 30px
@@ -40,6 +50,19 @@
width 160px width 160px
height 25px height 25px
.group-section-control text
color #EA4447
font-weight 600
font-size 14px
width 70px
height 25px
background-color rgba(226,33,113,0.1)
border none
outline none
border-radius 2px
margin-right 5px
padding 2px 5px
[id=export-wrap] [id=export-wrap]
height 90px height 90px
display flex display flex
@@ -75,6 +98,10 @@ body[data-theme="dark"]
background-color $ui-dark-noteList-backgroundColor background-color $ui-dark-noteList-backgroundColor
border 1px solid $ui-dark-borderColor 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 .group-section-label
color $ui-inactive-text-color color $ui-inactive-text-color

View File

@@ -0,0 +1,70 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
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>
<div styleName='group-section'>
<div styleName='group-section-label'>
Folder
</div>
<div styleName='group-section-control'>
<text>Trash</text>{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 id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o fa-fw' />
<p>.md</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o fa-fw' />
<p>.txt</p>
</button>
<button styleName='export--unable'>
<i className='fa fa-file-pdf-o fa-fw' />
<p>.pdf</p>
</button>
</div>
</div>
)
InfoPanelTrashed.propTypes = {
storageName: PropTypes.string.isRequired,
folderName: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired
}
export default CSSModules(InfoPanelTrashed, styles)

View File

@@ -17,7 +17,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel' import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
@@ -69,30 +72,12 @@ class MarkdownNoteDetail extends React.Component {
ee.off('topbar:togglelockbutton', this.toggleLockButton) ee.off('topbar:togglelockbutton', this.toggleLockButton)
} }
getPercentageOfCompleteTodo (noteContent) {
let splitted = noteContent.split('\n')
let numberOfTodo = 0
let numberOfCompletedTodo = 0
splitted.forEach((line) => {
let trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
numberOfTodo++
}
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
numberOfCompletedTodo++
}
})
return Math.floor(numberOfCompletedTodo / numberOfTodo * 100)
}
handleChange (e) { handleChange (e) {
let { note } = this.state let { note } = this.state
note.content = this.refs.content.value note.content = this.refs.content.value
if (this.refs.tags) note.tags = this.refs.tags.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() note.updatedAt = new Date()
this.setState({ this.setState({
@@ -190,8 +175,8 @@ class MarkdownNoteDetail extends React.Component {
if (isTrashed) { if (isTrashed) {
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), { let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete a note', message: 'Confirm note deletion',
detail: 'This work cannot be undone.', detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel'] buttons: ['Confirm', 'Cancel']
}) })
if (dialogueButtonIndex === 1) return if (dialogueButtonIndex === 1) return
@@ -300,10 +285,9 @@ class MarkdownNoteDetail extends React.Component {
<InfoButton <InfoButton
onClick={(e) => this.handleInfoButtonClick(e)} onClick={(e) => this.handleInfoButtonClick(e)}
/> />
<InfoPanel <InfoPanelTrashed
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd} exportAsMd={this.exportAsMd}
@@ -333,7 +317,7 @@ class MarkdownNoteDetail extends React.Component {
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
/> />
<TodoListPercentage <TodoListPercentage
percentageOfTodo={this.getPercentageOfCompleteTodo(note.content)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/> />
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
@@ -357,7 +341,7 @@ class MarkdownNoteDetail extends React.Component {
<button styleName='control-fullScreenButton' <button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)} onMouseDown={(e) => this.handleFullScreenButton(e)}
> >
<i className='fa fa-expand' styleName='fullScreen-button' /> <i className='fa fa-window-maximize' styleName='fullScreen-button' />
</button> </button>
<InfoButton <InfoButton
onClick={(e) => this.handleInfoButtonClick(e)} onClick={(e) => this.handleInfoButtonClick(e)}
@@ -370,6 +354,9 @@ class MarkdownNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd} exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt} exportAsTxt={this.exportAsTxt}
wordCount={note.content.split(' ').length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
/> />
</div> </div>
</div> </div>

View File

@@ -20,6 +20,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel' import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
function pass (name) { function pass (name) {
@@ -176,8 +177,8 @@ class SnippetNoteDetail extends React.Component {
if (isTrashed) { if (isTrashed) {
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), { let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete a note', message: 'Confirm note deletion',
detail: 'This work cannot be undone.', detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel'] buttons: ['Confirm', 'Cancel']
}) })
if (dialogueButtonIndex === 1) return if (dialogueButtonIndex === 1) return
@@ -270,7 +271,7 @@ class SnippetNoteDetail extends React.Component {
let syntax = CodeMirror.findModeByFileName(name.trim()) let syntax = CodeMirror.findModeByFileName(name.trim())
let mode = syntax != null ? syntax.name : null let mode = syntax != null ? syntax.name : null
if (mode != null) snippets[index].mode = mode if (mode != null) snippets[index].mode = mode
this.state.note.snippets = snippets this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
this.setState({ this.setState({
note: this.state.note note: this.state.note
@@ -283,7 +284,7 @@ class SnippetNoteDetail extends React.Component {
return (e) => { return (e) => {
let snippets = this.state.note.snippets.slice() let snippets = this.state.note.snippets.slice()
snippets[index].mode = name snippets[index].mode = name
this.state.note.snippets = snippets this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
this.setState({ this.setState({
note: this.state.note note: this.state.note
@@ -297,7 +298,7 @@ class SnippetNoteDetail extends React.Component {
return (e) => { return (e) => {
let snippets = this.state.note.snippets.slice() let snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value snippets[index].content = this.refs['code-' + index].value
this.state.note.snippets = snippets this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
this.setState({ this.setState({
note: this.state.note note: this.state.note
}, () => { }, () => {
@@ -519,6 +520,7 @@ class SnippetNoteDetail extends React.Component {
onChange={(e) => this.handleCodeChange(index)(e)} onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index} ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents} ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
storageKey={storageKey}
/> />
: <CodeEditor styleName='tabView-content' : <CodeEditor styleName='tabView-content'
mode={snippet.mode} mode={snippet.mode}
@@ -559,10 +561,9 @@ class SnippetNoteDetail extends React.Component {
<InfoButton <InfoButton
onClick={(e) => this.handleInfoButtonClick(e)} onClick={(e) => this.handleInfoButtonClick(e)}
/> />
<InfoPanel <InfoPanelTrashed
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
@@ -597,7 +598,7 @@ class SnippetNoteDetail extends React.Component {
<button styleName='control-fullScreenButton' <button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)} onMouseDown={(e) => this.handleFullScreenButton(e)}
> >
<i className='fa fa-expand' styleName='fullScreen-button' /> <i className='fa fa-window-maximize' styleName='fullScreen-button' />
</button> </button>
<InfoButton <InfoButton
onClick={(e) => this.handleInfoButtonClick(e)} onClick={(e) => this.handleInfoButtonClick(e)}
@@ -610,6 +611,7 @@ class SnippetNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
exportAsTxt={this.showWarning} exportAsTxt={this.showWarning}
type={note.type}
/> />
</div> </div>
</div> </div>

View File

@@ -19,7 +19,7 @@
.body .description .body .description
absolute top left right absolute top left right
height 80px height 50px
.body .description textarea .body .description textarea
outline none outline none
@@ -27,14 +27,14 @@
height 100% height 100%
width 100% width 100%
resize none resize none
border none border 1px solid $ui-borderColor
padding 10px padding 2px 5px
line-height 1.6 line-height 1.6
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
.tabList .tabList
absolute left right absolute left right
top 80px top 55px
height 30px height 30px
display flex display flex
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
@@ -50,16 +50,17 @@
.tabView .tabView
absolute left right bottom absolute left right bottom
top 130px top 100px
.tabView-content .tabView-content
absolute top left right bottom absolute top left right bottom
.override .override
absolute bottom left absolute bottom left
bottom 30px
left 60px left 60px
height 23px height 23px
z-index 1 z-index 101
button button
navButtonColor() navButtonColor()
height 24px height 24px
@@ -83,6 +84,7 @@ body[data-theme="dark"]
.body .description textarea .body .description textarea
background-color $ui-dark-noteDetail-backgroundColor background-color $ui-dark-noteDetail-backgroundColor
color $ui-dark-text-color color $ui-dark-text-color
border 1px solid $ui-dark-borderColor
.tabList .tabList
background-color $ui-button--active-backgroundColor background-color $ui-button--active-backgroundColor

View File

@@ -51,7 +51,7 @@
margin 2px 0 15px 2px margin 2px 0 15px 2px
vertical-align middle vertical-align middle
height 18px height 18px
box-sizing borde-box box-sizing border-box
border none border none
background-color transparent background-color transparent
outline none outline none

View File

@@ -49,7 +49,7 @@ class Detail extends React.Component {
tabIndex='0' tabIndex='0'
> >
<div styleName='empty'> <div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new post</div> <div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
</div> </div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

@@ -14,12 +14,14 @@ import InitModal from 'browser/main/modals/InitModal'
import mixpanel from 'browser/main/lib/mixpanel' import mixpanel from 'browser/main/lib/mixpanel'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig' import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import RealtimeNotification from 'browser/components/RealtimeNotification'
function focused () { function focused () {
mixpanel.track('MAIN_FOCUSED') mixpanel.track('MAIN_FOCUSED')
} }
class Main extends React.Component { class Main extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -172,8 +174,8 @@ class Main extends React.Component {
} }
hideLeftLists (noteDetail, noteList, mainBody) { hideLeftLists (noteDetail, noteList, mainBody) {
this.state.noteDetailWidth = noteDetail.style.left this.setState({noteDetailWidth: noteDetail.style.left})
this.state.mainBodyWidth = mainBody.style.left this.setState({mainBodyWidth: mainBody.style.left})
noteDetail.style.left = '0px' noteDetail.style.left = '0px'
mainBody.style.left = '0px' mainBody.style.left = '0px'
noteList.style.display = 'none' noteList.style.display = 'none'
@@ -188,6 +190,17 @@ class Main extends React.Component {
render () { render () {
let { config } = this.props let { config } = this.props
// the width of the navigation bar when it is folded/collapsed
const foldedNavigationWidth = 44
let notificationBarOffsetLeft
if (this.state.fullScreen) {
notificationBarOffsetLeft = 0
} else if (config.isSideNavFolded) {
notificationBarOffsetLeft = foldedNavigationWidth
} else {
notificationBarOffsetLeft = this.state.navWidth
}
return ( return (
<div <div
className='Main' className='Main'
@@ -216,7 +229,7 @@ class Main extends React.Component {
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'} <div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body' id='main-body'
ref='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}} <TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [ {..._.pick(this.props, [
@@ -255,6 +268,9 @@ class Main extends React.Component {
ignorePreviewPointerEvents={this.state.isRightSliderFocused} ignorePreviewPointerEvents={this.state.isRightSliderFocused}
/> />
</div> </div>
<RealtimeNotification
style={{left: notificationBarOffsetLeft}}
/>
</div> </div>
) )
} }

View File

@@ -0,0 +1,68 @@
.root
position relative
background-color $ui-noteList-backgroundColor
height $topBar-height - 1
margin-left: auto;
width: 64px;
margin-right: -15px;
.root--expanded
@extend .root
$control-height = 34px
.control
position absolute
top 13px
left 8px
right 8px
height $control-height
overflow hidden
display flex
.control-newNoteButton
display block
width 32px
height $control-height - 2
navButtonColor()
font-size 16px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .control-newNoteButton-tooltip
opacity 1
.control-newNoteButton-tooltip
tooltip()
position fixed
pointer-events none
top 50px
left 433px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.root, .root--expanded
background-color $ui-dark-noteList-backgroundColor
.control
border-color $ui-dark-borderColor
.control-newNoteButton
color $ui-inactive-text-color
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
&:hover
transition 0.15s
color $ui-dark-text-color
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
border-color $ui-dark-button--active-backgroundColor
.control-newNoteButton-tooltip
darkTooltip()

View File

@@ -0,0 +1,106 @@
import React, { PropTypes } 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
const OSX = window.process.platform === 'darwin'
class NewNoteButton extends React.Component {
constructor (props) {
super(props)
this.state = {
}
this.newNoteHandler = () => {
this.handleNewNoteButtonClick()
}
}
componentDidMount () {
eventEmitter.on('top:new-note', this.newNoteHandler)
}
componentWillUnmount () {
eventEmitter.off('top:new-note', this.newNoteHandler)
}
handleNewNoteButtonClick (e) {
const { config, location, dispatch } = this.props
const { storage, folder } = this.resolveTargetFolder()
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location
})
}
resolveTargetFolder () {
const { data, params } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (let kv of data.storageMap) {
storage = kv[1]
break
}
}
if (storage == null) this.showMessageBox('No storage to create a note')
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
if (folder == null) this.showMessageBox('No folder to create a note')
return {
storage,
folder
}
}
showMessageBox (message) {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: message,
buttons: ['OK']
})
}
render () {
const { config, style } = this.props
return (
<div className='NewNoteButton'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<i className='fa fa-pencil-square-o' />
<span styleName='control-newNoteButton-tooltip'>
Make a Note {OSX ? '⌘' : '^'} + n
</span>
</button>
</div>
</div>
)
}
}
NewNoteButton.propTypes = {
dispatch: PropTypes.func,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
})
}
export default CSSModules(NewNoteButton, styles)

View File

@@ -2,6 +2,7 @@ $control-height = 30px
.root .root
absolute left bottom absolute left bottom
bottom 30px
top $topBar-height - 1 top $topBar-height - 1
background-color $ui-noteList-backgroundColor background-color $ui-noteList-backgroundColor

View File

@@ -13,6 +13,7 @@ import fs from 'fs'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import markdown from 'browser/lib/markdown' import markdown from 'browser/lib/markdown'
import { findNoteTitle } from 'browser/lib/findNoteTitle' import { findNoteTitle } from 'browser/lib/findNoteTitle'
import stripgtags from 'striptags'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { Menu, MenuItem, dialog } = remote
@@ -347,6 +348,22 @@ class NoteList extends React.Component {
properties: ['openFile', 'multiSelections'] properties: ['openFile', 'multiSelections']
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => {
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 targetIndex = _.findIndex(this.notes, (note) => { const targetIndex = _.findIndex(this.notes, (note) => {
return note !== null && `${note.storage}-${note.key}` === location.query.key return note !== null && `${note.storage}-${note.key}` === location.query.key
}) })
@@ -354,28 +371,26 @@ class NoteList extends React.Component {
const storageKey = this.notes[targetIndex].storage const storageKey = this.notes[targetIndex].storage
const folderKey = this.notes[targetIndex].folder const folderKey = this.notes[targetIndex].folder
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => { if (filepaths === undefined) return
if (filepaths === undefined) return filepaths.forEach((filepath) => {
filepaths.forEach((filepath) => { fs.readFile(filepath, (err, data) => {
fs.readFile(filepath, (err, data) => { if (err) throw Error('File reading error: ', err)
if (err) throw Error('File reading error: ', err) const content = data.toString()
const content = data.toString() const newNote = {
const newNote = { content: content,
content: content, folder: folderKey,
folder: folderKey, title: markdown.strip(findNoteTitle(content)),
title: markdown.strip(findNoteTitle(content)), type: 'MARKDOWN_NOTE'
type: 'MARKDOWN_NOTE' }
} dataApi.createNote(storageKey, newNote)
dataApi.createNote(storageKey, newNote) .then((note) => {
.then((note) => { dispatch({
dispatch({ type: 'UPDATE_NOTE',
type: 'UPDATE_NOTE', note: note
note: note })
}) hashHistory.push({
hashHistory.push({ pathname: location.pathname,
pathname: location.pathname, query: {key: `${note.storage}-${note.key}`}
query: {key: `${note.storage}-${note.key}`}
})
}) })
}) })
}) })
@@ -438,6 +453,7 @@ class NoteList extends React.Component {
<div className='NoteList' <div className='NoteList'
styleName='root' styleName='root'
style={this.props.style} style={this.props.style}
onDrop={(e) => this.handleDrop(e)}
> >
<div styleName='control'> <div styleName='control'>
<div styleName='control-sortBy'> <div styleName='control-sortBy'>
@@ -446,9 +462,9 @@ class NoteList extends React.Component {
value={config.sortBy} value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)} onChange={(e) => this.handleSortByChange(e)}
> >
<option value='UPDATED_AT'>Updated Time</option> <option value='UPDATED_AT'>Last Updated</option>
<option value='CREATED_AT'>Created Time</option> <option value='CREATED_AT'>Creation Time</option>
<option value='ALPHABETICAL'>Alphabetical</option> <option value='ALPHABETICAL'>Alphabetically</option>
</select> </select>
</div> </div>
<div styleName='control-button-area'> <div styleName='control-button-area'>

View File

@@ -114,7 +114,7 @@ class StorageItem extends React.Component {
let index = dialog.showMessageBox(remote.getCurrentWindow(), { let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete Folder', message: 'Delete Folder',
detail: 'This work will deletes all notes in the folder and can not be undone.', detail: 'This will delete all notes in the folder and can not be undone.',
buttons: ['Confirm', 'Cancel'] buttons: ['Confirm', 'Cancel']
}) })

View File

@@ -3,6 +3,8 @@
.root .root
absolute bottom left right absolute bottom left right
height $statusBar-height height $statusBar-height
bottom 16px
z-index 100
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
display flex display flex

View File

@@ -59,8 +59,6 @@ class StatusBar extends React.Component {
{Math.floor(config.zoom * 100)}% {Math.floor(config.zoom * 100)}%
</button> </button>
<div styleName='blank' />
{status.updateReady {status.updateReady
? <button onClick={this.updateApp} styleName='update'> ? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update! <i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!

View File

@@ -2,12 +2,9 @@ import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './TopBar.styl' import styles from './TopBar.styl'
import _ from 'lodash' import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal' import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import ConfigManager from 'browser/main/lib/ConfigManager' import NewNoteButton from 'browser/main/NewNoteButton'
import dataApi from 'browser/main/lib/dataApi'
const { remote } = require('electron') const { remote } = require('electron')
const { dialog } = remote const { dialog } = remote
@@ -24,81 +21,19 @@ class TopBar extends React.Component {
isSearching: false isSearching: false
} }
this.newNoteHandler = () => {
this.handleNewPostButtonClick()
}
this.focusSearchHandler = () => { this.focusSearchHandler = () => {
this.handleOnSearchFocus() this.handleOnSearchFocus()
} }
} }
componentDidMount () { componentDidMount () {
ee.on('top:new-note', this.newNoteHandler)
ee.on('top:focus-search', this.focusSearchHandler) ee.on('top:focus-search', this.focusSearchHandler)
} }
componentWillUnmount () { componentWillUnmount () {
ee.off('top:new-note', this.newNoteHandler)
ee.off('top:focus-search', this.focusSearchHandler) ee.off('top:focus-search', this.focusSearchHandler)
} }
handleNewPostButtonClick (e) {
let { config, location } = this.props
if (location.pathname === '/trashed') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Cannot create new note',
detail: 'You cannot create new note in trash box.',
buttons: ['OK']
})
return
}
switch (config.ui.defaultNote) {
case 'MARKDOWN_NOTE':
this.createNote('MARKDOWN_NOTE')
break
case 'SNIPPET_NOTE':
this.createNote('SNIPPET_NOTE')
break
case 'ALWAYS_ASK':
default:
let { dispatch, location } = this.props
let { storage, folder } = this.resolveTargetFolder()
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location
})
}
}
resolveTargetFolder () {
let { data, params } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (let kv of data.storageMap) {
storage = kv[1]
break
}
}
if (storage == null) window.alert('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0]
if (folder == null) window.alert('No folder to create a note')
return {
storage,
folder
}
}
handleSearchChange (e) { handleSearchChange (e) {
let { router } = this.context let { router } = this.context
router.push('/searched') router.push('/searched')
@@ -107,22 +42,6 @@ class TopBar extends React.Component {
}) })
} }
handleOptionClick (uniqueKey) {
return (e) => {
this.setState({
isSearching: false
}, () => {
let { location } = this.props
hashHistory.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
})
}
}
handleSearchFocus (e) { handleSearchFocus (e) {
this.setState({ this.setState({
isSearching: true isSearching: true
@@ -147,60 +66,6 @@ class TopBar extends React.Component {
} }
} }
createNote (noteType) {
let { dispatch, location } = this.props
if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.')
let { storage, folder } = this.resolveTargetFolder()
let newNote = noteType === 'MARKDOWN_NOTE'
? {
type: 'MARKDOWN_NOTE',
folder: folder.key,
title: '',
content: ''
}
: {
type: 'SNIPPET_NOTE',
folder: folder.key,
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}
dataApi
.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.storage + '-' + note.key}
})
ee.emit('detail:focus')
})
}
setDefaultNote (defaultNote) {
let { config, dispatch } = this.props
let ui = Object.assign(config.ui)
ui.defaultNote = defaultNote
ConfigManager.set({
ui
})
dispatch({
type: 'SET_UI',
config: ConfigManager.get()
})
}
handleOnSearchFocus () { handleOnSearchFocus () {
if (this.state.isSearching) { if (this.state.isSearching) {
this.refs.search.childNodes[0].blur() this.refs.search.childNodes[0].blur()
@@ -210,7 +75,7 @@ class TopBar extends React.Component {
} }
render () { render () {
let { config, style, data } = this.props let { config, style, data, location } = this.props
return ( return (
<div className='TopBar' <div className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'} styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
@@ -242,14 +107,17 @@ class TopBar extends React.Component {
} }
</div> </div>
<button styleName='control-newPostButton'
onClick={(e) => this.handleNewPostButtonClick(e)}>
<i className='fa fa-pencil-square-o' />
<span styleName='control-newPostButton-tooltip'>
Make a Note {OSX ? '⌘' : '^'} + n
</span>
</button>
</div> </div>
{location.pathname === '/trashed' ? ''
: <NewNoteButton
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'params',
'location'
])}
/>}
</div> </div>
) )
} }

View File

@@ -64,7 +64,7 @@ textarea.block-input
fullsize() fullsize()
modalZIndex= 1000 modalZIndex= 1000
modalBackColor = transparentify(white, 65%) modalBackColor = white
.ace_focus .ace_focus
outline-color rgb(59, 153, 252) outline-color rgb(59, 153, 252)
outline-offset 0px outline-offset 0px
@@ -86,12 +86,12 @@ modalBackColor = transparentify(white, 65%)
body[data-theme="dark"] body[data-theme="dark"]
.ModalBase .ModalBase
.modalBack .modalBack
background-color alpha(black, 60%) background-color $ui-dark-backgroundColor
.CodeMirror .CodeMirror
font-family inherit !important font-family inherit !important
line-height 1.4em line-height 1.4em
height 100% height 96%
.CodeMirror > div > textarea .CodeMirror > div > textarea
margin-bottom -1em margin-bottom -1em
.CodeMirror-focused .CodeMirror-selected .CodeMirror-focused .CodeMirror-selected

View File

@@ -18,9 +18,10 @@ function initAwsMobileAnalytics () {
AWS.config.credentials.get((err) => { AWS.config.credentials.get((err) => {
if (!err) { if (!err) {
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId) console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
recordDynamicCustomEvent('APP_STARTED')
recordStaticCustomEvent()
} }
}) })
recordStaticCustomEvent()
} }
function recordDynamicCustomEvent (type) { function recordDynamicCustomEvent (type) {

View File

@@ -1,10 +1,13 @@
import _ from 'lodash' import _ from 'lodash'
import RcParser from 'browser/lib/RcParser'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32' const win = global.process.platform === 'win32'
const electron = require('electron') const electron = require('electron')
const { ipcRenderer } = electron const { ipcRenderer } = electron
const consts = require('browser/lib/consts') const consts = require('browser/lib/consts')
const path = require('path')
const fs = require('fs')
let isInitialized = false let isInitialized = false
@@ -22,6 +25,7 @@ export const DEFAULT_CONFIG = {
}, },
ui: { ui: {
theme: 'default', theme: 'default',
showCopyNotification: true,
disableDirectWrite: false, disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
}, },
@@ -57,17 +61,17 @@ function _save (config) {
} }
function get () { function get () {
let config = window.localStorage.getItem('config') const rawStoredConfig = window.localStorage.getItem('config')
const storedConfig = Object.assign({}, DEFAULT_CONFIG, JSON.parse(rawStoredConfig))
let config = storedConfig
try { try {
config = Object.assign({}, DEFAULT_CONFIG, JSON.parse(config)) const boostnotercConfig = RcParser.parse()
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, config.hotkey) config = assignConfigValues(storedConfig, boostnotercConfig)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, config.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, config.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, config.preview)
if (!validate(config)) throw new Error('INVALID CONFIG') if (!validate(config)) throw new Error('INVALID CONFIG')
} catch (err) { } catch (err) {
console.warn('Boostnote resets the malformed configuration.') console.warn('Boostnote resets the invalid configuration.')
config = DEFAULT_CONFIG config = DEFAULT_CONFIG
_save(config) _save(config)
} }
@@ -126,6 +130,15 @@ function set (updates) {
}) })
} }
function assignConfigValues (originalConfig, rcConfig) {
let 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)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
return config
}
export default { export default {
get, get,
set, set,

View File

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

View File

@@ -1,51 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
const electron = require('electron')
const ipc = electron.ipcRenderer
export default class DeleteArticleModal extends React.Component {
constructor (props) {
super(props)
this.confirmHandler = (e) => this.handleYesButtonClick()
}
componentDidMount () {
ReactDOM.findDOMNode(this.refs.no).focus()
ipc.on('modal-confirm', this.confirmHandler)
}
componentWillUnmount () {
ipc.removeListener('modal-confirm', this.confirmHandler)
}
handleNoButtonClick (e) {
this.props.close()
}
handleYesButtonClick (e) {
this.props.close()
}
render () {
return (
<div className='DeleteArticleModal modal'>
<div className='title'><i className='fa fa-fw fa-trash' /> Delete an article.</div>
<div className='message'>Do you really want to delete?</div>
<div className='control'>
<button ref='no' onClick={(e) => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close' /> No</button>
<button ref='yes' onClick={(e) => this.handleYesButtonClick(e)} className='danger'><i className='fa fa-fw fa-check' /> Yes</button>
</div>
</div>
)
}
}
DeleteArticleModal.propTypes = {
action: PropTypes.object,
articleKey: PropTypes.string,
close: PropTypes.func
}

View File

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

View File

@@ -31,10 +31,15 @@
.group-section-control .group-section-control
flex 1 flex 1
margin-left 5px
.group-section-control select .group-section-control select
outline none outline none
border 1px solid $ui-borderColor border 1px solid $ui-borderColor
font-size 16px
height 30px
width 250px
margin-bottom 5px
background-color transparent background-color transparent
.group-section-control-input .group-section-control-input
@@ -77,14 +82,16 @@
margin-right 10px margin-right 10px
.group-control-rightButton .group-control-rightButton
float right position absolute
top 10px
right 20px
colorPrimaryButton() colorPrimaryButton()
border none border none
border-radius 2px border-radius 2px
font-size $tab--button-font-size font-size $tab--button-font-size
height 35px height 40px
width 100px width 120px
margin-right 10px padding 0 15px
.group-hint .group-hint
border $ui-border border $ui-border

View File

@@ -81,7 +81,7 @@ class InfoTab extends React.Component {
<li> <li>
<a href='https://github.com/BoostIO/Boostnote/issues' <a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>GitHub Issues</a> : We'd love to hear your feedback 🙌 >GitHub Issues</a> : We&apos;d love to hear your feedback 🙌
</li> </li>
<li> <li>
<a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md' <a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md'
@@ -97,9 +97,9 @@ class InfoTab extends React.Component {
</ul> </ul>
<hr /> <hr />
<div styleName='policy'>Data collection policy</div> <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> <div>We collect only the number of DAU for Boostnote and **DO NOT collect** any detail information such as your note content.</div>
<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> <div>You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<p>These data are only used for Boostnote improvements.</p> <div>This data is only used for Boostnote improvements.</div>
<input onChange={(e) => this.handleConfigChange(e)} <input onChange={(e) => this.handleConfigChange(e)}
checked={this.state.config.amaEnabled} checked={this.state.config.amaEnabled}
ref='amaEnabled' ref='amaEnabled'

View File

@@ -43,6 +43,7 @@
text-decoration none text-decoration none
.policy .policy
width 100%
font-size 20px font-size 20px
margin-bottom 10px margin-bottom 10px

View File

@@ -4,9 +4,10 @@ top-bar--height = 50px
.root .root
modal() modal()
max-width 800px max-width 100vw
min-height 500px min-height 100vh
height 80% height 100vh
width 100vw
overflow hidden overflow hidden
position relative position relative
@@ -25,22 +26,22 @@ top-bar--height = 50px
top top-bar--height top top-bar--height
left 0 left 0
width 140px width 140px
margin-left 30px margin-left 10px
margin-top 20px margin-top 20px
background-color $ui-backgroundColor background-color $ui-backgroundColor
.nav-button .nav-button
font-size 14px font-size 14px
text-align left text-align left
width 100px width 120px
margin 4px 0 margin 5px 0
padding 5px 0 padding 7px 0
padding-left 10px padding-left 10px
border none border none
border-radius 2px border-radius 2px
background-color transparent background-color transparent
color $ui-text-color color $ui-text-color
font-size 14px font-size 16px
.nav-button--active .nav-button--active
@extend .nav-button @extend .nav-button

View File

@@ -298,8 +298,8 @@ class StorageItem extends React.Component {
let index = dialog.showMessageBox(remote.getCurrentWindow(), { let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Unlink Storage', message: 'Unlink Storage',
detail: 'This work just detatches a storage from Boostnote. (Any data won\'t be deleted.)', detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.',
buttons: ['Confirm', 'Cancel'] buttons: ['Unlink', 'Cancel']
}) })
if (index === 0) { if (index === 0) {

View File

@@ -5,7 +5,7 @@ import dataApi from 'browser/main/lib/dataApi'
import StorageItem from './StorageItem' import StorageItem from './StorageItem'
const electron = require('electron') const electron = require('electron')
const remote = electron.remote const { shell, remote } = electron
function browseFolder () { function browseFolder () {
let dialog = remote.dialog let dialog = remote.dialog
@@ -50,6 +50,11 @@ class StoragesTab extends React.Component {
}) })
} }
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
renderList () { renderList () {
let { data, boundingBox } = this.props let { data, boundingBox } = this.props
@@ -161,7 +166,10 @@ class StoragesTab extends React.Component {
<option value='FILESYSTEM'>File System</option> <option value='FILESYSTEM'>File System</option>
</select> </select>
<div styleName='addStorage-body-section-type-description'> <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'
onClick={(e) => this.handleLinkClick(e)}
>Cloud-Syncing</a>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -36,6 +36,7 @@ class UiTab extends React.Component {
const newConfig = { const newConfig = {
ui: { ui: {
theme: this.refs.uiTheme.value, theme: this.refs.uiTheme.value,
showCopyNotification: this.refs.showCopyNotification.checked,
disableDirectWrite: this.refs.uiD2w != null disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked ? this.refs.uiD2w.checked
: false : false
@@ -90,9 +91,8 @@ class UiTab extends React.Component {
<div styleName='group'> <div styleName='group'>
<div styleName='group-header'>UI</div> <div styleName='group-header'>UI</div>
<div styleName='group-header2'>Theme</div>
<div styleName='group-section'> <div styleName='group-section'>
Color Theme
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.ui.theme} <select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
@@ -103,6 +103,16 @@ class UiTab extends React.Component {
</select> </select>
</div> </div>
</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' global.process.platform === 'win32'
? <div styleName='group-checkBoxSection'> ? <div styleName='group-checkBoxSection'>
@@ -192,7 +202,7 @@ class UiTab extends React.Component {
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Switching Preview Switch to Preview
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.editor.switchPreview} <select value={config.editor.switchPreview}
@@ -200,7 +210,7 @@ class UiTab extends React.Component {
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
> >
<option value='BLUR'>When Editor Blurred</option> <option value='BLUR'>When Editor Blurred</option>
<option value='RIGHTCLICK'>When Right Clicking</option> <option value='RIGHTCLICK'>On Right Click</option>
</select> </select>
</div> </div>
</div> </div>
@@ -218,7 +228,7 @@ class UiTab extends React.Component {
<option value='vim'>vim</option> <option value='vim'>vim</option>
<option value='emacs'>emacs</option> <option value='emacs'>emacs</option>
</select> </select>
<span styleName='note-for-keymap'>Please reload boostnote after you change the keymap</span> <span styleName='note-for-keymap'>Please restart boostnote after you change the keymap</span>
</div> </div>
</div> </div>
@@ -271,7 +281,7 @@ class UiTab extends React.Component {
ref='previewLineNumber' ref='previewLineNumber'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Code block line numbering Show line numbers for preview code blocks
</label> </label>
</div> </div>

View File

@@ -5,7 +5,7 @@ $danger-color = #c9302c
$danger-lighten-color = lighten(#c9302c, 5%) $danger-lighten-color = lighten(#c9302c, 5%)
// Layouts // Layouts
$statusBar-height = 24px $statusBar-height = 36px
$sideNav-width = 200px $sideNav-width = 200px
$sideNav--folded-width = 44px $sideNav--folded-width = 44px
$topBar-height = 60px $topBar-height = 60px
@@ -150,10 +150,13 @@ modal()
position relative position relative
z-index $modal-z-index z-index $modal-z-index
width 100% width 100%
margin-left 80px
margin-right 80px
margin-bottom 80px
margin-top 100px
background-color $modal-background background-color $modal-background
overflow hidden overflow hidden
border-radius $modal-border-radius border-radius $modal-border-radius
box-shadow 0 0 1px rgba(76,86,103,.15), 0 2px 18px rgba(31,37,50,.22)
topBarButtonLight() topBarButtonLight()
width 34px width 34px
@@ -260,4 +263,3 @@ modalDark()
background-color $ui-dark-backgroundColor background-color $ui-dark-backgroundColor
overflow hidden overflow hidden
border-radius $modal-border-radius border-radius $modal-border-radius
box-shadow 2px 2px 10px black

View File

@@ -69,3 +69,21 @@ Pull requestをすることはその変化分のコードの著作権をMaisin&C
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。 これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
もし、このアプリケーションに料金が発生する時は、Boostnote専用のCloud storageの提供やMobile appとの連動、何か特殊なプレミアム機能の提供など形になります。 もし、このアプリケーションに料金が発生する時は、Boostnote専用のCloud storageの提供やMobile appとの連動、何か特殊なプレミアム機能の提供など形になります。
現在考えられているのは、GPL v3の場合、他のライセンスとの互換が不可能であるため、もしより自由なLicense(BSD, MIT)に変える時に改めて著作権者としてライセンスし直す選択肢を残すイメージです。 現在考えられているのは、GPL v3の場合、他のライセンスとの互換が不可能であるため、もしより自由なLicense(BSD, MIT)に変える時に改めて著作権者としてライセンスし直す選択肢を残すイメージです。
---
# Contributing to Boostnote (Simplified Chinese)
### 当您创建一个issue的时候
我们对您的issue格式没有要求但是我们有一个请求
**如果可能,请在开发者模式打开的情况下,为我们提供屏幕截图**
(您可以通过`Ctrl+Shift+I`打开开发者模式)。
感谢您对我们的支持。
### 关于您提供的Pull Request的著作权版权问题
如果您提供了一个Pull Request这表示您将您所修改的代码的著作权移交给Maisin&Co。
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益我们会尝试一些其他的方法比如说云存储、绑定手机软件等。
因为GPLv3过于严格不能和其他的一些协议兼容所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议比如说BSD、MIT。

View File

@@ -1,10 +1,11 @@
# Build # Build
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), and [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md).
## Environments ## Environments
* npm: 4.x * npm: 4.x
* node: 7.x * node: 7.x
You should use `npm v4.x` because `$ grand pre-build` fails on `v5.x`. You should use `npm v4.x` because `$ grunt pre-build` fails on `v5.x`.
## Development ## Development
@@ -43,6 +44,8 @@ You can build the program by using `grunt`. However, we don't recommend this bec
So, we've prepared a separate script which just makes an executable file. So, we've prepared a separate script which just makes an executable file.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -1,4 +1,6 @@
# How to debug Boostnote (Electron app) # How to debug Boostnote (Electron app)
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), and [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md)
Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome. Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome.
You can toggle the `Developer Tools` like this: You can toggle the `Developer Tools` like this:

View File

@@ -37,6 +37,8 @@ Gruntを使います。
それで、実行ファイルを作るスクリプトを用意しておきました。 それで、実行ファイルを作るスクリプトを用意しておきました。
このビルドはnpm v5.3.0では動かないのでv5.2.0で動かす必要があります。
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -37,6 +37,8 @@ yarn run hot
그래서, 실행파일만을 만드는 스크립트를 준비해 뒀습니다. 그래서, 실행파일만을 만드는 스크립트를 준비해 뒀습니다.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build
``` ```

View File

@@ -4,7 +4,7 @@
* npm: 4.x * npm: 4.x
* node: 7.x * node: 7.x
Вы должны использовать `npm v4.x`, так как `$ grand pre-build` не работает в `v5.x`. Вы должны использовать `npm v4.x`, так как `$ grunt pre-build` не работает в `v5.x`.
## Разработка ## Разработка
@@ -43,6 +43,8 @@ $ yarn run dev-start
Мы подготовили отдельный скрипт, который просто создает исполняемый файл: Мы подготовили отдельный скрипт, который просто создает исполняемый файл:
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
``` ```
grunt pre-build grunt pre-build
``` ```

56
docs/zh_CN/build.md Normal file
View File

@@ -0,0 +1,56 @@
# 构建Boostnote
## 环境
* npm: 4.x
* node: 7.x
因为`$ grunt pre-build`的问题,您只能使用`npm v4.x`而不能使用`npm v5.x`
## 开发
我们使用Webpack HMR来开发Boostnote。
在代码根目录下运行下列指令可以以默认配置运行Boostnote。
### 首先使用yarn安装所需的依赖包。
```
$ yarn
```
### 接着编译并且运行Boostnote。
```
$ yarn run dev-start
```
这个指令相当于在两个终端内同时运行`yarn run webpack``yarn run hot`
如果出现错误`Failed to load resource: net::ERR_CONNECTION_REFUSED`请尝试重新运行Boostnote。
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
### 然后您就可以进行开发了
当您对代码作出更改的时候,`webpack`会自动抓取并应用所有代码更改。
> ### 提示
> 在如下情况中您可能需要重新运行Boostnote才能应用代码更改
> 1. 当您在修改了一个组件的构造函数的时候When editing a constructor method of a component
> 2. 当您新建了一个CSS类的时候其实这和第1项是相同的因为每个CSS类都需在组件的构造函数中被重写
## 部署
我们使用Grunt来自动部署Boostnote。
因为部署需要协同设计(codesign)与验证码(authenticode),所以您可以但我们不建议通过`grunt`来部署。
所以我们准备了一个脚本文件来生成执行文件。
```
grunt pre-build
```
您只能使用`npm v5.2.0`而不能使用`npm v5.3.0`
接下来您就可以在`dist`目录中找到可执行文件。
> ### 提示
> 因为此可执行文件并没有被注册,所以自动更新不可用。
> 如果需要,您也可将协同设计(codesign)与验证码(authenticode)使用于这个可执行文件中。

15
docs/zh_CN/debug.md Normal file
View File

@@ -0,0 +1,15 @@
# 在Boostnote上Debug
Boostnote基于Electron所以Boostnote上的开发者工具和Google Chrome相同。
您可以像这样或者按下快捷键`Ctrl+Shift+I`打开开发者工具:
![how_to_toggle_devTools](https://cloud.githubusercontent.com/assets/11307908/24343585/162187e2-127c-11e7-9c01-23578db03ecf.png)
开发者工具大概形如这样:
![Developer_Tools](https://cloud.githubusercontent.com/assets/11307908/24343545/eff9f3a6-127b-11e7-94cf-cb67bfda634a.png)
您可以在`console`选项卡中找到运行错误,
也可以像这样在`debugger`选项卡中设置断点去分步Debug
![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png)
关于具体如何使用开发者工具,详见[Chrome 官档](https://developer.chrome.com/devtools)。如果您在中国大陆您可能需要一个VPN才能正常访问

View File

@@ -17,7 +17,8 @@ const mainWindow = new BrowserWindow({
webPreferences: { webPreferences: {
zoomFactor: 1.0, zoomFactor: 1.0,
blinkFeatures: 'OverlayScrollbars' blinkFeatures: 'OverlayScrollbars'
} },
icon: path.resolve(__dirname, '../resources/app.png')
}) })
const url = path.resolve(__dirname, './main.html') const url = path.resolve(__dirname, './main.html')

View File

@@ -20,6 +20,15 @@
font-weight: normal; font-weight: normal;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
@font-face {
font-family: 'Lato';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
#loadingCover{ #loadingCover{
background-color: #f4f4f4; background-color: #f4f4f4;
position: absolute; position: absolute;

View File

@@ -1,6 +1,7 @@
{ {
"name": "boost", "name": "boost",
"version": "0.8.12", "productName": "Boostnote",
"version": "0.8.15",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -65,6 +66,8 @@
"markdown-it-emoji": "^1.1.1", "markdown-it-emoji": "^1.1.1",
"markdown-it-footnote": "^3.0.0", "markdown-it-footnote": "^3.0.0",
"markdown-it-imsize": "^2.0.1", "markdown-it-imsize": "^2.0.1",
"markdown-it-multimd-table": "^2.0.1",
"markdown-it-named-headers": "^0.0.4",
"md5": "^2.0.0", "md5": "^2.0.0",
"mixpanel": "^0.4.1", "mixpanel": "^0.4.1",
"moment": "^2.10.3", "moment": "^2.10.3",
@@ -76,6 +79,7 @@
"react-redux": "^4.4.5", "react-redux": "^4.4.5",
"redux": "^3.5.2", "redux": "^3.5.2",
"sander": "^0.5.1", "sander": "^0.5.1",
"striptags": "^2.2.1",
"superagent": "^1.2.0", "superagent": "^1.2.0",
"superagent-promise": "^1.0.3" "superagent-promise": "^1.0.3"
}, },
@@ -99,6 +103,7 @@
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-config-standard-jsx": "^3.2.0", "eslint-config-standard-jsx": "^3.2.0",
"eslint-plugin-react": "^7.2.0",
"faker": "^3.1.0", "faker": "^3.1.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-electron-installer": "^1.2.0", "grunt-electron-installer": "^1.2.0",

View File

@@ -1,16 +1,13 @@
<h1 align="center"> New:zap:
<a href="https://github.com/BoostIO/Boostnote"><img src="./resources/app.png" alt="Boostnote" width="180"></a>
<br> [Android and iOS apps](https://github.com/BoostIO/Boostnote-mobile) are released!
Boostnote
<br>
<br>
</h1>
<h4 align="center">Note-taking app for programmers. </h4>
<h5 align="center">macOS, Windows and Linux. Android and iOS apps will be released soon!</h5>
<h5 align="center">Built with Electron, React + Redux, Webpack and CSSModules</h5>
![Boostnote app screenshot](./resources/repository/top.png) ![Boostnote app screenshot](./resources/repository/top.png)
<h4 align="center">Note-taking app for programmers. </h4>
<h5 align="center">Apps available for Mac, Windows, Linux, Android and iOS!</h5>
<h5 align="center">Built with Electron, React + Redux, Webpack and CSSModules</h5>
[![Build Status](https://travis-ci.org/BoostIO/Boostnote.svg?branch=master)](https://travis-ci.org/BoostIO/Boostnote) [![Build Status](https://travis-ci.org/BoostIO/Boostnote.svg?branch=master)](https://travis-ci.org/BoostIO/Boostnote)
## Authors & Maintainers ## Authors & Maintainers
@@ -25,11 +22,11 @@
## Slack Group ## Slack Group
Let's talk about Boostnote's great features, new feature requests and things like Japanese gourmet. 🍣 <br> Let's talk about Boostnote's great features, new feature requests and things like Japanese gourmet. 🍣 <br>
[Join us](https://join.slack.com/t/boostnote-group/shared_invite/MjE3NDcwODQ3NTkwLTE1MDA4NjQ3ODktM2IxOTk4ZTNjYQ) [Join us](https://join.slack.com/t/boostnote-group/shared_invite/enQtMjQ0MjYxNTMxNjY5LTI0NWFkZDEyYWYxYWE0YTMyYjI4NzA2YjBlNmFmOGVhNGFjY2I1MTNmZWJjYmMxYTdkM2VjNWNjYTFiYWQ3ZmQ)
## More Information ## More Information
* [Website](https://boostnote.io) * [Website](https://boostnote.io)
* [Boostnote Team](https://boostnote.io/team/) : Boostnote for the creative hacker teams. Share your markdown notes and snippets instantly with your team. **We will release it at August!** 🏃💨 * [10hz](https://boostnote.io/team/) : Boostnote for the creative hacker teams. Share your markdown notes and snippets instantly with your team. **We will release it at October!** 🏃💨
* [Support us via Bountysource](https://salt.bountysource.com/teams/boostnote) : Thank you for your support 🎉 * [Support us via Bountysource](https://salt.bountysource.com/teams/boostnote) : Thank you for your support 🎉
* [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md) : Development configurations for Boostnote 🚀 * [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md) : Development configurations for Boostnote 🚀
* Copyright (C) 2017 Maisin&Co. * Copyright (C) 2017 Maisin&Co.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1,33 @@
{
"amaEnabled": true,
"editor": {
"fontFamily": "Monaco, Consolas",
"fontSize": "14",
"indentSize": "2",
"indentType": "space",
"keyMap": "vim",
"switchPreview": "BLUR",
"theme": "monokai"
},
"hotkey": {
"toggleFinder": "Cmd + Alt + S",
"toggleMain": "Cmd + Alt + L"
},
"isSideNavFolded": false,
"listStyle": "DEFAULT",
"listWidth": 174,
"navWidth": 200,
"preview": {
"codeBlockTheme": "dracula",
"fontFamily": "Lato",
"fontSize": "14",
"lineNumber": true
},
"sortBy": "UPDATED_AT",
"ui": {
"defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false,
"theme": "default"
},
"zoom": 1
}

View File

@@ -0,0 +1,12 @@
{
"editor": {
"keyMap": "vim",
"switchPreview": "BLUR",
"theme": "monokai",
},
"hotkey": {
"toggleMain": "Control + L"
},
"listWidth": 135,
"navWidth": 135
}

View File

@@ -0,0 +1,12 @@
{
"editor": {
"keyMap": "vim",
"switchPreview": "BLUR",
"theme": "monokai"
},
"hotkey": {
"toggleMain": "Control + L"
},
"listWidth": 135,
"navWidth": 135
}

View File

@@ -0,0 +1,23 @@
const test = require('ava')
const { getTodoStatus } = require('browser/lib/getTodoStatus')
// Unit test
test('getTodoStatus should return a correct hash object', t => {
// [input, expected]
const testCases = [
['', { total: 0, completed: 0 }],
['* [ ] a\n', { total: 1, completed: 0 }],
['* [ ] a\n* [x] a\n', { total: 2, completed: 1 }],
['- [ ] a\n', { total: 1, completed: 0 }],
['- [ ] a\n- [x] a\n', { total: 2, completed: 1 }],
['+ [ ] a\n', { total: 1, completed: 0 }],
['+ [ ] a\n+ [x] a\n', { total: 2, completed: 1 }]
]
testCases.forEach(testCase => {
const [input, expected] = testCase
t.is(getTodoStatus(input).total, expected.total, `Test for getTodoStatus() input: ${input} expected: ${expected.total}`)
t.is(getTodoStatus(input).completed, expected.completed, `Test for getTodoStatus() input: ${input} expected: ${expected.completed}`)
})
})

View File

@@ -0,0 +1,33 @@
const test = require('ava')
const path = require('path')
const { parse } = require('browser/lib/RcParser')
// Unit test
test('RcParser should return a json object', t => {
const validJson = { 'editor': { 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleMain': 'Control + L' }, 'listWidth': 135, 'navWidth': 135 }
const allJson = { 'amaEnabled': true, 'editor': { 'fontFamily': 'Monaco, Consolas', 'fontSize': '14', 'indentSize': '2', 'indentType': 'space', 'keyMap': 'vim', 'switchPreview': 'BLUR', 'theme': 'monokai' }, 'hotkey': { 'toggleFinder': 'Cmd + Alt + S', 'toggleMain': 'Cmd + Alt + L' }, 'isSideNavFolded': false, 'listStyle': 'DEFAULT', 'listWidth': 174, 'navWidth': 200, 'preview': { 'codeBlockTheme': 'dracula', 'fontFamily': 'Lato', 'fontSize': '14', 'lineNumber': true }, 'sortBy': 'UPDATED_AT', 'ui': { 'defaultNote': 'ALWAYS_ASK', 'disableDirectWrite': false, 'theme': 'default' }, 'zoom': 1 }
// [input, expected]
const validTestCases = [
['.boostnoterc.valid', validJson],
['.boostnoterc.all', allJson]
]
const invalidTestCases = [
['.boostnoterc.invalid', {}]
]
validTestCases.forEach(validTestCase => {
const [input, expected] = validTestCase
t.is(parse(filePath(input)).editor.keyMap, expected.editor.keyMap, `Test for getTodoStatus() input: ${input} expected: ${expected.keyMap}`)
})
invalidTestCases.forEach(invalidTestCase => {
const [input, expected] = invalidTestCase
t.is(parse(filePath(input)).editor, expected.editor, `Test for getTodoStatus() input: ${input} expected: ${expected.editor}`)
})
})
function filePath (filename) {
return path.join('boostnoterc', filename)
}

633
yarn.lock

File diff suppressed because it is too large Load Diff