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

Merge branch '0.6.0'

This commit is contained in:
Rokt33r
2016-07-23 20:44:07 +09:00
120 changed files with 7506 additions and 6786 deletions

View File

@@ -2,43 +2,34 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
const ace = window.ace
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.changeHandler = e => this.handleChange(e)
this.changeHandler = (e) => this.handleChange(e)
this.blurHandler = (e) => {
if (e.relatedTarget === null) {
return
e.stopPropagation()
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.root) {
isStillFocused = true
break
}
el = el.parentNode
}
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
})
if (isFocusingToSearch) {
return
}
if (this.props.onBlur) this.props.onBlur(e)
if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e)
}
this.killedBuffer = ''
this.execHandler = (e) => {
console.log(e.command.name)
console.info('ACE COMMAND >> %s', e.command.name)
switch (e.command.name) {
case 'gotolinestart':
e.preventDefault()
@@ -84,7 +75,7 @@ export default class CodeEditor extends React.Component {
this.afterExecHandler = (e) => {
switch (e.command.name) {
case 'find':
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => {
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => {
el.removeEventListener('blur', this.blurHandler)
el.addEventListener('blur', this.blurHandler)
})
@@ -93,11 +84,6 @@ export default class CodeEditor extends React.Component {
}
this.state = {
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size'],
themeSyntax: config['theme-syntax']
}
this.silentChange = false
@@ -110,15 +96,16 @@ export default class CodeEditor extends React.Component {
}
componentDidMount () {
let { article } = this.props
var el = ReactDOM.findDOMNode(this)
var editor = this.editor = ace.edit(el)
let { mode, value, theme, fontSize } = this.props
this.value = value
let el = ReactDOM.findDOMNode(this)
let editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/' + this.state.themeSyntax)
editor.setTheme('ace/theme/' + theme)
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(this.state.fontSize)
editor.setFontSize(fontSize)
editor.on('blur', this.blurHandler)
@@ -132,49 +119,34 @@ export default class CodeEditor extends React.Component {
readOnly: true
})
editor.commands.addCommand({
name: 'Emacs cursor up',
name: 'Emacs kill buffer',
bindKey: {mac: 'Ctrl-Y'},
exec: function (editor) {
editor.insert(this.killedBuffer)
}.bind(this),
readOnly: true
})
editor.commands.addCommand({
name: 'Focus title',
bindKey: {win: 'Esc', mac: 'Esc'},
exec: function (editor, e) {
let currentWindow = remote.getCurrentWebContents()
if (config['switch-preview'] === 'rightclick') {
currentWindow.send('detail-preview')
}
currentWindow.send('list-focus')
},
readOnly: true
})
editor.commands.on('exec', this.execHandler)
editor.commands.on('afterExec', this.afterExecHandler)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: article.mode})
mode = _.find(modes, {name: mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
session.setOption('useWorker', false)
session.setUseSoftTabs(this.props.indentType === 'space')
session.setTabSize(this.props.indentSize)
session.setOption('useWorker', true)
session.setUseWrapMode(true)
session.setValue(this.props.article.content)
session.setValue(_.isString(value) ? value : '')
session.on('change', this.changeHandler)
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
this.editor.getSession().removeListener('change', this.changeHandler)
this.editor.removeListener('blur', this.blurHandler)
this.editor.commands.removeListener('exec', this.execHandler)
@@ -182,42 +154,36 @@ export default class CodeEditor extends React.Component {
}
componentDidUpdate (prevProps, prevState) {
var session = this.editor.getSession()
if (this.props.article.key !== prevProps.article.key) {
session.removeListener('change', this.changeHandler)
session.setValue(this.props.article.content)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
if (prevProps.article.mode !== this.props.article.mode) {
let mode = _.findWhere(modes, {name: this.props.article.mode})
let { value } = this.props
this.value = value
let editor = this.editor
let session = this.editor.getSession()
if (prevProps.mode !== this.props.mode) {
let mode = _.find(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
if (prevProps.theme !== this.props.theme) {
editor.setTheme('ace/theme/' + this.props.theme)
}
if (prevProps.fontSize !== this.props.fontSize) {
editor.setFontSize(this.props.fontSize)
}
if (prevProps.indentSize !== this.props.indentSize) {
session.setTabSize(this.props.indentSize)
}
if (prevProps.indentType !== this.props.indentType) {
session.setUseSoftTabs(this.props.indentType === 'space')
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size'],
themeSyntax: config['theme-syntax']
}, function () {
var editor = this.editor
editor.setTheme('ace/theme/' + this.state.themeSyntax)
var session = editor.getSession()
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
})
}
handleChange (e) {
if (this.props.onChange) {
var value = this.editor.getValue()
this.props.onChange(value)
this.value = this.editor.getValue()
this.props.onChange(e)
}
}
@@ -237,13 +203,37 @@ export default class CodeEditor extends React.Component {
this.editor.scrollToLine(num, false, false)
}
focus () {
this.editor.focus()
}
blur () {
this.editor.blur()
}
reload () {
let session = this.editor.getSession()
session.removeListener('change', this.changeHandler)
session.setValue(this.props.value)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
render () {
let { className, fontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return (
<div
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
className={className == null
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root'
tabIndex='-1'
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + ', monospace'
fontFamily: fontFamily.join(', ')
}}
/>
)
@@ -251,11 +241,8 @@ export default class CodeEditor extends React.Component {
}
CodeEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
mode: PropTypes.string,
key: PropTypes.string
}),
value: PropTypes.string,
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
@@ -263,7 +250,12 @@ CodeEditor.propTypes = {
}
CodeEditor.defaultProps = {
readOnly: false
readOnly: false,
theme: 'xcode',
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
}
export default CodeEditor

View File

@@ -1,20 +0,0 @@
import React, { PropTypes } from 'react'
const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component {
handleClick (e) {
shell.openExternal(this.props.href)
e.preventDefault()
}
render () {
return (
<a onClick={e => this.handleClick(e)} {...this.props}/>
)
}
}
ExternalLink.propTypes = {
href: PropTypes.string
}

View File

@@ -1,52 +0,0 @@
import React, { PropTypes } from 'react'
const BLUE = '#3460C7'
const LIGHTBLUE = '#2BA5F7'
const ORANGE = '#FF8E00'
const YELLOW = '#E8D252'
const GREEN = '#3FD941'
const DARKGREEN = '#1FAD85'
const RED = '#E10051'
const PURPLE = '#B013A4'
function getColorByIndex (index) {
switch (index % 8) {
case 0:
return RED
case 1:
return ORANGE
case 2:
return YELLOW
case 3:
return GREEN
case 4:
return DARKGREEN
case 5:
return LIGHTBLUE
case 6:
return BLUE
case 7:
return PURPLE
default:
return DARKGREEN
}
}
export default class FolderMark extends React.Component {
render () {
let color = getColorByIndex(this.props.color)
let className = 'FolderMark fa fa-square fa-fw'
if (this.props.className != null) {
className += ' active'
}
return (
<i className={className} style={{color: color}}/>
)
}
}
FolderMark.propTypes = {
color: PropTypes.number,
className: PropTypes.string
}

View File

@@ -0,0 +1,151 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
class MarkdownEditor extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'PREVIEW'
}
}
componentDidMount () {
this.value = this.refs.code.value
}
componentDidUpdate () {
this.value = this.refs.code.value
}
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
}
handleContextMenu (e) {
let { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
let newStatus = this.state.status === 'PREVIEW'
? 'CODE'
: 'PREVIEW'
this.setState({
status: newStatus
}, () => {
if (newStatus === 'CODE') {
this.refs.code.focus()
} else {
this.refs.code.blur()
this.refs.preview.focus()
}
})
}
}
handleBlur (e) {
let { config } = this.props
if (config.editor.switchPreview === 'BLUR') {
let cursorPosition = this.refs.code.getCursorPosition()
this.setState({
status: 'PREVIEW'
}, () => {
this.refs.preview.focus()
this.refs.preview.scrollTo(cursorPosition.row)
})
}
}
handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date()
}
handlePreviewMouseUp (e) {
let { config } = this.props
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
}
}
focus () {
if (this.state.status === 'PREVIEW') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
} else {
this.refs.code.focus()
}
}
reload () {
this.refs.code.reload()
}
render () {
let { className, value, config } = this.props
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 previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
return (
<div className={className == null
? 'MarkdownEditor'
: `MarkdownEditor ${className}`
}
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='-1'
>
<CodeEditor styleName='codeEditor'
ref='code'
mode='markdown'
value={value}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
: 'preview--hide'
}
style={previewStyle}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='0'
value={value}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
/>
</div>
)
}
}
MarkdownEditor.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownEditor, styles)

View File

@@ -0,0 +1,23 @@
.root
position relative
.codeEditor
absolute top bottom left right
.codeEditor--hide
@extend .codeEditor
.preview
display block
absolute top bottom left right
z-index 100
background-color white
height 100%
width 100%
.preview--hide
@extend .preview
z-index 0
opacity 0
pointer-events none

View File

@@ -1,214 +1,152 @@
import React, { PropTypes } from 'react'
import markdown from '../lib/markdown'
import ReactDOM from 'react-dom'
import sanitizeHtml from '@rokt33r/sanitize-html'
import markdown from 'browser/lib/markdown'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
import hljsTheme from 'browser/lib/hljsThemes'
const electron = require('electron')
const shell = electron.shell
const ipc = electron.ipcRenderer
const katex = window.katex
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
const goExternal = function (e) {
e.preventDefault()
e.stopPropagation()
shell.openExternal(e.target.href)
}
const OSX = global.process.platform === 'darwin'
const sanitizeOpts = {
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
allowedClasses: {
'a': ['lineAnchor'],
'div': ['math'],
'pre': ['hljs'],
'span': ['math', 'hljs-*', 'lineNumber'],
'code': ['language-*']
},
allowedAttributes: {
a: ['href', 'data-key'],
img: [ 'src' ],
label: ['for'],
input: ['checked', 'type'],
'*': ['id', 'name']
},
transformTags: {
'*': function (tagName, attribs) {
let href = attribs.href
if (tagName === 'input' && attribs.type !== 'checkbox') {
return false
}
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
return {
tagName: tagName,
attribs: attribs
}
}
}
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('\'Microsoft YaHei\'')
defaultFontFamily.unshift('meiryo')
}
function handleAnchorClick (e) {
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
return
}
e.preventDefault()
e.stopPropagation()
let href = this.href
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
shell.openExternal(href)
}
}
function stopPropagation (e) {
e.preventDefault()
e.stopPropagation()
}
function math2Katex (display) {
return function (el) {
try {
katex.render(el.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&'), el, {display: display})
el.className = 'math-rendered'
} catch (e) {
el.innerHTML = e.message
el.className = 'math-failed'
}
}
}
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family'],
lineNumber: config['preview-line-number']
}
}
componentDidMount () {
this.addListener()
this.renderMath()
ipc.on('config-apply', this.configApplyHandler)
this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e)
}
componentDidUpdate () {
this.addListener()
this.renderMath()
}
componentWillUnmount () {
this.removeListener()
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillUpdate () {
this.removeListener()
}
renderMath () {
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
Array.prototype.forEach.call(inline, math2Katex(false))
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
Array.prototype.forEach.call(block, math2Katex(true))
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener('click', handleAnchorClick)
anchor.addEventListener('mousedown', stopPropagation)
anchor.addEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.addEventListener('click', stopPropagation)
})
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.removeEventListener('click', handleAnchorClick)
anchor.removeEventListener('mousedown', stopPropagation)
anchor.removeEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.removeEventListener('click', stopPropagation)
})
}
handleClick (e) {
if (this.props.onClick) {
this.props.onClick(e)
}
}
handleDoubleClick (e) {
if (this.props.onDoubleClick) {
this.props.onDoubleClick(e)
}
handleContextMenu (e) {
this.props.onContextMenu(e)
}
handleMouseDown (e) {
if (this.props.onMouseDown) {
this.props.onMouseDown(e)
}
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
}
handleMouseUp (e) {
if (this.props.onMouseUp) {
this.props.onMouseUp(e)
}
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
handleMouseMove (e) {
if (this.props.onMouseMove) {
this.props.onMouseMove(e)
}
componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.rewriteIframe()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family'],
lineNumber: config['preview-line-number']
componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value ||
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber
) this.rewriteIframe()
}
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', goExternal)
})
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? [fontFamily].concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode'
this.refs.root.contentWindow.document.head.innerHTML = `
<style>
@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;
}
${markdownStyle}
body {
font-family: ${fontFamily.join(', ')};
font-size: ${fontSize}px;
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
opacity: 0.5;
}
</style>
<link rel="stylesheet" href="../node_modules/highlight.js/styles/${codeBlockTheme}.css">
<link rel="stylesheet" href="../resources/katex.min.css">
`
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('mousedown', goExternal)
})
}
render () {
let isEmpty = this.props.content.trim().length === 0
let content = isEmpty
? '(Empty content)'
: this.props.content
content = markdown(content)
content = sanitizeHtml(content, sanitizeOpts)
focus () {
this.refs.root.focus()
}
getWindow () {
return this.refs.root.contentWindow
}
scrollTo (targetRow) {
let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor')
for (let index = 0; index < lineAnchors.length; index++) {
let lineAnchor = lineAnchors[index]
let row = parseInt(lineAnchor.getAttribute('data-key'))
if (row > targetRow) {
let targetAnchor = lineAnchors[index - 1]
this.getWindow().scrollTo(0, targetAnchor.offsetTop)
break
}
}
}
render () {
let { className, style, tabIndex } = this.props
return (
<div
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '') + (this.state.lineNumber ? ' lineNumbered' : '')}
onClick={e => this.handleClick(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
onMouseDown={e => this.handleMouseDown(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}
dangerouslySetInnerHTML={{__html: ' ' + content}}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
}}
<iframe className={className != null
? 'MarkdownPreview ' + className
: 'MarkdownPreview'
}
style={style}
tabIndex={tabIndex}
ref='root'
/>
)
}
@@ -221,5 +159,5 @@ MarkdownPreview.propTypes = {
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
content: PropTypes.string
value: PropTypes.string
}

View File

@@ -1,168 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from '../lib/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
input: '',
isInputFocused: false
}
}
componentDidMount () {
this.blurInputBlurHandler = e => {
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0)
let newTag = tag.trim()
if (newTag.length === 0 && clearInput) {
this.setState({input: ''})
return
}
tags.push(newTag)
tags = _.uniq(tags)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags)
}
if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
}
handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus()
}
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) {
return e => {
e.stopPropagation()
let tags = this.props.tags.slice(0)
tags.splice(tags.indexOf(tag), 1)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(null, tags)
}
}
}
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () {
let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<div key={tag} className='TagSelect-tags-item'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='TagSelect-tags-item-remove'><i className='fa fa-fw fa-times'/></button>
<div className='TagSelect-tags-item-label'>{tag}</div>
</div>))
: null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='TagSelect-tags'>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='TagSelect-input'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='TagSelect-suggest'>
{suggestElements}
</div>
)
: null
}
</div>
)
}
}
TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
suggestTags: PropTypes.array
}
TagSelect.prototype.linkState = linkState

View File

@@ -0,0 +1,253 @@
global-reset()
borderColor = #D0D0D0 // using
highlightenBorderColor = darken(borderColor, 20%)
invBorderColor = #404849
brandBorderColor = #3FB399
focusBorderColor = #369DCD
buttonBorderColor = #4C4C4C
lightButtonColor = #898989
hoverBackgroundColor= transparentify(#444, 4%) // using
inactiveTextColor = #888 // using
textColor = #4D4D4D // using
backgroundColor= white
fontSize= 14px // using
shadowColor= #C5C5C5
invBackgroundColor = #4C4C4C
invTextColor = white
btnColor = #888
btnHighlightenColor = #000
brandColor = #2BAC8F
popupShadow = 0 0 5px 0 #888
tableHeadBgColor = white
tableOddBgColor = #F9F9F9
tableEvenBgColor = white
facebookColor= #3b5998
githubBtn= #201F1F
// using
successBackgroundColor= #E0F0D9
successTextColor= #3E753F
errorBackgroundColor= #F2DEDE
errorTextColor= #A64444
infoBackgroundColor= #D9EDF7
infoTextColor= #34708E
popupZIndex= 500
body
font-size 16px
padding 15px
font-family helvetica, arial, sans-serif
line-height 1.6
overflow-x hidden
user-select all
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space nowrap
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
background-color errorBackgroundColor
color errorTextColor
padding 5px
margin -5px
border-radius 5px
div.math-rendered
text-align center
.math-failed
background-color alpha(red, 0.1)
color darken(red, 15%)
padding 5px
margin 5px 0
border-radius 5px
sup
position relative
top -.4em
font-size 0.8em
vertical-align top
sub
position relative
bottom -.4em
font-size 0.8em
vertical-align top
a
color brandColor
text-decoration none
padding 5px
border-radius 5px
margin -5px
transition .1s
display inline-block
img
vertical-align sub
&:hover
color lighten(brandColor, 5%)
text-decoration underline
background-color alpha(#FFC95C, 0.3)
&:visited
color brandColor
&.lineAnchor
padding 0
margin 0
display block
font-size 0
height 0
hr
border-top none
border-bottom solid 1px borderColor
margin 15px 0
h1, h2, h3, h4, h5, h6
font-weight bold
h1
font-size 2.25em
padding-bottom 0.3em
line-height 1.2em
border-bottom solid 1px borderColor
margin 1em 0 0.44em
&:first-child
margin-top 0
h2
font-size 1.75em
padding-bottom 0.3em
line-height 1.225em
border-bottom solid 1px borderColor
margin 1em 0 0.57em
&:first-child
margin-top 0
h3
font-size 1.5em
line-height 1.43em
margin 1em 0 0.66em
h4
font-size 1.25em
line-height 1.4em
margin 1em 0 0.8em
h5
font-size 1em
line-height 1.4em
margin 1em 0 1em
h6
font-size 1em
line-height 1.4em
margin 1em 0 1em
color #777
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
margin-top 1em
p
line-height 1.6em
margin 0 0 1em
white-space pre-line
img
max-width 100%
strong, b
font-weight bold
em, i
font-style italic
s, del, strike
text-decoration line-through
u
text-decoration underline
blockquote
border-left solid 4px brandBorderColor
margin 0 0 1em
padding 0 25px
ul
list-style-type disc
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
&>li>ul
list-style-type circle
&>li>ul
list-style-type square
ol
list-style-type decimal
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
code
padding 0.2em 0.4em
background-color #f7f7f7
border-radius 3px
font-size 0.85em
text-decoration none
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre
padding 0.5em !important
border solid 1px alpha(borderColor, 0.5)
border-radius 5px
overflow-x auto
margin 0 0 1em
line-height 1.35
code
margin 0
background-color inherit
padding 0
border none
border-radius 0
pre
border none
margin -5px
&>span.lineNumber
display none
float left
font-size 0.85em
margin 0 0.5em 0 -0.5em
border-right 1px solid
text-align right
&>span
display block
padding 0 .5em 0 1em
table
display block
width 100%
margin 0 0 1em
thead
tr
background-color tableHeadBgColor
th
border-style solid
padding 6px 13px
line-height 1.6
border-width 1px 0 2px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor
tbody
tr:nth-child(2n + 1)
background-color tableOddBgColor
tr:nth-child(2n)
background-color tableEvenBgColor
td
border-style solid
padding 6px 13px
line-height 1.6
border-width 0 0 1px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor

View File

@@ -49,7 +49,7 @@ function notify (title, options) {
return new window.Notification(title, options)
}
require('../styles/finder/index.styl')
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'

View File

@@ -0,0 +1,5 @@
import CSSModules from 'react-css-modules'
export default function (component, styles) {
return CSSModules(component, styles, {errorWhenNotFound: false})
}

View File

@@ -4,7 +4,7 @@ import keygen from './keygen'
function getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen()
clientKey = keygen(20)
setClientKey(clientKey)
}

22
browser/lib/consts.js Normal file
View File

@@ -0,0 +1,22 @@
const consts = {
FOLDER_COLORS: [
'#E10051',
'#FF8E00',
'#E8D252',
'#3FD941',
'#30D5C8',
'#2BA5F7',
'#B013A4'
],
FOLDER_COLOR_NAMES: [
'Razzmatazz (Red)',
'Pizazz (Orange)',
'Confetti (Yellow)',
'Emerald (Green)',
'Turquoise',
'Dodger Blue',
'Violet Eggplant'
]
}
module.exports = consts

View File

@@ -1,24 +0,0 @@
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const userDataPath = remote.app.getPath('userData')
const configFile = 'config.json'
const defaultConfig = {
'editor-font-size': '14',
'editor-font-family': 'Monaco, Consolas',
'editor-indent-type': 'space',
'editor-indent-size': '4',
'preview-font-size': '14',
'preview-font-family': 'Lato',
'switch-preview': 'blur',
'disable-direct-write': false,
'theme-ui': 'light',
'theme-code': 'xcode',
'theme-syntax': 'xcode'
}
export default function fetchConfig () {
return Object.assign({}, defaultConfig, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
}

View File

@@ -1,7 +1,7 @@
var crypto = require('crypto')
const crypto = require('crypto')
const _ = require('lodash')
module.exports = function () {
var shasum = crypto.createHash('sha1')
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
return shasum.digest('hex')
module.exports = function (length) {
if (!_.isFinite(length)) length = 6
return crypto.randomBytes(length).toString('hex')
}

View File

@@ -1,36 +0,0 @@
function getIn (object, path) {
let stack = path.split('.')
while (stack.length > 1) {
object = object[stack.shift()]
}
return object[stack.shift()]
}
function updateIn (object, path, value) {
let current = object
let stack = path.split('.')
while (stack.length > 1) {
current = current[stack.shift()]
}
current[stack.shift()] = value
return object
}
function setPartialState (component, path, value) {
component.setState(
updateIn(component.state, path, value))
}
export default function linkState (path) {
return {
value: getIn(this.state, path),
requestChange: setPartialState.bind(null, this, path)
}
}
export function linkState2 (el, path) {
return {
value: getIn(el.state, path),
requestChange: setPartialState.bind(null, el, path)
}
}

View File

@@ -3,6 +3,8 @@ import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
const katex = window.katex
function createGutter (str) {
let lc = (str.match(/\n/g) || []).length
let lines = []
@@ -39,10 +41,22 @@ md.use(emoji, {
})
md.use(math, {
inlineRenderer: function (str) {
return `<span class='math'>${str}</span>`
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
return `<div class='math'>${str}</div>`
let output = ''
try {
output = katex.renderToString(str.trim(), {displayMode: true})
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-checkbox'))

View File

@@ -1,90 +1,9 @@
const modes = [
// Major
{
name: 'text',
label: 'Plain text',
mode: 'text'
},
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
// Minor
{
name: 'abap',
label: 'ABAP',
@@ -145,6 +64,12 @@ const modes = [
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
mode: 'batchfile'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cirru',
label: 'Cirru',
@@ -175,6 +100,24 @@ const modes = [
alias: ['cfm', 'cfc'],
mode: 'coldfusion'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'curly',
label: 'Curly',
@@ -283,6 +226,12 @@ const modes = [
alias: ['opengl', 'shading'],
mode: 'glsl'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
{
name: 'groovy',
label: 'Groovy',
@@ -313,6 +262,12 @@ const modes = [
alias: ['hx', 'hxml'],
mode: 'haxe'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'html_ruby',
label: 'HTML (Ruby)',
@@ -355,6 +310,18 @@ const modes = [
alias: [],
mode: 'jade'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'json',
label: 'JSON',
@@ -451,6 +418,12 @@ const modes = [
alias: [],
mode: 'makefile'
},
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'mask',
label: 'Mask',
@@ -529,6 +502,12 @@ const modes = [
alias: ['postgres'],
mode: 'pgsql'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'powershell',
label: 'PowerShell',
@@ -559,6 +538,12 @@ const modes = [
alias: ['protocol', 'buffers'],
mode: 'protobuf'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'r',
label: 'R',
@@ -571,6 +556,12 @@ const modes = [
alias: [],
mode: 'rdoc'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'rust',
label: 'Rust',
@@ -667,6 +658,12 @@ const modes = [
alias: [],
mode: 'svg'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'swig',
label: 'SWIG',

View File

@@ -1,7 +0,0 @@
const electron = require('electron')
const shell = electron.shell
export default function (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}

View File

@@ -1,40 +0,0 @@
'use strict'
var _ = require('lodash')
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const TAG_FILTER = 'TAG_FILTER'
export default function search (articles, search) {
let filters = search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => {
if (key.match(/^in:.+$/)) {
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
})
// let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.Tags, tag => tag.name.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
return articles
}

View File

@@ -0,0 +1,17 @@
.root
absolute top bottom right
border-width 1px 0
border-style solid
border-color $ui-borderColor
.empty
height 320px
display flex
align-items center
.empty-message
width 100%
font-size 42px
line-height 72px
text-align center
color $ui-inactive-text-color

View File

@@ -0,0 +1,286 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl'
import _ from 'lodash'
class FolderSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
search: '',
optionIndex: -1
}
}
componentDidMount () {
this.value = this.props.value
}
componentDidUpdate () {
this.value = this.props.value
}
handleClick (e) {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
}
handleFocus (e) {
if (this.state.status === 'IDLE') {
this.setState({
status: 'FOCUS'
})
}
}
handleBlur (e) {
if (this.state.status === 'FOCUS') {
this.setState({
status: 'IDLE'
})
}
}
handleKeyDown (e) {
switch (e.keyCode) {
case 13:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
}
break
case 40:
case 38:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: 0
}, () => {
this.refs.search.focus()
})
}
break
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]
if (previousEl != null) previousEl.focus()
}
}
}
handleSearchInputBlur (e) {
if (e.relatedTarget !== this.refs.root) {
this.setState({
status: 'IDLE'
})
}
}
handleSearchInputChange (e) {
let { folders } = this.props
let search = this.refs.search.value
let optionIndex = search.length > 0
? _.findIndex(folders, (folder) => {
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
})
: -1
this.setState({
search: this.refs.search.value,
optionIndex: optionIndex
})
}
handleSearchInputKeyDown (e) {
switch (e.keyCode) {
case 40:
e.stopPropagation()
this.nextOption()
break
case 38:
e.stopPropagation()
this.previousOption()
break
case 13:
e.stopPropagation()
this.selectOption()
break
case 27:
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.refs.root.focus()
})
}
}
nextOption () {
let { storages } = this.props
let { optionIndex } = this.state
optionIndex++
if (optionIndex >= folders.length) optionIndex = 0
this.setState({
optionIndex
})
}
previousOption () {
let { folders } = this.props
let { optionIndex } = this.state
optionIndex--
if (optionIndex < 0) optionIndex = folders.length - 1
this.setState({
optionIndex
})
}
selectOption () {
let { folders } = this.props
let optionIndex = this.state.optionIndex
let folder = folders[optionIndex]
if (folder != null) {
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(folder.key)
this.refs.root.focus()
})
}
}
handleOptionClick (storageKey, folderKey) {
return (e) => {
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
})
}
}
setValue (value) {
this.value = value
this.props.onChange()
}
render () {
let { className, storages, value } = this.props
let splitted = value.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let options = []
storages.forEach((storage, index) => {
storage.folders.forEach((folder) => {
options.push({
storage: storage,
folder: folder
})
})
})
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
let optionList = options
.map((option, index) => {
return (
<div styleName={index === this.state.optionIndex
? 'search-optionList-item--active'
: 'search-optionList-item'
}
key={option.storage.key + '-' + option.folder.key}
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
>
<span styleName='search-optionList-item-name'
style={{borderColor: option.folder.color}}
>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
</span>
</div>
)
})
return (
<div className={_.isString(className)
? 'FolderSelect ' + className
: 'FolderSelect'
}
styleName={this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
}
ref='root'
tabIndex='0'
onClick={(e) => this.handleClick(e)}
onFocus={(e) => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
>
{this.state.status === 'SEARCH'
? <div styleName='search'>
<input styleName='search-input'
ref='search'
value={this.state.search}
placeholder='Folder...'
onChange={(e) => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
/>
<div styleName='search-optionList'
ref='optionList'
>
{optionList}
</div>
</div>
: <div styleName='idle'>
<div styleName='idle-label'>
<span styleName='idle-label-name'
style={{borderColor: currentOption.folder.color}}
>
{currentOption.folder.name}
<span styleName='idle-label-name-surfix'>in {currentOption.storage.name}</span>
</span>
</div>
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
</div>
}
</div>
)
}
}
FolderSelect.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
}))
}
export default CSSModules(FolderSelect, styles)

View File

@@ -0,0 +1,88 @@
.root
position relative
border solid 1px transparent
line-height 34px
vertical-align middle
border-radius 2px
transition 0.15s
user-select none
&:hover
background-color white
border-color $ui-borderColor
.root--search, .root--focus
@extend .root
background-color white
border-color $ui-input--focus-borderColor
&:hover
background-color white
border-color $ui-input--focus-borderColor
.idle
position relative
cursor pointer
.idle-label
absolute left top
padding 0 0 0 5px
right 20px
overflow ellipsis
.idle-label-name
border-left solid 4px transparent
padding 2px 5px
.idle-label-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
.idle-caret
absolute right top
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
outline none
border none
height 20px
line-height 20px
background-color transparent
padding 0 10px
.search-optionList
position fixed
border $ui-border
z-index 200
background-color white
border-radius 2px
.search-optionList-item
height 34px
width 250px
box-sizing border-box
padding 0 5px
overflow ellipsis
cursor pointer
&:hover
background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
.search-optionList-item-name
border-left solid 4px transparent
padding 2px 5px
.search-optionList-item-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px

View File

@@ -0,0 +1,291 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownNoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
class MarkdownNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
note: Object.assign({
title: '',
content: '',
isMovingNote: false,
isDeleting: false
}, props.note)
}
this.dispatchTimer = null
}
focus () {
this.refs.content.focus()
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
this.setState({
note: Object.assign({}, nextProps.note),
isDeleting: false
}, () => {
this.refs.content.reload()
this.refs.tags.reset()
})
}
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.content = this.refs.content.value
note.tags = this.refs.tags.value
note.title = this.findTitle(note.content)
note.updatedAt = new Date()
this.setState({
note
}, () => {
this.save()
})
}
save () {
let { note, dispatch } = this.props
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
})
}
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
}
render () {
let { storages, config } = this.props
let { note } = this.state
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
</button>
</div>
</div>
}
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={this.state.note.content}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
</div>
</div>
)
}
}
MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -0,0 +1,90 @@
$info-height = 75px
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-delete
height 80px
clearfix()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
.info-delete-cancelButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
.body
absolute bottom left right
top $info-height
.body-noteEditor
absolute top bottom left right

