mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faf0efb20 | ||
|
|
ed70cb8e3d | ||
|
|
2544b5b821 | ||
|
|
90613b7cc8 | ||
|
|
173640d0b4 | ||
|
|
e70d09ebbf | ||
|
|
1ff33378e1 | ||
|
|
286739f770 | ||
|
|
45b1cd3942 | ||
|
|
b2d34ab95d | ||
|
|
f628ed55cf | ||
|
|
70ec9f50ab | ||
|
|
d6d130b8f9 | ||
|
|
40410eb10f | ||
|
|
a6e3dbd825 | ||
|
|
3882df41f1 | ||
|
|
6c67b96e30 | ||
|
|
b6d34472fe | ||
|
|
69d11b5cd0 | ||
|
|
f36f2029e5 | ||
|
|
af7476f79a | ||
|
|
890207daee | ||
|
|
9a5653f1e3 | ||
|
|
4f45ba1680 | ||
|
|
0ccfd36a83 | ||
|
|
8452a95114 | ||
|
|
9b98035ee7 | ||
|
|
6dc75afdc8 | ||
|
|
922ae7a274 | ||
|
|
2d46d12628 | ||
|
|
44f270f408 | ||
|
|
9ff70c4aef | ||
|
|
8c6a54397f | ||
|
|
ce915df2b2 | ||
|
|
84dd772b7c | ||
|
|
959f6c2c58 | ||
|
|
793b2b20e9 | ||
|
|
1c224fedc5 | ||
|
|
4299baec5d | ||
|
|
e931d3efbc | ||
|
|
36d81a6ec9 | ||
|
|
22c2d09d88 | ||
|
|
558659b54d | ||
|
|
eb1a0ba49f | ||
|
|
45212e7e14 | ||
|
|
c691af9712 | ||
|
|
0c2226b3fc | ||
|
|
eeda6b0208 | ||
|
|
d9096424c5 | ||
|
|
d171078ca4 | ||
|
|
cb18b8f347 | ||
|
|
4fdb72f93c | ||
|
|
549930b48c | ||
|
|
409a3de901 | ||
|
|
431ac5a403 | ||
|
|
999d4633ab | ||
|
|
5a26fc812d | ||
|
|
eb210e9072 | ||
|
|
dbfd25bb8e | ||
|
|
4292583d73 | ||
|
|
7c346d62eb | ||
|
|
14abbee3fb | ||
|
|
903979b3f2 | ||
|
|
913e649e5d | ||
|
|
7f8733796e | ||
|
|
1a98afee92 | ||
|
|
60daafe136 | ||
|
|
c7ad06a1f7 | ||
|
|
d07c795511 | ||
|
|
89d1071dc1 | ||
|
|
dfe34a9f22 | ||
|
|
93d3ea70fc | ||
|
|
89a76d9ead | ||
|
|
dc6bd1aae8 | ||
|
|
3412d70737 | ||
|
|
ce52d5cf42 | ||
|
|
da19066fb8 | ||
|
|
be20c2c800 | ||
|
|
c59638aaae | ||
|
|
00691f1225 | ||
|
|
735b79a4e0 | ||
|
|
443b529667 | ||
|
|
c3e62f58ab | ||
|
|
9869545777 | ||
|
|
04e5034f5f | ||
|
|
e99e3eb6c4 | ||
|
|
508615b600 | ||
|
|
4d3d416ecb | ||
|
|
be2897806a | ||
|
|
6ebde04d22 | ||
|
|
9deaa8694e | ||
|
|
5dff50ff92 | ||
|
|
ba4d90765a | ||
|
|
187ccaa95e | ||
|
|
18b6d8289f | ||
|
|
c851f8f006 | ||
|
|
9d60704c9b | ||
|
|
365d7a1afd | ||
|
|
a3d5f23861 | ||
|
|
330ea986d4 | ||
|
|
652c1aae73 | ||
|
|
c91adcd165 | ||
|
|
293c286d22 | ||
|
|
70244f79ba | ||
|
|
6ba49ad294 | ||
|
|
acf8cbfe0e | ||
|
|
f904fd00e5 | ||
|
|
831bec5baf | ||
|
|
f0257b0f87 | ||
|
|
66856d1229 | ||
|
|
27c22a4b09 | ||
|
|
1e7cce9056 | ||
|
|
219af76679 | ||
|
|
dcf773fd88 | ||
|
|
df51e6d429 | ||
|
|
6ea66b85e0 | ||
|
|
10aae90ae2 | ||
|
|
ea8e31561e | ||
|
|
45c627b0a5 | ||
|
|
1d2ca469fc | ||
|
|
e6a0c86f4e | ||
|
|
039429d452 | ||
|
|
46df9398e8 | ||
|
|
744907ac38 | ||
|
|
084677cdca | ||
|
|
891cd3124f | ||
|
|
b9fd3b8b7d | ||
|
|
f6208c1324 | ||
|
|
3a117c0f09 | ||
|
|
a0c83f33ca | ||
|
|
99b9fadd74 | ||
|
|
cf023847ad | ||
|
|
59357b274d | ||
|
|
1582184223 | ||
|
|
db9bcafb82 | ||
|
|
50b9838eec | ||
|
|
ff958b7cd6 | ||
|
|
33bc2aa220 | ||
|
|
03b331e5d5 | ||
|
|
86cc9fb7d8 | ||
|
|
f98efe0b97 | ||
|
|
6c5345df64 | ||
|
|
2d97661d28 | ||
|
|
3efe0c1ce2 | ||
|
|
6099c91216 | ||
|
|
934e4d9607 | ||
|
|
1198f6112e | ||
|
|
ac7e08ae2c | ||
|
|
9c5cbd348b | ||
|
|
8600710d23 | ||
|
|
d06d52deda | ||
|
|
948377ba93 | ||
|
|
d878028dcc | ||
|
|
65c969a321 | ||
|
|
777f7f9305 | ||
|
|
43fb37ab1d | ||
|
|
06734ec886 | ||
|
|
0b8ae93727 | ||
|
|
24cea97e08 | ||
|
|
18536e5db0 | ||
|
|
8c236cb5cb | ||
|
|
797c8ad7fa | ||
|
|
ba8d2dcb8b | ||
|
|
78a095d958 | ||
|
|
627172f6df | ||
|
|
a39f25961c | ||
|
|
e738ae5c8c | ||
|
|
749e85e8e6 | ||
|
|
cfa251b158 | ||
|
|
1ca38b8741 | ||
|
|
d7bc5a7088 | ||
|
|
3b9a2c3ee1 | ||
|
|
260ad77d39 | ||
|
|
5c508a0cd9 | ||
|
|
fd3d1607a4 | ||
|
|
abbc0fbcf1 | ||
|
|
00be41608d | ||
|
|
499c3f2e13 | ||
|
|
627845f6e4 | ||
|
|
07eea76057 | ||
|
|
fecc4e9b79 | ||
|
|
58e9302f15 | ||
|
|
86f649fab1 | ||
|
|
9ac0becfb2 | ||
|
|
eda547b868 | ||
|
|
07291d71f2 | ||
|
|
d1ca1ec4d9 | ||
|
|
6a4e8c95ea | ||
|
|
2f90890f50 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
Desktop.ini
|
||||||
|
Thumbs.db
|
||||||
node_modules/*
|
node_modules/*
|
||||||
!node_modules/boost
|
!node_modules/boost
|
||||||
/dist
|
/dist
|
||||||
/compiled
|
/compiled
|
||||||
/secret
|
/secret
|
||||||
|
*.log
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
|||||||
[submodule "submodules/ace"]
|
|
||||||
path = submodules/ace
|
|
||||||
url = https://github.com/ajaxorg/ace-builds.git
|
|
||||||
branch = master
|
|
||||||
@@ -2,43 +2,34 @@ import React, { PropTypes } from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import modes from '../lib/modes'
|
import modes from '../lib/modes'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import fetchConfig from '../lib/fetchConfig'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const remote = electron.remote
|
|
||||||
const ipc = electron.ipcRenderer
|
|
||||||
|
|
||||||
const ace = window.ace
|
const ace = window.ace
|
||||||
|
|
||||||
let config = fetchConfig()
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
ipc.on('config-apply', function (e, newConfig) {
|
|
||||||
config = newConfig
|
|
||||||
})
|
|
||||||
|
|
||||||
export default class CodeEditor extends React.Component {
|
export default class CodeEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(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) => {
|
this.blurHandler = (e) => {
|
||||||
if (e.relatedTarget === null) {
|
e.stopPropagation()
|
||||||
return
|
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 => {
|
if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.killedBuffer = ''
|
this.killedBuffer = ''
|
||||||
this.execHandler = (e) => {
|
this.execHandler = (e) => {
|
||||||
console.log(e.command.name)
|
console.info('ACE COMMAND >> %s', e.command.name)
|
||||||
switch (e.command.name) {
|
switch (e.command.name) {
|
||||||
case 'gotolinestart':
|
case 'gotolinestart':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -84,7 +75,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.afterExecHandler = (e) => {
|
this.afterExecHandler = (e) => {
|
||||||
switch (e.command.name) {
|
switch (e.command.name) {
|
||||||
case 'find':
|
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.removeEventListener('blur', this.blurHandler)
|
||||||
el.addEventListener('blur', this.blurHandler)
|
el.addEventListener('blur', this.blurHandler)
|
||||||
})
|
})
|
||||||
@@ -93,10 +84,6 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
fontSize: config['editor-font-size'],
|
|
||||||
fontFamily: config['editor-font-family'],
|
|
||||||
indentType: config['editor-indent-type'],
|
|
||||||
indentSize: config['editor-indent-size']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.silentChange = false
|
this.silentChange = false
|
||||||
@@ -109,15 +96,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
let { article } = this.props
|
let { mode, value, theme, fontSize } = this.props
|
||||||
var el = ReactDOM.findDOMNode(this)
|
this.value = value
|
||||||
var editor = this.editor = ace.edit(el)
|
let el = ReactDOM.findDOMNode(this)
|
||||||
|
let editor = this.editor = ace.edit(el)
|
||||||
editor.$blockScrolling = Infinity
|
editor.$blockScrolling = Infinity
|
||||||
editor.renderer.setShowGutter(true)
|
editor.renderer.setShowGutter(true)
|
||||||
editor.setTheme('ace/theme/xcode')
|
editor.setTheme('ace/theme/' + theme)
|
||||||
editor.moveCursorTo(0, 0)
|
editor.moveCursorTo(0, 0)
|
||||||
editor.setReadOnly(!!this.props.readOnly)
|
editor.setReadOnly(!!this.props.readOnly)
|
||||||
editor.setFontSize(this.state.fontSize)
|
editor.setFontSize(fontSize)
|
||||||
|
|
||||||
editor.on('blur', this.blurHandler)
|
editor.on('blur', this.blurHandler)
|
||||||
|
|
||||||
@@ -131,49 +119,34 @@ export default class CodeEditor extends React.Component {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
})
|
})
|
||||||
editor.commands.addCommand({
|
editor.commands.addCommand({
|
||||||
name: 'Emacs cursor up',
|
name: 'Emacs kill buffer',
|
||||||
bindKey: {mac: 'Ctrl-Y'},
|
bindKey: {mac: 'Ctrl-Y'},
|
||||||
exec: function (editor) {
|
exec: function (editor) {
|
||||||
editor.insert(this.killedBuffer)
|
editor.insert(this.killedBuffer)
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
readOnly: true
|
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('exec', this.execHandler)
|
||||||
editor.commands.on('afterExec', this.afterExecHandler)
|
editor.commands.on('afterExec', this.afterExecHandler)
|
||||||
|
|
||||||
var session = editor.getSession()
|
var session = editor.getSession()
|
||||||
let mode = _.findWhere(modes, {name: article.mode})
|
mode = _.find(modes, {name: mode})
|
||||||
let syntaxMode = mode != null
|
let syntaxMode = mode != null
|
||||||
? mode.mode
|
? mode.mode
|
||||||
: 'text'
|
: 'text'
|
||||||
session.setMode('ace/mode/' + syntaxMode)
|
session.setMode('ace/mode/' + syntaxMode)
|
||||||
|
|
||||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
session.setTabSize(this.props.indentSize)
|
||||||
session.setOption('useWorker', false)
|
session.setOption('useWorker', true)
|
||||||
session.setUseWrapMode(true)
|
session.setUseWrapMode(true)
|
||||||
session.setValue(this.props.article.content)
|
session.setValue(_.isString(value) ? value : '')
|
||||||
|
|
||||||
session.on('change', this.changeHandler)
|
session.on('change', this.changeHandler)
|
||||||
|
|
||||||
ipc.on('config-apply', this.configApplyHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
ipc.removeListener('config-apply', this.configApplyHandler)
|
|
||||||
this.editor.getSession().removeListener('change', this.changeHandler)
|
this.editor.getSession().removeListener('change', this.changeHandler)
|
||||||
this.editor.removeListener('blur', this.blurHandler)
|
this.editor.removeListener('blur', this.blurHandler)
|
||||||
this.editor.commands.removeListener('exec', this.execHandler)
|
this.editor.commands.removeListener('exec', this.execHandler)
|
||||||
@@ -181,38 +154,36 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
componentDidUpdate (prevProps, prevState) {
|
||||||
var session = this.editor.getSession()
|
let { value } = this.props
|
||||||
if (this.props.article.key !== prevProps.article.key) {
|
this.value = value
|
||||||
session.removeListener('change', this.changeHandler)
|
let editor = this.editor
|
||||||
session.setValue(this.props.article.content)
|
let session = this.editor.getSession()
|
||||||
session.getUndoManager().reset()
|
|
||||||
session.on('change', this.changeHandler)
|
if (prevProps.mode !== this.props.mode) {
|
||||||
}
|
let mode = _.find(modes, {name: this.props.mode})
|
||||||
if (prevProps.article.mode !== this.props.article.mode) {
|
|
||||||
let mode = _.findWhere(modes, {name: this.props.article.mode})
|
|
||||||
let syntaxMode = mode != null
|
let syntaxMode = mode != null
|
||||||
? mode.mode
|
? mode.mode
|
||||||
: 'text'
|
: 'text'
|
||||||
session.setMode('ace/mode/' + syntaxMode)
|
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']
|
|
||||||
}, function () {
|
|
||||||
var session = this.editor.getSession()
|
|
||||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
|
||||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
var value = this.editor.getValue()
|
this.value = this.editor.getValue()
|
||||||
this.props.onChange(value)
|
this.props.onChange(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,13 +203,37 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.scrollToLine(num, false, false)
|
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 () {
|
render () {
|
||||||
|
let { className, fontFamily } = this.props
|
||||||
|
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||||
|
? [fontFamily].concat(defaultEditorFontFamily)
|
||||||
|
: defaultEditorFontFamily
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
|
className={className == null
|
||||||
|
? 'CodeEditor'
|
||||||
|
: `CodeEditor ${className}`
|
||||||
|
}
|
||||||
|
ref='root'
|
||||||
|
tabIndex='-1'
|
||||||
style={{
|
style={{
|
||||||
fontSize: this.state.fontSize,
|
fontFamily: fontFamily.join(', ')
|
||||||
fontFamily: this.state.fontFamily.trim() + ', monospace'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -246,11 +241,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CodeEditor.propTypes = {
|
CodeEditor.propTypes = {
|
||||||
article: PropTypes.shape({
|
value: PropTypes.string,
|
||||||
content: PropTypes.string,
|
mode: PropTypes.string,
|
||||||
mode: PropTypes.string,
|
|
||||||
key: PropTypes.string
|
|
||||||
}),
|
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
@@ -258,7 +250,12 @@ CodeEditor.propTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CodeEditor.defaultProps = {
|
CodeEditor.defaultProps = {
|
||||||
readOnly: false
|
readOnly: false,
|
||||||
|
theme: 'xcode',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Monaco, Consolas',
|
||||||
|
indentSize: 4,
|
||||||
|
indentType: 'space'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeEditor
|
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,211 +1,152 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import markdown from '../lib/markdown'
|
import markdown from 'browser/lib/markdown'
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import sanitizeHtml from '@rokt33r/sanitize-html'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import fetchConfig from '../lib/fetchConfig'
|
import hljsTheme from 'browser/lib/hljsThemes'
|
||||||
|
|
||||||
const electron = require('electron')
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const shell = electron.shell
|
const { shell } = require('electron')
|
||||||
const ipc = electron.ipcRenderer
|
const goExternal = function (e) {
|
||||||
|
e.preventDefault()
|
||||||
const katex = window.katex
|
e.stopPropagation()
|
||||||
|
shell.openExternal(e.target.href)
|
||||||
|
}
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
const sanitizeOpts = {
|
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||||
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
if (!OSX) {
|
||||||
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
|
defaultFontFamily.unshift('meiryo')
|
||||||
allowedClasses: {
|
|
||||||
'a': ['lineAnchor'],
|
|
||||||
'div': ['math'],
|
|
||||||
'span': ['math', 'hljs-*'],
|
|
||||||
'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 defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||||
|
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||||
this.state = {
|
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||||
fontSize: config['preview-font-size'],
|
|
||||||
fontFamily: config['preview-font-family']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount () {
|
|
||||||
this.addListener()
|
|
||||||
this.renderMath()
|
|
||||||
ipc.on('config-apply', this.configApplyHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
handleContextMenu (e) {
|
||||||
this.addListener()
|
this.props.onContextMenu(e)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown (e) {
|
handleMouseDown (e) {
|
||||||
if (this.props.onMouseDown) {
|
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||||
this.props.onMouseDown(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUp (e) {
|
handleMouseUp (e) {
|
||||||
if (this.props.onMouseUp) {
|
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
||||||
this.props.onMouseUp(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove (e) {
|
componentDidMount () {
|
||||||
if (this.props.onMouseMove) {
|
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
|
||||||
this.props.onMouseMove(e)
|
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) {
|
componentWillUnmount () {
|
||||||
this.setState({
|
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||||
fontSize: config['preview-font-size'],
|
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||||
fontFamily: config['preview-font-family']
|
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 () {
|
focus () {
|
||||||
let isEmpty = this.props.content.trim().length === 0
|
this.refs.root.focus()
|
||||||
let content = isEmpty
|
}
|
||||||
? '(Empty content)'
|
|
||||||
: this.props.content
|
|
||||||
content = markdown(content)
|
|
||||||
content = sanitizeHtml(content, sanitizeOpts)
|
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<iframe className={className != null
|
||||||
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
|
? 'MarkdownPreview ' + className
|
||||||
onClick={e => this.handleClick(e)}
|
: 'MarkdownPreview'
|
||||||
onDoubleClick={e => this.handleDoubleClick(e)}
|
}
|
||||||
onMouseDown={e => this.handleMouseDown(e)}
|
style={style}
|
||||||
onMouseMove={e => this.handleMouseMove(e)}
|
tabIndex={tabIndex}
|
||||||
onMouseUp={e => this.handleMouseUp(e)}
|
ref='root'
|
||||||
dangerouslySetInnerHTML={{__html: ' ' + content}}
|
|
||||||
style={{
|
|
||||||
fontSize: this.state.fontSize,
|
|
||||||
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -218,5 +159,5 @@ MarkdownPreview.propTypes = {
|
|||||||
onMouseDown: PropTypes.func,
|
onMouseDown: PropTypes.func,
|
||||||
onMouseMove: PropTypes.func,
|
onMouseMove: PropTypes.func,
|
||||||
className: PropTypes.string,
|
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
|
||||||
@@ -9,11 +9,27 @@ import FinderDetail from './FinderDetail'
|
|||||||
import actions, { selectArticle, searchArticle } from './actions'
|
import actions, { selectArticle, searchArticle } from './actions'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import dataStore from 'browser/lib/dataStore'
|
import dataStore from 'browser/lib/dataStore'
|
||||||
|
import fetchConfig from '../lib/fetchConfig'
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { clipboard, ipcRenderer, remote } = electron
|
const { clipboard, ipcRenderer, remote } = electron
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
let config = fetchConfig()
|
||||||
|
applyConfig(config)
|
||||||
|
|
||||||
|
ipcRenderer.on('config-apply', function (e, newConfig) {
|
||||||
|
config = newConfig
|
||||||
|
applyConfig(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
function applyConfig () {
|
||||||
|
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') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
window.addEventListener('keydown', function (e) {
|
window.addEventListener('keydown', function (e) {
|
||||||
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||||
@@ -33,7 +49,7 @@ function notify (title, options) {
|
|||||||
return new window.Notification(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_FILTER = 'FOLDER_FILTER'
|
||||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_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 () {
|
function getClientKey () {
|
||||||
let clientKey = localStorage.getItem('clientKey')
|
let clientKey = localStorage.getItem('clientKey')
|
||||||
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
||||||
clientKey = keygen()
|
clientKey = keygen(20)
|
||||||
setClientKey(clientKey)
|
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,21 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function fetchConfig () {
|
|
||||||
return Object.assign({}, defaultConfig, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
|
|
||||||
}
|
|
||||||
78
browser/lib/hljsThemes.js
Normal file
78
browser/lib/hljsThemes.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const hljsThemeList = [
|
||||||
|
{caption: 'Default', name: 'default'},
|
||||||
|
{caption: 'Agate', name: 'agate'},
|
||||||
|
{caption: 'Androidstudio', name: 'androidstudio'},
|
||||||
|
{caption: 'Arduino Light', name: 'arduino-light'},
|
||||||
|
{caption: 'Arta', name: 'arta'},
|
||||||
|
{caption: 'Ascetic', name: 'ascetic'},
|
||||||
|
{caption: 'Atelier Cave Dark', name: 'atelier-cave-dark'},
|
||||||
|
{caption: 'Atelier Cave Light', name: 'atelier-cave-light'},
|
||||||
|
{caption: 'Atelier Dune Dark', name: 'atelier-dune-dark'},
|
||||||
|
{caption: 'Atelier Dune Light', name: 'atelier-dune-light'},
|
||||||
|
{caption: 'Atelier Estuary Dark', name: 'atelier-estuary-dark'},
|
||||||
|
{caption: 'Atelier Estuary Light', name: 'atelier-estuary-light'},
|
||||||
|
{caption: 'Atelier Forest Dark', name: 'atelier-forest-dark'},
|
||||||
|
{caption: 'Atelier Forest Light', name: 'atelier-forest-light'},
|
||||||
|
{caption: 'Atelier Heath Dark', name: 'atelier-heath-dark'},
|
||||||
|
{caption: 'Atelier Heath Light', name: 'atelier-heath-light'},
|
||||||
|
{caption: 'Atelier Lakeside Dark', name: 'atelier-lakeside-dark'},
|
||||||
|
{caption: 'Atelier Lakeside Light', name: 'atelier-lakeside-light'},
|
||||||
|
{caption: 'Atelier Plateau Dark', name: 'atelier-plateau-dark'},
|
||||||
|
{caption: 'Atelier Plateau Light', name: 'atelier-plateau-light'},
|
||||||
|
{caption: 'Atelier Savanna Dark', name: 'atelier-savanna-dark'},
|
||||||
|
{caption: 'Atelier Savanna Light', name: 'atelier-savanna-light'},
|
||||||
|
{caption: 'Atelier Seaside Dark', name: 'atelier-seaside-dark'},
|
||||||
|
{caption: 'Atelier Seaside Light', name: 'atelier-seaside-light'},
|
||||||
|
{caption: 'Atelier Sulphurpool Dark', name: 'atelier-sulphurpool-dark'},
|
||||||
|
{caption: 'Atelier Sulphurpool Light', name: 'atelier-sulphurpool-light'},
|
||||||
|
{caption: 'Brown Paper', name: 'brown-paper'},
|
||||||
|
{caption: 'Codepen Embed', name: 'codepen-embed'},
|
||||||
|
{caption: 'Color Brewer', name: 'color-brewer'},
|
||||||
|
{caption: 'Dark', name: 'dark'},
|
||||||
|
{caption: 'Darkula', name: 'darkula'},
|
||||||
|
{caption: 'Docco', name: 'docco'},
|
||||||
|
{caption: 'Dracula', name: 'dracula'},
|
||||||
|
{caption: 'Far', name: 'far'},
|
||||||
|
{caption: 'Foundation', name: 'foundation'},
|
||||||
|
{caption: 'Github Gist', name: 'github-gist'},
|
||||||
|
{caption: 'Github', name: 'github'},
|
||||||
|
{caption: 'Googlecode', name: 'googlecode'},
|
||||||
|
{caption: 'Grayscale', name: 'grayscale'},
|
||||||
|
{caption: 'Gruvbox Dark', name: 'gruvbox.dark'},
|
||||||
|
{caption: 'Gruvbox Light', name: 'gruvbox.light'},
|
||||||
|
{caption: 'Hopscotch', name: 'hopscotch'},
|
||||||
|
{caption: 'Hybrid', name: 'hybrid'},
|
||||||
|
{caption: 'Idea', name: 'idea'},
|
||||||
|
{caption: 'Ir Black', name: 'ir-black'},
|
||||||
|
{caption: 'Kimbie Dark', name: 'kimbie.dark'},
|
||||||
|
{caption: 'Kimbie Light', name: 'kimbie.light'},
|
||||||
|
{caption: 'Magula', name: 'magula'},
|
||||||
|
{caption: 'Mono Blue', name: 'mono-blue'},
|
||||||
|
{caption: 'Monokai Sublime', name: 'monokai-sublime'},
|
||||||
|
{caption: 'Monokai', name: 'monokai'},
|
||||||
|
{caption: 'Obsidian', name: 'obsidian'},
|
||||||
|
{caption: 'Paraiso Dark', name: 'paraiso-dark'},
|
||||||
|
{caption: 'Paraiso Light', name: 'paraiso-light'},
|
||||||
|
{caption: 'Pojoaque', name: 'pojoaque'},
|
||||||
|
{caption: 'Qtcreator Dark', name: 'qtcreator_dark'},
|
||||||
|
{caption: 'Qtcreator Light', name: 'qtcreator_light'},
|
||||||
|
{caption: 'Railscasts', name: 'railscasts'},
|
||||||
|
{caption: 'Rainbow', name: 'rainbow'},
|
||||||
|
{caption: 'School Book', name: 'school-book'},
|
||||||
|
{caption: 'Solarized Dark', name: 'solarized-dark'},
|
||||||
|
{caption: 'Solarized Light', name: 'solarized-light'},
|
||||||
|
{caption: 'Sunburst', name: 'sunburst'},
|
||||||
|
{caption: 'Tomorrow Night Blue', name: 'tomorrow-night-blue'},
|
||||||
|
{caption: 'Tomorrow Night Bright', name: 'tomorrow-night-bright'},
|
||||||
|
{caption: 'Tomorrow Night Eighties', name: 'tomorrow-night-eighties'},
|
||||||
|
{caption: 'Tomorrow Night', name: 'tomorrow-night'},
|
||||||
|
{caption: 'Tomorrow', name: 'tomorrow'},
|
||||||
|
{caption: 'Vs', name: 'vs'},
|
||||||
|
{caption: 'Xcode', name: 'xcode'},
|
||||||
|
{caption: 'Xt 256', name: 'xt256'},
|
||||||
|
{caption: 'Zenburn', name: 'zenburn'}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function hljsTheme () {
|
||||||
|
return hljsThemeList
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
var crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
module.exports = function () {
|
module.exports = function (length) {
|
||||||
var shasum = crypto.createHash('sha1')
|
if (!_.isFinite(length)) length = 6
|
||||||
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
|
return crypto.randomBytes(length).toString('hex')
|
||||||
return shasum.digest('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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,19 @@
|
|||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from 'markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
|
const katex = window.katex
|
||||||
|
|
||||||
|
function createGutter (str) {
|
||||||
|
let lc = (str.match(/\n/g) || []).length
|
||||||
|
let lines = []
|
||||||
|
for (let i = 1; i <= lc; i++) {
|
||||||
|
lines.push('<span>' + i + '</span>')
|
||||||
|
}
|
||||||
|
return '<span class="lineNumber">' + lines.join('') + '</span>'
|
||||||
|
}
|
||||||
|
|
||||||
var md = markdownit({
|
var md = markdownit({
|
||||||
typographer: true,
|
typographer: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
@@ -11,10 +22,18 @@ var md = markdownit({
|
|||||||
highlight: function (str, lang) {
|
highlight: function (str, lang) {
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
try {
|
try {
|
||||||
return hljs.highlight(lang, str).value
|
return '<pre class="hljs">' +
|
||||||
|
createGutter(str) +
|
||||||
|
'<code>' +
|
||||||
|
hljs.highlight(lang, str).value +
|
||||||
|
'</code></pre>'
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return str.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\"/g, '"')
|
return '<pre class="hljs">' +
|
||||||
|
createGutter(str) +
|
||||||
|
'<code>' +
|
||||||
|
str.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\"/g, '"') +
|
||||||
|
'</code></pre>'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
md.use(emoji, {
|
md.use(emoji, {
|
||||||
@@ -22,10 +41,22 @@ md.use(emoji, {
|
|||||||
})
|
})
|
||||||
md.use(math, {
|
md.use(math, {
|
||||||
inlineRenderer: function (str) {
|
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) {
|
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'))
|
md.use(require('markdown-it-checkbox'))
|
||||||
|
|||||||
@@ -1,90 +1,9 @@
|
|||||||
const modes = [
|
const modes = [
|
||||||
// Major
|
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'text',
|
||||||
label: 'Plain text',
|
label: 'Plain text',
|
||||||
mode: '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',
|
name: 'abap',
|
||||||
label: 'ABAP',
|
label: 'ABAP',
|
||||||
@@ -145,6 +64,12 @@ const modes = [
|
|||||||
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
||||||
mode: 'batchfile'
|
mode: 'batchfile'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'c',
|
||||||
|
label: 'C',
|
||||||
|
alias: ['c', 'h', 'clang', 'clang'],
|
||||||
|
mode: 'c_cpp'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'cirru',
|
name: 'cirru',
|
||||||
label: 'Cirru',
|
label: 'Cirru',
|
||||||
@@ -175,6 +100,24 @@ const modes = [
|
|||||||
alias: ['cfm', 'cfc'],
|
alias: ['cfm', 'cfc'],
|
||||||
mode: 'coldfusion'
|
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',
|
name: 'curly',
|
||||||
label: 'Curly',
|
label: 'Curly',
|
||||||
@@ -283,6 +226,12 @@ const modes = [
|
|||||||
alias: ['opengl', 'shading'],
|
alias: ['opengl', 'shading'],
|
||||||
mode: 'glsl'
|
mode: 'glsl'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'golang',
|
||||||
|
label: 'Go',
|
||||||
|
alias: ['go'],
|
||||||
|
mode: 'golang'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'groovy',
|
name: 'groovy',
|
||||||
label: 'Groovy',
|
label: 'Groovy',
|
||||||
@@ -313,6 +262,12 @@ const modes = [
|
|||||||
alias: ['hx', 'hxml'],
|
alias: ['hx', 'hxml'],
|
||||||
mode: 'haxe'
|
mode: 'haxe'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'html',
|
||||||
|
label: 'HTML',
|
||||||
|
alias: [],
|
||||||
|
mode: 'html'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'html_ruby',
|
name: 'html_ruby',
|
||||||
label: 'HTML (Ruby)',
|
label: 'HTML (Ruby)',
|
||||||
@@ -355,6 +310,18 @@ const modes = [
|
|||||||
alias: [],
|
alias: [],
|
||||||
mode: 'jade'
|
mode: 'jade'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'java',
|
||||||
|
label: 'Java',
|
||||||
|
alias: [],
|
||||||
|
mode: 'java'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'javascript',
|
||||||
|
label: 'JavaScript',
|
||||||
|
alias: ['js', 'jscript', 'babel', 'es'],
|
||||||
|
mode: 'javascript'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'json',
|
name: 'json',
|
||||||
label: 'JSON',
|
label: 'JSON',
|
||||||
@@ -451,6 +418,12 @@ const modes = [
|
|||||||
alias: [],
|
alias: [],
|
||||||
mode: 'makefile'
|
mode: 'makefile'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'markdown',
|
||||||
|
label: 'Markdown',
|
||||||
|
alias: ['md'],
|
||||||
|
mode: 'markdown'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'mask',
|
name: 'mask',
|
||||||
label: 'Mask',
|
label: 'Mask',
|
||||||
@@ -529,6 +502,12 @@ const modes = [
|
|||||||
alias: ['postgres'],
|
alias: ['postgres'],
|
||||||
mode: 'pgsql'
|
mode: 'pgsql'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'php',
|
||||||
|
label: 'PHP',
|
||||||
|
alias: [],
|
||||||
|
mode: 'php'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'powershell',
|
name: 'powershell',
|
||||||
label: 'PowerShell',
|
label: 'PowerShell',
|
||||||
@@ -559,6 +538,12 @@ const modes = [
|
|||||||
alias: ['protocol', 'buffers'],
|
alias: ['protocol', 'buffers'],
|
||||||
mode: 'protobuf'
|
mode: 'protobuf'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'python',
|
||||||
|
label: 'Python',
|
||||||
|
alias: ['py'],
|
||||||
|
mode: 'python'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'r',
|
name: 'r',
|
||||||
label: 'R',
|
label: 'R',
|
||||||
@@ -571,6 +556,12 @@ const modes = [
|
|||||||
alias: [],
|
alias: [],
|
||||||
mode: 'rdoc'
|
mode: 'rdoc'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ruby',
|
||||||
|
label: 'Ruby',
|
||||||
|
alias: ['rb'],
|
||||||
|
mode: 'ruby'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'rust',
|
name: 'rust',
|
||||||
label: 'Rust',
|
label: 'Rust',
|
||||||
@@ -667,6 +658,12 @@ const modes = [
|
|||||||
alias: [],
|
alias: [],
|
||||||
mode: 'svg'
|
mode: 'svg'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'swift',
|
||||||
|
label: 'Swift',
|
||||||
|
alias: [],
|
||||||
|
mode: 'swift'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'swig',
|
name: 'swig',
|
||||||
label: '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,225 +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) {
|
|
||||||
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) {
|
|
||||||
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
|
|
||||||
this.moveCount++
|
|
||||||
if (this.moveCount > 5) {
|
|
||||||
this.isDrag = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseDowm (e) {
|
|
||||||
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/boost-app-discussions/issues'>
|
|
||||||
<i className='fa fa-fw fa-bullhorn'/> Discuss
|
|
||||||
</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,241 +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 === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
|
|
||||||
remote.getCurrentWebContents().send('modal-confirm')
|
|
||||||
}
|
|
||||||
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,12 +1,13 @@
|
|||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import MainPage from './MainPage'
|
import Main from './Main'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
require('../styles/main/index.styl')
|
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||||
import { openModal } from 'browser/lib/modal'
|
|
||||||
import Tutorial from './modal/Tutorial'
|
|
||||||
import activityRecord from 'browser/lib/activityRecord'
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||||
|
import { syncHistoryWithStore } from 'react-router-redux'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const ipc = electron.ipcRenderer
|
const ipc = electron.ipcRenderer
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@@ -25,6 +26,15 @@ window.addEventListener('online', function () {
|
|||||||
ipc.send('check-update', 'check-update')
|
ipc.send('check-update', 'check-update')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('drop', function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
document.addEventListener('dragover', function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
|
||||||
function notify (title, options) {
|
function notify (title, options) {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||||
@@ -48,20 +58,26 @@ ipc.on('open-finder', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
let el = document.getElementById('content')
|
let el = document.getElementById('content')
|
||||||
|
const history = syncHistoryWithStore(hashHistory, store)
|
||||||
|
|
||||||
ReactDOM.render((
|
ReactDOM.render((
|
||||||
<div>
|
<Provider store={store}>
|
||||||
<Provider store={store}>
|
<Router history={history}>
|
||||||
<MainPage/>
|
<Route path='/' component={Main}>
|
||||||
</Provider>
|
<IndexRedirect to='/home'/>
|
||||||
</div>
|
<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 () {
|
), el, function () {
|
||||||
let loadingCover = document.getElementById('loadingCover')
|
let loadingCover = document.getElementById('loadingCover')
|
||||||
loadingCover.parentNode.removeChild(loadingCover)
|
loadingCover.parentNode.removeChild(loadingCover)
|
||||||
let status = JSON.parse(localStorage.getItem('status'))
|
|
||||||
if (status == null) status = {}
|
|
||||||
if (!status.introWatched) {
|
|
||||||
openModal(Tutorial)
|
|
||||||
status.introWatched = 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)) {
|
||||||
|
return Promise.reject(new Error('Path must be a string.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 React from 'react'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
import store from '../store'
|
||||||
const remote = require('electron').remote
|
|
||||||
|
|
||||||
class ModalBase extends React.Component {
|
class ModalBase extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -15,16 +15,16 @@ class ModalBase extends React.Component {
|
|||||||
|
|
||||||
close () {
|
close () {
|
||||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
||||||
|
|
||||||
remote.getCurrentWebContents().send('list-focus')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
<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 == 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>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -37,6 +37,8 @@ let modalBase = ReactDOM.render(<ModalBase/>, el)
|
|||||||
|
|
||||||
export function openModal (component, props) {
|
export function openModal (component, props) {
|
||||||
if (modalBase == null) { return }
|
if (modalBase == null) { return }
|
||||||
|
|
||||||
|
document.body.setAttribute('data-modal', 'open')
|
||||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,232 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import linkState from 'browser/lib/linkState'
|
|
||||||
import { updateUser } from '../../actions'
|
|
||||||
import fetchConfig from 'browser/lib/fetchConfig'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const ipc = electron.ipcRenderer
|
|
||||||
const remote = electron.remote
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDisableDirectWriteClick (e) {
|
|
||||||
let config = this.state.config
|
|
||||||
config['disable-direct-write'] = !config['disable-direct-write']
|
|
||||||
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
|
|
||||||
|
|
||||||
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'>Text</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='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>
|
|
||||||
{
|
|
||||||
global.process.platform === 'win32'
|
|
||||||
? (
|
|
||||||
<div className='sectionCheck'>
|
|
||||||
<label><input onClick={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='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,123 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import clientKey from 'browser/lib/clientKey'
|
|
||||||
import linkState from 'browser/lib/linkState'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import { request, SERVER_URL } from 'browser/lib/api'
|
|
||||||
|
|
||||||
const FORM_MODE = 'FORM_MODE'
|
|
||||||
const DONE_MODE = 'DONE_MODE'
|
|
||||||
|
|
||||||
export default class ContactTab extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
email: '',
|
|
||||||
mode: FORM_MODE,
|
|
||||||
alert: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
let titleInput = ReactDOM.findDOMNode(this.refs.title)
|
|
||||||
if (titleInput != null) titleInput.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBackButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
mode: FORM_MODE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSendButtonClick (e) {
|
|
||||||
let input = _.pick(this.state, ['title', 'content', 'email'])
|
|
||||||
input.clientKey = clientKey.get()
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
alert: {
|
|
||||||
type: 'info',
|
|
||||||
message: 'Sending...'
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
request.post(SERVER_URL + 'apis/inquiry')
|
|
||||||
.send(input)
|
|
||||||
.then(res => {
|
|
||||||
console.log('sent')
|
|
||||||
this.setState({
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
mode: DONE_MODE,
|
|
||||||
alert: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.code === 'ECONNREFUSED') {
|
|
||||||
this.setState({
|
|
||||||
alert: {
|
|
||||||
type: 'error',
|
|
||||||
message: 'Can\'t connect to API server.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({
|
|
||||||
alert: {
|
|
||||||
type: 'error',
|
|
||||||
message: err.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
switch (this.state.mode) {
|
|
||||||
case DONE_MODE:
|
|
||||||
return (
|
|
||||||
<div className='ContactTab content done'>
|
|
||||||
<div className='message'>
|
|
||||||
<i className='checkIcon fa fa-check-circle'/><br/>
|
|
||||||
Your message has been sent successfully!!
|
|
||||||
</div>
|
|
||||||
<div className='control'>
|
|
||||||
<button onClick={e => this.handleBackButtonClick(e)}>Back to Contact form</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case FORM_MODE:
|
|
||||||
default:
|
|
||||||
let alertElement = this.state.alert != null
|
|
||||||
? (
|
|
||||||
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
return (
|
|
||||||
<div className='ContactTab content form'>
|
|
||||||
<div className='title'>Contact form</div>
|
|
||||||
<div className='description'>
|
|
||||||
Your feedback is highly appreciated and will help us to improve our app. :D
|
|
||||||
</div>
|
|
||||||
<div className='iptGroup'>
|
|
||||||
<input ref='title' valueLink={this.linkState('title')} placeholder='Title' type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='iptGroup'>
|
|
||||||
<textarea valueLink={this.linkState('content')} placeholder='Content'/>
|
|
||||||
</div>
|
|
||||||
<div className='iptGroup'>
|
|
||||||
<input valueLink={this.linkState('email')} placeholder='E-mail (Optional)' type='email'/>
|
|
||||||
</div>
|
|
||||||
<div className='formControl'>
|
|
||||||
<button onClick={e => this.handleSendButtonClick(e)} className='primary'>Send</button>
|
|
||||||
{alertElement}
|
|
||||||
</div>
|
|
||||||
</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,121 +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 HELP = 'HELP'
|
|
||||||
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 HELP:
|
|
||||||
return (<HelpTab/>)
|
|
||||||
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 React, { PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import store from '../store'
|
import store from '../store'
|
||||||
import { destroyArticle } from '../actions'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const ipc = electron.ipcRenderer
|
const ipc = electron.ipcRenderer
|
||||||
@@ -10,7 +9,7 @@ export default class DeleteArticleModal extends React.Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.confirmHandler = e => this.handleYesButtonClick()
|
this.confirmHandler = (e) => this.handleYesButtonClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@@ -27,7 +26,7 @@ export default class DeleteArticleModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleYesButtonClick (e) {
|
handleYesButtonClick (e) {
|
||||||
store.dispatch(destroyArticle(this.props.articleKey))
|
// store.dispatch(destroyArticle(this.props.articleKey))
|
||||||
this.props.close()
|
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='message'>Do you really want to delete?</div>
|
||||||
|
|
||||||
<div className='control'>
|
<div className='control'>
|
||||||
<button ref='no' onClick={e => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close'/> No</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>
|
<button ref='yes' onClick={(e) => this.handleYesButtonClick(e)} className='danger'><i className='fa fa-fw fa-check'/> Yes</button>
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
||||||
|
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>
|
||||||
|
|
||||||
|
{this.state.legacyStorageExists &&
|
||||||
|
<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 { combineReducers, createStore } from 'redux'
|
||||||
import { 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)
|
let store = createStore(reducer)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import '../mixins/*'
|
@import '../mixins/*'
|
||||||
global-reset()
|
global-reset()
|
||||||
@import '../shared/*'
|
@import '../shared/*'
|
||||||
|
@import '../theme/*'
|
||||||
|
|
||||||
iptBgColor = #E6E6E6
|
iptBgColor = #E6E6E6
|
||||||
iptFocusBorderColor = #369DCD
|
iptFocusBorderColor = #369DCD
|
||||||
|
|||||||
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,391 +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
|
|
||||||
border-left 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
|
|
||||||
.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,103 +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
|
|
||||||
.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,212 +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
|
|
||||||
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,7 @@
|
|||||||
@import '../../../node_modules/nib/lib/nib'
|
|
||||||
@import '../vars'
|
@import '../vars'
|
||||||
@import '../mixins/*'
|
@import '../mixins/*'
|
||||||
global-reset()
|
global-reset()
|
||||||
@import '../shared/*'
|
@import '../theme/*'
|
||||||
@import './ArticleNavigator'
|
|
||||||
@import './ArticleTopbar'
|
|
||||||
@import './ArticleList'
|
|
||||||
@import './ArticleDetail'
|
|
||||||
@import './modal/*'
|
|
||||||
|
|
||||||
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||||
|
|
||||||
@@ -22,6 +16,10 @@ body
|
|||||||
font-size fontSize
|
font-size fontSize
|
||||||
font-weight 400
|
font-weight 400
|
||||||
|
|
||||||
|
body[data-modal="open"]
|
||||||
|
#content *
|
||||||
|
overflow hidden !important
|
||||||
|
|
||||||
button, input, select, textarea
|
button, input, select, textarea
|
||||||
font-family DEFAULT_FONTS
|
font-family DEFAULT_FONTS
|
||||||
|
|
||||||
@@ -91,38 +89,25 @@ textarea.block-input
|
|||||||
#content
|
#content
|
||||||
fullsize()
|
fullsize()
|
||||||
|
|
||||||
.Main
|
modalZIndex= 1000
|
||||||
.appUpdateButton
|
modalBackColor = transparentify(black, 65%)
|
||||||
position fixed
|
|
||||||
z-index 2000
|
.ModalBase
|
||||||
bottom 5px
|
fixed top left bottom right
|
||||||
right 53px
|
z-index modalZIndex
|
||||||
padding 10px 15px
|
&.hide
|
||||||
border none
|
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
|
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
|
|
||||||
|
|||||||
@@ -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 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
|
|
||||||
@@ -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%)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user