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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
151
browser/components/MarkdownEditor.js
Normal file
151
browser/components/MarkdownEditor.js
Normal 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)
|
||||
23
browser/components/MarkdownEditor.styl
Normal file
23
browser/components/MarkdownEditor.styl
Normal 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
|
||||
|
||||
@@ -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(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
253
browser/components/markdown.styl
Normal file
253
browser/components/markdown.styl
Normal 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
|
||||
@@ -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'
|
||||
|
||||
5
browser/lib/CSSModules.js
Normal file
5
browser/lib/CSSModules.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import CSSModules from 'react-css-modules'
|
||||
|
||||
export default function (component, styles) {
|
||||
return CSSModules(component, styles, {errorWhenNotFound: false})
|
||||
}
|
||||
@@ -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
22
browser/lib/consts.js
Normal 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
|
||||
@@ -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')))
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'))
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default function (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
17
browser/main/Detail/Detail.styl
Normal file
17
browser/main/Detail/Detail.styl
Normal 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
|
||||
286
browser/main/Detail/FolderSelect.js
Normal file
286
browser/main/Detail/FolderSelect.js
Normal 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)
|
||||
88
browser/main/Detail/FolderSelect.styl
Normal file
88
browser/main/Detail/FolderSelect.styl
Normal 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
|
||||
291
browser/main/Detail/MarkdownNoteDetail.js
Normal file
291
browser/main/Detail/MarkdownNoteDetail.js
Normal 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)
|
||||
90
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
90
browser/main/Detail/MarkdownNoteDetail.styl
Normal 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
|
||||
477
browser/main/Detail/SnippetNoteDetail.js
Normal file
477
browser/main/Detail/SnippetNoteDetail.js
Normal 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
|
||||
}
|
||||
<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)
|
||||
170
browser/main/Detail/SnippetNoteDetail.styl
Normal file
170
browser/main/Detail/SnippetNoteDetail.styl
Normal 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
|
||||
66
browser/main/Detail/StarButton.js
Normal file
66
browser/main/Detail/StarButton.js
Normal 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)
|
||||
11
browser/main/Detail/StarButton.styl
Normal file
11
browser/main/Detail/StarButton.styl
Normal 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)
|
||||
148
browser/main/Detail/TagSelect.js
Normal file
148
browser/main/Detail/TagSelect.js
Normal 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)
|
||||
63
browser/main/Detail/TagSelect.styl
Normal file
63
browser/main/Detail/TagSelect.styl
Normal 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
|
||||
105
browser/main/Detail/index.js
Normal file
105
browser/main/Detail/index.js
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'/> 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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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}/>
|
||||
{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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
159
browser/main/Main.js
Normal 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
30
browser/main/Main.styl
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
93
browser/main/NoteList/NoteList.styl
Normal file
93
browser/main/NoteList/NoteList.styl
Normal 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
|
||||
|
||||
299
browser/main/NoteList/index.js
Normal file
299
browser/main/NoteList/index.js
Normal 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)
|
||||
122
browser/main/SideNav/SideNav.styl
Normal file
122
browser/main/SideNav/SideNav.styl
Normal 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
|
||||
95
browser/main/SideNav/StorageItem.js
Normal file
95
browser/main/SideNav/StorageItem.js
Normal 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)
|
||||
89
browser/main/SideNav/StorageItem.styl
Normal file
89
browser/main/SideNav/StorageItem.styl
Normal 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
|
||||
116
browser/main/SideNav/index.js
Normal file
116
browser/main/SideNav/index.js
Normal 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)
|
||||
38
browser/main/StatusBar/StatusBar.styl
Normal file
38
browser/main/StatusBar/StatusBar.styl
Normal 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
|
||||
94
browser/main/StatusBar/index.js
Normal file
94
browser/main/StatusBar/index.js
Normal 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'/>
|
||||
{Math.floor(config.zoom * 100)}%
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StatusBar.propTypes = {
|
||||
config: PropTypes.shape({
|
||||
zoom: PropTypes.number
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(StatusBar, styles)
|
||||
106
browser/main/TopBar/TopBar.styl
Normal file
106
browser/main/TopBar/TopBar.styl
Normal 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
|
||||
216
browser/main/TopBar/index.js
Normal file
216
browser/main/TopBar/index.js
Normal 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'/>
|
||||
}
|
||||
{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)
|
||||
@@ -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
85
browser/main/global.styl
Normal 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
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
31
browser/main/lib/Commander.js
Normal file
31
browser/main/lib/Commander.js
Normal 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
|
||||
}
|
||||
84
browser/main/lib/ConfigManager.js
Normal file
84
browser/main/lib/ConfigManager.js
Normal 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
|
||||
}
|
||||
30
browser/main/lib/ZoomManager.js
Normal file
30
browser/main/lib/ZoomManager.js
Normal 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
565
browser/main/lib/dataApi.js
Normal 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
|
||||
}
|
||||
26
browser/main/lib/eventEmitter.js
Normal file
26
browser/main/lib/eventEmitter.js
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
243
browser/main/modals/InitModal.js
Normal file
243
browser/main/modals/InitModal.js
Normal 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)
|
||||
86
browser/main/modals/InitModal.styl
Normal file
86
browser/main/modals/InitModal.styl
Normal 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
|
||||
140
browser/main/modals/NewNoteModal.js
Normal file
140
browser/main/modals/NewNoteModal.js
Normal 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)
|
||||
56
browser/main/modals/NewNoteModal.styl
Normal file
56
browser/main/modals/NewNoteModal.styl
Normal 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
|
||||
412
browser/main/modals/PreferencesModal/ConfigTab.js
Normal file
412
browser/main/modals/PreferencesModal/ConfigTab.js
Normal 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>
|
||||
<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'
|
||||
/>
|
||||
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)
|
||||
88
browser/main/modals/PreferencesModal/ConfigTab.styl
Normal file
88
browser/main/modals/PreferencesModal/ConfigTab.styl
Normal 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
|
||||
|
||||
|
||||
58
browser/main/modals/PreferencesModal/InfoTab.js
Normal file
58
browser/main/modals/PreferencesModal/InfoTab.js
Normal 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
|
||||
<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)
|
||||
35
browser/main/modals/PreferencesModal/InfoTab.styl
Normal file
35
browser/main/modals/PreferencesModal/InfoTab.styl
Normal 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
|
||||
37
browser/main/modals/PreferencesModal/PreferencesModal.styl
Normal file
37
browser/main/modals/PreferencesModal/PreferencesModal.styl
Normal 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
|
||||
351
browser/main/modals/PreferencesModal/StorageItem.js
Normal file
351
browser/main/modals/PreferencesModal/StorageItem.js
Normal 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'/>
|
||||
{storage.name}
|
||||
<span styleName='header-label-path'>({storage.path})</span>
|
||||
<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)
|
||||
119
browser/main/modals/PreferencesModal/StorageItem.styl
Normal file
119
browser/main/modals/PreferencesModal/StorageItem.styl
Normal 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()
|
||||
223
browser/main/modals/PreferencesModal/StoragesTab.js
Normal file
223
browser/main/modals/PreferencesModal/StoragesTab.js
Normal 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)
|
||||
115
browser/main/modals/PreferencesModal/StoragesTab.styl
Normal file
115
browser/main/modals/PreferencesModal/StoragesTab.styl
Normal 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
|
||||
112
browser/main/modals/PreferencesModal/index.js
Normal file
112
browser/main/modals/PreferencesModal/index.js
Normal 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))
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
130
browser/styles/index.styl
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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%)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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%)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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%)
|
||||
@@ -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%)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user