View File

@@ -0,0 +1,477 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetNoteDetail.styl'
import CodeEditor from 'browser/components/CodeEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import modes from 'browser/lib/modes'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
class SnippetNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0,
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
}),
isDeleting: false
}
}
focus () {
this.refs.description.focus()
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) {
let nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
})
this.setState({
snippetIndex: 0,
note: nextNote,
isDeleting: false
}, () => {
let { snippets } = this.state.note
snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
this.refs.tags.reset()
})
}
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
note.title = this.findTitle(note.description)
this.setState({
note
}, () => {
this.save()
})
}
save () {
let { note, dispatch } = this.props
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
})
}
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
}
handleTabPlusButtonClick (e) {
let { note } = this.state
note.snippets = note.snippets.concat([{
name: '',
mode: 'text',
content: ''
}])
this.setState({
note
})
}
handleTabButtonClick (index) {
return (e) => {
this.setState({
snippetIndex: index
})
}
}
handleTabDeleteButtonClick (index) {
return (e) => {
if (this.state.note.snippets.length > 1) {
let snippets = this.state.note.snippets.slice()
snippets.splice(index, 1)
this.state.note.snippets = snippets
this.setState({
note: this.state.note
})
}
}
}
handleNameInputChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].name = e.target.value
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
handleModeButtonClick (index) {
return (e) => {
let menu = new Menu()
modes.forEach((mode) => {
menu.append(new MenuItem({
label: mode.label,
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
}))
})
menu.popup(remote.getCurrentWindow())
}
}
handleModeOptionClick (index, name) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].mode = name
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
handleCodeChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
render () {
let { storages, config } = this.props
let { note } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(index)(e)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
{note.snippets.length > 1 &&
<button styleName='tabList-item-deleteButton'
onClick={(e) => this.handleTabDeleteButtonClick(index)(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
let mode = snippet.mode === 'text'
? null
: modes.filter((mode) => mode.name === snippet.mode)[0]
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
<div styleName='tabView-top'>
<input styleName='tabView-top-name'
placeholder='Filename including extensions...'
value={snippet.name}
onChange={(e) => this.handleNameInputChange(index)(e)}
/>
<button styleName='tabView-top-mode'
onClick={(e) => this.handleModeButtonClick(index)(e)}
>
{mode == null
? 'Select Syntax...'
: mode.label
}&nbsp;
<i className='fa fa-caret-down'/>
</button>
</div>
<CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
</div>
})
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
</button>
</div>
</div>
}
<div styleName='body'>
<div styleName='body-description'>
<textarea styleName='body-description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={this.state.note.description}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='tabList'>
{tabList}
<button styleName='tabList-plusButton'
onClick={(e) => this.handleTabPlusButtonClick(e)}
>
<i className='fa fa-plus'/>
</button>
</div>
{viewList}
</div>
</div>
)
}
}
SnippetNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -0,0 +1,170 @@
$info-height = 75px
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-delete
height 80px
clearfix()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
.info-delete-cancelButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
.body
absolute bottom left right
top $info-height
.body-description
absolute top left right
height 80px
border-bottom $ui-border
.body-description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
.tabList
absolute left right
top 80px
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList-item
position relative
flex 1
border-right $ui-border
&:hover
.tabList-item-deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
.tabList-item--active
@extend .tabList-item
.tabList-item-button
border-color $brand-color
.tabList-item-button
width 100%
height 100%
navButtonColor()
border-left 4px solid transparent
.tabList-item-deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.tabList-plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-top
absolute top left right
height 30px
border-bottom $ui-border
display flex
.tabView-top-name
flex 1
border none
padding 0 10px
font-size 14px
.tabView-top-mode
width 110px
padding 0
border none
border-left $ui-border
colorDefaultButton()
color $ui-inactive-text-color
&:hover
color $ui-text-color
.tabView-content
absolute left right bottom
top 30px

View File

@@ -0,0 +1,66 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl'
import _ from 'lodash'
class StarButton extends React.Component {
constructor (props) {
super(props)
this.state = {
isActive: false
}
}
handleMouseDown (e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
this.setState({
isActive: false
})
}
render () {
let { className } = this.props
return (
<button className={_.isString(className)
? 'StarButton ' + className
: 'StarButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<i className={this.state.isActive || this.props.isActive
? 'fa fa-star'
: 'fa fa-star-o'
}
/>
</button>
)
}
}
StarButton.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CSSModules(StarButton, styles)

View File

@@ -0,0 +1,11 @@
.root
position relative
padding 0
transition transform 0.15s
&:hover
transform rotate(-72deg)
.root--active
@extend .root
color $ui-active-color
transform rotate(-72deg)

View File

@@ -0,0 +1,148 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
newTag: ''
}
}
componentDidMount () {
this.value = this.props.value
}
componentDidUpdate () {
this.value = this.props.value
}
handleNewTagInputKeyDown (e) {
switch (e.keyCode) {
case 13:
this.submitTag()
break
case 8:
if (this.refs.newTag.value.length === 0) {
this.removeLastTag()
}
}
}
removeLastTag () {
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value.pop()
value = _.uniq(value)
this.value = value
this.props.onChange()
}
reset () {
this.setState({
newTag: ''
})
}
submitTag () {
let { value } = this.props
let newTag = this.refs.newTag.value.trim()
if (newTag.length <= 0) {
this.setState({
newTag: ''
})
return
}
value = _.isArray(value)
? value.slice()
: []
value.push(newTag)
value = _.uniq(value)
this.setState({
newTag: ''
}, () => {
this.value = value
this.props.onChange()
})
}
handleNewTagInputChange (e) {
this.setState({
newTag: this.refs.newTag.value
})
}
handleTagRemoveButtonClick (tag) {
return (e) => {
let { value } = this.props
value.splice(value.indexOf(tag), 1)
value = _.uniq(value)
this.value = value
this.props.onChange()
}
}
render () {
let { value, className } = this.props
let tagList = _.isArray(value)
? value.map((tag) => {
return (
<span styleName='tag'
key={tag}
>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
>
<i className='fa fa-times fa-fw'/>
</button>
<span styleName='tag-label'>{tag}</span>
</span>
)
})
: []
return (
<div className={_.isString(className)
? 'TagSelect ' + className
: 'TagSelect'
}
styleName='root'
>
<i styleName='icon'
className='fa fa-tags'
/>
{tagList}
<input styleName='newTag'
ref='newTag'
value={this.state.newTag}
placeholder='Add tag...'
onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
/>
</div>
)
}
}
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
}
export default CSSModules(TagSelect, styles)

View File

@@ -0,0 +1,63 @@
.root
position relative
user-select none
.icon
display inline-block
width 30px
vertical-align middle
text-align center
color $ui-button-color
.tag
display inline-block
margin 0 2px
vertical-align middle
height 20px
background-color white
border-radius 3px
overflow hidden
clearfix()
.tag-removeButton
float left
height 20px
width 18px
margin 0
padding 0
border-style solid
border-color $ui-borderColor
border-width 0 0 0 3px
line-height 18px
background-color transparent
color $ui-button-color
&:hover
background-color $ui-button--hover-backgroundColor
&:active, &:active:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
border-color $ui-button--focus-borderColor
&:focus
border-color $ui-button--focus-borderColor
.tag-label
float left
height 20px
line-height 20px
padding 0 6px
.newTag
display inline-block
margin 0 2px
vertical-align middle
height 24px
box-sizing borde-box
border none
border-bottom $ui-border
background-color transparent
outline none
padding 0 4px
&:focus
border-color $ui-input--focus-borderColor = #369DCD
&:disabled
background-color $ui-input--disabled-backgroundColor = #DDD

View File

@@ -0,0 +1,105 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Detail.styl'
import _ from 'lodash'
import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter'
const OSX = global.process.platform === 'darwin'
class Detail extends React.Component {
constructor (props) {
super(props)
this.focusHandler = () => {
this.refs.root != null && this.refs.root.focus()
}
this.deleteHandler = () => {
this.refs.root != null && this.refs.root.handleDeleteMenuClick()
}
}
componentDidMount () {
ee.on('detail:focus', this.focusHandler)
ee.on('detail:delete', this.deleteHandler)
}
componentWillUnmount () {
ee.off('detail:focus', this.focusHandler)
ee.off('detail:delete', this.deleteHandler)
}
render () {
let { location, notes, config } = this.props
let note = null
if (location.query.key != null) {
let splitted = location.query.key.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let noteKey = splitted.shift()
note = _.find(notes, {
storage: storageKey,
folder: folderKey,
key: noteKey
})
}
if (note == null) {
return (
<div styleName='root'
style={this.props.style}
tabIndex='0'
>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
if (note.type === 'SNIPPET_NOTE') {
return (
<SnippetNoteDetail
note={note}
config={config}
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'style',
'ignorePreviewPointerEvents',
'location'
])}
/>
)
}
return (
<MarkdownNoteDetail
note={note}
config={config}
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'style',
'ignorePreviewPointerEvents',
'location'
])}
/>
)
}
}
Detail.propTypes = {
dispatch: PropTypes.func,
storages: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(Detail, styles)

View File

@@ -1,243 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
import activityRecord from 'browser/lib/activityRecord'
import fetchConfig from 'browser/lib/fetchConfig'
const electron = require('electron')
const ipc = electron.ipcRenderer
export const PREVIEW_MODE = 'PREVIEW_MODE'
export const EDIT_MODE = 'EDIT_MODE'
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class ArticleEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.isMouseDown = false
this.state = {
status: PREVIEW_MODE,
cursorPosition: null,
firstVisibleRow: null,
switchPreview: config['switch-preview'],
isTemporary: false
}
}
componentDidMount () {
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillReceiveProps (nextProps) {
if (nextProps.article.key !== this.props.article.key) {
this.setState({
content: this.props.article.content
})
}
}
handleConfigApply (e, newConfig) {
this.setState({
switchPreview: newConfig['switch-preview']
})
}
resetCursorPosition () {
this.setState({
cursorPosition: null,
firstVisibleRow: null
}, function () {
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
if (previewEl) previewEl.scrollTop = 0
})
}
switchPreviewMode (isTemporary = false) {
if (this.props.article.mode !== 'markdown') return true
let cursorPosition = this.refs.editor.getCursorPosition()
let firstVisibleRow = this.refs.editor.getFirstVisibleRow()
this.setState({
status: PREVIEW_MODE,
cursorPosition,
firstVisibleRow,
isTemporary: isTemporary
}, function () {
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
let anchors = previewEl.querySelectorAll('.lineAnchor')
for (let i = 0; i < anchors.length; i++) {
if (parseInt(anchors[i].dataset.key, 10) > cursorPosition.row || i === anchors.length - 1) {
var targetAnchor = anchors[i > 0 ? i - 1 : 0]
previewEl.scrollTop = targetAnchor.offsetTop - 100
break
}
}
})
}
switchEditMode (isTemporary = false) {
this.setState({
status: EDIT_MODE,
isTemporary: false
}, function () {
if (this.state.cursorPosition != null) {
this.refs.editor.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column)
this.refs.editor.scrollToLine(this.state.firstVisibleRow)
}
this.refs.editor.editor.focus()
if (!isTemporary) activityRecord.emit('ARTICLE_UPDATE', this.props.article)
})
}
handleBlurCodeEditor (e) {
let isFocusingToThis = e.relatedTarget === ReactDOM.findDOMNode(this)
if (isFocusingToThis || this.state.switchPreview !== 'blur') {
return
}
let { article } = this.props
if (article.mode === 'markdown') {
this.switchPreviewMode()
}
}
handleCodeEditorChange (value) {
this.props.onChange(value)
}
handleRightClick (e) {
let { article } = this.props
if (this.state.switchPreview === 'rightclick' && article.mode === 'markdown') {
if (this.state.status === EDIT_MODE) this.switchPreviewMode()
else this.switchEditMode()
}
}
handleMouseUp (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isMouseDown = false
this.moveCount = 0
if (!this.isDrag) {
this.switchEditMode()
}
break
case 2:
if (this.state.isTemporary) this.switchEditMode(true)
}
break
case 'rightclick':
}
}
handleMouseMove (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
this.moveCount++
if (this.moveCount > 5) {
this.isDrag = true
}
}
}
handleMouseDowm (e) {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
if (!showPreview) {
return false
}
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isDrag = false
this.isMouseDown = true
this.moveCount = 0
break
case 2:
if (this.state.status === EDIT_MODE && this.props.article.mode === 'markdown') {
this.switchPreviewMode(true)
}
}
break
case 'rightclick':
}
}
render () {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
return (
<div
tabIndex='5'
onContextMenu={e => this.handleRightClick(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseDown={e => this.handleMouseDowm(e)}
className='ArticleEditor'
>
{showPreview
? <MarkdownPreview
ref='preview'
content={article.content}
/>
: <CodeEditor
ref='editor'
onBlur={e => this.handleBlurCodeEditor(e)}
onChange={value => this.handleCodeEditorChange(value)}
article={article}
/>
}
{article.mode === 'markdown'
? <div className='ArticleDetail-panel-content-tooltip' children={
showPreview
? this.state.switchPreview === 'blur'
? 'Click to Edit'
: 'Right Click to Edit'
: this.state.switchPreview === 'blur'
? 'Press ESC to Watch Preview'
: 'Right Click to Watch Preview'
}
/>
: null
}
</div>
)
}
}
ArticleEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
key: PropTypes.string,
mode: PropTypes.string
}),
onChange: PropTypes.func,
parent: PropTypes.object
}

View File

@@ -1,361 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import {
switchFolder,
updateArticle
} from '../../actions'
import linkState from 'browser/lib/linkState'
import TagSelect from 'browser/components/TagSelect'
import ModeSelect from 'browser/components/ModeSelect'
import ShareButton from './ShareButton'
import { openModal, isModalOpen } from 'browser/lib/modal'
import DeleteArticleModal from '../../modal/DeleteArticleModal'
import ArticleEditor from './ArticleEditor'
const electron = require('electron')
const ipc = electron.ipcRenderer
import fetchConfig from 'browser/lib/fetchConfig'
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const tagSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
<svg x='0' y='-15'>
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
</svg>
</svg>
)
const modeSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
<svg x='300' y='0'>
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
</svg>
</svg>
)
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
this.deleteHandler = e => {
if (isModalOpen()) return true
this.handleDeleteButtonClick()
}
this.uncacheHandler = e => {
if (isModalOpen()) return true
this.handleUncache()
}
this.titleHandler = e => {
if (isModalOpen()) return true
if (this.refs.title) {
this.focusTitle()
}
}
this.editHandler = e => {
if (isModalOpen()) return true
if (this.refs.editor) this.refs.editor.switchEditMode()
}
this.previewHandler = e => {
if (isModalOpen()) return true
if (this.refs.editor) this.refs.editor.switchPreviewMode()
}
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
article: Object.assign({content: ''}, props.activeArticle),
openShareDropdown: false,
fontFamily: config['editor-font-family']
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
this.shareDropdownInterceptor = e => {
e.stopPropagation()
}
ipc.on('detail-delete', this.deleteHandler)
ipc.on('detail-uncache', this.uncacheHandler)
ipc.on('detail-title', this.titleHandler)
ipc.on('detail-edit', this.editHandler)
ipc.on('detail-preview', this.previewHandler)
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ipc.removeListener('detail-delete', this.deleteHandler)
ipc.removeListener('detail-uncache', this.uncacheHandler)
ipc.removeListener('detail-title', this.titleHandler)
ipc.removeListener('detail-edit', this.editHandler)
ipc.removeListener('detail-preview', this.previewHandler)
}
componentDidUpdate (prevProps, prevState) {
if (this.props.activeArticle == null || prevProps.activeArticle == null || this.props.activeArticle.key !== prevProps.activeArticle.key) {
if (this.refs.editor) this.refs.editor.resetCursorPosition()
}
if (prevProps.activeArticle == null && this.props.activeArticle) {
}
}
handleConfigApply (e, config) {
this.setState({
fontFamily: config['editor-font-family']
})
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
<div className='ArticleDetail-empty-box'>
<div className='ArticleDetail-empty-box-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
handleOthersButtonClick (e) {
this.deleteHandler()
}
handleFolderKeyChange (e) {
let { dispatch, activeArticle, status, folders } = this.props
let article = Object.assign({}, activeArticle, {
FolderKey: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
let targetFolderKey = e.target.value
if (status.targetFolders.length > 0) {
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
dispatch(switchFolder(targetFolder.name))
}
}
handleTitleChange (e) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
title: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleTagsChange (newTag, tags) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
tags: tags,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleModeChange (value) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
mode: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
this.switchEditMode()
}
handleContentChange (value) {
let { dispatch, activeArticle } = this.props
if (activeArticle.content !== value) {
let article = Object.assign({}, activeArticle, {
content: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
}
handleDeleteButtonClick (e) {
if (this.props.activeArticle) {
openModal(DeleteArticleModal, {articleKey: this.props.activeArticle.key})
}
}
handleTitleKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.refs.mode.handleIdleSelectClick()
}
}
handleModeSelectKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.switchEditMode()
}
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault()
this.focusTitle()
}
if (e.keyCode === 27) {
this.focusTitle()
}
}
switchEditMode () {
this.refs.editor.switchEditMode()
}
focusTitle () {
if (this.refs.title) {
let titleEl = ReactDOM.findDOMNode(this.refs.title)
titleEl.focus()
titleEl.select()
}
}
render () {
let { folders, status, tags, activeArticle, modified, user } = this.props
if (activeArticle == null) return this.renderEmpty()
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
return (
<div tabIndex='4' className='ArticleDetail'>
<div className='ArticleDetail-info'>
<div className='ArticleDetail-info-row'>
<select
className='ArticleDetail-info-folder'
value={activeArticle.FolderKey}
onChange={e => this.handleFolderKeyChange(e)}
>
{folderOptions}
</select>
<span className='ArticleDetail-info-status'
children={
isUnsaved
? <span> <span className='unsaved-mark'></span> Unsaved</span>
: `Created : ${moment(activeArticle.createdAt).format('YYYY/MM/DD')} Updated : ${moment(activeArticle.updatedAt).format('YYYY/MM/DD')}`
}
/>
<div className='ArticleDetail-info-control'>
{/*<div className={'ArticleDetail-info-control-save' + (!isUnsaved ? ' hide' : '')}>
<button
onClick={e => this.handleSaveButtonClick(e)}
className='ArticleDetail-info-control-save-button'
disabled={!isUnsaved}
>
<i className='fa fa-fw fa-save'/>&nbsp;Save
<span className='tooltip' children={`Save Post (${OSX ? '⌘' : '^'} + S)`}/>
</button>
</div>*/}
<ShareButton
article={activeArticle}
user={user}
/>
<button className='ArticleDetail-info-control-delete-button' onClick={e => this.handleOthersButtonClick(e)}>
<i className='fa fa-fw fa-trash'/>
<span className='tooltip' children={`Delete Post (^ + Del)`}/>
</button>
</div>
</div>
<div className='ArticleDetail-info-row2'>
<TagSelect
tags={activeArticle.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/>
{status.isTutorialOpen ? tagSelectTutorialElement : null}
</div>
</div>
<div className='ArticleDetail-panel'>
<div className='ArticleDetail-panel-header'>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder='(Untitled)'
ref='title'
value={activeArticle.title}
onChange={e => this.handleTitleChange(e)}
style={{
fontFamily: this.state.fontFamily
}}
/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
onKeyDown={e => this.handleModeSelectKeyDown(e)}
value={activeArticle.mode}
className='ArticleDetail-panel-header-mode'
/>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
</div>
<ArticleEditor
ref='editor'
article={activeArticle}
onChange={content => this.handleContentChange(content)}
/>
</div>
</div>
)
}
}
ArticleDetail.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape(),
tags: PropTypes.array,
user: PropTypes.shape(),
folders: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}
ArticleDetail.prototype.linkState = linkState

View File

@@ -1,207 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment'
import { switchArticle } from '../actions'
import FolderMark from 'browser/components/FolderMark'
import TagLink from './TagLink'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
export default class ArticleList extends React.Component {
constructor (props) {
super(props)
this.focusHandler = e => this.focus()
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ipc.on('list-focus', this.focusHandler)
this.focus()
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ipc.removeListener('list-focus', this.focusHandler)
}
componentDidUpdate () {
let { articles, activeArticle } = this.props
var index = articles.indexOf(activeArticle)
var el = ReactDOM.findDOMNode(this)
var li = el.querySelectorAll('.ArticleList>div')[index]
if (li == null) {
return
}
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
if (overflowBelow) {
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
}
var overflowAbove = el.scrollTop > li.offsetTop
if (overflowAbove) {
el.scrollTop = li.offsetTop
}
}
focus () {
ReactDOM.findDOMNode(this).focus()
}
// 移動ができなかったらfalseを返す:
selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) - 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
selectNextArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) + 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
dispatch(switchArticle(article.key))
}
}
handleArticleListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('top-new-post')
}
if (e.keyCode === 65 && e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('nav-new-folder')
}
if (e.keyCode === 68) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-delete')
}
if (e.keyCode === 84) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-title')
}
if (e.keyCode === 69) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-edit')
}
if (e.keyCode === 83) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-save')
}
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorArticle()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextArticle()
}
}
render () {
let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let modifiedArticle = _.findWhere(modified, {key: article.key})
let originalArticle = article
if (modifiedArticle) {
article = Object.assign({}, article)
}
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.slice().map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
let folderChanged = originalArticle.FolderKey !== article.FolderKey
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
let title = article.title.trim().length === 0
? <small>(Untitled)</small>
: article.title
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='ArticleList-item-top'>
{folder != null
? folderChanged
? <span className='folderName'>
<FolderMark color={originalFolder.color}/>{originalFolder.name}
->
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span className='folderName'>
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span><FolderMark color={-1}/>Unknown</span>
}
<span className='updatedAt'
children={
modifiedArticle != null
? <span><span className='unsaved-mark'></span> Unsaved</span>
: moment(article.updatedAt).fromNow()
}
/>
</div>
<div className='ArticleList-item-middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div>
<div className='ArticleList-item-middle2'>
<pre><code children={article.content.trim().length === 0 ? '(Empty content)' : article.content.substring(0, 50)}/></pre>
</div>
<div className='ArticleList-item-bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div>
</div>
<div className='divider'></div>
</div>
)
})
return (
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
{articleElements}
</div>
)
}
}
ArticleList.propTypes = {
dispatch: PropTypes.func,
folders: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}

View File

@@ -1,202 +0,0 @@
import React, { PropTypes } from 'react'
import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
import { openModal, isModalOpen } from 'browser/lib/modal'
import FolderMark from 'browser/components/FolderMark'
import Preferences from '../modal/Preferences'
import CreateNewFolder from '../modal/CreateNewFolder'
import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
const ipc = require('electron').ipcRenderer
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const preferenceTutorialElement = (
<svg width='300' height='300' className='tutorial'>
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
<svg x='-30' y='-270' width='400' height='400'>
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
</svg>
</svg>
)
const newFolderTutorialElement = (
<svg width='800' height='500' className='tutorial'>
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
<svg x='50' y='10' width='300' height='400'>
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
</svg>
</svg>
)
export default class ArticleNavigator extends React.Component {
constructor (props) {
super(props)
this.newFolderHandler = e => {
if (isModalOpen()) return true
this.handleNewFolderButton(e)
}
}
componentDidMount () {
ipc.on('nav-new-folder', this.newFolderHandler)
}
componentWillUnmount () {
ipc.removeListener('nav-new-folder', this.newFolderHandler)
}
handlePreferencesButtonClick (e) {
openModal(Preferences)
}
handleNewFolderButton (e) {
let { user } = this.props
openModal(CreateNewFolder, {user: user})
}
handleFolderButtonClick (name) {
return e => {
let { dispatch } = this.props
dispatch(switchFolder(name))
}
}
handleAllFoldersButtonClick (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(''))
}
handleUnsavedItemClick (article) {
let { dispatch } = this.props
return e => {
let { articles } = this.props
let isInArticleList = articles.some(_article => _article.key === article.key)
if (!isInArticleList) dispatch(clearSearch())
dispatch(switchArticle(article.key))
}
}
handleUncacheButtonClick (article) {
let { dispatch } = this.props
return e => {
dispatch(uncacheArticle(article.key))
}
}
handleSaveAllClick (e) {
let { dispatch } = this.props
dispatch(saveAllArticles())
}
render () {
let { status, user, folders, allArticles, modified, activeArticle } = this.props
let { targetFolders } = status
if (targetFolders == null) targetFolders = []
let modifiedElements = modified.map(modifiedArticle => {
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
if (originalArticle == null) return false
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
let className = 'ArticleNavigator-unsaved-list-item'
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
return (
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
<div className='ArticleNavigator-unsaved-list-item-label'>
<ModeIcon mode={combinedArticle.mode}/>&nbsp;
{combinedArticle.title.trim().length > 0
? combinedArticle.title
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
</div>
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
</div>
)
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
let hasModified = modifiedElements.length > 0
let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
</button>
)
})
return (
<div tabIndex='1' className='ArticleNavigator'>
<div className='userInfo'>
<div className='userProfileName'>{user.name}</div>
<div className='userName'>localStorage</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
<i className='fa fa-fw fa-chevron-down'/>
<span className='tooltip'>Preferences</span>
</button>
{status.isTutorialOpen ? preferenceTutorialElement : null}
</div>
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
<div className='ArticleNavigator-unsaved-list'>
{modifiedElements}
</div>
<div className='ArticleNavigator-unsaved-control'>
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
</div>
</div>*/}
<div className={'ArticleNavigator-folders expand'}>
{status.isTutorialOpen ? newFolderTutorialElement : null}
<div className='ArticleNavigator-folders-header'>
<div className='title'>Folders</div>
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
<i className='fa fa-fw fa-plus'/>
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
</button>
</div>
<div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
{folderElememts}
</div>
</div>
</div>
)
}
}
ArticleNavigator.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape({
folderId: PropTypes.number
}),
user: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape({
key: PropTypes.string
})
}

View File

@@ -1,269 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ExternalLink from 'browser/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial, saveArticle, switchFolder } from '../actions'
import { isModalOpen } from 'browser/lib/modal'
import keygen from 'browser/lib/keygen'
import activityRecord from 'browser/lib/activityRecord'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
const OSX = global.process.platform === 'darwin'
const BRAND_COLOR = '#18AF90'
const searchTutorialElement = (
<svg width='750' height='300' className='tutorial'>
<text x='125' y='63' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
<text x='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : /{folder_name}\n'}</text>
<text x='140' y='135' fill={BRAND_COLOR} fontSize='14'>
{'exact match : //{folder_name}'}</text>
<svg x='90' width='500' height='300'>
<path fill='white' d='M27.2,6.9c-1.7,3.5-6,4.8-8,8.2c-1.8,3.1-2.1,6.8-1.8,10.2c0.7,7,4.2,16.7,10.3,20.7c0.5,0.4,1.4,0.2,1.8-0.2
c0.1-0.1,0.2-0.2,0.3-0.3c0.6-0.6,0.6-1.5,0-2.1c-0.2-0.2-0.3-0.4-0.5-0.5c-1.3-1.4-3.2,0.7-1.9,2.1c0.2,0.2-0.3,0.4,0.7,0.5
c0-0.7,0-1.4,0-2.1c0,0.1-0.4,0.2-0.5,0.3c0.6-0.1,1.1-0.2,1.7-0.2c-5.7-3.7-9.2-14.5-9-20.9c0.1-4,1.6-6.7,4.8-9.1
c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
<path fill='white' d='M9.5,24.4c2.4-2.7,4.9-5.4,7.3-8c2.5-2.8,5.7-7.6,9.9-7.8c-0.5-0.5-1-1-1.5-1.5c0.1,6.8,1.9,13.1,5.3,18.9
c1,1.7,3.6,0.2,2.6-1.5c-3.2-5.4-4.8-11.1-4.9-17.4c0-0.8-0.7-1.5-1.5-1.5c-3.6,0.2-5.9,2.1-8.3,4.7c-3.7,3.9-7.3,8-11,12
C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
</svg>
</svg>
)
const newPostTutorialElement = (
<svg width='900' height='900' className='tutorial'>
<text x='470' y='50' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
<text x='490' y='75' fill={BRAND_COLOR} fontSize='16' children={`press \`${OSX ? '⌘' : '^'} + n\``}/>
<svg x='415' y='20' width='400' height='400'>
<path fill='white' d='M11.6,14.7c1,5.5,2.9,10.7,5.7,15.5c1,1.7,3.5,0.2,2.6-1.5c-2.6-4.7-4.4-9.6-5.4-14.8
C14.1,12,11.3,12.8,11.6,14.7L11.6,14.7z'/>
<path fill='white' d='M16.8,17.1c4,0.2,7.6-1.1,10.7-3.6c1.5-1.2-0.6-3.3-2.1-2.1c-2.4,2-5.4,2.9-8.6,2.7C14.9,14,14.9,17,16.8,17.1
L16.8,17.1z'/>
<path fill='white' d='M13.8,17.6c11.9,3.5,24.1,4.9,36.4,3.9c1.9-0.1,1.9-3.1,0-3c-12.1,0.9-24-0.3-35.6-3.8
C12.7,14.1,11.9,17,13.8,17.6L13.8,17.6z'/>
</svg>
</svg>
)
export default class ArticleTopBar extends React.Component {
constructor (props) {
super(props)
this.saveAllHandler = e => {
if (isModalOpen()) return true
this.handleSaveAllButtonClick(e)
}
this.focusSearchHandler = e => {
if (isModalOpen()) return true
this.focusInput(e)
}
this.newPostHandler = e => {
if (isModalOpen()) return true
this.handleNewPostButtonClick(e)
}
this.state = {
isTooltipHidden: true,
isLinksDropdownOpen: false
}
}
componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
this.linksButton = ReactDOM.findDOMNode(this.refs.links)
this.showLinksDropdown = e => {
e.preventDefault()
e.stopPropagation()
if (!this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: true})
}
}
this.linksButton.addEventListener('click', this.showLinksDropdown)
this.hideLinksDropdown = e => {
if (this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: false})
}
}
document.addEventListener('click', this.hideLinksDropdown)
// ipc.on('top-save-all', this.saveAllHandler)
ipc.on('top-focus-search', this.focusSearchHandler)
ipc.on('top-new-post', this.newPostHandler)
}
componentWillUnmount () {
document.removeEventListener('click', this.hideLinksDropdown)
this.linksButton.removeEventListener('click', this.showLinksDropdown())
// ipc.removeListener('top-save-all', this.saveAllHandler)
ipc.removeListener('top-focus-search', this.focusSearchHandler)
ipc.removeListener('top-new-post', this.newPostHandler)
}
handleTooltipRequest (e) {
if (this.searchInput.value.length === 0 && (document.activeElement === this.searchInput)) {
this.setState({isTooltipHidden: false})
} else {
this.setState({isTooltipHidden: true})
}
}
isInputFocused () {
return document.activeElement === ReactDOM.findDOMNode(this.refs.searchInput)
}
escape () {
let { status, dispatch } = this.props
if (status.search.length > 0) {
dispatch(clearSearch())
return
}
}
focusInput () {
this.searchInput.focus()
}
blurInput () {
this.searchInput.blur()
}
handleSearchChange (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(e.target.value))
this.handleTooltipRequest()
}
handleSearchClearButton (e) {
this.searchInput.value = ''
this.focusInput()
}
handleNewPostButtonClick (e) {
let { dispatch, folders, status } = this.props
let { targetFolders } = status
let isFolderFilterApplied = targetFolders.length > 0
let FolderKey = isFolderFilterApplied
? targetFolders[0].key
: folders[0].key
let newArticle = {
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
craetedAt: new Date(),
updatedAt: new Date()
}
dispatch(saveArticle(newArticle.key, newArticle, true))
if (isFolderFilterApplied) dispatch(switchFolder(targetFolders[0].name))
remote.getCurrentWebContents().send('detail-title')
activityRecord.emit('ARTICLE_CREATE')
}
handleTutorialButtonClick (e) {
let { dispatch } = this.props
dispatch(toggleTutorial())
}
render () {
let { status } = this.props
return (
<div tabIndex='2' className='ArticleTopBar'>
<div className='ArticleTopBar-left'>
<div className='ArticleTopBar-left-search'>
<i className='fa fa-search fa-fw' />
<input
ref='searchInput'
onFocus={e => this.handleSearchChange(e)}
onBlur={e => this.handleSearchChange(e)}
value={this.props.status.search}
onChange={e => this.handleSearchChange(e)}
placeholder='Search'
type='text'
/>
{
this.props.status.search != null && this.props.status.search.length > 0
? <button onClick={e => this.handleSearchClearButton(e)} className='ArticleTopBar-left-search-clear-button'><i className='fa fa-times'/></button>
: null
}
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
<ul>
<li>- Search by tag : #{'{string}'}</li>
<li>- Search by folder : /{'{folder_name}'}<br/><small>exact match : //{'{folder_name}'}</small></li>
<li>- Only unsaved : --unsaved</li>
</ul>
</div>
</div>
{status.isTutorialOpen ? searchTutorialElement : null}
<div className={'ArticleTopBar-left-control'}>
<button className='ArticleTopBar-left-control-new-post-button' onClick={e => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus'/>
<span className='tooltip'>New Post ({OSX ? '⌘' : '^'} + n)</span>
</button>
{status.isTutorialOpen ? newPostTutorialElement : null}
</div>
</div>
<div className='ArticleTopBar-right'>
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
</button>
<a ref='links' className='ArticleTopBar-right-links-button' href>
<img src='../resources/app.png' width='44' height='44'/>
</a>
{
this.state.isLinksDropdownOpen
? (
<div className='ArticleTopBar-right-links-button-dropdown'>
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://b00st.io'>
<i className='fa fa-fw fa-home'/>Boost official page
</ExternalLink>
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://github.com/BoostIO/Boostnote/issues'>
<i className='fa fa-fw fa-github'/> Issues
</ExternalLink>
</div>
)
: null
}
</div>
{status.isTutorialOpen ? (
<div className='tutorial'>
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
<svg width='500' height='250' className='finder'>
<text x='100' y='25' fontSize='32' fill={BRAND_COLOR}>Also, you can open Finder!!</text>
<text x='150' y='55' fontSize='18' fill={BRAND_COLOR} children={'with pressing ' + (OSX ? '`⌘ + Alt + s`' : '`Win + Alt + s`')}/>
</svg>
<svg width='450' className='global'>
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
<text x='50' y='75' fontSize='18' fill={BRAND_COLOR}>Press any key or click to escape tutorial mode</text>
</svg>
<div className='back'></div>
</div>
) : null}
</div>
)
}
}
ArticleTopBar.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape({
search: PropTypes.string
}),
folders: PropTypes.array
}

View File

@@ -1,18 +0,0 @@
import React, { PropTypes } from 'react'
import store from '../store'
import { setTagFilter } from '../actions'
export default class TagLink extends React.Component {
handleClick (e) {
store.dispatch(setTagFilter(this.props.tag))
}
render () {
return (
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
)
}
}
TagLink.propTypes = {
tag: PropTypes.string
}

View File

@@ -1,41 +0,0 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import ProfileImage from 'browser/components/ProfileImage'
export default class UserNavigator extends Component {
renderUserList () {
if (this.props.users == null) return null
var users = this.props.users.map((user, index) => (
<li key={'user-' + user.id}>
<Link to={'/users/' + user.id} activeClassName='active'>
<ProfileImage email={user.email} size='44'/>
<div className='userTooltip'>{user.name}</div>
{index < 9 ? <div className='keyLabel'>{'⌘' + (index + 1)}</div> : null}
</Link>
</li>
))
return (
<ul className='userList'>
{users}
</ul>
)
}
render () {
return (
<div className='UserNavigator'>
{this.renderUserList()}
<button className='createTeamBtn'>
+
<div className='tooltip'>Create a new team</div>
</button>
</div>
)
}
}
UserNavigator.propTypes = {
users: PropTypes.array
}

View File

@@ -1,238 +0,0 @@
import React, { PropTypes} from 'react'
import { connect } from 'react-redux'
import ReactDOM from 'react-dom'
import { toggleTutorial } from '../actions'
import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './ArticleTopBar'
import ArticleList from './ArticleList'
import ArticleDetail from './ArticleDetail'
import _ from 'lodash'
import { isModalOpen, closeModal } from 'browser/lib/modal'
const electron = require('electron')
const remote = electron.remote
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
const OSX = global.process.platform === 'darwin'
class HomePage extends React.Component {
componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、
// `window`に直接かける
this.keyHandler = e => this.handleKeyDown(e)
window.addEventListener('keydown', this.keyHandler)
}
componentWillUnmount () {
window.removeEventListener('keydown', this.keyHandler)
}
handleKeyDown (e) {
if (isModalOpen()) {
if (e.keyCode === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { top, list } = this.refs
let listElement = ReactDOM.findDOMNode(list)
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
e.preventDefault()
return
}
if (e.keyCode === 13 && top.isInputFocused()) {
listElement.focus()
return
}
if (e.keyCode === 27 && top.isInputFocused()) {
if (status.search.length > 0) top.escape()
else listElement.focus()
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (e.keyCode === 27) {
if (document.activeElement !== listElement) {
listElement.focus()
} else {
top.focusInput()
}
return
}
}
render () {
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
status={status}
user={user}
folders={folders}
allArticles={allArticles}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
folders={folders}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
status={status}
tags={tags}
user={user}
folders={folders}
modified={modified}
activeArticle={activeArticle}
/>
</div>
)
}
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/) && !key.match(/^--/)
}
// Build filter object by key
function buildFilter (key) {
if (key.match(/^\/\/.+/)) {
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
}
if (key.match(/^\/.+/)) {
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
}
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
}
function remap (state) {
let { user, folders, status } = state
let _articles = state.articles
let articles = _articles != null ? _articles.data : []
let modified = _articles != null ? _articles.modified : []
articles.sort((a, b) => {
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
if (match === 0) match = b.title.localeCompare(a.title)
if (match === 0) match = b.key.localeCompare(a.key)
return match
})
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
// Filter articles
let filters = status.search.split(' ')
.map(key => key.trim())
.filter(ignoreInvalidKey)
.map(buildFilter)
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) {
let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) {
articles = articles.filter(article => {
return _.findWhere(targetFolders, {key: article.FolderKey})
})
}
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
}
// Grab active article
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
return {
user,
folders,
status,
articles,
allArticles,
modified,
activeArticle,
tags
}
}
HomePage.propTypes = {
status: PropTypes.shape(),
user: PropTypes.shape({
name: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
tags: PropTypes.array
}
export default connect(remap)(HomePage)

159
browser/main/Main.js Normal file
View File

@@ -0,0 +1,159 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Main.styl'
import { connect } from 'react-redux'
import SideNav from './SideNav'
import TopBar from './TopBar'
import NoteList from './NoteList'
import Detail from './Detail'
import dataApi from 'browser/main/lib/dataApi'
import StatusBar from './StatusBar'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
class Main extends React.Component {
constructor (props) {
super(props)
let { config } = props
this.state = {
isSliderFocused: false,
listWidth: config.listWidth
}
}
componentDidMount () {
let { dispatch } = this.props
// Reload all data
dataApi.init()
.then((data) => {
dispatch({
type: 'INIT_ALL',
storages: data.storages,
notes: data.notes
})
if (data.storages.length < 1) {
modal.open(InitModal)
}
})
}
handleSlideMouseDown (e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
handleMouseUp (e) {
if (this.state.isSliderFocused) {
this.setState({
isSliderFocused: false
}, () => {
let { dispatch } = this.props
let newListWidth = this.state.listWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({listWidth: newListWidth})
dispatch({
type: 'SET_LIST_WIDTH',
listWidth: newListWidth
})
})
}
}
handleMouseMove (e) {
if (this.state.isSliderFocused) {
let offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
} else if (newListWidth > 600) {
newListWidth = 600
}
this.setState({
listWidth: newListWidth
})
}
}
render () {
let { config } = this.props
return (
<div
className='Main'
styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, [
'dispatch',
'storages',
'config',
'location'
])}
/>
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
ref='body'
>
<TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'config',
'notes',
'params',
'location'
])}
/>
<NoteList style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'config',
'params',
'location'
])}
/>
<div styleName={this.state.isSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.listWidth}}
onMouseDown={(e) => this.handleSlideMouseDown(e)}
draggable='false'
>
<div styleName='slider-hitbox'/>
</div>
<Detail
style={{left: this.state.listWidth + 1}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'config',
'params',
'location'
])}
ignorePreviewPointerEvents={this.state.isSliderFocused}
/>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
/>
</div>
)
}
}
Main.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array
}
export default connect((x) => x)(CSSModules(Main, styles))

30
browser/main/Main.styl Normal file
View File

@@ -0,0 +1,30 @@
.root
absolute top left bottom right
.body
absolute right top
bottom $statusBar-height - 1
left $sideNav-width
.body--expanded
@extend .body
left $sideNav--folded-width
.slider
absolute top bottom
width 1px
background-color $ui-borderColor
border-width 0
border-style solid
border-color $ui-borderColor
.slider--active
@extend .slider
background-color $ui-button--active-backgroundColor
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize

View File

@@ -1,54 +0,0 @@
const electron = require('electron')
const ipc = electron.ipcRenderer
import React, { PropTypes } from 'react'
import HomePage from './HomePage'
export default class MainContainer extends React.Component {
constructor (props) {
super(props)
this.state = {updateAvailable: false}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
ipc.send('update-app', 'Deal with it.')
}
handleWheel (e) {
if (e.ctrlKey && global.process.platform !== 'darwin') {
if (window.document.body.style.zoom == null) {
window.document.body.style.zoom = 1
}
let zoom = Number(window.document.body.style.zoom)
if (e.deltaY > 0 && zoom < 4) {
document.body.style.zoom = zoom + 0.05
} else if (e.deltaY < 0 && zoom > 0.5) {
document.body.style.zoom = zoom - 0.05
}
}
}
render () {
return (
<div
className='Main'
onWheel={(e) => this.handleWheel(e)}
>
{this.state.updateAvailable ? (
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
) : null}
<HomePage/>
</div>
)
}
}
MainContainer.propTypes = {
children: PropTypes.element
}

View File

@@ -0,0 +1,93 @@
.root
absolute left bottom
border-top $ui-border
border-bottom $ui-border
overflow auto
top $topBar-height - 1
.item
position relative
height 80px
border-bottom $ui-border
padding 0 5px
user-select none
cursor pointer
transition background-color 0.15s
&:hover
background-color alpha($ui-active-color, 10%)
.item--active
@extend .item
.item-border
border-color $ui-active-color
.item-border
absolute top bottom left right
border-style solid
border-width 2px
border-color transparent
transition 0.15s
.item-info
height 30px
clearfix()
font-size 12px
color $ui-inactive-text-color
line-height 30px
overflow-y hidden
.item-info-left
float left
overflow ellipsis
.item-info-left-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
.item-info-left-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.item-info-right
float right
.item-title
height 20px
line-height 20px
padding 0 5px 0 0
font-weight bold
overflow ellipsis
color $ui-text-color
.item-title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.item-title-empty
font-weight normal
color $ui-inactive-text-color
.item-tagList
height 30px
font-size 12px
line-height 30px
overflow ellipsis
.item-tagList-icon
vertical-align middle
color $ui-button-color
.item-tagList-item
margin 0 4px
padding 0 4px
height 20px
border-radius 3px
vertical-align middle
border-style solid
border-color $ui-borderColor
border-width 0 0 0 3px
background-color $ui-backgroundColor
.item-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -0,0 +1,299 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.selectNextNoteHandler = () => {
console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
this.selectPriorNote()
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
}
componentDidUpdate () {
let { location } = this.props
if (this.notes.length > 0 && location.query.key == null) {
let { router } = this.context
router.replace({
pathname: location.pathname,
query: {
key: this.notes[0].uniqueKey
}
})
return
}
// Auto scroll
if (_.isString(location.query.key)) {
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex > -1) {
let list = this.refs.root
let item = list.childNodes[targetIndex]
let 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
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
}
selectPriorNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex === 0) {
return
}
targetIndex--
if (targetIndex < 0) targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
}
})
}
selectNextNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex === this.notes.length - 1) {
return
}
targetIndex++
if (targetIndex < 0) targetIndex = 0
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
}
})
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
// if (e.keyCode === 65 && !e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('top-new-post')
// }
// if (e.keyCode === 65 && e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('nav-new-folder')
// }
// if (e.keyCode === 68) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-delete')
// }
// if (e.keyCode === 84) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-title')
// }
// if (e.keyCode === 69) {
// e.preventDefault()
// }
// if (e.keyCode === 83) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-save')
// }
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorNote()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextNote()
}
}
getNotes () {
let { storages, notes, params, location } = this.props
if (location.pathname.match(/\/home/)) {
return notes
}
if (location.pathname.match(/\/starred/)) {
return notes
.filter((note) => note.isStarred)
}
let storageKey = params.storageKey
let folderKey = params.folderKey
let storage = _.find(storages, {key: storageKey})
if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) {
return notes
.filter((note) => note.storage === storageKey)
}
return notes
.filter((note) => note.folder === folderKey)
}
handleNoteClick (uniqueKey) {
return (e) => {
let { router } = this.context
let { location } = this.props
router.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
}
}
render () {
let { location, storages, notes } = this.props
this.notes = notes = this.getNotes()
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
let noteList = notes
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
let tagElements = _.isArray(note.tags)
? note.tags.map((tag) => {
return (
<span styleName='item-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
let isActive = location.query.key === note.uniqueKey
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={note.uniqueKey}
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)}
>
<div styleName='item-border'/>
<div styleName='item-info'>
<div styleName='item-info-left'>
<span styleName='item-info-left-folder'
style={{borderColor: folder.color}}
>
{folder.name}
<span styleName='item-info-left-folder-surfix'>in {storage.name}</span>
</span>
</div>
<div styleName='item-info-right'>
{moment(note.updatedAt).fromNow()}
</div>
</div>
<div styleName='item-title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code'/>
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o'/>
}
{note.title.trim().length > 0
? note.title
: <span styleName='item-title-empty'>Empty</span>
}
</div>
<div styleName='item-tagList'>
<i styleName='item-tagList-icon'
className='fa fa-tags fa-fw'
/>
{tagElements.length > 0
? tagElements
: <span styleName='item-tagList-empty'>Not tagged yet</span>
}
</div>
</div>
)
})
return (
<div className='NoteList'
styleName='root'
ref='root'
tabIndex='0'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
style={this.props.style}
>
{noteList}
</div>
)
}
}
NoteList.contextTypes = {
router: PropTypes.shape([])
}
NoteList.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
width: PropTypes.number
})
}
export default CSSModules(NoteList, styles)

View File

@@ -0,0 +1,122 @@
.root
absolute top left
bottom $statusBar-height - 1
width $sideNav-width
border-right $ui-border
border-bottom $ui-border
background-color $ui-backgroundColor
user-select none
color $ui-text-color
.top
height $topBar-height
border-bottom $ui-border
.top-menu
navButtonColor()
height $topBar-height - 1
padding 0 10px
font-size 14px
width 100%
text-align left
.top-menu-label
margin-left 5px
.menu
margin-top 15px
.menu-button
navButtonColor()
height 44px
padding 0 10px
font-size 14px
width 100%
text-align left
.menu-button--active
@extend .menu-button
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.menu-button-label
margin-left 5px
.storageList
absolute left right
bottom 44px
top 178px
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
.root--folded
@extend .root
width 44px
.storageList-empty
white-space nowrap
transform rotate(90deg)
.top-menu
width 44px - 1
text-align center
&:hover .top-menu-label
width 100px
.top-menu-label
position fixed
display inline-block
height 34px
left 44px
width 0
margin-top -5px
margin-left 0
overflow hidden
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
pointer-events none
.menu-button, .menu-button--active
width 44px - 1
text-align center
&:hover .menu-button-label
width 100px
// TODO: extract tooltip style to a mixin
.menu-button-label
position fixed
display inline-block
height 34px
left 44px
width 0
padding-left 0
margin-top -9px
margin-left 0
overflow ellipsis
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
pointer-events none

View File

@@ -0,0 +1,95 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
class StorageItem extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleHeaderInfoClick (e) {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key)
}
handleFolderButtonClick (folderKey) {
return (e) => {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
}
}
render () {
let { storage, location } = this.props
let folderList = storage.folders.map((folder) => {
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))
return <button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
key={folder.key}
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
>
<span styleName='folderList-item-name'
style={{borderColor: folder.color}}
>
{folder.name}
</span>
</button>
})
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
return (
<div styleName='root'
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}>
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
}
/>
</button>
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
{storage.name}
</span>
<span styleName='header-info-path'>
({storage.path})
</span>
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
{folderList}
</div>
}
</div>
)
}
}
StorageItem.propTypes = {
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,89 @@
.root
width 100%
user-select none
.header
position relative
height 30px
width 100%
&:hover
background-color $ui-button--hover-backgroundColor
&:active
.header-toggleButton
color white
.header--active
@extend .header
.header-info
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-toggleButton
color white
&:active
color white
.header-toggleButton
position absolute
left 0
width 25px
height 30px
padding 0
border none
color $ui-inactive-text-color
background-color transparent
&:hover
color $ui-text-color
&:active
color $ui-active-color
.header-info
display block
width 100%
height 30px
padding-left 25px
padding-right 10px
line-height 30px
cursor pointer
font-size 14px
border none
overflow ellipsis
text-align left
background-color transparent
color $ui-inactive-text-color
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-info-path
font-size 10px
margin 0 5px
.folderList-item
display block
width 100%
height 3 0px
background-color transparent
color $ui-inactive-text-color
padding 0
margin 2px 0
text-align left
border none
font-size 14px
&:hover
background-color $ui-button--hover-backgroundColor
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
&:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item-name
display block
padding 0 10px
height 30px
line-height 30px
border-width 0 0 0 4px
border-style solid
border-color transparent

View File

@@ -0,0 +1,116 @@
import React, { PropTypes } 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'
const electron = require('electron')
const { remote } = electron
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
handleMenuButtonClick (e) {
openModal(PreferencesModal)
}
handleHomeButtonClick (e) {
let { router } = this.context
router.push('/home')
}
handleStarredButtonClick (e) {
let { router } = this.context
router.push('/starred')
}
handleToggleButtonClick (e) {
let { dispatch, config } = this.props
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
dispatch({
type: 'SET_IS_SIDENAV_FOLDED',
isFolded: !config.isSideNavFolded
})
}
render () {
let { storages, location, config } = this.props
let isFolded = config.isSideNavFolded
let isHomeActive = location.pathname.match(/^\/home$/)
let isStarredActive = location.pathname.match(/^\/starred$/)
let storageList = storages.map((storage) => {
return <StorageItem
key={storage.key}
storage={storage}
location={location}
/>
})
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
tabIndex='1'
>
<div styleName='top'>
<button styleName='top-menu'
onClick={(e) => this.handleMenuButtonClick(e)}
>
<i className='fa fa-navicon fa-fw'/>
<span styleName='top-menu-label'>Menu</span>
</button>
</div>
<div styleName='menu'>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleHomeButtonClick(e)}
>
<i className='fa fa-home fa-fw'/>
<span styleName='menu-button-label'>Home</span>
</button>
<button styleName={isStarredActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleStarredButtonClick(e)}
>
<i className='fa fa-star fa-fw'/>
<span styleName='menu-button-label'>Starred</span>
</button>
</div>
<div styleName='storageList'>
{storageList.length > 0 ? storageList : (
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>
{false &&
<button styleName='navToggle'
onClick={(e) => this.handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right'/>
: <i className='fa fa-angle-double-left'/>
}
</button>
}
</div>
)
}
}
SideNav.contextTypes = {
router: PropTypes.shape({})
}
SideNav.propTypes = {
dispatch: PropTypes.func,
storages: PropTypes.array,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
}),
location: PropTypes.shape({
pathname: PropTypes.string
})
}
export default CSSModules(SideNav, styles)

View File

@@ -0,0 +1,38 @@
.root
absolute bottom left right
height $statusBar-height - 1
background-color $ui-backgroundColor
.pathname
absolute left
height 24px
overflow ellipsis
right 185px
line-height 24px
font-size 12px
padding 0 15px
color $ui-inactive-text-color
.zoom
navButtonColor()
absolute right
height 24px
width 60px
border-width 0 1px
border-style solid
border-color $ui-borderColor
.update
navButtonColor()
position absolute
right 60px
height 24px
width 125px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
.update-icon
color $brand-color

View File

@@ -0,0 +1,94 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
const electron = require('electron')
const ipc = electron.ipcRenderer
const { remote } = electron
const { Menu, MenuItem, dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3]
class StatusBar extends React.Component {
constructor (props) {
super(props)
this.state = {
updateAvailable: false
}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
})
if (index === 0) {
ipc.send('update-app', 'Deal with it.')
}
}
handleZoomButtonClick (e) {
let menu = new Menu()
zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({
label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom)
}))
})
menu.popup(remote.getCurrentWindow())
}
handleZoomMenuItemClick (zoomFactor) {
let { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
type: 'SET_ZOOM',
zoom: zoomFactor
})
}
render () {
let { config, location } = this.props
return (
<div className='StatusBar'
styleName='root'
>
<div styleName='pathname'>{location.pathname + location.search}</div>
{this.state.updateAvailable
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download'/> Update is available!
</button>
: null
}
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<i className='fa fa-search-plus'/>&nbsp;
{Math.floor(config.zoom * 100)}%
</button>
</div>
)
}
}
StatusBar.propTypes = {
config: PropTypes.shape({
zoom: PropTypes.number
})
}
export default CSSModules(StatusBar, styles)

View File

@@ -0,0 +1,106 @@
.root
position relative
width 100%
background-color $ui-backgroundColor
height $topBar-height - 1
$control-height = 34px
.control
position absolute
top 8px
left 8px
right 8px
height $control-height
border $ui-border
border-radius 20px
overflow hidden
.control-search
absolute top left bottom
right 40px
background-color white
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-inactive-text-color
.control-search-input
display block
absolute top bottom right
left 30px
input
width 100%
height 100%
outline none
border none
.control-search-optionList
position fixed
z-index 200
width 275px
height 175px
overflow-y auto
background-color $modal-background
border-radius 2px
box-shadow 2px 2px 10px gray
.control-search-optionList-item
height 50px
border-bottom $ui-border
transition background-color 0.15s
padding 5px
cursor pointer
overflow ellipsis
&:hover
background-color alpha($ui-active-color, 10%)
.control-search-optionList-item-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
overflow ellipsis
font-size 12px
height 16px
margin-bottom 4px
.control-search-optionList-item-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.control-search-optionList-item-type
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.control-search-optionList-empty
height 150px
color $ui-inactive-text-color
line-height 150px
text-align center
.control-newPostButton
display block
absolute top right bottom
width 40px
height $control-height - 2
navButtonColor()
border-left $ui-border
font-size 14px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .left-control-newPostButton-tooltip
display block
.control-newPostButton-tooltip
position fixed
line-height 1.4
background-color $ui-tooltip-backgroundColor
color $ui-tooltip-text-color
font-size 10px
margin-left -25px
margin-top 5px
padding 5px
z-index 1
border-radius 5px
display none
pointer-events none

View File

@@ -0,0 +1,216 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TopBar.styl'
import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const OSX = window.process.platform === 'darwin'
class TopBar extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
searchOptions: [],
searchPopupOpen: false
}
this.newNoteHandler = () => {
this.handleNewPostButtonClick()
}
}
componentDidMount () {
ee.on('top:new-note', this.newNoteHandler)
}
componentWillUnmount () {
ee.off('top:new-note', this.newNoteHandler)
}
handleNewPostButtonClick (e) {
let { storages, params, dispatch, location } = this.props
let storage = _.find(storages, {key: params.storageKey})
if (storage == null) storage = storages[0]
if (storage == null) throw new Error('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0]
if (folder == null) throw new Error('No folder to craete a note')
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location
})
}
handleSearchChange (e) {
this.setState({
search: this.refs.searchInput.value
})
}
getOptions () {
let { notes } = this.props
let { search } = this.state
if (search.trim().length === 0) return []
let searchBlocks = search.split(' ')
searchBlocks.forEach((block) => {
if (block.match(/^#.+/)) {
let tag = block.match(/#(.+)/)[1]
notes = notes
.filter((note) => {
if (!_.isArray(note.tags)) return false
return note.tags.some((_tag) => {
return _tag === tag
})
})
} else {
notes = notes.filter((note) => {
if (note.type === 'SNIPPET_NOTE') {
return note.description.match(block)
} else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(block)
}
return false
})
}
})
return notes
}
handleOptionClick (uniqueKey) {
return (e) => {
this.setState({
searchPopupOpen: false
}, () => {
let { location } = this.props
hashHistory.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
})
}
}
handleSearchFocus (e) {
this.setState({
searchPopupOpen: true
})
}
handleSearchBlur (e) {
e.stopPropagation()
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.search) {
isStillFocused = true
break
}
el = el.parentNode
}
if (!isStillFocused) {
this.setState({
searchPopupOpen: false
})
}
}
render () {
let { config, style, storages } = this.props
let searchOptionList = this.getOptions()
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
return <div styleName='control-search-optionList-item'
key={note.uniqueKey}
onClick={(e) => this.handleOptionClick(note.uniqueKey)(e)}
>
<div styleName='control-search-optionList-item-folder'
style={{borderColor: folder.color}}>
{folder.name}
<span styleName='control-search-optionList-item-folder-surfix'>in {storage.name}</span>
</div>
{note.type === 'SNIPPET_NOTE'
? <i styleName='control-search-optionList-item-type' className='fa fa-code'/>
: <i styleName='control-search-optionList-item-type' className='fa fa-file-text-o'/>
}&nbsp;
{note.title}
</div>
})
return (
<div className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<div styleName='control-search'>
<i styleName='control-search-icon' className='fa fa-search fa-fw'/>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
tabIndex='-1'
ref='search'
>
<input
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
placeholder='Search'
type='text'
/>
{this.state.searchPopupOpen &&
<div styleName='control-search-optionList'>
{searchOptionList.length > 0
? searchOptionList
: <div styleName='control-search-optionList-empty'>Empty List</div>
}
</div>
}
</div>
{this.state.search > 0 &&
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
<button styleName='control-newPostButton'
onClick={(e) => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus'/>
<span styleName='control-newPostButton-tooltip'>
New Note {OSX ? '⌘' : '^'} + n
</span>
</button>
</div>
</div>
)
}
}
TopBar.contextTypes = {
router: PropTypes.shape({
push: PropTypes.func
})
}
TopBar.propTypes = {
dispatch: PropTypes.func,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
})
}
export default CSSModules(TopBar, styles)

View File

@@ -1,178 +0,0 @@
// Action types
export const USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Article status
export const NEW = 'NEW'
export function updateUser (input) {
return {
type: USER_UPDATE,
data: input
}
}
// DB
export function cacheArticle (key, article) {
return {
type: ARTICLE_CACHE,
data: { key, article }
}
}
export function uncacheArticle (key) {
return {
type: ARTICLE_UNCACHE,
data: { key }
}
}
export function uncacheAllArticles () {
return {
type: ARTICLE_UNCACHE_ALL
}
}
export function saveArticle (key, article, forceSwitch) {
return {
type: ARTICLE_SAVE,
data: { key, article, forceSwitch }
}
}
export function saveAllArticles () {
return {
type: ARTICLE_SAVE_ALL
}
}
export function updateArticle (article) {
return {
type: ARTICLE_UPDATE,
data: { article }
}
}
export function destroyArticle (key) {
return {
type: ARTICLE_DESTROY,
data: { key }
}
}
export function createFolder (folder) {
return {
type: FOLDER_CREATE,
data: { folder }
}
}
export function updateFolder (folder) {
return {
type: FOLDER_UPDATE,
data: { folder }
}
}
export function destroyFolder (key) {
return {
type: FOLDER_DESTROY,
data: { key }
}
}
export function replaceFolder (a, b) {
return {
type: FOLDER_REPLACE,
data: {
a,
b
}
}
}
export function switchFolder (folderName) {
return {
type: SWITCH_FOLDER,
data: folderName
}
}
export function switchArticle (articleKey) {
return {
type: SWITCH_ARTICLE,
data: {
key: articleKey
}
}
}
export function setSearchFilter (search) {
return {
type: SET_SEARCH_FILTER,
data: search
}
}
export function setTagFilter (tag) {
return {
type: SET_TAG_FILTER,
data: tag
}
}
export function clearSearch () {
return {
type: CLEAR_SEARCH
}
}
export function toggleTutorial () {
return {
type: TOGGLE_TUTORIAL
}
}
export default {
updateUser,
updateArticle,
destroyArticle,
cacheArticle,
uncacheArticle,
uncacheAllArticles,
saveArticle,
saveAllArticles,
createFolder,
updateFolder,
destroyFolder,
replaceFolder,
switchFolder,
switchArticle,
setSearchFilter,
setTagFilter,
clearSearch,
toggleTutorial
}

85
browser/main/global.styl Normal file
View File

@@ -0,0 +1,85 @@
global-reset()
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
html, body
width 100%
height 100%
overflow hidden
body
font-family DEFAULT_FONTS
color textColor
font-size fontSize
font-weight 400
button, input, select, textarea
font-family DEFAULT_FONTS
div, span, a, button, input, textarea
box-sizing border-box
a
color $brand-color
&:hover
color lighten($brand-color, 5%)
&:visited
color $brand-color
hr
border-top none
border-bottom solid 1px $border-color
margin 15px 0
button
font-weight 400
cursor pointer
font-size 12px
&:focus, &.focus
outline none
&:disabled
cursor not-allowed
input
&:disabled
cursor not-allowed
.noSelect
noSelect()
.text-center
text-align center
.form-group
margin-bottom 15px
&>label
display block
margin-bottom 5px
textarea.block-input
resize vertical
height 125px
border-radius 5px
padding 5px 10px
#content
fullsize()
modalZIndex= 1000
modalBackColor = transparentify(white, 65%)
.ace_focus
outline-color rgb(59, 153, 252)
outline-offset 0px
outline-style auto
outline-width 5px
.ModalBase
fixed top left bottom right
z-index modalZIndex
display flex
align-items center
justify-content center
&.hide
display none
.modalBack
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1

View File

@@ -1,34 +1,18 @@
import { Provider } from 'react-redux'
import MainPage from './MainPage'
import Main from './Main'
import store from './store'
import React from 'react'
import ReactDOM from 'react-dom'
require('../styles/main/index.styl')
import { openModal } from 'browser/lib/modal'
import OSSAnnounceModal from './modal/OSSAnnounceModal'
require('!!style!css!stylus?sourceMap!./global.styl')
import activityRecord from 'browser/lib/activityRecord'
import fetchConfig from '../lib/fetchConfig'
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
const electron = require('electron')
const ipc = electron.ipcRenderer
const path = require('path')
const remote = electron.remote
let config = fetchConfig()
applyConfig(config)
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
applyConfig(config)
})
function applyConfig (config) {
let body = document.body
body.setAttribute('data-theme', config['theme-ui'])
let hljsCss = document.getElementById('hljs-css')
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
}
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
@@ -74,21 +58,26 @@ ipc.on('open-finder', function () {
})
let el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
ReactDOM.render((
<div>
<Provider store={store}>
<MainPage/>
</Provider>
</div>
<Provider store={store}>
<Router history={history}>
<Route path='/' component={Main}>
<IndexRedirect to='/home'/>
<Route path='home'/>
<Route path='starred'/>
<Route path='storages'>
<IndexRedirect to='/home'/>
<Route path=':storageKey'>
<IndexRoute/>
<Route path='folders/:folderKey'/>
</Route>
</Route>
</Route>
</Router>
</Provider>
), el, function () {
let loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
let status = JSON.parse(localStorage.getItem('status'))
if (status == null) status = {}
if (!status.ossAnnounceWatched) {
openModal(OSSAnnounceModal)
status.ossAnnounceWatched = true
localStorage.setItem('status', JSON.stringify(status))
}
})

View File

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

View File

@@ -0,0 +1,84 @@
import _ from 'lodash'
const OSX = global.process.platform === 'darwin'
const electron = require('electron')
const { ipcRenderer } = electron
const defaultConfig = {
zoom: 1,
isSideNavFolded: false,
listWidth: 250,
hotkey: {
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
},
ui: {
theme: 'default',
disableDirectWrite: false
},
editor: {
theme: 'xcode',
fontSize: '14',
fontFamily: 'Monaco, Consolas',
indentType: 'space',
indentSize: '4',
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
},
preview: {
fontSize: '14',
fontFamily: 'Lato',
codeBlockTheme: 'xcode',
lineNumber: true
}
}
function validate (config) {
if (!_.isObject(config)) return false
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
if (!_.isBoolean(config.isSideNavFolded)) return false
if (!_.isNumber(config.listWidth) || config.listWidth <= 0) return false
return true
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
function get () {
let config = window.localStorage.getItem('config')
try {
config = Object.assign({}, defaultConfig, JSON.parse(config))
if (!validate(config)) throw new Error('INVALID CONFIG')
} catch (err) {
console.warn('Boostnote resets the malformed configuration.')
config = defaultConfig
_save(config)
}
return config
}
function set (updates) {
let currentConfig = get()
let newConfig = Object.assign({}, defaultConfig, currentConfig, updates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: false
})
}
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: true
})
export default {
get,
set,
validate
}

View File

@@ -0,0 +1,30 @@
import ConfigManager from './ConfigManager'
const electron = require('electron')
const { remote } = electron
_init()
function _init () {
setZoom(getZoom(), true)
}
function _saveZoom (zoomFactor) {
ConfigManager.set({zoom: zoomFactor})
}
function setZoom (zoomFactor, noSave = false) {
if (!noSave) _saveZoom(zoomFactor)
remote.getCurrentWebContents().setZoomFactor(zoomFactor)
}
function getZoom () {
let config = ConfigManager.get()
return config.zoom
}
export default {
setZoom,
getZoom
}

565
browser/main/lib/dataApi.js Normal file
View File

@@ -0,0 +1,565 @@
const keygen = require('browser/lib/keygen')
const CSON = require('season')
const path = require('path')
const _ = require('lodash')
const sander = require('sander')
const consts = require('browser/lib/consts')
let storages = []
let notes = []
let queuedTasks = []
function queueSaveFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Failed to queue: Storage doesn\'t exist.')
let targetTasks = queuedTasks.filter((task) => task.storage === storageKey && task.folder === folderKey)
targetTasks.forEach((task) => {
clearTimeout(task.timer)
})
queuedTasks = queuedTasks.filter((task) => task.storage !== storageKey || task.folder !== folderKey)
let newTimer = setTimeout(() => {
let folderNotes = notes.filter((note) => note.storage === storageKey && note.folder === folderKey)
sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
}, 1500)
queuedTasks.push({
storage: storageKey,
folder: folderKey,
timer: newTimer
})
}
class Storage {
constructor (cache) {
this.key = cache.key
this.cache = cache
}
loadJSONData () {
return new Promise((resolve, reject) => {
try {
let data = CSON.readFileSync(path.join(this.cache.path, 'boostnote.json'))
this.data = data
resolve(this)
} catch (err) {
reject(err)
}
})
}
toJSON () {
return Object.assign({}, this.cache, this.data)
}
initStorage () {
return this.loadJSONData()
.catch((err) => {
console.error(err.code)
if (err.code === 'ENOENT') {
let initialStorage = {
folders: []
}
return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage))
} else throw err
})
.then(() => this.loadJSONData())
}
saveData () {
return sander
.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(this.data))
.then(() => this)
}
saveCache () {
_saveCaches()
}
static forge (cache) {
let instance = new this(cache)
return instance
}
}
class Note {
constructor (note) {
this.storage = note.storage
this.folder = note.folder
this.key = note.key
this.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
this.data = note
}
toJSON () {
return Object.assign({}, this.data, {
uniqueKey: this.uniqueKey
})
}
save () {
let storage = _.find(storages, {key: this.storage})
if (storage == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
let folder = _.find(storage.data.folders, {key: this.folder})
if (folder == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
// FS MUST BE MANIPULATED BY ASYNC METHOD
queueSaveFolder(storage.key, folder.key)
return Promise.resolve(this)
}
static forge (note) {
let instance = new this(note)
return Promise.resolve(instance)
}
}
function init () {
let fetchStorages = function () {
let caches
try {
caches = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(caches)) throw new Error('Cached data is not valid.')
} catch (e) {
console.error(e)
caches = []
localStorage.setItem('storages', JSON.stringify(caches))
}
return caches.map((cache) => {
return Storage
.forge(cache)
.loadJSONData()
.catch((err) => {
console.error(err)
console.error('Failed to load a storage JSON File: %s', cache)
return null
})
})
}
let fetchNotes = function (storages) {
let notes = []
let modifiedStorages = []
storages
.forEach((storage) => {
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
if (modifiedStorages.some((modified) => modified.key === storage.key)) modifiedStorages.push(storage)
return
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
notes.push(Note.forge(note))
})
})
}, [])
return Promise
.all(modifiedStorages.map((storage) => storage.saveData()))
.then(() => Promise.all(notes))
}
return Promise.all(fetchStorages())
.then((_storages) => {
storages = _storages.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
_saveCaches()
return storages
})
.then(fetchNotes)
.then((_notes) => {
notes = _notes
return {
storages: storages.map((storage) => storage.toJSON()),
notes: notes.map((note) => note.toJSON())
}
})
}
function _saveCaches () {
localStorage.setItem('storages', JSON.stringify(storages.map((storage) => storage.cache)))
}
function addStorage (input) {
if (!_.isString(input.path) || !input.path.match(/^\//)) {
return Promise.reject(new Error('Path must be absolute.'))
}
let key = keygen()
while (storages.some((storage) => storage.key === key)) {
key = keygen()
}
return Storage
.forge({
name: input.name,
key: key,
type: input.type,
path: input.path
})
.initStorage()
.then((storage) => {
let _notes = []
let isFolderRemoved = false
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
isFolderRemoved = true
return true
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
_notes.push(Note.forge(note))
})
})
return Promise.all(_notes)
.then((_notes) => {
notes = notes.concat(_notes)
let data = {
storage: storage,
notes: _notes
}
return isFolderRemoved
? storage.saveData().then(() => data)
: data
})
})
.then((data) => {
storages = storages.filter((storage) => storage.key !== data.storage.key)
storages.push(data.storage)
_saveCaches()
if (data.storage.data.folders.length < 1) {
return createFolder(data.storage.key, {
name: 'Default',
color: consts.FOLDER_COLORS[0]
}).then(() => data)
}
return data
})
.then((data) => {
return {
storage: data.storage.toJSON(),
notes: data.notes.map((note) => note.toJSON())
}
})
}
function removeStorage (key) {
storages = storages.filter((storage) => storage.key !== key)
_saveCaches()
notes = notes.filter((note) => note.storage !== key)
return Promise.resolve(true)
}
function renameStorage (key, name) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.cache.name = name
storage.saveCache()
return Promise.resolve(storage.toJSON())
}
function migrateFromV5 (key, data) {
let oldFolders = data.folders
let oldArticles = data.articles
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let migrateFolders = oldFolders.map((oldFolder) => {
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.data.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
let folderNotes = []
articles.forEach((article) => {
let noteKey = keygen()
while (notes.some((note) => note.storage === key && note.folder === folderKey && note.key === noteKey)) {
key = keygen()
}
if (article.mode === 'markdown') {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
})
notes.push(newNote)
folderNotes.push(newNote)
} else {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
}]
})
notes.push(newNote)
folderNotes.push(newNote)
}
})
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
})
return Promise.all(migrateFolders)
.then(() => storage.saveData())
.then(() => {
return {
storage: storage.toJSON(),
notes: notes.filter((note) => note.storage === key)
.map((note) => note.toJSON())
}
})
}
function createFolder (key, input) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: input.name,
color: input.color
}
const defaultData = {notes: []}
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify(defaultData))
.then(() => {
storage.data.folders.push(newFolder)
return storage
.saveData()
.then((storage) => storage.toJSON())
})
}
function updateFolder (storageKey, folderKey, input) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: folderKey})
folder.color = input.color
folder.name = input.name
return storage
.saveData()
.then((storage) => storage.toJSON())
}
function removeFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.data.folders = storage.data.folders.filter((folder) => folder.key !== folderKey)
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey)
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.rimraf(path.join(storage.cache.path, folderKey))
.catch((err) => {
if (err.code === 'ENOENT') return true
else throw err
})
.then(() => storage.saveData())
.then((storage) => storage.toJSON())
}
function createMarkdownNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
content: ''
}, input, {
type: 'MARKDOWN_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function createSnippetNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}, input, {
type: 'SNIPPET_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function updateNote (storageKey, folderKey, noteKey, input) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
switch (note.data.type) {
case 'MARKDOWN_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.content = input.content
note.data.updatedAt = input.updatedAt
break
case 'SNIPPET_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.description = input.description
note.data.snippets = input.snippets
note.data.updatedAt = input.updatedAt
}
return note.save()
.then(() => note.toJSON())
}
function removeNote (storageKey, folderKey, noteKey) {
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey || note.key !== noteKey)
queueSaveFolder(storageKey, folderKey)
return Promise.resolve(null)
}
function moveNote (storageKey, folderKey, noteKey, newStorageKey, newFolderKey) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
if (note == null) throw new Error('Note doesn\'t exist.')
let storage = _.find(storages, {key: newStorageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: newFolderKey})
if (folder == null) throw new Error('Folder doesn\'t exist.')
note.storage = storage.key
note.data.storage = storage.key
note.folder = folder.key
note.data.folder = folder.key
let key = note.key
while (notes.some((note) => note.storage === storage.key && note.folder === folder.key && note.key === key)) {
key = keygen()
}
note.key = key
note.data.key = key
note.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
console.log(note.uniqueKey)
queueSaveFolder(storageKey, folderKey)
return note.save()
.then(() => note.toJSON())
}
export default {
init,
addStorage,
removeStorage,
renameStorage,
createFolder,
updateFolder,
removeFolder,
createMarkdownNote,
createSnippetNote,
updateNote,
removeNote,
moveNote,
migrateFromV5
}

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom'
const remote = require('electron').remote
import store from '../store'
class ModalBase extends React.Component {
constructor (props) {
@@ -15,17 +15,16 @@ class ModalBase extends React.Component {
close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
document.body.setAttribute('data-modal', 'close')
remote.getCurrentWebContents().send('list-focus')
}
render () {
return (
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
<div onClick={e => this.close(e)} className='modalBack'/>
<div onClick={(e) => this.close(e)} className='modalBack'/>
{this.state.component == null ? null : (
<this.state.component {...this.state.componentProps} close={this.close}/>
<Provider store={store}>
<this.state.component {...this.state.componentProps} close={this.close}/>
</Provider>
)}
</div>
)

View File

@@ -1,107 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import linkState from 'browser/lib/linkState'
import { createFolder } from '../actions'
import store from '../store'
import FolderMark from 'browser/components/FolderMark'
export default class CreateNewFolder extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
color: Math.round(Math.random() * 7),
alert: null
}
}
componentDidMount () {
ReactDOM.findDOMNode(this.refs.folderName).focus()
}
handleCloseButton (e) {
this.props.close()
}
handleConfirmButton (e) {
this.setState({alert: null}, () => {
let { close } = this.props
let { name, color } = this.state
let input = {
name,
color
}
try {
store.dispatch(createFolder(input))
} catch (e) {
this.setState({alert: {
type: 'error',
message: e.message
}})
return
}
close()
})
}
handleColorClick (colorIndex) {
return e => {
this.setState({
color: colorIndex
})
}
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.handleConfirmButton()
}
}
render () {
let alert = this.state.alert
let alertElement = alert != null ? (
<p className={`alert ${alert.type}`}>
{alert.message}
</p>
) : null
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorElements = colorIndexes.map(index => {
let className = 'option'
if (index === this.state.color) className += ' active'
return (
<span className={className} key={index} onClick={e => this.handleColorClick(index)(e)}>
<FolderMark color={index}/>
</span>
)
})
return (
<div className='CreateNewFolder modal'>
<button onClick={e => this.handleCloseButton(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
<div className='title'>Create new folder</div>
<input ref='folderName' onKeyDown={e => this.handleKeyDown(e)} className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/>
<div className='colorSelect'>
{colorElements}
</div>
{alertElement}
<button onClick={e => this.handleConfirmButton(e)} className='confirmBtn'>Create</button>
</div>
)
}
}
CreateNewFolder.propTypes = {
close: PropTypes.func
}
CreateNewFolder.prototype.linkState = linkState

View File

@@ -1,29 +0,0 @@
import React, { PropTypes } from 'react'
import ExternalLink from 'browser/components/ExternalLink'
export default class OSSAnnounceModal extends React.Component {
handleCloseBtnClick (e) {
this.props.close()
}
render () {
return (
<div className='OSSAnnounceModal modal'>
<div className='OSSAnnounceModal-title'>Boostnote has been Open-sourced</div>
<ExternalLink className='OSSAnnounceModal-link' href='https://github.com/BoostIO/Boostnote'>
https://github.com/BoostIO/Boostnote
</ExternalLink>
<button
className='OSSAnnounceModal-closeBtn'
onClick={(e) => this.handleCloseBtnClick(e)}
>Close</button>
</div>
)
}
}
OSSAnnounceModal.propTypes = {
close: PropTypes.func
}

View File

@@ -1,276 +0,0 @@
import React, { PropTypes } from 'react'
import linkState from 'browser/lib/linkState'
import { updateUser } from '../../actions'
import fetchConfig from 'browser/lib/fetchConfig'
import hljsTheme from 'browser/lib/hljsThemes'
const electron = require('electron')
const ipc = electron.ipcRenderer
const remote = electron.remote
const ace = window.ace
const OSX = global.process.platform === 'darwin'
export default class AppSettingTab extends React.Component {
constructor (props) {
super(props)
let keymap = Object.assign({}, remote.getGlobal('keymap'))
let config = Object.assign({}, fetchConfig())
let userName = props.user != null ? props.user.name : null
this.state = {
user: {
name: userName,
alert: null
},
userAlert: null,
keymap: keymap,
keymapAlert: null,
config: config,
configAlert: null
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully done!'
}})
}
this.handleSettingError = err => {
this.setState({keymapAlert: {
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)
}
submitHotKey () {
ipc.send('hotkeyUpdated', this.state.keymap)
}
submitConfig () {
ipc.send('configUpdated', this.state.config)
}
handleSaveButtonClick (e) {
this.submitHotKey()
}
handleConfigSaveButtonClick (e) {
this.submitConfig()
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
handleConfigKeyDown (e) {
if (e.keyCode === 13) {
this.submitConfig()
}
}
handleLineNumberingClick (e) {
let config = this.state.config
config['preview-line-number'] = e.target.checked
this.setState({
config
})
}
handleDisableDirectWriteClick (e) {
let config = this.state.config
config['disable-direct-write'] = e.target.checked
this.setState({
config
})
}
handleNameSaveButtonClick (e) {
let { dispatch } = this.props
dispatch(updateUser({name: this.state.user.name}))
this.setState({
userAlert: {
type: 'success',
message: 'Successfully done!'
}
})
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
? (
<p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
) : null
let userAlert = this.state.userAlert
let userAlertElement = userAlert != null
? (
<p className={`alert ${userAlert.type}`}>
{userAlert.message}
</p>
) : null
let aceThemeList = ace.require("ace/ext/themelist")
let hljsThemeList = hljsTheme()
return (
<div className='AppSettingTab content'>
<div className='section'>
<div className='sectionTitle'>User&apos;s info</div>
<div className='sectionInput'>
<label>User name</label>
<input valueLink={this.linkState('user.name')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleNameSaveButtonClick(e)}>Save</button>
{userAlertElement}
</div>
</div>
<div className='section'>
<div className='sectionTitle'>Editor</div>
<div className='sectionInput'>
<label>Editor Font Size</label>
<input valueLink={this.linkState('config.editor-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionInput'>
<label>Editor Font Family</label>
<input valueLink={this.linkState('config.editor-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionMultiSelect'>
<label>Editor Indent Style</label>
<div className='sectionMultiSelect-input'>
type
<select valueLink={this.linkState('config.editor-indent-type')}>
<option value='space'>Space</option>
<option value='tab'>Tab</option>
</select>
size
<select valueLink={this.linkState('config.editor-indent-size')}>
<option value='2'>2</option>
<option value='4'>4</option>
<option value='8'>8</option>
</select>
</div>
</div>
<div className='sectionTitle'>Preview</div>
<div className='sectionInput'>
<label>Preview Font Size</label>
<input valueLink={this.linkState('config.preview-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionInput'>
<label>Preview Font Family</label>
<input valueLink={this.linkState('config.preview-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionSelect'>
<label>Switching Preview</label>
<select valueLink={this.linkState('config.switch-preview')}>
<option value='blur'>When Editor Blurred</option>
<option value='rightclick'>When Right Clicking</option>
</select>
</div>
<div className='sectionCheck'>
<label><input onChange={e => this.handleLineNumberingClick(e)} checked={this.state.config['preview-line-number']} type='checkbox'/>Code block line numbering</label>
</div>
{
global.process.platform === 'win32'
? (
<div className='sectionCheck'>
<label><input onChange={e => this.handleDisableDirectWriteClick(e)} checked={this.state.config['disable-direct-write']} disabled={OSX} type='checkbox'/>Disable Direct Write<span className='sectionCheck-warn'>It will be applied after restarting</span></label>
</div>
)
: null
}
<div className='sectionTitle'>Theme</div>
<div className='sectionSelect'>
<label>UI Theme</label>
<select valueLink={this.linkState('config.theme-ui')}>
<option value='light'>Light</option>
<option value='dark'>Dark</option>
</select>
</div>
<div className='sectionSelect'>
<label>Code block Theme</label>
<select valueLink={this.linkState('config.theme-code')}>
{
hljsThemeList.map(function(v, i){
return (<option value={v.name} key={v.name}>{v.caption}</option>)
})
}
</select>
</div>
<div className='sectionSelect'>
<label>Editor Theme</label>
<select valueLink={this.linkState('config.theme-syntax')}>
{
aceThemeList.themes.map(function(v, i){
return (<option value={v.name} key={v.name}>{v.caption}</option>)
})
}
</select>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleConfigSaveButtonClick(e)}>Save</button>
</div>
</div>
<div className='section'>
<div className='sectionTitle'>Hotkey</div>
<div className='sectionInput'>
<label>Toggle Main</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleMain')} type='text'/>
</div>
<div className='sectionInput'>
<label>Toggle Finder(popup)</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{keymapAlertElement}
</div>
<div className='description'>
<ul>
<li><code>0</code> to <code>9</code></li>
<li><code>A</code> to <code>Z</code></li>
<li><code>F1</code> to <code>F24</code></li>
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
<li><code>Plus</code></li>
<li><code>Space</code></li>
<li><code>Backspace</code></li>
<li><code>Delete</code></li>
<li><code>Insert</code></li>
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
<li><code>Home</code> and <code>End</code></li>
<li><code>PageUp</code> and <code>PageDown</code></li>
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
</ul>
</div>
</div>
</div>
)
}
}
AppSettingTab.prototype.linkState = linkState
AppSettingTab.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
dispatch: PropTypes.func
}

View File

@@ -1,24 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import linkState from 'browser/lib/linkState'
import ExternalLink from 'browser/components/ExternalLink'
export default class ContactTab extends React.Component {
componentDidMount () {
let titleInput = ReactDOM.findDOMNode(this.refs.title)
if (titleInput != null) titleInput.focus()
}
render () {
return (
<div className='ContactTab content'>
<div className='title'>Contact</div>
<p>
- Issues: <ExternalLink href='https://github.com/BoostIO/Boostnote/issues'>https://github.com/BoostIO/Boostnote/issues</ExternalLink>
</p>
</div>
)
}
}
ContactTab.prototype.linkState = linkState

View File

@@ -1,187 +0,0 @@
import React, { PropTypes } from 'react'
import linkState from 'browser/lib/linkState'
import FolderMark from 'browser/components/FolderMark'
import store from '../../store'
import { updateFolder, destroyFolder, replaceFolder } from '../../actions'
const IDLE = 'IDLE'
const EDIT = 'EDIT'
const DELETE = 'DELETE'
export default class FolderRow extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE
}
}
handleUpClick (e) {
let { index } = this.props
if (index > 0) {
store.dispatch(replaceFolder(index, index - 1))
}
}
handleDownClick (e) {
let { index, count } = this.props
if (index < count - 1) {
store.dispatch(replaceFolder(index, index + 1))
}
}
handleCancelButtonClick (e) {
this.setState({
mode: IDLE
})
}
handleEditButtonClick (e) {
this.setState({
mode: EDIT,
name: this.props.folder.name,
color: this.props.folder.color,
isColorEditing: false
})
}
handleDeleteButtonClick (e) {
this.setState({mode: DELETE})
}
handleNameInputKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleColorSelectClick (e) {
this.setState({
isColorEditing: true
})
}
handleColorButtonClick (index) {
return (e) => {
this.setState({
color: index,
isColorEditing: false
})
}
}
handleSaveButtonClick (e) {
let { folder, setAlert } = this.props
setAlert(null, () => {
let input = {
name: this.state.name,
color: this.state.color
}
folder = Object.assign({}, folder, input)
try {
store.dispatch(updateFolder(folder))
this.setState({
mode: IDLE
})
} catch (e) {
console.error(e)
setAlert({
type: 'error',
message: e.message
})
}
})
}
handleDeleteConfirmButtonClick (e) {
let { folder } = this.props
store.dispatch(destroyFolder(folder.key))
}
render () {
let folder = this.props.folder
switch (this.state.mode) {
case EDIT:
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorOptions = colorIndexes.map(index => {
let className = this.state.color === index
? 'active'
: null
return (
<button onClick={(e) => this.handleColorButtonClick(index)(e)} className={className} key={index}>
<FolderMark color={index}/>
</button>
)
})
return (
<div className='FolderRow edit'>
<div className='folderColor'>
<button onClick={(e) => this.handleColorSelectClick(e)} className='select'>
<FolderMark color={this.state.color}/>
</button>
{this.state.isColorEditing
? (
<div className='options'>
<div className='label'>Color select</div>
{colorOptions}
</div>
)
: null
}
</div>
<div className='folderName'>
<input onKeyDown={(e) => this.handleNameInputKeyDown(e)} valueLink={this.linkState('name')} type='text'/>
</div>
<div className='folderControl'>
<button onClick={(e) => this.handleSaveButtonClick(e)} className='primary'>Save</button>
<button onClick={(e) => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</div>
)
case DELETE:
return (
<div className='FolderRow delete'>
<div className='folderDeleteLabel'>Are you sure to delete <strong>{folder.name}</strong> folder?</div>
<div className='folderControl'>
<button onClick={(e) => this.handleDeleteConfirmButtonClick(e)} className='primary'>Sure</button>
<button onClick={(e) => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</div>
)
case IDLE:
default:
return (
<div className='FolderRow'>
<div className='sortBtns'>
<button onClick={(e) => this.handleUpClick(e)}><i className='fa fa-sort-up fa-fw'/></button>
<button onClick={(e) => this.handleDownClick(e)}><i className='fa fa-sort-down fa-fw'/></button>
</div>
<div className='folderColor'><FolderMark color={folder.color}/></div>
<div className='folderName'>{folder.name}</div>
<div className='folderControl'>
<button onClick={(e) => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button>
<button onClick={(e) => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-close'/></button>
</div>
</div>
)
}
}
}
FolderRow.propTypes = {
folder: PropTypes.shape(),
index: PropTypes.number,
count: PropTypes.number,
setAlert: PropTypes.func
}
FolderRow.prototype.linkState = linkState

View File

@@ -1,97 +0,0 @@
import React, { PropTypes } from 'react'
import FolderRow from './FolderRow'
import linkState from 'browser/lib/linkState'
import { createFolder } from '../../actions'
export default class FolderSettingTab extends React.Component {
constructor (props) {
super(props)
this.state = {
name: ''
}
}
handleNewFolderNameKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleSaveButtonClick (e) {
this.setState({alert: null}, () => {
let { dispatch } = this.props
try {
dispatch(createFolder({
name: this.state.name
}))
} catch (e) {
this.setState({alert: {
type: 'error',
message: e.message
}})
return
}
this.setState({name: ''})
})
}
setAlert (alert, cb) {
this.setState({alert: alert}, cb)
}
render () {
let { folders } = this.props
let folderElements = folders.map((folder, index) => {
return (
<FolderRow
key={'folder-' + folder.key}
folder={folder}
index={index}
count={folders.length}
setAlert={(alert, cb) => this.setAlert(alert, cb)}
/>
)
})
let alert = this.state.alert
let alertElement = alert != null ? (
<p className={`alert ${alert.type}`}>
{alert.message}
</p>
) : null
return (
<div className='FolderSettingTab content'>
<div className='section'>
<div className='sectionTitle'>Manage folder</div>
<div className='folderTable'>
<div className='folderHeader'>
<div className='folderName'>Folder</div>
<div className='folderControl'>Edit/Delete</div>
</div>
{folderElements}
<div className='newFolder'>
<div className='folderName'>
<input onKeyDown={(e) => this.handleNewFolderNameKeyDown(e)} valueLink={this.linkState('name')} type='text' placeholder='New Folder'/>
</div>
<div className='folderControl'>
<button onClick={(e) => this.handleSaveButtonClick(e)} className='primary'>Add</button>
</div>
</div>
{alertElement}
</div>
</div>
</div>
)
}
}
FolderSettingTab.propTypes = {
folders: PropTypes.array,
dispatch: PropTypes.func
}
FolderSettingTab.prototype.linkState = linkState

View File

@@ -1,118 +0,0 @@
import React, { PropTypes } from 'react'
import { connect, Provider } from 'react-redux'
import linkState from 'browser/lib/linkState'
import store from '../store'
import AppSettingTab from './Preference/AppSettingTab'
import FolderSettingTab from './Preference/FolderSettingTab'
import ContactTab from './Preference/ContactTab'
import { closeModal } from 'browser/lib/modal'
const APP = 'APP'
const FOLDER = 'FOLDER'
const CONTACT = 'CONTACT'
class Preferences extends React.Component {
constructor (props) {
super(props)
this.state = {
currentTab: APP
}
}
switchTeam (teamId) {
this.setState({currentTeamId: teamId})
}
handleNavButtonClick (tab) {
return e => {
this.setState({currentTab: tab})
}
}
render () {
let content = this.renderContent()
let tabs = [
{target: APP, label: 'Preferences'},
{target: FOLDER, label: 'Manage folder'},
{target: CONTACT, label: 'Contact form'}
]
let navButtons = tabs.map(tab => (
<button key={tab.target} onClick={e => this.handleNavButtonClick(tab.target)(e)} className={this.state.currentTab === tab.target ? 'active' : ''}>{tab.label}</button>
))
return (
<div className='Preferences modal'>
<div className='header'>
<div className='title'>Setting</div>
<button onClick={e => closeModal()} className='closeBtn'>Done</button>
</div>
<div className='nav'>
{navButtons}
</div>
{content}
</div>
)
}
renderContent () {
let { user, folders, dispatch } = this.props
switch (this.state.currentTab) {
case FOLDER:
return (
<FolderSettingTab
dispatch={dispatch}
folders={folders}
/>
)
case CONTACT:
return (
<ContactTab/>
)
case APP:
default:
return (
<AppSettingTab
user={user}
dispatch={dispatch}
/>
)
}
}
}
Preferences.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
folders: PropTypes.array,
dispatch: PropTypes.func
}
Preferences.prototype.linkState = linkState
function remap (state) {
let { user, folders, status } = state
return {
user,
folders,
status
}
}
let RootComponent = connect(remap)(Preferences)
export default class PreferencesModal extends React.Component {
render () {
return (
<Provider store={store}>
<RootComponent/>
</Provider>
)
}
}

View File

@@ -1,115 +0,0 @@
import React, { PropTypes } from 'react'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
export default class Tutorial extends React.Component {
constructor (props) {
super(props)
this.state = {
slideIndex: 0
}
}
handlePriorSlideClick () {
if (this.state.slideIndex > 0) this.setState({slideIndex: this.state.slideIndex - 1})
}
handleNextSlideClick () {
if (this.state.slideIndex < 4) this.setState({slideIndex: this.state.slideIndex + 1})
}
startButtonClick (e) {
this.props.close()
}
render () {
let content = this.renderContent(this.state.slideIndex)
let dotElements = []
for (let i = 0; i < 5; i++) {
dotElements.push(<i key={i} className={'fa fa-fw fa-circle' + (i === this.state.slideIndex ? ' active' : '')}/>)
}
return (
<div className='Tutorial modal'>
<button onClick={(e) => this.handlePriorSlideClick()} className={'priorBtn' + (this.state.slideIndex === 0 ? ' hide' : '')}>
<i className='fa fa-fw fa-angle-left'/>
</button>
<button onClick={(e) => this.handleNextSlideClick()} className={'nextBtn' + (this.state.slideIndex === 4 ? ' hide' : '')}>
<i className='fa fa-fw fa-angle-right'/>
</button>
{content}
<div className='dots'>
{dotElements}
</div>
</div>
)
}
renderContent (index) {
switch (index) {
case 0:
return (<div className='slide slide0'>
<div className='title'>Welcome to Boost</div>
<div className='content'>
Boost is a brand new note app for software<br/>
Don't waste time cleaning up your data.<br/>
devote that time to more creative work.<br/>
Hack your memory.
</div>
</div>)
case 1:
let content = '## Boost is a note app for engineer.\n\n - Write with markdown\n - Stylize beautiful'
return (<div className='slide slide1'>
<div className='title'>Write with Markdown</div>
<div className='content'>
Markdown is available.<br/>
Your notes will be stylized beautifully and quickly.
<div className='markdown'>
<pre className='left'>{content}</pre>
<MarkdownPreview className='right' content={content}/>
</div>
</div>
</div>)
case 2:
let code = 'import shell from \'shell\'\r\nvar React = require(\'react\')\r\nvar { PropTypes } = React\r\nimport markdown from \'boost\/markdown\'\r\nvar ReactDOM = require(\'react-dom\')\r\n\r\nfunction handleAnchorClick (e) {\r\n shell.openExternal(e.target.href)\r\n e.preventDefault()\r\n}\r\n\r\nexport default class MarkdownPreview extends React.Component {\r\n componentDidMount () {\r\n this.addListener()\r\n }\r\n\r\n componentDidUpdate () {\r\n this.addListener()\r\n }\r\n\r\n componentWillUnmount () {\r\n this.removeListener()\r\n }'
return (<div className='slide slide2'>
<div className='title'>Beautiful code highlighting</div>
<div className='content'>
Boost supports code syntax highlighting.<br/>
There are more than 100 different type of language.
<div className='code'>
<CodeEditor readOnly article={{content: code, mode: 'jsx'}}/>
</div>
</div>
</div>)
case 3:
return (<div className='slide slide3'>
<div className='title'>Easy to access with Finder</div>
<div className='content'>
The Finder helps you organize all of the files and documents.<br/>
There is a short-cut key [ + alt + s] to open the Finder.<br/>
It is available to save your articles on the Clipboard<br/>
by selecting your file with pressing Enter key,<br/>
and to paste the contents of the Clipboard with [{process.platform === 'darwin' ? 'Command' : 'Control'}-V]
<img width='480' src='../resources/finder.png'/>
</div>
</div>)
case 4:
return (<div className='slide slide4'>
<div className='title'>Are you ready?</div>
<div className='content'>
<button onClick={(e) => this.startButtonClick(e)}>Start<br/>Boost</button>
</div>
</div>)
default:
return null
}
}
}
Tutorial.propTypes = {
close: PropTypes.func
}

View File

@@ -1,7 +1,6 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import store from '../store'
import { destroyArticle } from '../actions'
const electron = require('electron')
const ipc = electron.ipcRenderer
@@ -10,7 +9,7 @@ export default class DeleteArticleModal extends React.Component {
constructor (props) {
super(props)
this.confirmHandler = e => this.handleYesButtonClick()
this.confirmHandler = (e) => this.handleYesButtonClick()
}
componentDidMount () {
@@ -27,7 +26,7 @@ export default class DeleteArticleModal extends React.Component {
}
handleYesButtonClick (e) {
store.dispatch(destroyArticle(this.props.articleKey))
// store.dispatch(destroyArticle(this.props.articleKey))
this.props.close()
}
@@ -39,8 +38,8 @@ export default class DeleteArticleModal extends React.Component {
<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>
<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>
)

View File

@@ -0,0 +1,243 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InitModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { hashHistory } from 'react-router'
import _ from 'lodash'
const CSON = require('season')
const path = require('path')
const electron = require('electron')
const { remote } = electron
function browseFolder () {
let dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
defaultPath,
properties: ['openDirectory', 'createDirectory']
}, function (targetPaths) {
if (targetPaths == null) return resolve('')
resolve(targetPaths[0])
})
})
}
class InitModal extends React.Component {
constructor (props) {
super(props)
this.state = {
path: path.join(remote.app.getPath('home'), 'Boostnote'),
migrationRequested: true,
isLoading: true,
data: null,
legacyStorageExists: false,
isSending: false
}
}
handleCloseButtonClick (e) {
this.props.close()
}
handlePathChange (e) {
this.setState({
path: e.target.value
})
}
componentDidMount () {
let data = null
try {
data = CSON.readFileSync(path.join(remote.app.getPath('userData'), 'local.json'))
} catch (err) {
if (err.code === 'ENOENT') {
return
}
console.error(err)
}
let newState = {
isLoading: false
}
if (data != null) {
newState.legacyStorageExists = true
newState.data = data
}
this.setState(newState, () => {
this.refs.createButton.focus()
})
}
handlePathBrowseButtonClick (e) {
browseFolder()
.then((targetPath) => {
if (targetPath.length > 0) {
this.setState({
path: targetPath
})
}
})
.catch((err) => {
console.error('BrowseFAILED')
console.error(err)
})
}
handleSubmitButtonClick (e) {
this.setState({
isSending: true
}, () => {
dataApi
.addStorage({
name: 'My Storage',
path: this.state.path
})
.then((data) => {
if (this.state.migrationRequested && _.isObject(this.state.data) && _.isArray(this.state.data.folders) && _.isArray(this.state.data.articles)) {
return dataApi.migrateFromV5(data.storage.key, this.state.data)
}
return data
})
.then((data) => {
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
notes: data.notes
})
let defaultMarkdownNote = dataApi
.createMarkdownNote(data.storage.key, data.storage.folders[0].key, {
title: 'Welcome to Boostnote :)',
content: '# Welcome to Boostnote :)\nThis is a markdown note.\n\nClick to edit this note.'
})
.then((note) => {
store.dispatch({
type: 'CREATE_NOTE',
note: note
})
})
let defaultSnippetNote = dataApi
.createSnippetNote(data.storage.key, data.storage.folders[0].key, {
title: 'Snippet note example',
description: 'Snippet note example\nYou can store a series of snippet as a single note like Gist.',
snippets: [
{
name: 'example.html',
mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Hello World</h1>\n</body>\n</html>'
},
{
name: 'example.js',
mode: 'javascript',
content: 'var html = document.getElementById(\'hello\').innerHTML\n\nconsole.log(html)'
}
]
})
.then((note) => {
store.dispatch({
type: 'CREATE_NOTE',
note: note
})
})
return Promise.resolve(defaultSnippetNote)
.then(defaultMarkdownNote)
.then(() => data.storage)
})
.then((storage) => {
hashHistory.push('/storages/' + storage.key)
this.props.close()
})
.catch((err) => {
this.setState({
isSending: false
})
throw err
})
})
}
handleMigrationRequestedChange (e) {
this.setState({
migrationRequested: e.target.checked
})
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
if (this.state.isLoading) {
return <div styleName='root--loading'>
<i styleName='spinner' className='fa fa-spin fa-spinner'/>
<div styleName='loadingMessage'>Preparing initialization...</div>
</div>
}
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='header-title'>Initialize Storage</div>
</div>
<button styleName='closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>Close</button>
<div styleName='body'>
<div styleName='body-welcome'>
Welcome you!
</div>
<div styleName='body-description'>
Boostnote will use this directory as a default storage.
</div>
<div styleName='body-path'>
<input styleName='body-path-input'
placeholder='Select Folder'
value={this.state.path}
onChange={(e) => this.handlePathChange(e)}
/>
<button styleName='body-path-button'
onClick={(e) => this.handlePathBrowseButtonClick(e)}
>
...
</button>
</div>
<div styleName='body-migration'>
<label><input type='checkbox' checked={this.state.migrationRequested} onChange={(e) => this.handleMigrationRequestedChange(e)}/> Migrate old data from the legacy app v0.5</label>
</div>
<div styleName='body-control'>
<button styleName='body-control-createButton'
ref='createButton'
onClick={(e) => this.handleSubmitButtonClick(e)}
disabled={this.state.isSending}
>
{this.state.isSending
? <span>
<i className='fa fa-spin fa-spinner'/> Loading...
</span>
: 'Let\'s Go!'
}
</button>
</div>
</div>
</div>
)
}
}
InitModal.propTypes = {
}
export default CSSModules(InitModal, styles)

View File

@@ -0,0 +1,86 @@
.root
modal()
max-width 540px
overflow hidden
position relative
.root--loading
@extend .root
text-align center
.spinner
font-size 100px
margin 35px auto
color $ui-text-color
.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
.closeButton
position absolute
top 10px
right 10px
height 30px
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.body
padding 30px
.body-welcome
text-align center
margin-bottom 25px
font-size 32px
color $ui-text-color
.body-description
font-size 14px
color $ui-text-color
text-align center
margin-bottom 25px
.body-path
margin 0 auto 25px
width 280px
.body-path-input
height 30px
vertical-align middle
width 250px
font-size 12px
border-style solid
border-width 1px 0 1px 1px
border-color $border-color
border-top-left-radius 2px
border-bottom-left-radius 2px
padding 0 5px
.body-path-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
.body-migration
margin 0 auto 25px
text-align center
.body-control
text-align center
.body-control-createButton
colorPrimaryButton()
border none
border-radius 2px
height 40px
padding 0 25px

View File

@@ -0,0 +1,140 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
class NewNoteModal extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
componentDidMount () {
this.refs.markdownButton.focus()
}
handleCloseButtonClick (e) {
this.props.close()
}
handleMarkdownNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props
dataApi
.createMarkdownNote(storage, folder, {
title: '',
content: ''
})
.then((note) => {
dispatch({
type: 'CREATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.uniqueKey}
})
ee.emit('detail:focus')
this.props.close()
})
}
handleMarkdownNoteButtonKeyDown (e) {
if (e.keyCode === 9) {
e.preventDefault()
this.refs.snippetButton.focus()
}
}
handleSnippetNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props
dataApi
.createSnippetNote(storage, folder, {
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
})
.then((note) => {
dispatch({
type: 'CREATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.uniqueKey}
})
ee.emit('detail:focus')
this.props.close()
})
}
handleSnippetNoteButtonKeyDown (e) {
if (e.keyCode === 9) {
e.preventDefault()
this.refs.markdownButton.focus()
}
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>New Note</div>
</div>
<button styleName='closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>Close</button>
<div styleName='control'>
<button styleName='control-button'
onClick={(e) => this.handleMarkdownNoteButtonClick(e)}
onKeyDown={(e) => this.handleMarkdownNoteButtonKeyDown(e)}
ref='markdownButton'
>
<i styleName='control-button-icon'
className='fa fa-file-text-o'
/><br/>
<span styleName='control-button-label'>Markdown Note</span><br/>
<span styleName='control-button-description'>It is good for any type of documents. Check List, Code block and Latex block are available.</span>
</button>
<button styleName='control-button'
onClick={(e) => this.handleSnippetNoteButtonClick(e)}
onKeyDown={(e) => this.handleSnippetNoteButtonKeyDown(e)}
ref='snippetButton'
>
<i styleName='control-button-icon'
className='fa fa-code'
/><br/>
<span styleName='control-button-label'>Snippet Note</span><br/>
<span styleName='control-button-description'>This format is specialized on managing snippets like Gist. Multiple snippets can be grouped as a note.
</span>
</button>
</div>
<div styleName='description'><i className='fa fa-arrows-h'/> Tab to switch format</div>
</div>
)
}
}
NewNoteModal.propTypes = {
}
export default CSSModules(NewNoteModal, styles)

View File

@@ -0,0 +1,56 @@
.root
modal()
max-width 540px
overflow hidden
position relative
.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
.closeButton
position absolute
top 10px
right 10px
height 30px
width 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.control
padding 25px 15px 15px
text-align center
.control-button
width 220px
height 220px
margin 0 15px
border $ui-border
border-radius 5px
color $ui-text-color
colorDefaultButton()
padding 10px
&:focus
colorPrimaryButton()
.control-button-icon
font-size 50px
margin-bottom 15px
.control-button-label
font-size 18px
line-height 32px
.control-button-description
font-size 12px
.description
color $ui-inactive-text-color
text-align center
margin-bottom 25px

View File

@@ -0,0 +1,412 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import hljsTheme from 'browser/lib/hljsThemes'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
const electron = require('electron')
const ipc = electron.ipcRenderer
const ace = window.ace
const OSX = global.process.platform === 'darwin'
class ConfigTab extends React.Component {
constructor (props) {
super(props)
this.state = {
isHotkeyHintOpen: false,
config: props.config
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully applied!'
}})
}
this.handleSettingError = (err) => {
this.setState({keymapAlert: {
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)
}
handleSaveButtonClick (e) {
let newConfig = {
hotkey: this.state.config.hotkey
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
handleConfigKeyDown (e) {
if (e.keyCode === 13) {
this.submitConfig()
}
}
handleLineNumberingClick (e) {
let config = this.state.config
config['preview-line-number'] = e.target.checked
this.setState({
config
})
}
handleDisableDirectWriteClick (e) {
let config = this.state.config
config['disable-direct-write'] = e.target.checked
this.setState({
config
})
}
handleHintToggleButtonClick (e) {
this.setState({
isHotkeyHintOpen: !this.state.isHotkeyHintOpen
})
}
handleHotkeyChange (e) {
let { config } = this.state
config.hotkey = {
toggleFinder: this.refs.toggleFinder.value,
toggleMain: this.refs.toggleMain.value
}
this.setState({
config
})
}
handleUIChange (e) {
let { config } = this.state
config.ui = {
theme: this.refs.uiTheme.value,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
}
config.editor = {
theme: this.refs.editorTheme.value,
fontSize: this.refs.editorFontSize.value,
fontFamily: this.refs.editorFontFamily.value,
indentType: this.refs.editorIndentType.value,
indentSize: this.refs.editorIndentSize.value,
switchPreview: this.refs.editorSwitchPreview.value
}
config.preview = {
fontSize: this.refs.previewFontSize.value,
fontFamily: this.refs.previewFontFamily.value,
codeBlockTheme: this.refs.previewCodeBlockTheme.value,
lineNumber: this.refs.previewLineNumber.checked
}
this.setState({
config
})
}
handleSaveUIClick (e) {
let newConfig = {
ui: this.state.config.ui,
editor: this.state.config.editor,
preview: this.state.config.preview
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
? <p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
: null
let aceThemeList = ace.require('ace/ext/themelist')
let hljsThemeList = hljsTheme()
let { config } = this.state
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>Hotkey</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Toggle Main</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleMain'
value={config.hotkey.toggleMain}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Toggle Finder(popup)</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleFinder'
value={config.hotkey.toggleFinder}
type='text'
disabled
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}
>
{this.state.isHotkeyHintOpen
? 'Hide Hint'
: 'Show Hint'
}
</button>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>Save Hotkey
</button>
{keymapAlertElement}
</div>
{this.state.isHotkeyHintOpen &&
<div styleName='group-hint'>
<p>Available Keys</p>
<ul>
<li><code>0</code> to <code>9</code></li>
<li><code>A</code> to <code>Z</code></li>
<li><code>F1</code> to <code>F24</code></li>
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
<li><code>Plus</code></li>
<li><code>Space</code></li>
<li><code>Backspace</code></li>
<li><code>Delete</code></li>
<li><code>Insert</code></li>
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
<li><code>Home</code> and <code>End</code></li>
<li><code>PageUp</code> and <code>PageDown</code></li>
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
</ul>
</div>
}
</div>
<div styleName='group'>
<div styleName='group-header'>UI</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Theme</div>
<div styleName='group-section-control'>
<select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)}
ref='uiTheme'
disabled
>
<option value='default'>Light</option>
<option value='dark'>Dark</option>
</select>
</div>
</div>
{
global.process.platform === 'win32'
? <div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.disableDirectWrite}
refs='uiD2w'
disabled={OSX}
type='checkbox'
/>
Disable Direct Write(It will be applied after restarting)
</label>
</div>
: null
}
<div styleName='group-header2'>Editor</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Theme
</div>
<div styleName='group-section-control'>
<select value={config.editor.theme}
ref='editorTheme'
onChange={(e) => this.handleUIChange(e)}
>
{
aceThemeList.themes.map((theme) => {
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
})
}
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Size
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='editorFontSize'
value={config.editor.fontSize}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Family
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='editorFontFamily'
value={config.editor.fontFamily}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Indent Style
</div>
<div styleName='group-section-control'>
<select value={config.editor.indentSize}
ref='editorIndentSize'
onChange={(e) => this.handleUIChange(e)}
>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='4'>4</option>
<option value='8'>8</option>
</select>&nbsp;
<select value={config.editor.indentType}
ref='editorIndentType'
onChange={(e) => this.handleUIChange(e)}
>
<option value='space'>Spaces</option>
<option value='tab'>Tabs</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Switching Preview
</div>
<div styleName='group-section-control'>
<select value={config.editor.switchPreview}
ref='editorSwitchPreview'
onChange={(e) => this.handleUIChange(e)}
>
<option value='BLUR'>When Editor Blurred</option>
<option value='RIGHTCLICK'>When Right Clicking</option>
</select>
</div>
</div>
<div styleName='group-header2'>Preview</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Size
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={config.preview.fontSize}
ref='previewFontSize'
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Family
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewFontFamily'
value={config.preview.fontFamily}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Code block Theme</div>
<div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme'
onChange={(e) => this.handleUIChange(e)}
>
{
hljsThemeList.map((theme) => {
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
})
}
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.lineNumber}
ref='previewLineNumber'
type='checkbox'
/>&nbsp;
Code block line numbering
</label>
</div>
<div className='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}
>
Save UI Config
</button>
</div>
</div>
</div>
)
}
}
ConfigTab.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
dispatch: PropTypes.func
}
export default CSSModules(ConfigTab, styles)

View File

@@ -0,0 +1,88 @@
.root
padding 15px
color $ui-text-color
.group
margin-bottom 45px
.group-header
font-size 24px
color $ui-text-color
padding 5px
border-bottom $default-border
margin-bottom 15px
.group-header2
font-size 18px
color $ui-text-color
padding 5px
margin-bottom 15px
.group-section
margin-bottom 15px
display flex
line-height 30px
.group-section-label
width 150px
text-align right
margin-right 10px
.group-section-control
flex 1
.group-section-control-input
height 30px
vertical-align middle
width 150px
font-size 12px
border solid 1px $border-color
border-radius 2px
padding 0 5px
&:disabled
background-color $ui-input--disabled-backgroundColor
.group-checkBoxSection
margin-bottom 15px
display flex
line-height 30px
padding-left 15px
.group-control
border-top $default-border
padding-top 10px
box-sizing border-box
height 40px
text-align right
:global
.alert
font-size 12px
line-height 30px
padding 0 5px
float right
.group-control-leftButton
float left
colorDefaultButton()
border $default-border
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.group-control-rightButton
float right
colorPrimaryButton()
border none
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.group-hint
border $ui-border
padding 10px 15px
margin 15px 0
border-radius 5px
background-color $ui-backgroundColor
color $ui-inactive-text-color
ul
list-style inherit
padding-left 1em
line-height 1.2
p
line-height 1.2

View File

@@ -0,0 +1,58 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoTab.styl'
const electron = require('electron')
const { shell, remote } = electron
const appVersion = remote.app.getVersion()
class InfoTab 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='top'>
<img styleName='icon' src='../resources/app.png' width='150' height='150'/>
<div styleName='appId'>Boostnote {appVersion}</div>
<div styleName='description'>
A simple markdown/snippet note app for developer.
</div>
<div styleName='madeBy'>Made by&nbsp;
<a href='http://maisin.co/'
onClick={(e) => this.handleLinkClick(e)}
>MAISIN&CO.</a></div>
<div styleName='copyright'>Copyright 2016 MAISIN&CO. All rights reserved.</div>
</div>
<ul styleName='list'>
<li>
The codes of this app is published under GPLv3 license.
</li>
<li>
Any kinds of feedback, creating a new issue or a pull request, would be welcomed.
</li>
<li>
Issue Tracker : <a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)}
>https://github.com/BoostIO/Boostnote/issues</a>
</li>
</ul>
</div>
)
}
}
InfoTab.propTypes = {
}
export default CSSModules(InfoTab, styles)

View File

@@ -0,0 +1,35 @@
.root
padding 15px
white-space pre
line-height 1.4
color $ui-text-color
width 100%
.top
text-align center
margin-bottom 25px
.appId
font-size 24px
.description
overflow hidden
white-space normal
line-height 1.5
margin 5px auto 10px
font-size 14px
text-align center
.madeBy
font-size 12px
$ui-inactive-text-color
.copyright
font-size 12px
$ui-inactive-text-color
.list
list-style square
padding-left 2em
li
white-space normal

View File

@@ -0,0 +1,37 @@
.root
modal()
max-width 540px
min-height 400px
height 80%
overflow hidden
position relative
.nav
absolute top left right
height 50px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
.nav-button
width 80px
height 50px
border none
background-color transparent
color #939395
font-size 14px
&:hover
color #515151
.nav-button--active
@extend .nav-button
color #6AA5E9
&:hover
color #6AA5E9
.nav-button-icon
display block
.content
absolute left right bottom
top 50px
overflow-y auto

View File

@@ -0,0 +1,351 @@
import React, { PropTypes } 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'
const electron = require('electron')
const { shell, remote } = electron
const { Menu, MenuItem } = remote
class UnstyledFolderItem extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
folder: {
color: props.color,
name: props.name
}
}
}
handleEditChange (e) {
let { folder } = this.state
folder.name = this.refs.nameInput.value
this.setState({
folder
})
}
handleConfirmButtonClick (e) {
let { storage, folder } = this.props
dataApi
.updateFolder(storage.key, folder.key, {
color: this.state.folder.color,
name: this.state.folder.name
})
.then((storage) => {
store.dispatch({
type: 'UPDATE_STORAGE',
storage: storage
})
this.setState({
status: 'IDLE'
})
})
}
handleColorButtonClick (e) {
var menu = new Menu()
consts.FOLDER_COLORS.forEach((color, index) => {
menu.append(new MenuItem({
label: consts.FOLDER_COLOR_NAMES[index],
click: (e) => {
let { folder } = this.state
folder.color = color
this.setState({
folder
})
}
}))
})
menu.popup(remote.getCurrentWindow())
}
handleCancelButtonClick (e) {
this.setState({
status: 'IDLE'
})
}
renderEdit (e) {
return (
<div styleName='folderList-item'>
<div styleName='folderList-item-left'>
<button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}}
onClick={(e) => this.handleColorButtonClick(e)}
>
<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
.removeFolder(storage.key, folder.key)
.then((storage) => {
store.dispatch({
type: 'REMOVE_FOLDER',
key: folder.key,
storage: storage
})
})
}
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 } = this.props
this.setState({
status: 'EDIT',
folder: {
color: folder.color,
name: folder.name
}
}, () => {
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) {
super(props)
this.state = {
isLabelEditing: false
}
}
handleNewFolderButtonClick (e) {
let { storage } = this.props
let input = {
name: 'Untitled',
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
dataApi.createFolder(storage.key, input)
.then((storage) => {
store.dispatch({
type: 'ADD_FOLDER',
storage: storage
})
})
.catch((err) => {
console.error(err)
})
}
handleExternalButtonClick () {
let { storage } = this.props
shell.showItemInFolder(storage.path)
}
handleUnlinkButtonClick (e) {
let { storage } = this.props
dataApi.removeStorage(storage.key)
.then(() => {
store.dispatch({
type: 'REMOVE_STORAGE',
key: storage.key
})
})
.catch((err) => {
console.error(err)
})
}
handleLabelClick (e) {
let { storage } = this.props
this.setState({
isLabelEditing: true,
name: storage.name
}, () => {
this.refs.label.focus()
})
}
handleLabelChange (e) {
this.setState({
name: this.refs.label.value
})
}
handleLabelBlur (e) {
let { storage } = this.props
dataApi
.renameStorage(storage.key, this.state.name)
.then((storage) => {
store.dispatch({
type: 'RENAME_STORAGE',
storage: storage
})
this.setState({
isLabelEditing: false
})
})
}
render () {
let { storage } = this.props
let folderList = storage.folders.map((folder) => {
return <FolderItem key={folder.key}
folder={folder}
storage={storage}
/>
})
return (
<div styleName='root' key={storage.key}>
<div styleName='header'>
{this.state.isLabelEditing
? <div styleName='header-label--edit'>
<input styleName='header-label-input'
value={this.state.name}
ref='label'
onChange={(e) => this.handleLabelChange(e)}
onBlur={(e) => this.handleLabelBlur(e)}
/>
</div>
: <div styleName='header-label'
onClick={(e) => this.handleLabelClick(e)}
>
<i className='fa fa-folder-open'/>&nbsp;
{storage.name}&nbsp;
<span styleName='header-label-path'>({storage.path})</span>&nbsp;
<i styleName='header-label-editButton' className='fa fa-pencil'/>
</div>
}
<div styleName='header-control'>
<button styleName='header-control-button'
onClick={(e) => this.handleNewFolderButtonClick(e)}
>
<i className='fa fa-plus'/>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleExternalButtonClick(e)}
>
<i className='fa fa-external-link'/>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleUnlinkButtonClick(e)}
>
<i className='fa fa-unlink'/>
</button>
</div>
</div>
<div styleName='folderList'>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>No Folders</div>
}
</div>
</div>
)
}
}
StorageItem.propTypes = {
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.string,
color: PropTypes.string,
name: PropTypes.string
})
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,119 @@
.root
position relative
margin-bottom 15px
.header
height 35px
line-height 30px
padding 0 10px 5px
box-sizing border-box
border-bottom $default-border
margin-bottom 5px
.header-label
float left
cursor pointer
&:hover
.header-label-editButton
opacity 1
.header-label-path
color $ui-inactive-text-color
font-size 10px
margin 0 5px
.header-label-editButton
color $ui-text-color
opacity 0
transition 0.2s
.header-label--edit
@extend .header-label
.header-label-input
height 25px
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
padding 0 5px
.header-control
float right
.header-control-button
width 30px
height 25px
colorDefaultButton()
border-radius 2px
border $ui-border
margin-right 5px
&:last-child
margin-right 0
.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 4px 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
.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()

View File

@@ -0,0 +1,223 @@
import React, { PropTypes } 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
function browseFolder () {
let dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
defaultPath,
properties: ['openDirectory', 'createDirectory']
}, function (targetPaths) {
if (targetPaths == null) return resolve('')
resolve(targetPaths[0])
})
})
}
class StoragesTab extends React.Component {
constructor (props) {
super(props)
this.state = {
page: 'LIST',
newStorage: {
name: 'Unnamed',
type: 'FILESYSTEM',
path: ''
}
}
}
handleAddStorageButton (e) {
this.setState({
page: 'ADD_STORAGE',
newStorage: {
name: 'Unnamed',
type: 'FILESYSTEM',
path: ''
}
}, () => {
this.refs.addStorageName.select()
})
}
renderList () {
let { storages } = this.props
let storageList = storages.map((storage) => {
return <StorageItem
key={storage.key}
storage={storage}
/>
})
return (
<div styleName='list'>
{storageList.length > 0
? storageList
: <div styleName='list-empty'>No storage found.</div>
}
<div styleName='list-control'>
<button styleName='list-control-addStorageButton'
onClick={(e) => this.handleAddStorageButton(e)}
>
<i className='fa fa-plus'/> Add Storage
</button>
</div>
</div>
)
}
handleAddStorageBrowseButtonClick (e) {
browseFolder()
.then((targetPath) => {
if (targetPath.length > 0) {
let { newStorage } = this.state
newStorage.path = targetPath
this.setState({
newStorage
})
}
})
.catch((err) => {
console.error('BrowseFAILED')
console.error(err)
})
}
handleAddStorageChange (e) {
let { newStorage } = this.state
newStorage.name = this.refs.addStorageName.value
newStorage.path = this.refs.addStoragePath.value
this.setState({
newStorage
})
}
handleAddStorageCreateButton (e) {
dataApi
.addStorage({
name: this.state.newStorage.name,
path: this.state.newStorage.path
})
.then((data) => {
let { dispatch } = this.props
dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
notes: data.notes
})
this.setState({
page: 'LIST'
})
})
}
handleAddStorageCancelButton (e) {
this.setState({
page: 'LIST'
})
}
renderAddStorage () {
return (
<div styleName='addStorage'>
<div styleName='addStorage-header'>Add Storage</div>
<div styleName='addStorage-body'>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>
Name
</div>
<div styleName='addStorage-body-section-name'>
<input styleName='addStorage-body-section-name-input'
ref='addStorageName'
value={this.state.newStorage.name}
onChange={(e) => this.handleAddStorageChange(e)}
/>
</div>
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Type</div>
<div styleName='addStorage-body-section-type'>
<select styleName='addStorage-body-section-type-select'
value={this.state.newStorage.type}
readOnly
>
<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.
</div>
</div>
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Location
</div>
<div styleName='addStorage-body-section-path'>
<input styleName='addStorage-body-section-path-input'
ref='addStoragePath'
placeholder='Select Folder'
value={this.state.newStorage.path}
onChange={(e) => this.handleAddStorageChange(e)}
/>
<button styleName='addStorage-body-section-path-button'
onClick={(e) => this.handleAddStorageBrowseButtonClick(e)}
>
...
</button>
</div>
</div>
<div styleName='addStorage-body-control'>
<button styleName='addStorage-body-control-createButton'
onClick={(e) => this.handleAddStorageCreateButton(e)}
>Create</button>
<button styleName='addStorage-body-control-cancelButton'
onClick={(e) => this.handleAddStorageCancelButton(e)}
>Cancel</button>
</div>
</div>
</div>
)
}
renderContent () {
switch (this.state.page) {
case 'ADD_STORAGE':
case 'ADD_FOLDER':
return this.renderAddStorage()
case 'LIST':
default:
return this.renderList()
}
}
render () {
return (
<div styleName='root'>
{this.renderContent()}
</div>
)
}
}
StoragesTab.propTypes = {
dispatch: PropTypes.func
}
export default CSSModules(StoragesTab, styles)

View File

@@ -0,0 +1,115 @@
.root
padding 15px
color $ui-text-color
.list
margin-bottom 15px
font-size 14px
.folderList
padding 0 15px
.folderList-item
height 30px
line-height 30px
border-bottom $ui-border
.folderList-empty
height 30px
line-height 30px
font-size 12px
color $ui-inactive-text-color
.list-empty
height 30px
color $ui-inactive-text-color
.list-control
height 30px
.list-control-addStorageButton
height 30px
padding 0 15px
border $ui-border
colorDefaultButton()
border-radius 2px
.addStorage
margin-bottom 15px
.addStorage-header
font-size 24px
color $ui-text-color
padding 5px
border-bottom $default-border
margin-bottom 15px
.addStorage-body-section
margin-bottom 15px
display flex
line-height 30px
.addStorage-body-section-label
width 150px
text-align right
margin-right 10px
.addStorage-body-section-name
flex 1
.addStorage-body-section-name-input
height 30px
vertical-align middle
width 150px
font-size 12px
border solid 1px $border-color
border-radius 2px
padding 0 5px
.addStorage-body-section-type
flex 1
.addStorage-body-section-type-select
height 30px
.addStorage-body-section-type-description
margin 5px
font-size 12px
color $ui-inactive-text-color
line-height 16px
.addStorage-body-section-path
flex 1
.addStorage-body-section-path-input
height 30px
vertical-align middle
width 150px
font-size 12px
border-style solid
border-width 1px 0 1px 1px
border-color $border-color
border-top-left-radius 2px
border-bottom-left-radius 2px
padding 0 5px
.addStorage-body-section-path-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
.addStorage-body-control
border-top $default-border
padding-top 10px
box-sizing border-box
height 40px
text-align right
.addStorage-body-control-createButton
colorPrimaryButton()
border none
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.addStorage-body-control-cancelButton
colorDefaultButton()
border $default-border
border-radius 2px
height 30px
padding 0 15px

View File

@@ -0,0 +1,112 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import ConfigTab from './ConfigTab'
import InfoTab from './InfoTab'
import StoragesTab from './StoragesTab'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferencesModal.styl'
class Preferences extends React.Component {
constructor (props) {
super(props)
this.state = {
currentTab: 'STORAGES'
}
}
componentDidMount () {
this.refs.root.focus()
}
switchTeam (teamId) {
this.setState({currentTeamId: teamId})
}
handleNavButtonClick (tab) {
return (e) => {
this.setState({currentTab: tab})
}
}
renderContent () {
let { dispatch, config, storages } = this.props
switch (this.state.currentTab) {
case 'INFO':
return <InfoTab/>
case 'CONFIG':
return (
<ConfigTab
dispatch={dispatch}
config={config}
/>
)
case 'STORAGES':
default:
return (
<StoragesTab
dispatch={dispatch}
storages={storages}
/>
)
}
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
let content = this.renderContent()
let tabs = [
{target: 'STORAGES', label: 'Storages', icon: 'database'},
{target: 'CONFIG', label: 'Config', icon: 'cogs'},
{target: 'INFO', label: 'Info', icon: 'info-circle'}
]
let navButtons = tabs.map((tab) => {
let isActive = this.state.currentTab === tab.target
return (
<button styleName={isActive
? 'nav-button--active'
: 'nav-button'
}
key={tab.target}
onClick={(e) => this.handleNavButtonClick(tab.target)(e)}
>
<i styleName='nav-button-icon'
className={'fa fa-' + tab.icon}
/>
<span styleName='nav-button-label'>
{tab.label}
</span>
</button>
)
})
return (
<div styleName='root'
ref='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='nav'>
{navButtons}
</div>
<div styleName='content'>
{content}
</div>
</div>
)
}
}
Preferences.propTypes = {
dispatch: PropTypes.func
}
export default connect((x) => x)(CSSModules(Preferences, styles))

View File

@@ -1,306 +0,0 @@
import { combineReducers } from 'redux'
import _ from 'lodash'
import {
// Status action type
SWITCH_FOLDER,
SWITCH_ARTICLE,
SET_SEARCH_FILTER,
SET_TAG_FILTER,
CLEAR_SEARCH,
TOGGLE_TUTORIAL,
// user
USER_UPDATE,
// Article action type
ARTICLE_UPDATE,
ARTICLE_DESTROY,
ARTICLE_CACHE,
ARTICLE_UNCACHE,
ARTICLE_UNCACHE_ALL,
ARTICLE_SAVE,
ARTICLE_SAVE_ALL,
// Folder action type
FOLDER_CREATE,
FOLDER_UPDATE,
FOLDER_DESTROY,
FOLDER_REPLACE
} from './actions'
import dataStore from 'browser/lib/dataStore'
import keygen from 'browser/lib/keygen'
import activityRecord from 'browser/lib/activityRecord'
const initialStatus = {
search: '',
isTutorialOpen: false
}
dataStore.init()
let data = dataStore.getData()
let initialArticles = {
data: data && data.articles ? data.articles : [],
modified: []
}
let initialFolders = data && data.folders ? data.folders : []
let initialUser = dataStore.getUser().user
function user (state = initialUser, action) {
switch (action.type) {
case USER_UPDATE:
let updated = Object.assign(state, action.data)
dataStore.saveUser(null, updated)
return updated
default:
return state
}
}
function folders (state = initialFolders, action) {
state = state.slice()
switch (action.type) {
case FOLDER_CREATE:
{
let newFolder = action.data.folder
if (!_.isString(newFolder.name)) throw new Error('Folder name must be a string')
newFolder.name = newFolder.name.trim().replace(/\s/g, '_')
Object.assign(newFolder, {
key: keygen(),
createdAt: new Date(),
updatedAt: new Date()
})
if (newFolder.name == null || newFolder.name.length === 0) throw new Error('Folder name is required')
if (newFolder.name.match(/\//)) throw new Error('`/` is not available for folder name')
let conflictFolder = _.find(state, (folder) => folder.name.toLowerCase() === newFolder.name.toLowerCase())
if (conflictFolder != null) throw new Error(`${conflictFolder.name} already exists!`)
state.push(newFolder)
dataStore.setFolders(state)
activityRecord.emit('FOLDER_CREATE')
return state
}
case FOLDER_UPDATE:
{
let folder = action.data.folder
let targetFolder = _.findWhere(state, {key: folder.key})
if (!_.isString(folder.name)) throw new Error('Folder name must be a string')
folder.name = folder.name.trim().replace(/\s/g, '_')
if (folder.name.length === 0) throw new Error('Folder name is required')
if (folder.name.match(/\//)) throw new Error('`/` is not available for folder name')
// Folder existence check
if (targetFolder == null) throw new Error('Folder doesnt exist')
// Name conflict check
if (targetFolder.name !== folder.name) {
let conflictFolder = _.find(state, (_folder) => {
return folder.name.toLowerCase() === _folder.name.toLowerCase() && folder.key !== _folder.key
})
if (conflictFolder != null) throw new Error('Name conflicted')
}
Object.assign(targetFolder, folder, {
updatedAt: new Date()
})
dataStore.setFolders(state)
activityRecord.emit('FOLDER_UPDATE')
return state
}
case FOLDER_DESTROY:
{
if (state.length < 2) throw new Error('Folder must exist more than one')
let targetKey = action.data.key
let targetIndex = _.findIndex(state, (folder) => folder.key === targetKey)
if (targetIndex >= 0) {
state.splice(targetIndex, 1)
}
dataStore.setFolders(state)
activityRecord.emit('FOLDER_DESTROY')
return state
}
case FOLDER_REPLACE:
{
let { a, b } = action.data
let folderA = state[a]
let folderB = state[b]
state.splice(a, 1, folderB)
state.splice(b, 1, folderA)
}
dataStore.setFolders(state)
return state
default:
return state
}
}
function compareArticle (original, modified) {
var keys = _.keys(_.pick(modified, ['mode', 'title', 'tags', 'content', 'FolderKey']))
return keys.reduce((sum, key) => {
if ((key === 'tags' && !_.isEqual(original[key], modified[key])) || (key !== 'tags' && original[key] !== modified[key])) {
if (sum == null) {
sum = {
key: original.key
}
}
sum[key] = modified[key]
}
return sum
}, null)
}
function articles (state = initialArticles, action) {
switch (action.type) {
case ARTICLE_CACHE:
{
let modified = action.data.article
let targetKey = action.data.key
let originalIndex = _.findIndex(state.data, (_article) => targetKey === _article.key)
if (originalIndex === -1) return state
let modifiedIndex = _.findIndex(state.modified, (_article) => targetKey === _article.key)
modified = compareArticle(state.data[originalIndex], modified)
if (modified == null) {
if (modifiedIndex !== -1) state.modified.splice(modifiedIndex, 1)
return state
}
if (modifiedIndex === -1) state.modified.push(modified)
else Object.assign(state.modified[modifiedIndex], modified)
return state
}
case ARTICLE_UNCACHE:
{
let targetKey = action.data.key
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
return state
}
case ARTICLE_UNCACHE_ALL:
state.modified = []
return state
case ARTICLE_SAVE:
{
let targetKey = action.data.key
let override = action.data.article
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
let modified = modifiedIndex !== -1 ? state.modified.splice(modifiedIndex, 1)[0] : null
let targetIndex = _.findIndex(state.data, _article => targetKey === _article.key)
// Make a new if target article is not found.
if (targetIndex === -1) {
state.data.push(Object.assign({
title: '',
content: '',
mode: 'markdown',
tags: [],
craetedAt: new Date()
}, modified, override, {key: targetKey, updatedAt: new Date()}))
return state
}
Object.assign(state.data[targetIndex], modified, override, {key: targetKey, updatedAt: new Date()})
dataStore.setArticles(state.data)
return state
}
case ARTICLE_SAVE_ALL:
if (state.modified.length > 0) {
state.modified.forEach(modifiedArticle => {
let targetIndex = _.findIndex(state.data, _article => modifiedArticle.key === _article.key)
Object.assign(state.data[targetIndex], modifiedArticle, {key: modifiedArticle.key, updatedAt: new Date()})
})
}
state.modified = []
dataStore.setArticles(state.data)
return state
case ARTICLE_UPDATE:
{
let article = action.data.article
let targetIndex = _.findIndex(state.data, _article => article.key === _article.key)
if (targetIndex < 0) state.data.unshift(article)
else Object.assign(state.data[targetIndex], article)
dataStore.setArticles(state.data)
return state
}
case ARTICLE_DESTROY:
{
let articleKey = action.data.key
let targetIndex = _.findIndex(state.data, _article => articleKey === _article.key)
if (targetIndex >= 0) state.data.splice(targetIndex, 1)
let modifiedIndex = _.findIndex(state.modified, _article => articleKey === _article.key)
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
dataStore.setArticles(state.data)
return state
}
case FOLDER_DESTROY:
{
let folderKey = action.data.key
state.data = state.data.filter(article => article.FolderKey !== folderKey)
dataStore.setArticles(state.data)
return state
}
default:
return state
}
}
function status (state = initialStatus, action) {
state = Object.assign({}, state)
switch (action.type) {
case TOGGLE_TUTORIAL:
state.isTutorialOpen = !state.isTutorialOpen
return state
}
switch (action.type) {
case ARTICLE_SAVE:
if (action.data.forceSwitch) {
let article = action.data.article
state.articleKey = article.key
state.search = ''
}
return state
case SWITCH_FOLDER:
state.search = `\/\/${action.data} `
return state
case SWITCH_ARTICLE:
state.articleKey = action.data.key
return state
case SET_SEARCH_FILTER:
state.search = action.data
return state
case SET_TAG_FILTER:
state.search = `#${action.data}`
return state
case CLEAR_SEARCH:
state.search = ''
return state
default:
return state
}
}
export default combineReducers({
user,
folders,
articles,
status
})

View File

@@ -1,5 +1,126 @@
import reducer from './reducer'
import { createStore } from 'redux'
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import ConfigManager from 'browser/main/lib/ConfigManager'
function storages (state = [], action) {
console.info('REDUX >> ', action)
switch (action.type) {
case 'INIT_ALL':
return action.storages
case 'ADD_STORAGE':
{
let storages = state.slice()
storages.push(action.storage)
return storages
}
case 'ADD_FOLDER':
case 'REMOVE_FOLDER':
case 'UPDATE_STORAGE':
case 'RENAME_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.storage.key)
storages.push(action.storage)
return storages
}
case 'REMOVE_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.key)
return storages
}
}
return state
}
function notes (state = [], action) {
switch (action.type) {
case 'INIT_ALL':
return action.notes
case 'ADD_STORAGE':
{
let notes = state.concat(action.notes)
return notes
}
case 'REMOVE_STORAGE':
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.key)
return notes
}
case 'REMOVE_FOLDER':
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.storage.key || note.folder !== action.key)
return notes
}
case 'CREATE_NOTE':
{
let notes = state.slice()
notes.push(action.note)
return notes
}
case 'UPDATE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
notes.push(action.note)
return notes
}
case 'MOVE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
notes.push(action.newNote)
return notes
}
case 'REMOVE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
return notes
}
}
return state
}
const defaultConfig = ConfigManager.get()
function config (state = defaultConfig, action) {
switch (action.type) {
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
let reducer = combineReducers({
storages,
notes,
config,
routing: routerReducer
})
let store = createStore(reducer)

130
browser/styles/index.styl Normal file
View File

@@ -0,0 +1,130 @@
$brand-color = #6AA5E9
$danger-color = #c9302c
$danger-lighten-color = lighten(#c9302c, 5%)
// Layouts
$statusBar-height = 24px
$sideNav-width = 200px
$sideNav--folded-width = 44px
$topBar-height = 50px
// UI default
$ui-text-color = #515151
$ui-inactive-text-color = #939395
$ui-borderColor = #D1D1D1
$ui-backgroundColor = #FAFAFA
$ui-border = solid 1px $ui-borderColor
$ui-active-color = #6AA5E9
// UI Button
$ui-button-color = #939395
$ui-button--hover-backgroundColor = rgba(126, 127, 129, 0.08)
$ui-button--active-color = white
$ui-button--active-backgroundColor = #6AA5E9
$ui-button--focus-borderColor = lighten(#369DCD, 25%)
// UI Tooltip
$ui-tooltip-text-color = white
$ui-tooltip-backgroundColor = alpha(#444, 70%)
$ui-tooltip-button-backgroundColor = #D1D1D1
$ui-tooltip-button--hover-backgroundColor = lighten(#D1D1D1, 30%)
// UI Input
$ui-input--focus-borderColor = #369DCD
$ui-input--disabled-backgroundColor = #DDD
/*
* # Border
*/
$border-color = #D0D0D0
$active-border-color = #369DCD
$focus-border-color = #369DCD
$default-border = solid 1px $border-color
$active-border = solid 1px $active-border-color
/**
* # Button styles
*/
// Default button
$default-button-background = white
$default-button-background--hover = #e6e6e6
$default-button-background--active = #d4d4d4
colorDefaultButton()
background-color $default-button-background
&:hover
background-color $default-button-background--hover
&:active
background-color $default-button-background--active
&:active:hover
background-color $default-button-background--active
// Primary button(Brand color)
$primary-button-background = $brand-color
$primary-button-background--hover = darken($brand-color, 5%)
$primary-button-background--active = darken($brand-color, 10%)
colorPrimaryButton()
color white
background-color $primary-button-background
&:hover
background-color $primary-button-background--hover
&:active
background-color $primary-button-background--active
&:active:hover
background-color $primary-button-background--activ
// Danger button(Brand color)
$danger-button-background = #c9302c
$danger-button-background--hover = darken(#c9302c, 5%)
$danger-button-background--active = darken(#c9302c, 10%)
colorDangerButton()
color white
background-color $danger-button-background
&:hover
background-color $danger-button-background--hover
&:active
background-color $danger-button-background--active
&:active:hover
background-color $danger-button-background--active
/**
* Nav
*/
navButtonColor()
border none
color $ui-button-color
background-color transparent
transition color background-color 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
&:active
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:active, &:active:hover
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
/**
* # Modal Stuff
* These will be moved lib/modal
*/
$modal-z-index = 1002
$modal-background = white
$modal-margin = 64px auto 64px
$modal-border-radius = 5px
modal()
position relative
z-index $modal-z-index
width 100%
background-color $modal-background
overflow hidden
border-radius $modal-border-radius
box-shadow 2px 2px 10px gray

View File

@@ -1,393 +0,0 @@
noTagsColor = #999
infoButton()
display inline-block
border-radius 16.5px
cursor pointer
height 33px
width 33px
line-height 33px
margin-right 5px
font-size 18px
color inactiveTextColor
background-color white
padding 0
border 1px solid white
&:focus
border-color focusBorderColor
&:hover
color inherit
.ArticleDetail
absolute right bottom
top 60px
left 450px
padding 10px
background-color #E6E6E6
border-top 1px solid borderColor
&.empty
.ArticleDetail-empty-box
line-height 72px
font-size 42px
height 320px
display flex
align-items center
.ArticleDetail-empty-box-message
text-align center
width 100%
color inactiveTextColor
.ArticleDetail-info
height 70px
width 100%
font-size 12px
user-select none
&>.tutorial
position fixed
z-index 35
.ArticleDetail-info-folder
display inline-block
max-width 100px
overflow ellipsis
height 10px
width 150px
height 27px
outline none
background-color darken(white, 5%)
border 1px solid transparent
&:hover
background-color white
&:focus
border-color focusBorderColor
&>.tutorial
position fixed
z-index 35
.ArticleDetail-info-status
padding 0 5px
.unsaved-mark
color brandColor
.ArticleDetail-info-control
float right
clearfix
.ShareButton
display block
float left
&>button, .ShareButton-open-button
infoButton()
.tooltip
tooltip()
margin-top 30px
&:hover
.tooltip
opacity 1
&>button
float left
&:nth-child(1) .tooltip
margin-left -65px
.ArticleDetail-info-control-delete-button
.tooltip
right 5px
.ArticleDetail-info-control-save
float left
width 80px
margin-right 5px
overflow hidden
transition width 0.15s ease-in-out
border-radius 16.5px
&.hide
width 0px
opacity 0.2
.ArticleDetail-info-control-save-button
infoButton()
background-color brandColor
color white
font-size 12px
width 100%
border 1px solid brandBorderColor
white-space nowrap
.fa
font-size 18px
&:hover
color white
background-color lighten(brandColor, 15%)
&:focus
color white
background-color lighten(brandColor, 15%)
.tooltip
tooltip()
margin-top 30px
margin-left -90px
&:hover .tooltip
opacity 1
.ShareButton-open-button .tooltip
margin-left -40px
.ShareButton-dropdown
position fixed
width 185px
z-index 35
background-color #F0F0F0
padding 4px 0
border-radius 5px
right 5px
top 95px
box-shadow 0px 0px 10px 1px alpha(#bbb, 0.8)
border 1px solid #bcbcbc
&.hide
display none
&>button
background-color transparent
height 21px
width 100%
border none
padding-left 20px
text-align left
font-size 13px
font-family '.HelveticaNeueDeskInterface-Regular', sans-serif
&:hover
background-color #4297FE
color white
.ShareButton-url
height 40px
width 100%
position relative
padding 0 5px
.ShareButton-url-input
height 21px
border none
width 143px
float left
border-top-left-radius 3px
border-bottom-left-radius 3px
border 1px solid borderColor
border-right none
.ShareButton-url-button
height 21px
border none
width 30px
float left
background-color #F0F0F0
border-top-right-radius 3px
border-bottom-right-radius 3px
border 1px solid borderColor
.ShareButton-url-button-tooltip
tooltip()
right 10px
margin-top 5px
&:hover
.ShareButton-url-button-tooltip
opacity 1
&:active
background-color #4297FE
color white
.ShareButton-url-alert
padding 10px
line-height 16px
.ArticleDetail-info-row2
.tutorial
position fixed
z-index 35
font-style italic
.TagSelect
margin-top 5px
.TagSelect-tags
white-space nowrap
overflow-x auto
position relative
noSelect()
z-index 30
background-color #E6E6E6
clearfix()
.TagSelect-tags-item
background-color transparent
color white
margin 0 2px
padding 0
height 17px
float left
button.TagSelect-tags-item-remove
display block
float left
background-color transparent
border none
font-size 8px
color white
width 15px
height 17px
text-align center
line-height 12px
padding 0
margin 0
border-top solid 1px darken(brandColor, 5%)
border-bottom solid 1px darken(brandColor, 5%)
border-left solid 1px darken(brandColor, 5%)
border-right solid 1px transparent
border-radius left 2px
background-color brandColor
&:hover
background-color lighten(brandColor, 10%)
border-color lighten(brandColor, 10%)
&:focus
background-color lighten(brandColor, 10%)
border-color focusBorderColor
.TagSelect-tags-item-label
background-color brandColor
float left
font-size 12px
border-top solid 1px darken(brandColor, 5%)
border-bottom solid 1px darken(brandColor, 5%)
border-right solid 1px darken(brandColor, 5%)
line-height 15px
padding 0 5px
border-radius right 2px
input.TagSelect-input
background-color transparent
border none
border-bottom 1px solid transparent
outline none
margin 0 2px
transition 0.15s
height 18px
&:focus
border-color focusBorderColor
.TagSelect-suggest
position fixed
width 150px
max-height 150px
background-color white
z-index 50
border 1px solid borderColor
border-radius 5px
overflow-y auto
&>button
width 100%
display block
padding 0 15px
height 33px
line-height 33px
background-color transparent
border none
text-align left
font-size 14px
&:hover
background-color darken(white, 10%)
.ArticleDetail-panel
position absolute
top 70px
left 10px
right 10px
bottom 10px
overflow-x hidden
overflow-y auto
background-color white
border-radius 5px
border solid 1px lighten(borderColor, 15%)
&>.ArticleDetail-panel-header
display block
height 60px
&>.tutorial
fixed right
z-index 35
font-style italic
.ArticleDetail-panel-header-mode
z-index 30
background-color white
absolute top bottom
right 10px
display block
height 33px
margin-top 14px
width 120px
margin-right 15px
border solid 1px borderColor
border-radius 5px
transition width 0.15s
user-select none
&.idle
cursor pointer
&:hover
background-color darken(white, 5%)
.ModeIcon
padding 0 5px
line-height 33px
&.edit
border-color focusBorderColor
input
width 120px
line-height 31px
padding 0 10px
border none
outline none
background-color transparent
font-size 14px
.ModeSelect-options
position fixed
width 120px
z-index 10
border 1px solid borderColor
border-radius 5px
background-color white
max-height 250px
overflow-y auto
margin-top 5px
.ModeSelect-options-item
height 33px
line-height 33px
cursor pointer
&.active, &:hover.active
background-color brandColor
color white
.ModeIcon
width 30px
text-align center
display inline-block
&:hover
background-color darken(white, 10%)
.ArticleDetail-panel-header-title
absolute left top
right 145px
padding 0 15px
background-color transparent
input
border none
line-height 60px
width 100%
font-size 24px
outline none
.ArticleEditor
absolute left right bottom
top 60px
.ArticleDetail-panel-content-tooltip
absolute bottom right
height 24px
background-color alpha(black, 0.5)
line-height 24px
color white
padding 0 15px
opacity 0
transition 0.1s
z-index 35
&:hover .ArticleDetail-panel-content-tooltip
opacity 1
.MarkdownPreview
absolute top left right bottom
marked()
box-sizing border-box
padding 5px 15px
border-top solid 1px borderColor
overflow-y auto
user-select all
&.empty
color lighten(inactiveTextColor, 10%)
user-select none
font-size 14px
&.lineNumbered
.lineNumber
display block
.CodeEditor
absolute top left right bottom
border-top solid 1px borderColor
min-height 300px
border-bottom-left-radius 5px
border-bottom-right-radius 5px

View File

@@ -1,105 +0,0 @@
articleItemHoverBgColor = darken(white, 5%)
articleItemColor = #777
.ArticleList
absolute bottom
top 60px
left 200px
width 250px
border-top 1px solid borderColor
border-right 1px solid borderColor
&:focus
border-color focusBorderColor
overflow-y auto
noSelect()
&>div
.ArticleList-item
border solid 2px transparent
position relative
min-height 110px
width 100%
cursor pointer
transition 0.1s
background-color white
padding 0 10px
font-size 12px
.ArticleList-item-top
clearfix()
padding-top 2px
line-height 18px
height 20px
color articleItemColor
font-size 11px
i
margin-right 4px
.folderName
overflow ellipsis
display inline-block
width 120px
.updatedAt
float right
line-height 18px
.unsaved-mark
color brandColor
.ArticleList-item-middle
font-size 16px
position relative
padding-top 6px
height 22px
.mode
position absolute
left 0
font-size 12px
line-height 16px
.title
position absolute
left 19px
right 0
overflow ellipsis
small
color #AAA
.ArticleList-item-middle2
padding-top 8px
pre
color lighten(inactiveTextColor, 10%)
white-space pre-wrap
overflow hidden
height 33px
line-height 14px
font-size 10px
code
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
.ArticleList-item-bottom
padding-bottom 5px
.tags
color articleItemColor
line-height 18px
word-wrap break-word
clearfix()
i.fa-tags
display inline
float left
padding 2px 2px 0 0
height 14px
line-height 13px
a
background-color brandColor
float left
color white
border-radius 2px
padding 1px 5px
margin 2px
height 14px
line-height 13px
font-size 10px
opacity 0.8
&:hover
opacity 1
&:hover, &.hover
background-color articleItemHoverBgColor
&:active, &.active
background-color white
&:active, &.active
border-color brandBorderColor
.divider
border-bottom solid 1px borderColor

View File

@@ -1,213 +0,0 @@
articleNavBgColor = #353535
articleCount = #999
.ArticleNavigator
background-color articleNavBgColor
absolute top bottom left
width 200px
border-right 1px solid borderColor
color white
user-select none
.userInfo
height 60px
display block
box-sizing content-box
border-bottom 1px solid borderColor
.userProfileName
color brandColor
font-size 28px
padding 6px 37px 0 10px
white-space nowrap
text-overflow ellipsis
overflow hidden
.userName
color white
padding-left 20px
margin-top 3px
.tutorial
position fixed
z-index 35
top 0
left 0
pointer-event none
font-style italic
transition 0.1s
&.hide
opacity 0
.settingBtn
width 22px
height 22px
line-height 22px
border-radius 11px
position absolute
top 19px
right 14px
color white
padding 0
background-color transparent
border 1px solid white
z-index 31
.tooltip
tooltip()
margin-top -5px
margin-left 10px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.ArticleNavigator-unsaved
position absolute
top 100px
width 100%
height 225px
transition opacity 0.2s ease-in-out
&.hide
opacity 0.2
.ArticleNavigator-unsaved-header
border-bottom 1px solid alpha(borderColor, 0.5)
padding-bottom 5px
clearfix()
position relative
padding-left 10px
font-size 18px
line-height 22px
.ArticleNavigator-unsaved-list
height 165px
padding 5px 0
overflow-y scroll
.ArticleNavigator-unsaved-list-item
height 33px
padding-left 15px
clearfix()
transition 0.1s
cursor pointer
overflow hidden
&:hover
background-color alpha(white, 0.05)
&.active, &:active
background-color alpha(lighten(brandColor, 25%), 70%)
.ArticleNavigator-unsaved-list-item-label
float left
width 151px
line-height 33px
overflow ellipsis
.ArticleNavigator-unsaved-list-item-label-untitled
color inactiveTextColor
.ArticleNavigator-unsaved-list-item-discard-button
float right
width 33px
line-height 30px
height 33px
border none
background-color transparent
color white
font-size 18px
opacity 0.5
&:hover
opacity 1
.ArticleNavigator-unsaved-list-empty
height 33px
padding-left 15px
color alpha(white, 0.4)
transition 0.1s
line-height 33px
&:hover
color alpha(white, 0.6)
.ArticleNavigator-unsaved-control
absolute bottom
height 33px
border-top 1px solid alpha(borderColor, 0.5)
width 100%
.ArticleNavigator-unsaved-control-save-all-button
border none
background-color transparent
font-size 14px
color brandColor
padding-left 15px
width 100%
height 33px
text-align left
&:hover
color lighten(brandColor, 15%)
background-color alpha(white, 0.05)
&:active
color white
&:disabled
color alpha(brandColor, 0.5)
&:hover
color alpha(lighten(brandColor, 25%), 0.5)
background-color transparent
.ArticleNavigator-folders
absolute bottom
top 365px
width 100%
transition top 0.15s ease-in-out
background-color articleNavBgColor
.tutorial
position fixed
z-index 35
font-style italic
&.expand
top 100px
.ArticleNavigator-folders-header
border-bottom 1px solid alpha(borderColor, 0.5)
padding-bottom 5px
clearfix()
position relative
z-index 30
.title
float left
padding-left 10px
font-size 18px
line-height 22px
.addBtn
float right
margin-right 15px
width 22px
height 22px
font-size 10px
padding 0
line-height 22px
border 1px solid white
border-radius 11px
background-color transparent
color white
padding 0
font-weight bold
.tooltip
tooltip()
margin-top -6px
margin-left 11px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.folderList
absolute bottom
top 33px
overflow-y auto
.folderList button
height 33px
width 199px
border none
text-align left
font-size 14px
background-color transparent
color white
padding-left 15px
overflow ellipsis
&:hover
background-color alpha(white, 0.05)
&.active, &:active
background-color alpha(lighten(brandColor, 25%), 70%)
.articleCount
color white
.articleCount
color articleCount
font-size 12px

View File

@@ -1,224 +0,0 @@
bgColor = #E6E6E6
inputBgColor = white
topBarBtnColor = #B3B3B3
topBarBtnBgColor = #B3B3B3
topBarBtnBgActiveColor = #3A3A3A
infoBtnColor = bgColor
infoBtnBgColor = #B3B3B3
infoBtnActiveBgColor = #3A3A3A
.ArticleTopBar
absolute top right
left 200px
height 60px
background-color bgColor
user-select none
&>.tutorial
.clickJammer
fixed top left bottom right
z-index 40
background transparent
.global
fixed bottom right
height 100px
z-index 35
font-style italic
.finder
fixed bottom right
height 250px
left 50%
margin-left -250px
z-index 35
font-style italic
.back
fixed top left bottom right
z-index 20
background-color transparentify(black, 80%)
&>.ArticleTopBar-left
float left
&>.tutorial
fixed top
left 100px
top 30px
z-index 36
font-style italic
&>.ArticleTopBar-left-search
position relative
float left
height 33px
margin-top 13.5px
margin-left 15px
width 350px
padding 5px 15px
transition 0.1s
font-size 16px
border 1px solid transparent
z-index 30
.tooltip
tooltip()
margin-left -24px
margin-top 35px
opacity 1
&.hide
opacity 0
ul
li
line-height 18px
li:last-child
line-height 10px
margin-bottom 3px
small
font-size 10px
position relative
top -2px
margin-left 15px
input
absolute top left
width 350px
border-radius 16.5px
background-color inputBgColor
border 1px solid transparent
padding-left 35px
outline none
font-size 14px
height 33px
line-height 33px
z-index 0
&:focus
border-color focusBorderColor
i.fa.fa-search
position absolute
display block
top 0
left 10px
line-height 33px
z-index 1
pointer-events none
.ArticleTopBar-left-search-clear-button
position absolute
top 6px
right 10px
width 20px
height 20px
border-radius 10px
border none
background-color transparent
color topBarBtnColor
transition 0.1s
line-height 20px
text-align center
padding 0
&:focus
color textColor
&:hover
color white
background-color topBarBtnBgColor
&:active
color white
background-color darken(topBarBtnBgColor, 35%)
.ArticleTopBar-left-control
line-height 33px
float left
height 33px
margin-top 13.5px
margin-left 20px
.tutorial
fixed top
left 200px
z-index 36
font-style italic
button.ArticleTopBar-left-control-new-post-button
position fixed
background bgColor
font-size 20px
border none
outline none
color inactiveTextColor
width 33px
height 33px
border-radius 16.5px
transition 0.1s
border 1px solid transparent
z-index 30
&:hover
color textColor
&:active
color textColor
background-color lighten(topBarBtnBgColor, 15%)
&:disabled
color inactiveTextColor
background transparent
&:focus
color textColor
.tooltip
tooltip()
margin-left -80px
margin-top 40px
&:hover
.tooltip
opacity 1
&>.ArticleTopBar-right
float right
&>button
display block
position absolute
right 74px
top 20px
width 20px
height 20px
font-size 14px
line-height 14px
background-color infoBtnBgColor
color bgColor
border-radius 11px
border 1px solid bgColor
transition 0.1s
&:focus
background-color lighten(infoBtnActiveBgColor, 15%)
.tooltip
tooltip()
margin-left -50px
margin-top 20px
&:hover
background-color infoBtnActiveBgColor
.tooltip
opacity 1
&>.ArticleTopBar-right-links-button
display block
position absolute
top 8px
right 15px
opacity 0.7
border-radius 23px
height 46px
width 46px
border 1px solid transparent
&:focus
border-color focusBorderColor
&:hover
opacity 1
.tooltip
opacity 1
&>.ArticleTopBar-right-links-button-dropdown
position fixed
z-index 50
right 10px
top 40px
background-color transparentify(invBackgroundColor, 80%)
padding 5px 0
.ArticleTopBar-right-links-button-dropdown-item
padding 0 10px
height 33px
width 100%
display block
line-height 33px
text-decoration none
color white
&:hover
background-color transparentify(lighten(invBackgroundColor, 30%), 80%)

View File

@@ -1,86 +0,0 @@
userNavigatorBgColor = #1B1C1C
userNavigatorColor = #DDD
userAnchorColor = #979797
userAnchorBgColor = #BEBEBE
userAnchorActiveColor = textColor
userAnchorActiveBgColor = white
.UserNavigator
noSelect()
background-color userNavigatorBgColor
absolute left top bottom
width 60px
text-align center
box-sizing border-box
ul.userList
position absolute
top 25px
left 0
right 0
bottom 70px
// overflow-y auto
&>li
a
display block
width 38px
height 64px
margin 0 auto 10px
text-align center
text-decoration none
color userAnchorColor
line-height 44px
font-size 1.1em
cursor pointer
transition 0.1s
img.ProfileImage
width 38px
height 38px
border-radius 22px
opacity 0.7
&:hover
img.ProfileImage
opacity 1
.userTooltip
opacity 1
&.active
img.ProfileImage
opacity 1
.userTooltip
tooltip()
position absolute
margin-top -52px
margin-left 44px
.keyLabel
margin-top -25px
font-size 0.8em
color userNavigatorColor
button.createTeamBtn
display block
margin 0 auto
width 30px
height 30px
border-radius 15px
border 2px solid darken(white, 5%)
color darken(white, 5%)
text-align center
background-image none
background-color transparent
box-sizing border-box
absolute left right
bottom 15px
font-size 22px
line-height 22px
transition 0.1s
.tooltip
tooltip()
margin-top -26px
margin-left 30px
&:hover, &.hover, &:focus, &.focus
color white
border-color white
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor

View File

@@ -1,13 +1,6 @@
@import '../../../node_modules/nib/lib/nib'
@import '../vars'
@import '../mixins/*'
global-reset()
@import '../shared/*'
@import './ArticleNavigator'
@import './ArticleTopBar'
@import './ArticleList'
@import './ArticleDetail'
@import './modal/*'
@import '../theme/*'
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
@@ -96,63 +89,25 @@ textarea.block-input
#content
fullsize()
.Main
.appUpdateButton
position fixed
z-index 2000
bottom 5px
right 53px
padding 10px 15px
border none
border-radius 5px
background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)
.contactButton
position fixed
z-index 2000
bottom 5px
right 5px
padding 10px 15px
border none
border-radius 5px
background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)
.tooltip
tooltip()
margin-top -22px
margin-left -107px
&:hover .tooltip
opacity 1
modalZIndex= 1000
modalBackColor = transparentify(black, 65%)
.OSSAnnounceModal
height 250
text-align center
.OSSAnnounceModal-title
font-size 32px
padding 45px 0
.OSSAnnounceModal-link
display block
font-size 20px
margin 25px 0 65px
.OSSAnnounceModal-closeBtn
display block
margin 0 auto
border none
.ModalBase
fixed top left bottom right
z-index modalZIndex
&.hide
display none
.modalBack
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
.modal
position relative
width 650px
margin 50px auto 0
z-index modalZIndex + 2
background-color white
padding 15px
color #666666
border-radius 5px
width 150px
height 33px
background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)

View File

@@ -1,91 +0,0 @@
tabNavColor = #999999
iptFocusBorderColor = #369DCD
.CreateNewFolder.modal
width 600px
height 450px
.closeBtn
position absolute
top 15px
right 15px
width 33px
height 33px
font-size 18px
line-height 33px
padding 0
text-align center
background-color transparent
border none
color stripBtnColor
&:hover
color stripHoverBtnColor
.title
font-size 32px
text-align center
font-weight bold
margin-top 25px
.ipt
display block
width 330px
font-size 14px
height 44px
line-height 44px
padding 0 15px
border-radius 5px
border solid 1px borderColor
outline none
margin 75px auto 20px
&:focus
border-color iptFocusBorderColor
.colorSelect
text-align center
.option
cursor pointer
font-size 22px
height 48px
width 48px
margin 0 2px
border 1px solid transparent
border-radius 5px
overflow hidden
line-height 45px
text-align center
transition 0.1s
display inline-block
&:hover
border-color borderColor
font-size 28px
&.active
font-size 28px
border-color iptFocusBorderColor
.alert
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 15px auto 0
&.error
color errorTextColor
background-color errorBackgroundColor
.confirmBtn
display block
position absolute
left 205px
bottom 44px
width 240px
font-size 24px
height 44px
line-height 24px
font-weight bold
background-color brandColor
color white
border none
border-radius 5px
margin 0 auto
transition 0.1s
&:hover
transform scale(1.1)
&:disabled
opacity 0.7

View File

@@ -1,199 +0,0 @@
tabNavColor = #999999
iptFocusBorderColor = #369DCD
stripHoverBtnColor = #333
stripBtnColor = lighten(stripHoverBtnColor, 35%)
.CreateNewTeam.modal
width 600px
height 450px
.closeBtn
position absolute
top 15px
right 15px
width 33px
height 33px
font-size 18px
line-height 33px
padding 0
text-align center
background-color transparent
border none
color stripBtnColor
&:hover
color stripHoverBtnColor
.title
font-size 32px
text-align center
font-weight bold
margin-top 25px
.ipt
display block
width 330px
font-size 14px
height 44px
line-height 44px
padding 0 15px
border-radius 5px
border solid 1px borderColor
outline none
&:focus
border-color iptFocusBorderColor
.alert
padding 0 15px
height 44px
line-height 44px
width 300px
margin 0 auto
border-radius 5px
color infoTextColor
background-color infoBackgroundColor
white-space nowrap
overflow-x auto
&.error
color errorTextColor
background-color errorBackgroundColor
.confirmBtn
display block
position absolute
left 180px
bottom 44px
width 240px
font-size 24px
height 44px
line-height 24px
font-weight bold
background-color brandColor
color white
border none
border-radius 5px
margin 0 auto
transition 0.1s
&:hover
transform scale(1.1)
&:disabled
opacity 0.7
.tabNav
absolute left right
bottom 15px
height 33px
line-height 33px
width 150px
text-align center
font-size 12px
color tabNavColor
margin 0 auto
transition 0.1s
i.active
color brandColor
.createTab
.ipt
margin 105px auto 15px
.selectTab
.memberForm
display block
margin 25px auto 15px
width 330px
clearfix()
padding 0
font-size 14px
height 44px
line-height 44px
outline none
.Select.memberName
display block
margin 0
float left
width 280px
height 44px
font-size 14px
border none
line-height 44px
background-color transparent
outline none
&.is-focus
.Select-control
border-color iptFocusBorderColor
.Select-control
height 44px
line-height 44px
padding 0 0 0 15px
border-radius 5px 0 0 5px
border 1px solid borderColor
border-right none
.Select-placeholder
padding 0 0 0 15px
.Seleect-arrow
top 21px
.Select-clear
padding 0 10px
.Select-noresults, .Select-option
line-height 44px
padding 0 0 0 15px
&:focus, &.focus
border-color iptFocusBorderColor
button
font-weight 400
height 44px
cursor pointer
margin 0
padding 0
width 50px
float right
border none
background-color brandColor
border-top-right-radius 5px
border-bottom-right-radius 5px
color white
font-size 14px
.memberList
width 480px
margin 0 auto
height 190px
overflow scroll
border-bottom 1px solid borderColor
&>li
border-bottom 1px solid borderColor
height 44px
padding 0 25px
clearfix()
&:nth-last-child(1)
border-bottom-color transparent
.userPhoto
width 30px
height 30px
float left
margin-top 7px
margin-right 15px
border-radius 15px
.userInfo
float left
margin-top 7px
.userName
font-size 16px
margin-bottom 2px
.userEmail
font-size 12px
.userControl
float right
.userRole
float left
height 30px
background-color transparent
border 1px solid transparent
margin-top 7px
margin-right 35px
outline none
cursor pointer
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
button
border none
height 30px
margin-top 7px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor

View File

@@ -1,33 +0,0 @@
.DeleteArticleModal.modal
width 350px !important
top 100px
user-select none
.title
font-size 24px
margin-bottom 15px
.message
font-size 14px
margin-bottom 15px
.control
text-align right
button
border-radius 5px
height 33px
padding 0 15px
font-size 14px
background-color white
border 1px solid borderColor
border-radius 5px
margin-left 5px
&:hover
background-color darken(white, 10%)
&:focus
border-color focusBorderColor
&.danger
border-color #E9432A
background-color #E9432A
color white
&:hover
background-color lighten(#E9432A, 15%)
&:focus
background-color lighten(#E9432A, 15%)

View File

@@ -1,628 +0,0 @@
menuColor = #808080
menuBgColor = #E6E6E6
closeBtnBgColor = #1790C6
iptFocusBorderColor = #369DCD
.Preferences.modal
padding 0
border-radius 5px
overflow hidden
width 720px
height 600px
&>.header
absolute top left right
height 50px
border-bottom 1px solid borderColor
background-color menuBgColor
&>.title
font-size 22px
font-weight bold
float left
padding-left 30px
line-height 50px
&>.closeBtn
float right
font-size 14px
background-color closeBtnBgColor
color white
padding 0 15px
height 33px
margin-top 9px
margin-right 15px
border none
border-radius 5px
&:hover
background-color lighten(closeBtnBgColor, 10%)
&>.nav
absolute left bottom
top 50px
width 180px
background-color menuBgColor
border-right 1px solid borderColor
&>button
width 100%
height 44px
font-size 18px
color menuColor
border none
background-color transparent
transition 0.1s
text-align left
padding-left 15px
&:hover
background-color darken(menuBgColor, 10%)
&.active, &:active
background-color brandColor
color white
&>.content
absolute right bottom
top 50px
left 180px
overflow-y auto
&>.section
padding 10px 20px
border-bottom 1px solid borderColor
overflow-y auto
&:nth-last-child(1)
border-bottom none
&>.sectionTitle
font-size 18px
margin 10px 0 5px
color brandColor
&>.sectionCheck
margin-bottom 5px
height 33px
label
width 150px
padding-left 15px
line-height 33px
.sectionCheck-warn
font-size 12px
margin-left 10px
border-left 2px solid brandColor
padding-left 5px
&>.sectionInput
margin-bottom 5px
clearfix()
height 33px
label
width 150px
padding-left 15px
float left
line-height 33px
input
width 250px
float left
height 33px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
&:focus
border-color iptFocusBorderColor
&>.sectionSelect
margin-bottom 5px
clearfix()
height 33px
label
width 150px
padding-left 15px
float left
line-height 33px
select
float left
width 200px
height 25px
margin-top 4px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
&:focus
border-color iptFocusBorderColor
&>.sectionMultiSelect
margin-bottom 5px
clearfix()
height 33px
label
width 150px
padding-left 15px
float left
line-height 33px
.sectionMultiSelect-input
float left
select
width 80px
height 25px
margin-top 4px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
margin-left 5px
margin-right 15px
&:focus
border-color iptFocusBorderColor
&>.sectionConfirm
clearfix()
padding 5px 15px
button
float right
background-color brandColor
color white
border none
border-radius 5px
height 33px
padding 0 15px
font-size 14px
&:hover
background-color lighten(brandColor, 10%)
.alert
float right
width 250px
padding 10px 15px
margin 0 10px 0
.alert
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 10px auto
&.error
color errorTextColor
background-color errorBackgroundColor
&.ContactTab
padding 10px
.title
font-size 18px
color brandColor
margin-top 10px
margin-bottom 10px
p
line-height 2
&.AppSettingTab
.description
marked()
&.TeamSettingTab
.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.teamDeleteConfirm
label
line-height 33px
font-size 14px
.teamDelete
label
line-height 33px
font-size 18px
color brandColor
.teamDelete, .teamDeleteConfirm
padding 15px 20px 15px 15px
button
background-color white
height 33px
font-size 14px
padding 0 15px
border 1px solid borderColor
float right
margin 0 5px
border-radius 5px
&:hover
background-color darken(white, 10%)
button.deleteBtn
background-color brandColor
border none
color white
&:hover
background-color lighten(brandColor, 10%)
&.MemberSettingTab
&>.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.membersTableSection
.addMember
clearfix()
padding 10px
.addMemberLabel
font-size 14px
line-height 33px
float left
.addMemberControl
width 330px
float left
margin-left 25px
.Select
display block
margin 0
float left
width 280px
height 33px
font-size 14px
border none
line-height 33px
background-color transparent
outline none
&.is-focus
.Select-control
border-color iptFocusBorderColor
.Select-control
height 33px
line-height 33px
padding 0 0 0 15px
border-radius 5px 0 0 5px
border 1px solid borderColor
border-right none
.Select-placeholder
padding 0 0 0 15px
.Seleect-arrow
top 21px
.Select-clear
padding 0 10px
.Select-noresults, .Select-option
line-height 33px
padding 0 0 0 15px
button
font-weight 400
height 33px
cursor pointer
margin 0
padding 0
width 50px
float right
border none
background-color brandColor
border-top-right-radius 5px
border-bottom-right-radius 5px
color white
font-size 14px
.memberList
&>.header
clearfix()
&>.userName
float left
&>.role
float left
&>.control
float right
&>li
&.edit
.colDescription
font-size 14px
line-height 33px
padding-left 15px
float left
strong
font-size 16px
color brandColor
.colDeleteConfirm
float right
margin-right 15px
button
border none
height 30px
width 60px
margin-top 1.5px
font-size 14px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)
border-bottom 1px solid borderColor
height 44px
padding 0 25px
width 420px
margin 0 auto
clearfix()
&:nth-last-child(1)
border-bottom-color transparent
.colUserName
float left
width 250px
clearfix()
.userPhoto
width 30px
height 30px
float left
margin-top 7px
margin-right 15px
border-radius 15px
.userInfo
float left
margin-top 7px
width 205px
.userName
font-size 16px
margin-bottom 2px
overflow ellipsis
.userEmail
font-size 12px
overflow ellipsis
.colRole
float left
width 75px
.userRole
height 30px
background-color transparent
border 1px solid transparent
margin-top 7px
margin-right 35px
outline none
cursor pointer
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
&:disabled
border-color transparent
cursor not-allowed
.colDelete
width 45px
float right
text-align center
button.deleteButton
border none
height 30px
width 30px
margin-top 7px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.header
.colRole, .colDelete
text-align center
.colUserName, .colRole, .colDelete
line-height 44px
&.FolderSettingTab
&>.header
border-bottom 1px solid borderColor
padding 10px
font-size 18px
color brandColor
line-height 33px
.teamSelect
border 1px solid borderColor
height 33px
width 200px
margin 0 10px
outline none
font-size 14px
&:focus
border-color iptFocusBorderColor
.section
.folderTable
width 420px
margin 15px auto
&>div
border-bottom 1px solid borderColor
clearfix()
height 43px
line-height 33px
padding 5px 0
&:last-child
border-color transparent
.folderColor
float left
margin-left 10px
text-align center
width 44px
.folderName
float left
width 175px
overflow ellipsis
.folderControl
float right
width 125px
text-align center
&.folderHeader
.folderName
padding-left 25px
&.newFolder
.alert
display block
color infoTextColor
background-color infoBackgroundColor
font-size 14px
padding 15px 15px
width 330px
border-radius 5px
margin 0 auto
&.error
color errorTextColor
background-color errorBackgroundColor
.folderName input
height 33px
border 1px solid transparent
border-radius 5px
padding 0 10px
font-size 14px
outline none
width 150px
overflow ellipsis
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
.folderPublic select
height 33px
border 1px solid transparent
background-color white
outline none
display block
margin 0 auto
font-size 14px
&:hover
border-color borderColor
&:focus
border-color iptFocusBorderColor
.folderControl
button
border none
height 30px
margin-top 1.5px
font-size 14px
background-color transparent
color brandColor
&:hover
color lighten(brandColor, 10%)
&.FolderRow
.sortBtns
float left
display block
height 30px
width 30px
margin-top 1.5px
position absolute
button
absolute left
background-color transparent
border none
height 15px
padding 0
margin 0
color stripBtnColor
&:first-child
top 0
&:last-child
top 15px
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
.folderName input
height 33px
border 1px solid borderColor
border-radius 5px
padding 0 10px
font-size 14px
outline none
width 150px
&:focus
border-color iptFocusBorderColor
.folderColor
.select
height 33px
width 33px
border 1px solid borderColor
background-color white
outline none
display block
margin 0 auto
font-size 14px
border-radius 5px
&:focus
border-color iptFocusBorderColor
.options
position absolute
background-color white
text-align left
border 1px solid borderColor
border-radius 5px
padding 0 5px 5px
margin-left 5px
margin-top -34px
clearfix()
.label
margin-left 5px
line-height 22px
font-size 12px
button
float left
border none
width 33px
height 33px
margin-right 5px
border 1px solid transparent
line-height 29px
overflow hidden
border-radius 5px
background-color transparent
outline none
transition 0.1s
&:hover
border-color borderColor
&.active
border-color iptFocusBorderColor
.FolderMark
transform scale(1.4)
.folderControl
button
border none
height 30px
width 30px
margin-top 1.5px
font-size 14px
background-color transparent
color stripBtnColor
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
&.edit
.folderControl
button
width 60px
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)
&.delete
.folderDeleteLabel
float left
height 33px
width 250px
padding-left 15px
overflow ellipsis
strong
font-size 16px
color brandColor
.folderControl
button
width 60px
&.primary
color brandColor
&:hover
color lighten(brandColor, 10%)

View File

@@ -1,132 +0,0 @@
slideBgColor0 = #2BAC8F
slideBgColor1 = #F68F92
slideBgColor2 = #D6AD56
slideBgColor3 = #26969B
slideBgColor4 = #00B493
.Tutorial.modal
background-color slideBgColor0
color white !important
width 720px
height 480px
margin-top 75px
border-radius 5px
overflow hidden
.priorBtn, .nextBtn
font-size 72px
position absolute
background-color transparent
color transparentify(white, 50%)
transition 0.1s
border none
line-height 72px
padding 0
width 93px
height 72px
z-index 2
top 189px
&:hover
color white
&.hide
opacity 0
.priorBtn
left 15px
.nextBtn
right 15px
.title
text-align center
font-size 54px
margin 40px 0
.content
text-align center
font-size 22px
line-height 1.8
.dots
position absolute
left 0
right 0
bottom 25px
margin 0 auto
color gray
text-align center
z-index 2
&>i
transition 0.3s
&.active
color white
.slide
absolute top bottom left right
z-index 1
.slide0
background-color slideBgColor0
.content
margin-top 100px
.slide1
background-color slideBgColor1
.content
.markdown
background-color white
color textColor
width 480px
height 140px
margin 45px auto 0
clearfix()
text-align left
border-radius 5px
overflow hidden
.left
float left
width 240px
height 140px
box-sizing border-box
font-size 0.5em
padding 30px
border-right 1px solid borderColor
.right
width 240px
height 140px
float right
box-sizing border-box
padding: 28px 0 0 10px
font-size 0.45em
marked()
ul
padding-left 20px
.slide2
background-color slideBgColor2
.code
border-radius 5px
overflow hidden
text-align left
width 480px
heght 140px
margin 45px auto 0
font-size 14px
.ace_editor
height 140px
.slide3
background-color slideBgColor3
.title
margin-bottom 15px
.content
font-size 18px
&>img
margin-top 25px
.slide4
background-color slideBgColor4
.content
&>button
background-color white
color brandColor
font-size 60px
width 250px
height 250px
border-radius 125px
border none
transition 0.1s
&:hover
transform scale(1.2)

View File

@@ -1,21 +0,0 @@
modalZIndex= 1000
modalBackColor = transparentify(black, 65%)
.ModalBase
fixed top left bottom right
z-index modalZIndex
&.hide
display none
.modalBack
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
.modal
position relative
width 650px
margin 50px auto 0
z-index modalZIndex + 2
background-color white
padding 15px
color #666666
border-radius 5px

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