mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
378 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52cfb2cd2 | ||
|
|
4e5d923388 | ||
|
|
f9598dd619 | ||
|
|
00f1c62646 | ||
|
|
8997728cbc | ||
|
|
99a0704516 | ||
|
|
434e361d5d | ||
|
|
f112b0bf03 | ||
|
|
0ce67ccd35 | ||
|
|
c947feadeb | ||
|
|
2fa381980d | ||
|
|
4e2c42f65c | ||
|
|
38b7d60af7 | ||
|
|
4714593d52 | ||
|
|
7729ed4f72 | ||
|
|
7107777df3 | ||
|
|
53989b918e | ||
|
|
fdd0c84441 | ||
|
|
8f0789bc6d | ||
|
|
2fdea6cd61 | ||
|
|
ed5f3a6202 | ||
|
|
d98e909256 | ||
|
|
6e0def310f | ||
|
|
823da07a5e | ||
|
|
40d2960562 | ||
|
|
6799f45352 | ||
|
|
1209a044ce | ||
|
|
9a8760c120 | ||
|
|
8d2a7716f3 | ||
|
|
5f240ada59 | ||
|
|
1c86dea4be | ||
|
|
92fd7ac09c | ||
|
|
90b490c28b | ||
|
|
8fd03a09de | ||
|
|
041232fbdd | ||
|
|
f595121ab3 | ||
|
|
bd733312f4 | ||
|
|
70db2f78da | ||
|
|
0298aba86f | ||
|
|
60b344eea8 | ||
|
|
eb63b743b1 | ||
|
|
d09ffc9dd8 | ||
|
|
f1b2643941 | ||
|
|
9e9d3ddc57 | ||
|
|
99a134494c | ||
|
|
d1156f963d | ||
|
|
2a6a2694d4 | ||
|
|
cc24e6b801 | ||
|
|
9c85719270 | ||
|
|
394e86f765 | ||
|
|
6bb9366ec8 | ||
|
|
8a9d4df6c7 | ||
|
|
fdc2e91170 | ||
|
|
40bf026e82 | ||
|
|
975e82710e | ||
|
|
1763dbcff1 | ||
|
|
908281356e | ||
|
|
123d5638cf | ||
|
|
79d0006f3f | ||
|
|
240b5daf3e | ||
|
|
cd0bede2f2 | ||
|
|
c5d984732a | ||
|
|
7358e68394 | ||
|
|
27b09e5b73 | ||
|
|
1705511b10 | ||
|
|
e17fa6d0ed | ||
|
|
9091976337 | ||
|
|
58d098503b | ||
|
|
33fe4f5295 | ||
|
|
0646c4f8bd | ||
|
|
960469f07c | ||
|
|
665f81ac9c | ||
|
|
26f05b343e | ||
|
|
f50968f992 | ||
|
|
4c486c399b | ||
|
|
9f4dd909a8 | ||
|
|
2b85aa1b88 | ||
|
|
8e4c3a3b21 | ||
|
|
a4160d2994 | ||
|
|
27e0252ccd | ||
|
|
0a707b3f02 | ||
|
|
6fc421810f | ||
|
|
22cf7443f4 | ||
|
|
8e7b4d2444 | ||
|
|
54437cec19 | ||
|
|
40fc63ea0c | ||
|
|
6bd81fe12a | ||
|
|
519ea1a33f | ||
|
|
f07f309393 | ||
|
|
7132e9ff24 | ||
|
|
34ae3cd704 | ||
|
|
fba972c98e | ||
|
|
a391ac682d | ||
|
|
4ee49d5991 | ||
|
|
0d573651a3 | ||
|
|
52efc23984 | ||
|
|
aefb84df3b | ||
|
|
ba374e08ff | ||
|
|
33a11ac2e5 | ||
|
|
357c4a382d | ||
|
|
d7e8f26ace | ||
|
|
5c312c1939 | ||
|
|
5163ab134e | ||
|
|
73dd0db529 | ||
|
|
8921db89ab | ||
|
|
8d624459d4 | ||
|
|
ec96021b00 | ||
|
|
25f50b8cdf | ||
|
|
0127d5143a | ||
|
|
ffe3b689c4 | ||
|
|
ff123be895 | ||
|
|
8178ec5671 | ||
|
|
67dd089e67 | ||
|
|
8d96368ea6 | ||
|
|
eb163ef03c | ||
|
|
5558403358 | ||
|
|
db3a4d0f01 | ||
|
|
d01fe62757 | ||
|
|
87cfc8f1de | ||
|
|
3a8bef26d3 | ||
|
|
b550c0e9e3 | ||
|
|
8f1ee30553 | ||
|
|
458174a5f5 | ||
|
|
c5414aadd1 | ||
|
|
eacd01e77e | ||
|
|
089b919a68 | ||
|
|
2e5945642d | ||
|
|
68a5b6fc50 | ||
|
|
88538257ac | ||
|
|
fb8041fb4b | ||
|
|
e685c4302d | ||
|
|
103b56ea3d | ||
|
|
0490b115ad | ||
|
|
5e1dd4a9ad | ||
|
|
93dd97a14a | ||
|
|
52d065a38d | ||
|
|
ad33e9f32d | ||
|
|
b9f00c6971 | ||
|
|
dba5ea156a | ||
|
|
631c86865f | ||
|
|
324b8fc74a | ||
|
|
282ca3ea2a | ||
|
|
17223db3ea | ||
|
|
cff3fdae6e | ||
|
|
108e83a402 | ||
|
|
fc237848c8 | ||
|
|
92b86bfa0b | ||
|
|
b0fd17047c | ||
|
|
bf8f82fbd8 | ||
|
|
5468c60eaa | ||
|
|
8736d87b95 | ||
|
|
dd0440519b | ||
|
|
b7400553fc | ||
|
|
9d2f570515 | ||
|
|
7eeba0c082 | ||
|
|
2e6bb21fda | ||
|
|
e30cfdf942 | ||
|
|
b07fb92e5c | ||
|
|
5729125fbb | ||
|
|
4577038abd | ||
|
|
7649c1df9e | ||
|
|
033bd111ad | ||
|
|
2cbe07b373 | ||
|
|
f6ec5c67a7 | ||
|
|
6bb78d3216 | ||
|
|
fa717a357d | ||
|
|
eed6bcc044 | ||
|
|
a01fd739bd | ||
|
|
3b7ed5ffd7 | ||
|
|
b3d9beea6d | ||
|
|
e256c7a7d9 | ||
|
|
ef866f957a | ||
|
|
58d25415db | ||
|
|
15f51a4064 | ||
|
|
7fbe456e79 | ||
|
|
9d8daac4cf | ||
|
|
ce199374d5 | ||
|
|
4af8615624 | ||
|
|
b6bb438507 | ||
|
|
49acd8a4f3 | ||
|
|
3e699a99d5 | ||
|
|
e4238f9283 | ||
|
|
9cd6d6d4c1 | ||
|
|
49a4b5feb4 | ||
|
|
c6eff157de | ||
|
|
80d16233e7 | ||
|
|
65e1a39027 | ||
|
|
d73b567bd4 | ||
|
|
787bb0a9e6 | ||
|
|
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 |
6
.babelrc
6
.babelrc
@@ -3,6 +3,12 @@
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
},
|
||||
"test": {
|
||||
"presets": ["react", "es2015"],
|
||||
"plugins": [
|
||||
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
.DS_Store
|
||||
.env
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
node_modules/*
|
||||
!node_modules/boost
|
||||
/dist
|
||||
/compiled
|
||||
/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
LICENSE
2
LICENSE
@@ -1,3 +1,5 @@
|
||||
GPL-3.0
|
||||
|
||||
Boostnote - the simplest note app
|
||||
|
||||
Copyright (C) 2016 MAISIN&CO.
|
||||
|
||||
@@ -1,244 +1,172 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
import CodeMirror from 'codemirror'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const ace = window.ace
|
||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
this.changeHandler = e => this.handleChange(e)
|
||||
this.blurHandler = (e) => {
|
||||
if (e.relatedTarget === null) {
|
||||
return
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.blurHandler = (editor, e) => {
|
||||
if (e == null) return null
|
||||
let el = e.relatedTarget
|
||||
while (el != null) {
|
||||
if (el === this.refs.root) {
|
||||
return
|
||||
}
|
||||
el = el.parentNode
|
||||
}
|
||||
|
||||
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
|
||||
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
|
||||
})
|
||||
if (isFocusingToSearch) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.onBlur) this.props.onBlur(e)
|
||||
}
|
||||
|
||||
this.killedBuffer = ''
|
||||
this.execHandler = (e) => {
|
||||
console.log(e.command.name)
|
||||
switch (e.command.name) {
|
||||
case 'gotolinestart':
|
||||
e.preventDefault()
|
||||
{
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, 0)
|
||||
}
|
||||
break
|
||||
case 'gotolineend':
|
||||
e.preventDefault()
|
||||
let position = this.editor.getCursorPosition()
|
||||
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
|
||||
break
|
||||
case 'jumptomatching':
|
||||
e.preventDefault()
|
||||
this.editor.navigateUp()
|
||||
break
|
||||
case 'removetolineend':
|
||||
e.preventDefault()
|
||||
let range = this.editor.getSelectionRange()
|
||||
let session = this.editor.getSession()
|
||||
if (range.isEmpty()) {
|
||||
range.setEnd(range.start.row, session.getLine(range.start.row).length)
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
if (this.killedBuffer.length > 0) {
|
||||
console.log('remove to lineend')
|
||||
session.remove(range)
|
||||
} else {
|
||||
if (session.getLength() === range.start.row) {
|
||||
return
|
||||
}
|
||||
range.setStart(range.start.row, range.end.col)
|
||||
range.setEnd(range.start.row + 1, 0)
|
||||
this.killedBuffer = '\n'
|
||||
session.remove(range)
|
||||
}
|
||||
} else {
|
||||
this.killedBuffer = session.getTextRange(range)
|
||||
session.remove(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.afterExecHandler = (e) => {
|
||||
switch (e.command.name) {
|
||||
case 'find':
|
||||
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => {
|
||||
el.removeEventListener('blur', this.blurHandler)
|
||||
el.addEventListener('blur', this.blurHandler)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.readOnly !== this.props.readOnly) {
|
||||
this.editor.setReadOnly(!!nextProps.readOnly)
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { article } = this.props
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var editor = this.editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.renderer.setShowGutter(true)
|
||||
editor.setTheme('ace/theme/xcode')
|
||||
editor.moveCursorTo(0, 0)
|
||||
editor.setReadOnly(!!this.props.readOnly)
|
||||
editor.setFontSize(this.state.fontSize)
|
||||
|
||||
editor.on('blur', this.blurHandler)
|
||||
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs cursor up',
|
||||
bindKey: {mac: 'Ctrl-P'},
|
||||
exec: function (editor) {
|
||||
editor.navigateUp(1)
|
||||
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
editor.commands.addCommand({
|
||||
name: 'Emacs cursor up',
|
||||
bindKey: {mac: 'Ctrl-Y'},
|
||||
exec: function (editor) {
|
||||
editor.insert(this.killedBuffer)
|
||||
}.bind(this),
|
||||
readOnly: true
|
||||
})
|
||||
editor.commands.addCommand({
|
||||
name: 'Focus title',
|
||||
bindKey: {win: 'Esc', mac: 'Esc'},
|
||||
exec: function (editor, e) {
|
||||
let currentWindow = remote.getCurrentWebContents()
|
||||
if (config['switch-preview'] === 'rightclick') {
|
||||
currentWindow.send('detail-preview')
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
value: this.props.value,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
theme: this.props.theme,
|
||||
indentUnit: this.props.indentSize,
|
||||
tabSize: this.props.indentSize,
|
||||
indentWithTabs: this.props.indentType !== 'space',
|
||||
keyMap: 'sublime',
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
if (cm.getOption('indentWithTabs')) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
}
|
||||
currentWindow.send('list-focus')
|
||||
},
|
||||
readOnly: true
|
||||
}
|
||||
})
|
||||
|
||||
editor.commands.on('exec', this.execHandler)
|
||||
editor.commands.on('afterExec', this.afterExecHandler)
|
||||
this.setMode(this.props.mode)
|
||||
|
||||
var session = editor.getSession()
|
||||
let mode = _.findWhere(modes, {name: article.mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
|
||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||
session.setOption('useWorker', false)
|
||||
session.setUseWrapMode(true)
|
||||
session.setValue(this.props.article.content)
|
||||
|
||||
session.on('change', this.changeHandler)
|
||||
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||
this.editor.getSession().removeListener('change', this.changeHandler)
|
||||
this.editor.removeListener('blur', this.blurHandler)
|
||||
this.editor.commands.removeListener('exec', this.execHandler)
|
||||
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
var session = this.editor.getSession()
|
||||
if (this.props.article.key !== prevProps.article.key) {
|
||||
session.removeListener('change', this.changeHandler)
|
||||
session.setValue(this.props.article.content)
|
||||
session.getUndoManager().reset()
|
||||
session.on('change', this.changeHandler)
|
||||
let needRefresh = false
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
if (prevProps.article.mode !== this.props.article.mode) {
|
||||
let mode = _.findWhere(modes, {name: this.props.article.mode})
|
||||
let syntaxMode = mode != null
|
||||
? mode.mode
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
if (prevProps.theme !== this.props.theme) {
|
||||
this.editor.setOption('theme', this.props.theme)
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.fontSize !== this.props.fontSize) {
|
||||
needRefresh = true
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily) {
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.indentSize !== this.props.indentSize) {
|
||||
this.editor.setOption('indentUnit', this.props.indentSize)
|
||||
this.editor.setOption('tabSize', this.props.indentSize)
|
||||
}
|
||||
if (prevProps.indentType !== this.props.indentType) {
|
||||
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
setMode (mode) {
|
||||
let syntax = CodeMirror.findModeByName(pass(mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
this.editor.setOption('mode', syntax.mime)
|
||||
CodeMirror.autoLoadMode(this.editor, syntax.mode)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.editor.getValue()
|
||||
if (this.props.onChange) {
|
||||
var value = this.editor.getValue()
|
||||
this.props.onChange(value)
|
||||
this.props.onChange(e)
|
||||
}
|
||||
}
|
||||
|
||||
getFirstVisibleRow () {
|
||||
return this.editor.getFirstVisibleRow()
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
return this.editor.getCursorPosition()
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
this.editor.moveCursorTo(row, col)
|
||||
}
|
||||
|
||||
scrollToLine (num) {
|
||||
this.editor.scrollToLine(num, false, false)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.editor.focus()
|
||||
}
|
||||
|
||||
blur () {
|
||||
this.editor.blur()
|
||||
}
|
||||
|
||||
reload () {
|
||||
// Change event shouldn't be fired when switch note
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.value = this.props.value
|
||||
this.editor.setValue(this.props.value)
|
||||
this.editor.clearHistory()
|
||||
this.editor.on('change', this.changeHandler)
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
let cursor = this.editor.getCursor()
|
||||
this.editor.setValue(value)
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, fontFamily, fontSize } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
return (
|
||||
<div
|
||||
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
|
||||
className={className == null
|
||||
? 'CodeEditor'
|
||||
: `CodeEditor ${className}`
|
||||
}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontSize: this.state.fontSize,
|
||||
fontFamily: this.state.fontFamily.trim() + ', monospace'
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -246,11 +174,8 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
article: PropTypes.shape({
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
value: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
@@ -258,7 +183,10 @@ CodeEditor.propTypes = {
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
readOnly: false
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
|
||||
export default CodeEditor
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default class ExternalLink extends React.Component {
|
||||
handleClick (e) {
|
||||
shell.openExternal(this.props.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<a onClick={e => this.handleClick(e)} {...this.props}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
href: PropTypes.string
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
const BLUE = '#3460C7'
|
||||
const LIGHTBLUE = '#2BA5F7'
|
||||
const ORANGE = '#FF8E00'
|
||||
const YELLOW = '#E8D252'
|
||||
const GREEN = '#3FD941'
|
||||
const DARKGREEN = '#1FAD85'
|
||||
const RED = '#E10051'
|
||||
const PURPLE = '#B013A4'
|
||||
|
||||
function getColorByIndex (index) {
|
||||
switch (index % 8) {
|
||||
case 0:
|
||||
return RED
|
||||
case 1:
|
||||
return ORANGE
|
||||
case 2:
|
||||
return YELLOW
|
||||
case 3:
|
||||
return GREEN
|
||||
case 4:
|
||||
return DARKGREEN
|
||||
case 5:
|
||||
return LIGHTBLUE
|
||||
case 6:
|
||||
return BLUE
|
||||
case 7:
|
||||
return PURPLE
|
||||
default:
|
||||
return DARKGREEN
|
||||
}
|
||||
}
|
||||
|
||||
export default class FolderMark extends React.Component {
|
||||
render () {
|
||||
let color = getColorByIndex(this.props.color)
|
||||
let className = 'FolderMark fa fa-square fa-fw'
|
||||
if (this.props.className != null) {
|
||||
className += ' active'
|
||||
}
|
||||
|
||||
return (
|
||||
<i className={className} style={{color: color}}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FolderMark.propTypes = {
|
||||
color: PropTypes.number,
|
||||
className: PropTypes.string
|
||||
}
|
||||
176
browser/components/MarkdownEditor.js
Normal file
176
browser/components/MarkdownEditor.js
Normal file
@@ -0,0 +1,176 @@
|
||||
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.editor.getCursor()
|
||||
this.setState({
|
||||
status: 'PREVIEW'
|
||||
}, () => {
|
||||
this.refs.preview.focus()
|
||||
this.refs.preview.scrollTo(cursorPosition.line)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let idMatch = /checkbox-([0-9]+)/
|
||||
let checkedMatch = /\[x\]/i
|
||||
let uncheckedMatch = /\[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
let lines = this.refs.code.value
|
||||
.split('\n')
|
||||
|
||||
let targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
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='GitHub Flavored 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}
|
||||
theme={config.ui.theme}
|
||||
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)}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(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,187 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from '../lib/markdown'
|
||||
import ReactDOM from 'react-dom'
|
||||
import sanitizeHtml from '@rokt33r/sanitize-html'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
import fetchConfig from '../lib/fetchConfig'
|
||||
import hljsTheme from 'browser/lib/hljsThemes'
|
||||
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
const katex = window.katex
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const { shell } = require('electron')
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const sanitizeOpts = {
|
||||
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
|
||||
allowedClasses: {
|
||||
'a': ['lineAnchor'],
|
||||
'div': ['math'],
|
||||
'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 defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
if (!OSX) {
|
||||
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
|
||||
function handleAnchorClick (e) {
|
||||
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let href = this.href
|
||||
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
|
||||
shell.openExternal(href)
|
||||
}
|
||||
}
|
||||
|
||||
function stopPropagation (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
function math2Katex (display) {
|
||||
return function (el) {
|
||||
try {
|
||||
katex.render(el.innerHTML.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&'), el, {display: display})
|
||||
el.className = 'math-rendered'
|
||||
} catch (e) {
|
||||
el.innerHTML = e.message
|
||||
el.className = 'math-failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config = fetchConfig()
|
||||
ipc.on('config-apply', function (e, newConfig) {
|
||||
config = newConfig
|
||||
})
|
||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||
|
||||
this.state = {
|
||||
fontSize: config['preview-font-size'],
|
||||
fontFamily: config['preview-font-family']
|
||||
}
|
||||
}
|
||||
componentDidMount () {
|
||||
this.addListener()
|
||||
this.renderMath()
|
||||
ipc.on('config-apply', this.configApplyHandler)
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.addListener()
|
||||
this.renderMath()
|
||||
}
|
||||
handlePreviewAnchorClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
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)
|
||||
let href = e.target.getAttribute('href')
|
||||
if (_.isString(href) && href.match(/^#/)) {
|
||||
let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
} else {
|
||||
shell.openExternal(e.target.href)
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
if (this.props.onDoubleClick) {
|
||||
this.props.onDoubleClick(e)
|
||||
}
|
||||
handleCheckboxClick (e) {
|
||||
this.props.onCheckboxClick(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
this.props.onContextMenu(e)
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
if (this.props.onMouseDown) {
|
||||
this.props.onMouseDown(e)
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
case 'INPUT':
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (this.props.onMouseUp) {
|
||||
this.props.onMouseUp(e)
|
||||
if (e.target != null && e.target.tagName === 'A') {
|
||||
return null
|
||||
}
|
||||
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.props.onMouseMove) {
|
||||
this.props.onMouseMove(e)
|
||||
}
|
||||
componentDidMount () {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
|
||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.rewriteIframe()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||
}
|
||||
|
||||
handleConfigApply (e, config) {
|
||||
this.setState({
|
||||
fontSize: config['preview-font-size'],
|
||||
fontFamily: config['preview-font-family']
|
||||
componentWillUnmount () {
|
||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value ||
|
||||
prevProps.fontFamily !== this.props.fontFamily ||
|
||||
prevProps.fontSize !== this.props.fontSize ||
|
||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||
prevProps.lineNumber !== this.props.lineNumber ||
|
||||
prevProps.theme !== this.props.theme
|
||||
) this.rewriteIframe()
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, theme } = 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="../compiled/katex-style.css">
|
||||
`
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
|
||||
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let isEmpty = this.props.content.trim().length === 0
|
||||
let content = isEmpty
|
||||
? '(Empty content)'
|
||||
: this.props.content
|
||||
content = markdown(content)
|
||||
content = sanitizeHtml(content, sanitizeOpts)
|
||||
focus () {
|
||||
this.refs.root.focus()
|
||||
}
|
||||
|
||||
getWindow () {
|
||||
return this.refs.root.contentWindow
|
||||
}
|
||||
|
||||
scrollTo (targetRow) {
|
||||
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
let block = blocks[index]
|
||||
let row = parseInt(block.getAttribute('data-line'))
|
||||
if (row > targetRow || index === blocks.length - 1) {
|
||||
block = blocks[index - 1]
|
||||
block != null && this.getWindow().scrollTo(0, block.offsetTop)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, style, tabIndex } = this.props
|
||||
return (
|
||||
<div
|
||||
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
|
||||
onClick={e => this.handleClick(e)}
|
||||
onDoubleClick={e => this.handleDoubleClick(e)}
|
||||
onMouseDown={e => this.handleMouseDown(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
dangerouslySetInnerHTML={{__html: ' ' + content}}
|
||||
style={{
|
||||
fontSize: this.state.fontSize,
|
||||
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
|
||||
}}
|
||||
<iframe className={className != null
|
||||
? 'MarkdownPreview ' + className
|
||||
: 'MarkdownPreview'
|
||||
}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
ref='root'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -216,7 +192,6 @@ MarkdownPreview.propTypes = {
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class ModeIcon extends React.Component {
|
||||
getClassName () {
|
||||
var mode = this.props.mode
|
||||
switch (mode) {
|
||||
// Script
|
||||
case 'javascript':
|
||||
return 'devicon-javascript-plain'
|
||||
case 'jsx':
|
||||
return 'devicon-react-original'
|
||||
case 'coffee':
|
||||
return 'devicon-coffeescript-original'
|
||||
case 'ruby':
|
||||
return 'devicon-ruby-plain'
|
||||
case 'erlang':
|
||||
return 'devicon-erlang-plain'
|
||||
case 'php':
|
||||
return 'devicon-php-plain'
|
||||
|
||||
// HTML
|
||||
case 'html':
|
||||
return 'devicon-html5-plain'
|
||||
|
||||
// Stylesheet
|
||||
case 'css':
|
||||
return 'devicon-css3-plain'
|
||||
case 'less':
|
||||
return 'devicon-less-plain-wordmark'
|
||||
case 'sass':
|
||||
case 'scss':
|
||||
return 'devicon-sass-original'
|
||||
|
||||
// Compile
|
||||
case 'c':
|
||||
return 'devicon-c-plain'
|
||||
case 'cpp':
|
||||
return 'devicon-cplusplus-plain'
|
||||
case 'csharp':
|
||||
return 'devicon-csharp-plain'
|
||||
case 'objc':
|
||||
return 'devicon-apple-original'
|
||||
case 'golang':
|
||||
return 'devicon-go-plain'
|
||||
case 'java':
|
||||
return 'devicon-java-plain'
|
||||
|
||||
// Framework
|
||||
case 'django':
|
||||
return 'devicon-django-plain'
|
||||
|
||||
// Config
|
||||
case 'dockerfile':
|
||||
return 'devicon-docker-plain'
|
||||
case 'gitignore':
|
||||
return 'devicon-git-plain'
|
||||
|
||||
// Shell
|
||||
case 'sh':
|
||||
case 'batchfile':
|
||||
case 'powershell':
|
||||
return 'fa fa-fw fa-terminal'
|
||||
|
||||
case 'text':
|
||||
case 'markdown':
|
||||
return 'fa fa-fw fa-file-text-o'
|
||||
}
|
||||
return 'fa fa-fw fa-code'
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
|
||||
return (
|
||||
<i className={className}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from './ModeIcon'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
|
||||
const IDLE_MODE = 'IDLE_MODE'
|
||||
const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
export default class ModeSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.blurHandler = e => {
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
|
||||
this.handleBlur(e)
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('click', this.blurHandler)
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (searchElement != null && this.searchKeyDownListener != null) {
|
||||
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
|
||||
}
|
||||
}
|
||||
|
||||
handleIdleSelectClick (e) {
|
||||
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
|
||||
ReactDOM.findDOMNode(this.refs.search).select()
|
||||
})
|
||||
}
|
||||
|
||||
handleModeOptionClick (modeName) {
|
||||
return e => {
|
||||
this.props.onChange(modeName)
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// up
|
||||
case 38:
|
||||
e.preventDefault()
|
||||
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
|
||||
break
|
||||
// down
|
||||
case 40:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
|
||||
else this.setState({focusIndex: this.state.focusIndex + 1})
|
||||
}
|
||||
break
|
||||
// enter
|
||||
case 13:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
let targetMode = filteredModes[this.state.focusIndex]
|
||||
if (targetMode != null) {
|
||||
this.props.onChange(targetMode.name)
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
break
|
||||
// esc
|
||||
case 27:
|
||||
case 9:
|
||||
e.stopPropagation()
|
||||
this.setIdle()
|
||||
}
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(e)
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
|
||||
setIdle () {
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = this.props.className != null
|
||||
? `ModeSelect ${this.props.className}`
|
||||
: this.props.className
|
||||
|
||||
if (this.state.mode === IDLE_MODE) {
|
||||
let mode = _.findWhere(modes, {name: this.props.value})
|
||||
let modeName = mode != null ? mode.name : 'text'
|
||||
let modeLabel = mode != null ? mode.label : this.props.value
|
||||
|
||||
return (
|
||||
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
||||
<ModeIcon mode={modeName}/>{modeLabel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredOptions = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
.map((mode, index) => {
|
||||
return (
|
||||
<div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={className + ' edit'}>
|
||||
<input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
|
||||
<div ref='options' className='ModeSelect-options hide'>
|
||||
{filteredOptions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onKeyDown: PropTypes.func
|
||||
}
|
||||
@@ -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
|
||||
297
browser/components/markdown.styl
Normal file
297
browser/components/markdown.styl
Normal file
@@ -0,0 +1,297 @@
|
||||
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
|
||||
li
|
||||
label.taskListItem
|
||||
margin-left -2em
|
||||
background-color white
|
||||
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
|
||||
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
|
||||
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
|
||||
p
|
||||
margin 0
|
||||
&>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
|
||||
p
|
||||
margin 0
|
||||
&>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
|
||||
pre
|
||||
padding 0.5em !important
|
||||
border solid 1px #D1D1D1
|
||||
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
|
||||
font-weight bold
|
||||
&: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
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #DDDDDD
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
themeDarkPreview = #282C34
|
||||
themeDarkTableOdd = themeDarkPreview
|
||||
themeDarkTableEven = darken(themeDarkPreview, 10%)
|
||||
themeDarkTableHead = themeDarkTableEven
|
||||
themeDarkTableBorder = themeDarkBorder
|
||||
themeDarkModalButtonDefault = themeDarkPreview
|
||||
themeDarkModalButtonDanger = #BF360C
|
||||
body[data-theme="dark"]
|
||||
color themeDarkText
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
a:hover
|
||||
background-color alpha(lighten(brandColor, 30%), 0.2) !important
|
||||
|
||||
code
|
||||
border-color darken(themeDarkBorder, 10%)
|
||||
background-color lighten(themeDarkPreview, 5%)
|
||||
|
||||
pre
|
||||
border-color lighten(#21252B, 20%)
|
||||
code
|
||||
background-color transparent
|
||||
|
||||
label.taskListItem
|
||||
background-color themeDarkPreview
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeDarkTableHead
|
||||
th
|
||||
border-color themeDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDarkTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeDarkTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeDarkTableEven
|
||||
td
|
||||
border-color themeDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeDarkTableBorder
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
export default class FinderDetail extends React.Component {
|
||||
render () {
|
||||
let { activeArticle } = this.props
|
||||
|
||||
if (activeArticle != null) {
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'>
|
||||
<div className='left'>
|
||||
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
|
||||
</div>
|
||||
<div className='right'>
|
||||
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
|
||||
<i className='fa fa-clipboard fa-fw'/>
|
||||
<span className='tooltip'>Copy to clipboard (Enter)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='content'>
|
||||
{activeArticle.mode === 'markdown'
|
||||
? <MarkdownPreview content={activeArticle.content}/>
|
||||
: <CodeEditor readOnly article={activeArticle}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='nothing'>Nothing selected</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderDetail.propTypes = {
|
||||
activeArticle: PropTypes.shape(),
|
||||
saveToClipboard: PropTypes.func
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class FinderInput extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='FinderInput'>
|
||||
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderInput.propTypes = {
|
||||
handleSearchChange: PropTypes.func,
|
||||
value: PropTypes.string
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import { selectArticle } from './actions'
|
||||
|
||||
export default class FinderList extends React.Component {
|
||||
componentDidUpdate () {
|
||||
var index = this.props.articles.indexOf(this.props.activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('li')[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
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
return (e) => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(selectArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let articleElements = this.props.articles.map(function (article) {
|
||||
if (article == null) {
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
|
||||
return (
|
||||
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>
|
||||
<ModeIcon mode={article.mode}/> {article.title}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='FinderList'>
|
||||
<ul>
|
||||
{articleElements}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderList.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
110
browser/finder/FinderMain.styl
Normal file
110
browser/finder/FinderMain.styl
Normal file
@@ -0,0 +1,110 @@
|
||||
$search-height = 50px
|
||||
$nav-width = 175px
|
||||
$list-width = 250px
|
||||
|
||||
.root
|
||||
absolute top left right bottom
|
||||
|
||||
.search
|
||||
height $search-height
|
||||
padding 10px
|
||||
box-sizing border-box
|
||||
border-bottom $ui-border
|
||||
text-align center
|
||||
|
||||
.search-input
|
||||
height 30px
|
||||
width 100%
|
||||
margin 0 auto
|
||||
font-size 18px
|
||||
border none
|
||||
outline none
|
||||
text-align center
|
||||
background-color transparent
|
||||
.result
|
||||
absolute left right bottom
|
||||
top $search-height
|
||||
|
||||
.result-nav
|
||||
user-select none
|
||||
absolute left top bottom
|
||||
width $nav-width
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.result-nav-filter
|
||||
margin-bottom 5px
|
||||
|
||||
.result-nav-filter-option
|
||||
height 25px
|
||||
line-height 25px
|
||||
padding 0 10px
|
||||
label
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu
|
||||
navButtonColor()
|
||||
height 32px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
width 100%
|
||||
outline none
|
||||
text-align left
|
||||
line-height 32px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
|
||||
.result-nav-menu--active
|
||||
@extend .result-nav-menu
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.result-nav-storageList
|
||||
absolute bottom left right
|
||||
top 80px + 32px + 10px
|
||||
overflow-y auto
|
||||
|
||||
.result-list
|
||||
user-select none
|
||||
absolute top bottom
|
||||
left $nav-width
|
||||
width $list-width
|
||||
border-width 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
.search
|
||||
border-color $ui-dark-borderColor
|
||||
.search-input
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav
|
||||
background-color $ui-dark-backgroundColor
|
||||
label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.result-nav-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.result-nav-menu--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.result-list
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.result-detail
|
||||
absolute top bottom right
|
||||
left $nav-width + $list-width
|
||||
202
browser/finder/NoteDetail.js
Normal file
202
browser/finder/NoteDetail.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteDetail.styl'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import CodeMirror from 'codemirror'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard } = electron
|
||||
const path = require('path')
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
function notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
class NoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
snippetIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note !== this.props.note) {
|
||||
this.setState({
|
||||
snippetIndex: 0
|
||||
}, () => {
|
||||
if (nextProps.note.type === 'SNIPPET_NOTE') {
|
||||
nextProps.note.snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectPriorSnippet () {
|
||||
let { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNextSnippet () {
|
||||
let { note } = this.props
|
||||
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
let { note } = this.props
|
||||
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
clipboard.writeText(note.content)
|
||||
} else {
|
||||
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
|
||||
}
|
||||
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
|
||||
handleTabButtonClick (e, index) {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { note, config } = this.props
|
||||
if (note == null) {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
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(e, index)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='tabList-item-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
})
|
||||
|
||||
let viewList = note.snippets.map((snippet, index) => {
|
||||
let isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
>
|
||||
{snippet.mode === 'markdown'
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
config={config}
|
||||
value={snippet.content}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
: <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}
|
||||
readOnly
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='description'>
|
||||
<textarea styleName='description-textarea'
|
||||
style={{
|
||||
fontFamily: config.preview.fontFamily,
|
||||
fontSize: parseInt(config.preview.fontSize, 10)
|
||||
}}
|
||||
ref='description'
|
||||
placeholder='Description...'
|
||||
value={note.description}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div styleName='tabList'>
|
||||
{tabList}
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownPreview styleName='root'
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
value={note.content}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteDetail.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteDetail, styles)
|
||||
109
browser/finder/NoteDetail.styl
Normal file
109
browser/finder/NoteDetail.styl
Normal file
@@ -0,0 +1,109 @@
|
||||
.root
|
||||
absolute top bottom left right
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.description
|
||||
absolute top left right
|
||||
height 80px
|
||||
border-bottom $ui-border
|
||||
box-sizing border-box
|
||||
|
||||
.description-textarea
|
||||
display block
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
line-height 1.6
|
||||
box-sizing border-box
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
box-sizing border-box
|
||||
height 30px
|
||||
border-bottom $ui-border
|
||||
display flex
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.tabList-item
|
||||
position relative
|
||||
flex 1
|
||||
border-right $ui-border
|
||||
|
||||
.tabList-item--active
|
||||
@extend .tabList-item
|
||||
.tabList-item-button
|
||||
border-color $brand-color
|
||||
|
||||
.tabList-item-button
|
||||
width 100%
|
||||
height 29px
|
||||
navButtonColor()
|
||||
outline none
|
||||
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-content
|
||||
absolute top left right bottom
|
||||
box-sizing border-box
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
body[data-theme="dark"]
|
||||
.description
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.description-textarea
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
border-bottom-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
.tabList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.tabList-item--active
|
||||
border-color $ui-dark-borderColor
|
||||
.tabList-item-button
|
||||
border-color $brand-color
|
||||
.tabList-item-button
|
||||
navDarkButtonColor()
|
||||
border-left 4px solid transparent
|
||||
.tabList-plusButton
|
||||
navDarkButtonColor()
|
||||
|
||||
.tabView-top
|
||||
border-color $ui-dark-borderColor
|
||||
.tabView-top-name
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.tabView-top-mode
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $dark-default-button-background
|
||||
color $ui-dark-inactive-text-color
|
||||
85
browser/finder/NoteItem.js
Normal file
85
browser/finder/NoteItem.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NoteItem.styl'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
|
||||
class NoteItem extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
this.props.onClick(e)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { note, folder, storage, isActive } = this.props
|
||||
|
||||
let tagList = _.isArray(note.tags)
|
||||
? note.tags.map((tag) => {
|
||||
return (
|
||||
<span styleName='bottom-tagList-item'
|
||||
key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
return (
|
||||
<div styleName={isActive
|
||||
? 'root--active'
|
||||
: 'root'
|
||||
}
|
||||
key={note.storage + '-' + note.key}
|
||||
onClick={(e) => this.handleClick(e)}
|
||||
>
|
||||
<div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<span styleName='info-left-folder'
|
||||
style={{borderColor: folder.color}}
|
||||
>
|
||||
{folder.name}
|
||||
<span styleName='info-left-folder-surfix'>in {storage.name}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='title'>
|
||||
{note.type === 'SNIPPET_NOTE'
|
||||
? <i styleName='title-icon' className='fa fa-fw fa-code'/>
|
||||
: <i styleName='title-icon' className='fa fa-fw fa-file-text-o'/>
|
||||
}
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='title-empty'>Empty</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div styleName='bottom'>
|
||||
<i styleName='bottom-tagIcon'
|
||||
className='fa fa-tags fa-fw'
|
||||
/>
|
||||
<div styleName='bottom-tagList'>
|
||||
{tagList.length > 0
|
||||
? tagList
|
||||
: <span styleName='bottom-tagList-empty'>Not tagged yet</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div styleName='bottom-time'>
|
||||
{moment(note.updatedAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteItem.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(NoteItem, styles)
|
||||
179
browser/finder/NoteItem.styl
Normal file
179
browser/finder/NoteItem.styl
Normal file
@@ -0,0 +1,179 @@
|
||||
.root
|
||||
position relative
|
||||
border-bottom $ui-border
|
||||
padding 2px 5px
|
||||
user-select none
|
||||
cursor pointer
|
||||
transition background-color 0.15s
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
background-color $ui-active-color
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.info-left-folder
|
||||
.info-left-folder-surfix
|
||||
.title
|
||||
.title-icon
|
||||
.title-empty
|
||||
.bottom-tagIcon
|
||||
.bottom-tagList-item
|
||||
.bottom-tagList-empty
|
||||
.bottom-time
|
||||
color white
|
||||
.bottom-tagList-item
|
||||
color white
|
||||
background-color transparent
|
||||
|
||||
.border
|
||||
absolute top bottom left right
|
||||
border-style solid
|
||||
border-width 2px
|
||||
border-color transparent
|
||||
transition 0.15s
|
||||
|
||||
.info
|
||||
height 20px
|
||||
clearfix()
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
line-height 20px
|
||||
overflow-y hidden
|
||||
|
||||
.info-left
|
||||
float left
|
||||
overflow ellipsis
|
||||
|
||||
.info-left-folder
|
||||
border-left 4px solid transparent
|
||||
padding 2px 5px
|
||||
color $ui-text-color
|
||||
.info-left-folder-surfix
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
color $ui-inactive-text-color
|
||||
.info-right
|
||||
float right
|
||||
|
||||
.title
|
||||
height 24px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
height 20px
|
||||
line-height 20px
|
||||
padding 0 5px 0 0
|
||||
overflow ellipsis
|
||||
color $ui-text-color
|
||||
|
||||
.title-icon
|
||||
font-size 12px
|
||||
color $ui-inactive-text-color
|
||||
padding-right 3px
|
||||
|
||||
.title-empty
|
||||
font-weight normal
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.bottom
|
||||
margin-top 2px
|
||||
height 20px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
|
||||
.bottom-tagIcon
|
||||
vertical-align middle
|
||||
color $ui-button-color
|
||||
height 20px
|
||||
line-height 20px
|
||||
|
||||
.bottom-tagList
|
||||
flex 1
|
||||
overflow ellipsis
|
||||
line-height 20px
|
||||
|
||||
.bottom-tagList-item
|
||||
margin 0 4px
|
||||
padding 0 4px
|
||||
height 20px
|
||||
box-sizing border-box
|
||||
border-radius 3px
|
||||
vertical-align middle
|
||||
border-style solid
|
||||
border-color $ui-button--focus-borderColor
|
||||
border-width 0 0 0 3px
|
||||
background-color $ui-backgroundColor
|
||||
color $ui-text-color
|
||||
transition 0.15s
|
||||
|
||||
.bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
|
||||
.bottom-time
|
||||
color $ui-inactive-text-color
|
||||
margin-left 5px
|
||||
font-size 10px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
.info-left-folder
|
||||
.info-left-folder-surfix
|
||||
.title
|
||||
.title-icon
|
||||
.title-empty
|
||||
.bottom-tagIcon
|
||||
.bottom-tagList-item
|
||||
.bottom-tagList-empty
|
||||
.bottom-time
|
||||
color white
|
||||
.bottom-tagList-item
|
||||
color white
|
||||
background-color transparent
|
||||
|
||||
.info
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.info-left-folder
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-left-folder-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.title
|
||||
color $ui-dark-text-color
|
||||
|
||||
.title-icon
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.tagList-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.bottom-tagIcon
|
||||
color $ui-dark-button-color
|
||||
|
||||
.bottom-tagList-item
|
||||
color $ui-dark-text-color
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.bottom-tagList-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.bottom-time
|
||||
color $ui-dark-inactive-text-color
|
||||
89
browser/finder/NoteList.js
Normal file
89
browser/finder/NoteList.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import NoteItem from './NoteItem'
|
||||
import _ from 'lodash'
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
range: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.search !== nextProps.search) {
|
||||
this.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
let { index } = this.props
|
||||
|
||||
if (index > -1) {
|
||||
let list = this.refs.root
|
||||
let item = list.childNodes[index]
|
||||
if (item == null) return null
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.root.scrollTop = 0
|
||||
this.setState({
|
||||
range: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
let { notes } = this.props
|
||||
|
||||
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
|
||||
this.setState({
|
||||
range: this.state.range + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storageMap, notes, index } = this.props
|
||||
|
||||
let notesList = notes
|
||||
.slice(0, 10 + 10 * this.state.range)
|
||||
.map((note, _index) => {
|
||||
let storage = storageMap[note.storage]
|
||||
let folder = _.find(storage.folders, {key: note.folder})
|
||||
return (
|
||||
<NoteItem
|
||||
note={note}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
storage={storage}
|
||||
folder={folder}
|
||||
isActive={index === _index}
|
||||
onClick={(e) => this.props.handleNoteClick(e, _index)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div className={this.props.className}
|
||||
onScroll={(e) => this.handleScroll(e)}
|
||||
ref='root'
|
||||
>
|
||||
{notesList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
}
|
||||
|
||||
export default NoteList
|
||||
79
browser/finder/StorageSection.js
Normal file
79
browser/finder/StorageSection.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageSection.styl'
|
||||
|
||||
class StorageSection extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
handleHeaderClick (e) {
|
||||
let { storage } = this.props
|
||||
this.props.handleStorageButtonClick(e, storage.key)
|
||||
}
|
||||
|
||||
handleFolderClick (e, folder) {
|
||||
let { storage } = this.props
|
||||
this.props.handleFolderButtonClick(e, storage.key, folder.key)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storage, filter } = this.props
|
||||
let folderList = storage.folders
|
||||
.map((folder) => {
|
||||
return (
|
||||
<button styleName={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
style={{borderColor: folder.color}}
|
||||
key={folder.key}
|
||||
onClick={(e) => this.handleFolderClick(e, folder)}
|
||||
>
|
||||
{folder.name}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>
|
||||
<button styleName='header-toggleButton'
|
||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
<i className={this.state.isOpen
|
||||
? 'fa fa-caret-down'
|
||||
: 'fa fa-caret-right'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
|
||||
? 'header-name--active'
|
||||
: 'header-name'
|
||||
}
|
||||
onClick={(e) => this.handleHeaderClick(e)}
|
||||
>{storage.name}</button>
|
||||
</div>
|
||||
{this.state.isOpen &&
|
||||
<div styleName='folderList'>
|
||||
{folderList}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageSection.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(StorageSection, styles)
|
||||
85
browser/finder/StorageSection.styl
Normal file
85
browser/finder/StorageSection.styl
Normal file
@@ -0,0 +1,85 @@
|
||||
.root
|
||||
position relative
|
||||
|
||||
.header
|
||||
height 26px
|
||||
.header-toggleButton
|
||||
absolute top left
|
||||
width 25px
|
||||
height 26px
|
||||
navButtonColor()
|
||||
border none
|
||||
outline none
|
||||
.header-name
|
||||
display block
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
display block
|
||||
width 100%
|
||||
height 26px
|
||||
navButtonColor()
|
||||
padding 0 10px 0 25px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
line-height 26px
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
outline none
|
||||
padding 0 10px
|
||||
margin 2px 0
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
body[data-theme="dark"]
|
||||
.header-toggleButton
|
||||
navDarkButtonColor()
|
||||
.header-name
|
||||
navDarkButtonColor()
|
||||
|
||||
.header-name--active
|
||||
@extend .header-name
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.folderList-item
|
||||
navDarkButtonColor()
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
background-color $ui-button--active-backgroundColor
|
||||
color $ui-button--active-color
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -1,39 +0,0 @@
|
||||
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
|
||||
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
|
||||
export const REFRESH_DATA = 'REFRESH_DATA'
|
||||
|
||||
export function selectArticle (key) {
|
||||
return {
|
||||
type: SELECT_ARTICLE,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function searchArticle (input) {
|
||||
return {
|
||||
type: SEARCH_ARTICLE,
|
||||
data: { input }
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshData (data) {
|
||||
console.log('refreshing data')
|
||||
let { folders, articles } = data
|
||||
|
||||
return {
|
||||
type: REFRESH_DATA,
|
||||
data: {
|
||||
articles,
|
||||
folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
SELECT_ARTICLE,
|
||||
SEARCH_ARTICLE,
|
||||
REFRESH_DATA,
|
||||
selectArticle,
|
||||
searchArticle,
|
||||
refreshData
|
||||
}
|
||||
@@ -1,69 +1,90 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
import FinderInput from './FinderInput'
|
||||
import FinderList from './FinderList'
|
||||
import FinderDetail from './FinderDetail'
|
||||
import actions, { selectArticle, searchArticle } from './actions'
|
||||
import _ from 'lodash'
|
||||
import dataStore from 'browser/lib/dataStore'
|
||||
import ipc from './ipcClient'
|
||||
import store from './store'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './FinderMain.styl'
|
||||
import StorageSection from './StorageSection'
|
||||
import NoteList from './NoteList'
|
||||
import NoteDetail from './NoteDetail'
|
||||
require('!!style!css!stylus?sourceMap!../main/global.styl')
|
||||
require('../lib/customMeta')
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard, ipcRenderer, remote } = electron
|
||||
const path = require('path')
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||
remote.getCurrentWindow().toggleDevTools()
|
||||
}
|
||||
})
|
||||
}
|
||||
const { remote } = electron
|
||||
const { Menu } = remote
|
||||
|
||||
function hideFinder () {
|
||||
ipcRenderer.send('hide-finder')
|
||||
}
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
let finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'win32') {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
if (global.process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
remote.getCurrentWindow().hide()
|
||||
}
|
||||
|
||||
require('../styles/finder/index.styl')
|
||||
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
|
||||
|
||||
class FinderMain extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
search: '',
|
||||
index: 0,
|
||||
filter: {
|
||||
includeSnippet: true,
|
||||
includeMarkdown: false,
|
||||
type: 'ALL',
|
||||
storage: null,
|
||||
folder: null
|
||||
}
|
||||
}
|
||||
|
||||
this.focusHandler = (e) => this.handleWindowFocus(e)
|
||||
this.blurHandler = (e) => this.handleWindowBlur(e)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.keyDownHandler = e => this.handleKeyDown(e)
|
||||
document.addEventListener('keydown', this.keyDownHandler)
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
this.focusHandler = e => {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(''))
|
||||
dispatch(selectArticle(null))
|
||||
}
|
||||
this.refs.search.focus()
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
window.addEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.keyDownHandler)
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
window.removeEventListener('blur', this.blurHandler)
|
||||
}
|
||||
|
||||
handleWindowFocus (e) {
|
||||
this.refs.search.focus()
|
||||
}
|
||||
|
||||
handleWindowBlur (e) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'ALL'
|
||||
this.setState({
|
||||
search: '',
|
||||
filter,
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
this.refs.search.focus()
|
||||
if (e.keyCode === 9) {
|
||||
if (e.shiftKey) {
|
||||
this.refs.detail.selectPriorSnippet()
|
||||
} else {
|
||||
this.refs.detail.selectNextSnippet()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
@@ -75,7 +96,8 @@ class FinderMain extends React.Component {
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.saveToClipboard()
|
||||
this.refs.detail.saveToClipboard()
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
@@ -85,26 +107,13 @@ class FinderMain extends React.Component {
|
||||
if (e.keyCode === 91 || e.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
}
|
||||
|
||||
saveToClipboard () {
|
||||
let { activeArticle } = this.props
|
||||
clipboard.writeText(activeArticle.content)
|
||||
|
||||
ipcRenderer.send('copy-finder')
|
||||
notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
hideFinder()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(e.target.value))
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
index: 0
|
||||
})
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
@@ -112,151 +121,237 @@ class FinderMain extends React.Component {
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index - 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
if (this.state.index > 0) {
|
||||
this.setState({
|
||||
index: this.state.index - 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index + 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
if (this.state.index < this.noteCount - 1) {
|
||||
this.setState({
|
||||
index: this.state.index + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleOnlySnippetCheckboxChange (e) {
|
||||
let { filter } = this.state
|
||||
filter.includeSnippet = e.target.checked
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnlyMarkdownCheckboxChange (e) {
|
||||
let { filter } = this.state
|
||||
filter.includeMarkdown = e.target.checked
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter: filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleAllNotesButtonClick (e) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'ALL'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'STARRED'
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleStorageButtonClick (e, storage) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'STORAGE'
|
||||
filter.storage = storage
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderButtonClick (e, storage, folder) {
|
||||
let { filter } = this.state
|
||||
filter.type = 'FOLDER'
|
||||
filter.storage = storage
|
||||
filter.folder = folder
|
||||
this.refs.list.resetScroll()
|
||||
this.setState({
|
||||
filter,
|
||||
index: 0
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, index) {
|
||||
this.setState({
|
||||
index
|
||||
}, () => {
|
||||
this.refs.search.focus()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { articles, activeArticle, status, dispatch } = this.props
|
||||
let saveToClipboard = () => this.saveToClipboard()
|
||||
let { data, config } = this.props
|
||||
let { filter, search } = this.state
|
||||
let storageList = []
|
||||
for (let key in data.storageMap) {
|
||||
let storage = data.storageMap[key]
|
||||
let item = (
|
||||
<StorageSection
|
||||
filter={filter}
|
||||
storage={storage}
|
||||
key={storage.key}
|
||||
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
|
||||
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
|
||||
/>
|
||||
)
|
||||
storageList.push(item)
|
||||
}
|
||||
let notes = []
|
||||
let noteIds
|
||||
|
||||
switch (filter.type) {
|
||||
case 'STORAGE':
|
||||
noteIds = data.storageNoteMap[filter.storage]
|
||||
break
|
||||
case 'FOLDER':
|
||||
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
|
||||
break
|
||||
case 'STARRED':
|
||||
noteIds = data.starredSet
|
||||
}
|
||||
if (noteIds != null) {
|
||||
noteIds.forEach((id) => {
|
||||
notes.push(data.noteMap[id])
|
||||
})
|
||||
} else {
|
||||
for (let key in data.noteMap) {
|
||||
notes.push(data.noteMap[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!filter.includeSnippet && filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
|
||||
} else if (filter.includeSnippet && !filter.includeMarkdown) {
|
||||
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
|
||||
}
|
||||
|
||||
if (search.trim().length > 0) {
|
||||
let needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
|
||||
notes = notes.filter((note) => note.title.match(needle))
|
||||
}
|
||||
notes = notes
|
||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||
|
||||
let activeNote = notes[this.state.index]
|
||||
this.noteCount = notes.length
|
||||
|
||||
return (
|
||||
<div className='Finder'>
|
||||
<FinderInput
|
||||
handleSearchChange={e => this.handleSearchChange(e)}
|
||||
ref='finderInput'
|
||||
onChange={this.handleChange}
|
||||
value={status.search}
|
||||
/>
|
||||
<FinderList
|
||||
ref='finderList'
|
||||
activeArticle={activeArticle}
|
||||
articles={articles}
|
||||
dispatch={dispatch}
|
||||
selectArticle={article => this.selectArticle(article)}
|
||||
/>
|
||||
<FinderDetail
|
||||
activeArticle={activeArticle}
|
||||
saveToClipboard={saveToClipboard}
|
||||
/>
|
||||
<div className='Finder'
|
||||
styleName='root'
|
||||
ref='-1'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='search'>
|
||||
<input
|
||||
styleName='search-input'
|
||||
ref='search'
|
||||
value={search}
|
||||
placeholder='Search...'
|
||||
onChange={(e) => this.handleSearchChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='result'>
|
||||
<div styleName='result-nav'>
|
||||
<div styleName='result-nav-filter'>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeSnippet}
|
||||
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
|
||||
/> Only Snippets</label>
|
||||
</div>
|
||||
<div styleName='result-nav-filter-option'>
|
||||
<label>
|
||||
<input type='checkbox'
|
||||
checked={filter.includeMarkdown}
|
||||
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
|
||||
/> Only Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<button styleName={filter.type === 'ALL'
|
||||
? 'result-nav-menu--active'
|
||||
: 'result-nav-menu'
|
||||
}
|
||||
onClick={(e) => this.handleAllNotesButtonClick(e)}
|
||||
><i className='fa fa-files-o fa-fw'/> All Notes</button>
|
||||
<button styleName={filter.type === 'STARRED'
|
||||
? 'result-nav-menu--active'
|
||||
: 'result-nav-menu'
|
||||
}
|
||||
onClick={(e) => this.handleStarredButtonClick(e)}
|
||||
><i className='fa fa-star fa-fw'/> Starred</button>
|
||||
<div styleName='result-nav-storageList'>
|
||||
{storageList}
|
||||
</div>
|
||||
</div>
|
||||
<NoteList styleName='result-list'
|
||||
storageMap={data.storageMap}
|
||||
notes={notes}
|
||||
ref='list'
|
||||
search={search}
|
||||
index={this.state.index}
|
||||
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
|
||||
/>
|
||||
<div styleName='result-detail'>
|
||||
<NoteDetail
|
||||
note={activeNote}
|
||||
config={config}
|
||||
ref='detail'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderMain.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
tags: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.shape(),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
// Ignore invalid key
|
||||
function ignoreInvalidKey (key) {
|
||||
return key.length > 0 && !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 { articles, folders, status } = state
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
return {
|
||||
articles,
|
||||
activeArticle,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
var Finder = connect(remap)(FinderMain)
|
||||
var store = createStore(reducer)
|
||||
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
|
||||
|
||||
function refreshData () {
|
||||
let data = dataStore.getData(true)
|
||||
store.dispatch(actions.refreshData(data))
|
||||
}
|
||||
|
||||
window.onfocus = e => {
|
||||
refreshData()
|
||||
// let data = dataStore.getData(true)
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
|
||||
122
browser/finder/ipcClient.js
Normal file
122
browser/finder/ipcClient.js
Normal file
@@ -0,0 +1,122 @@
|
||||
const nodeIpc = require('node-ipc')
|
||||
const { remote, ipcRenderer } = require('electron')
|
||||
const { app, Menu } = remote
|
||||
const path = require('path')
|
||||
const store = require('./store')
|
||||
const consts = require('browser/lib/consts')
|
||||
|
||||
nodeIpc.config.id = 'finder'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
function killFinder () {
|
||||
let finderWindow = remote.getCurrentWindow()
|
||||
finderWindow.removeAllListeners()
|
||||
if (global.process.platform === 'darwin') {
|
||||
// Only OSX has another app process.
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
} else {
|
||||
finderWindow.close()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFinder () {
|
||||
let finderWindow = remote.getCurrentWindow()
|
||||
if (global.process.platform === 'darwin') {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.hide()
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
}
|
||||
} else {
|
||||
if (finderWindow.isVisible()) {
|
||||
finderWindow.blur()
|
||||
finderWindow.hide()
|
||||
} else {
|
||||
nodeIpc.of.node.emit('request-data-from-finder')
|
||||
finderWindow.show()
|
||||
finderWindow.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeIpc.connectTo(
|
||||
'node',
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Conncted successfully')
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('open-finder', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
|
||||
ipcRenderer.on('open-finder-from-tray', function () {
|
||||
toggleFinder()
|
||||
})
|
||||
ipcRenderer.on('open-main-from-tray', function () {
|
||||
nodeIpc.of.node.emit('open-main-from-finder')
|
||||
})
|
||||
|
||||
ipcRenderer.on('quit-from-tray', function () {
|
||||
nodeIpc.of.node.emit('quit-from-finder')
|
||||
killFinder()
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('throttle-data', function (payload) {
|
||||
console.log('Received data from Main renderer')
|
||||
store.default.dispatch({
|
||||
type: 'THROTTLE_DATA',
|
||||
data: payload
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('config-renew', function (payload) {
|
||||
const { config } = payload
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
|
||||
store.default.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: config
|
||||
})
|
||||
})
|
||||
|
||||
nodeIpc.of.node.on('quit-finder-app', function () {
|
||||
nodeIpc.of.node.emit('quit-finder-app-confirm')
|
||||
killFinder()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ipc = {}
|
||||
|
||||
module.exports = ipc
|
||||
@@ -1,47 +0,0 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
|
||||
|
||||
let initialArticles = []
|
||||
let initialFolders = []
|
||||
let initialStatus = {
|
||||
articleKey: null,
|
||||
search: ''
|
||||
}
|
||||
|
||||
function status (state = initialStatus, action) {
|
||||
switch (action.type) {
|
||||
case SELECT_ARTICLE:
|
||||
state.articleKey = action.data.key
|
||||
return Object.assign({}, state)
|
||||
case SEARCH_ARTICLE:
|
||||
state.search = action.data.input
|
||||
return Object.assign({}, state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function articles (state = initialArticles, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
return action.data.articles
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function folders (state = initialFolders, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
console.log(action)
|
||||
return action.data.folders
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
status,
|
||||
folders,
|
||||
articles
|
||||
})
|
||||
51
browser/finder/store.js
Normal file
51
browser/finder/store.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
|
||||
|
||||
let defaultData = {
|
||||
storageMap: {},
|
||||
noteMap: {},
|
||||
starredSet: [],
|
||||
storageNoteMap: {},
|
||||
folderNoteMap: {},
|
||||
tagNoteMap: {}
|
||||
}
|
||||
|
||||
function data (state = defaultData, action) {
|
||||
switch (action.type) {
|
||||
case 'THROTTLE_DATA':
|
||||
console.log(action)
|
||||
state = action.data
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function config (state = DEFAULT_CONFIG, action) {
|
||||
switch (action.type) {
|
||||
case 'INIT_CONFIG':
|
||||
case 'SET_CONFIG':
|
||||
return Object.assign({}, state, action.config)
|
||||
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_UI':
|
||||
return Object.assign({}, state, action.config)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
let reducer = combineReducers({
|
||||
data,
|
||||
config,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
let store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
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})
|
||||
}
|
||||
106
browser/lib/Mutable.js
Normal file
106
browser/lib/Mutable.js
Normal file
@@ -0,0 +1,106 @@
|
||||
class MutableMap {
|
||||
constructor (iterable) {
|
||||
this._map = new Map(iterable)
|
||||
Object.defineProperty(this, 'size', {
|
||||
get: () => this._map.size,
|
||||
set: function (value) {
|
||||
this['size'] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get (...args) {
|
||||
return this._map.get(...args)
|
||||
}
|
||||
|
||||
set (...args) {
|
||||
return this._map.set(...args)
|
||||
}
|
||||
|
||||
delete (...args) {
|
||||
return this._map.delete(...args)
|
||||
}
|
||||
|
||||
has (...args) {
|
||||
return this._map.has(...args)
|
||||
}
|
||||
|
||||
clear (...args) {
|
||||
return this._map.clear(...args)
|
||||
}
|
||||
|
||||
forEach (...args) {
|
||||
return this._map.forEach(...args)
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this._map[Symbol.iterator]()
|
||||
}
|
||||
|
||||
map (cb) {
|
||||
let result = []
|
||||
for (let [key, value] of this._map) {
|
||||
result.push(cb(value, key))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
toJS () {
|
||||
let result = {}
|
||||
for (let [key, value] of this._map) {
|
||||
if (value instanceof MutableSet || value instanceof MutableMap) {
|
||||
value = value.toJS()
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class MutableSet {
|
||||
constructor (iterable) {
|
||||
this._set = new Set(iterable)
|
||||
Object.defineProperty(this, 'size', {
|
||||
get: () => this._set.size,
|
||||
set: function (value) {
|
||||
this['size'] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
add (...args) {
|
||||
return this._set.add(...args)
|
||||
}
|
||||
|
||||
delete (...args) {
|
||||
return this._set.delete(...args)
|
||||
}
|
||||
|
||||
forEach (...args) {
|
||||
return this._set.forEach(...args)
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this._set[Symbol.iterator]()
|
||||
}
|
||||
|
||||
map (cb) {
|
||||
let result = []
|
||||
this._set.forEach(function (value, key) {
|
||||
result.push(cb(value, key))
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
toJS () {
|
||||
return Array.from(this._set)
|
||||
}
|
||||
}
|
||||
|
||||
const Mutable = {
|
||||
Map: MutableMap,
|
||||
Set: MutableSet
|
||||
}
|
||||
|
||||
module.exports = Mutable
|
||||
@@ -1,138 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import dataStore from './dataStore'
|
||||
import { request, SERVER_URL } from './api'
|
||||
import clientKey from './clientKey'
|
||||
|
||||
const electron = require('electron')
|
||||
const version = electron.remote.app.getVersion()
|
||||
|
||||
function isSameDate (a, b) {
|
||||
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
||||
b = moment(b).utcOffset(+540).format('YYYYMMDD')
|
||||
|
||||
return a === b
|
||||
}
|
||||
|
||||
export function init () {
|
||||
let records = getAllRecords()
|
||||
if (records == null) {
|
||||
saveAllRecords([])
|
||||
}
|
||||
emit(null)
|
||||
|
||||
postRecords()
|
||||
if (window != null) {
|
||||
window.addEventListener('online', postRecords)
|
||||
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllRecords () {
|
||||
return JSON.parse(localStorage.getItem('activityRecords'))
|
||||
}
|
||||
|
||||
export function saveAllRecords (records) {
|
||||
localStorage.setItem('activityRecords', JSON.stringify(records))
|
||||
}
|
||||
|
||||
/*
|
||||
Post all records(except today)
|
||||
and remove all posted records
|
||||
*/
|
||||
export function postRecords (data) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('post failed - NOT PRODUCTION ')
|
||||
return
|
||||
}
|
||||
|
||||
let records = getAllRecords()
|
||||
records = records.filter(record => {
|
||||
return !isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
if (records.length === 0) {
|
||||
console.log('No records to post')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('posting...', records)
|
||||
let input = {
|
||||
clientKey: clientKey.get(),
|
||||
records
|
||||
}
|
||||
return request.post(SERVER_URL + 'apis/activity')
|
||||
.send(input)
|
||||
.then(res => {
|
||||
let records = getAllRecords()
|
||||
let todayRecord = _.find(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
if (todayRecord != null) saveAllRecords([todayRecord])
|
||||
else saveAllRecords([])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
export function emit (type, data = {}) {
|
||||
let records = getAllRecords()
|
||||
|
||||
let index = _.findIndex(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
let todayRecord
|
||||
if (index < 0) {
|
||||
todayRecord = {date: new Date()}
|
||||
records.push(todayRecord)
|
||||
}
|
||||
else todayRecord = records[index]
|
||||
switch (type) {
|
||||
case 'ARTICLE_CREATE':
|
||||
case 'ARTICLE_UPDATE':
|
||||
case 'ARTICLE_DESTROY':
|
||||
case 'FOLDER_CREATE':
|
||||
case 'FOLDER_UPDATE':
|
||||
case 'FOLDER_DESTROY':
|
||||
case 'FINDER_OPEN':
|
||||
case 'FINDER_COPY':
|
||||
case 'MAIN_DETAIL_COPY':
|
||||
case 'ARTICLE_SHARE':
|
||||
todayRecord[type] = todayRecord[type] == null
|
||||
? 1
|
||||
: todayRecord[type] + 1
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
|
||||
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
|
||||
let recordKey = type + '_BY_SYNTAX'
|
||||
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
|
||||
|
||||
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
|
||||
? 1
|
||||
: todayRecord[recordKey][data.mode] + 1
|
||||
}
|
||||
|
||||
let storeData = dataStore.getData()
|
||||
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
|
||||
todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
|
||||
todayRecord.CLIENT_VERSION = version
|
||||
|
||||
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
|
||||
if (sum[article.mode] == null) sum[article.mode] = 1
|
||||
else sum[article.mode]++
|
||||
return sum
|
||||
}, {}) : 0
|
||||
|
||||
saveAllRecords(records)
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
emit,
|
||||
postRecords
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import keygen from './keygen'
|
||||
|
||||
function getClientKey () {
|
||||
let clientKey = localStorage.getItem('clientKey')
|
||||
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
||||
clientKey = keygen()
|
||||
setClientKey(clientKey)
|
||||
}
|
||||
|
||||
return clientKey
|
||||
}
|
||||
|
||||
function setClientKey (newKey) {
|
||||
localStorage.setItem('clientKey', newKey)
|
||||
}
|
||||
|
||||
getClientKey()
|
||||
|
||||
export default {
|
||||
get: getClientKey,
|
||||
set: setClientKey
|
||||
}
|
||||
36
browser/lib/consts.js
Normal file
36
browser/lib/consts.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const path = require('path')
|
||||
const fs = require('sander')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
|
||||
const themePath = process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
|
||||
: require('path').resolve('./node_modules/codemirror/theme')
|
||||
const themes = fs.readdirSync(themePath)
|
||||
.map((themePath) => {
|
||||
return themePath.substring(0, themePath.lastIndexOf('.'))
|
||||
})
|
||||
|
||||
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'
|
||||
],
|
||||
THEMES: ['default'].concat(themes)
|
||||
}
|
||||
|
||||
module.exports = consts
|
||||
17
browser/lib/context.js
Normal file
17
browser/lib/context.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
function popup (templates) {
|
||||
let menu = new Menu()
|
||||
templates.forEach((item) => {
|
||||
menu.append(new MenuItem(item))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
const context = {
|
||||
popup
|
||||
}
|
||||
|
||||
module.export = context
|
||||
export default context
|
||||
4
browser/lib/customMeta.js
Normal file
4
browser/lib/customMeta.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import CodeMirror from 'codemirror'
|
||||
import _ from 'lodash'
|
||||
|
||||
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl']})
|
||||
@@ -1,156 +0,0 @@
|
||||
import keygen from './keygen'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const jetpack = require('fs-jetpack')
|
||||
const path = require('path')
|
||||
|
||||
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n* * * *\n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\* * * *\n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを!\n\n## Hack your memory**'
|
||||
|
||||
let data = null
|
||||
|
||||
function getLocalPath () {
|
||||
return path.join(remote.app.getPath('userData'), 'local.json')
|
||||
}
|
||||
|
||||
function forgeInitialRepositories () {
|
||||
let defaultRepo = {
|
||||
key: keygen(),
|
||||
name: 'local',
|
||||
type: 'userData',
|
||||
user: {
|
||||
name: 'New user'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
defaultRepo.user.name = remote.process.env.USER
|
||||
} else if (process.platform === 'win32') {
|
||||
defaultRepo.user.name = remote.process.env.USERNAME
|
||||
}
|
||||
|
||||
return [defaultRepo]
|
||||
}
|
||||
|
||||
function getRepositories () {
|
||||
let raw = localStorage.getItem('repositories')
|
||||
try {
|
||||
let parsed = JSON.parse(raw)
|
||||
if (!_.isArray(parsed)) {
|
||||
throw new Error('repositories data is corrupted. re-init data.')
|
||||
}
|
||||
return parsed
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
let newRepos = forgeInitialRepositories()
|
||||
saveRepositories(newRepos)
|
||||
return newRepos
|
||||
}
|
||||
}
|
||||
|
||||
function saveRepositories (repos) {
|
||||
localStorage.setItem('repositories', JSON.stringify(repos))
|
||||
}
|
||||
|
||||
export function getUser (repoName) {
|
||||
if (repoName == null) {
|
||||
return getRepositories()[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function saveUser (repoName, user) {
|
||||
let repos = getRepositories()
|
||||
if (repoName == null) {
|
||||
Object.assign(repos[0].user, user)
|
||||
}
|
||||
saveRepositories(repos)
|
||||
}
|
||||
|
||||
export function init () {
|
||||
// set repositories info
|
||||
getRepositories()
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
if (data == null) {
|
||||
let defaultFolder = {
|
||||
name: 'default',
|
||||
key: keygen()
|
||||
}
|
||||
let defaultArticle = {
|
||||
title: 'About Boost',
|
||||
tags: ['boost', 'intro'],
|
||||
content: defaultContent,
|
||||
mode: 'markdown',
|
||||
key: keygen(),
|
||||
FolderKey: defaultFolder.key,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
data = {
|
||||
articles: [defaultArticle],
|
||||
folders: [defaultFolder],
|
||||
version: '0.4'
|
||||
}
|
||||
saveData()
|
||||
}
|
||||
}
|
||||
|
||||
export function getData (forceRead) {
|
||||
if (forceRead) {
|
||||
try {
|
||||
data = jetpack.read(getLocalPath(), 'json')
|
||||
} catch (e) {}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
let timer = null
|
||||
let isSaving = false
|
||||
let saveAgain = false
|
||||
function saveData () {
|
||||
timer = null
|
||||
isSaving = true
|
||||
jetpack.writeAsync(getLocalPath(), data)
|
||||
.then(function () {
|
||||
isSaving = false
|
||||
if (saveAgain) {
|
||||
saveAgain = false
|
||||
queueSave()
|
||||
}
|
||||
})
|
||||
}
|
||||
function queueSave () {
|
||||
if (!isSaving) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(saveData, 500)
|
||||
} else {
|
||||
saveAgain = true
|
||||
}
|
||||
}
|
||||
|
||||
export function setArticles (articles) {
|
||||
if (!_.isArray(articles)) throw new Error('Articles must be an array')
|
||||
let data = getData()
|
||||
data.articles = articles
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export function setFolders (folders) {
|
||||
if (!_.isArray(folders)) throw new Error('Folders must be an array')
|
||||
let data = getData()
|
||||
data.folders = folders
|
||||
queueSave()
|
||||
}
|
||||
|
||||
export default {
|
||||
getUser,
|
||||
saveUser,
|
||||
init,
|
||||
getData,
|
||||
setArticles,
|
||||
setFolders
|
||||
}
|
||||
@@ -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 () {
|
||||
var shasum = crypto.createHash('sha1')
|
||||
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
|
||||
return shasum.digest('hex')
|
||||
module.exports = function (length) {
|
||||
if (!_.isFinite(length)) length = 10
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
function getIn (object, path) {
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
object = object[stack.shift()]
|
||||
}
|
||||
return object[stack.shift()]
|
||||
}
|
||||
|
||||
function updateIn (object, path, value) {
|
||||
let current = object
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
current = current[stack.shift()]
|
||||
}
|
||||
current[stack.shift()] = value
|
||||
return object
|
||||
}
|
||||
|
||||
function setPartialState (component, path, value) {
|
||||
component.setState(
|
||||
updateIn(component.state, path, value))
|
||||
}
|
||||
|
||||
export default function linkState (path) {
|
||||
return {
|
||||
value: getIn(this.state, path),
|
||||
requestChange: setPartialState.bind(null, this, path)
|
||||
}
|
||||
}
|
||||
|
||||
export function linkState2 (el, path) {
|
||||
return {
|
||||
value: getIn(el.state, path),
|
||||
requestChange: setPartialState.bind(null, el, path)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
import markdownit from 'markdown-it'
|
||||
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 _ from 'lodash'
|
||||
|
||||
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({
|
||||
typographer: true,
|
||||
@@ -11,10 +23,18 @@ var md = markdownit({
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return hljs.highlight(lang, str).value
|
||||
return '<pre class="hljs">' +
|
||||
createGutter(str) +
|
||||
'<code>' +
|
||||
hljs.highlight(lang, str).value +
|
||||
'</code></pre>'
|
||||
} 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, {
|
||||
@@ -22,27 +42,127 @@ md.use(emoji, {
|
||||
})
|
||||
md.use(math, {
|
||||
inlineRenderer: function (str) {
|
||||
return `<span class='math'>${str}</span>`
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim())
|
||||
} catch (err) {
|
||||
output = `<span class="katex-error">${err.message}</span>`
|
||||
}
|
||||
return output
|
||||
},
|
||||
blockRenderer: function (str) {
|
||||
return `<div class='math'>${str}</div>`
|
||||
let output = ''
|
||||
try {
|
||||
output = katex.renderToString(str.trim(), {displayMode: true})
|
||||
} catch (err) {
|
||||
output = `<div class="katex-error">${err.message}</div>`
|
||||
}
|
||||
return output
|
||||
}
|
||||
})
|
||||
md.use(require('markdown-it-checkbox'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
// Override task item
|
||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine*/) {
|
||||
let content, terminate, i, l, token
|
||||
let nextLine = startLine + 1
|
||||
let terminatorRules = state.md.block.ruler.getRules('paragraph')
|
||||
let endLine = state.lineMax
|
||||
|
||||
let originalRenderToken = md.renderer.renderToken
|
||||
md.renderer.renderToken = function renderToken (tokens, idx, options) {
|
||||
let token = tokens[idx]
|
||||
// jump line-by-line until empty one or EOF
|
||||
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
|
||||
// this would be a code block normally, but after paragraph
|
||||
// it's considered a lazy continuation regardless of what's there
|
||||
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
|
||||
|
||||
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
|
||||
if (token.map != null) {
|
||||
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
|
||||
// quirk for blockquotes, this line should already be checked by that rule
|
||||
if (state.sCount[nextLine] < 0) { continue }
|
||||
|
||||
// Some tags can terminate paragraph without empty line.
|
||||
terminate = false
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (terminate) { break }
|
||||
}
|
||||
|
||||
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
token = state.push('paragraph_open', 'p', 1)
|
||||
token.map = [ startLine, state.line ]
|
||||
|
||||
if (state.parentType === 'list') {
|
||||
let match = content.match(/^\[( |x)\] ?(.+)/i)
|
||||
if (match) {
|
||||
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
|
||||
}
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.content = content
|
||||
token.map = [ startLine, state.line ]
|
||||
token.children = []
|
||||
|
||||
token = state.push('paragraph_close', 'p', -1)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Add line number attribute for scrolling
|
||||
let originalRender = md.renderer.render
|
||||
md.renderer.render = function render (tokens, options, env) {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
})
|
||||
let result = originalRender.call(md.renderer, tokens, options, env)
|
||||
return result
|
||||
}
|
||||
window.md = md
|
||||
|
||||
export default function markdown (content) {
|
||||
if (content == null) content = ''
|
||||
|
||||
return md.render(content.toString())
|
||||
function strip (input) {
|
||||
var output = input
|
||||
try {
|
||||
output = output
|
||||
.replace(/^([\s\t]*)([\*\-\+]|\d\.)\s+/gm, '$1')
|
||||
.replace(/\n={2,}/g, '\n')
|
||||
.replace(/~~/g, '')
|
||||
.replace(/`{3}.*\n/g, '')
|
||||
.replace(/<(.*?)>/g, '$1')
|
||||
.replace(/^[=\-]{2,}\s*$/g, '')
|
||||
.replace(/\[\^.+?\](\: .*?$)?/g, '')
|
||||
.replace(/\s{0,2}\[.*?\]: .*?$/g, '')
|
||||
.replace(/\!\[.*?\][\[\(].*?[\]\)]/g, '')
|
||||
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
|
||||
.replace(/>/g, '')
|
||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
|
||||
.replace(/^\#{1,6}\s*([^#]*)\s*(\#{1,6})?/gm, '$1')
|
||||
.replace(/([\*_]{1,3})(\S.*?\S)\1/g, '$2')
|
||||
.replace(/(`{3,})(.*?)\1/gm, '$2')
|
||||
.replace(/^-{3,}\s*$/g, '')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\n{2,}/g, '\n\n')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return input
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
return md.render(content)
|
||||
},
|
||||
strip
|
||||
}
|
||||
export default markdown
|
||||
|
||||
@@ -1,756 +0,0 @@
|
||||
const modes = [
|
||||
// Major
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Plain text',
|
||||
mode: 'text'
|
||||
},
|
||||
{
|
||||
name: 'markdown',
|
||||
label: 'Markdown',
|
||||
alias: ['md'],
|
||||
mode: 'markdown'
|
||||
},
|
||||
{
|
||||
name: 'javascript',
|
||||
label: 'JavaScript',
|
||||
alias: ['js', 'jscript', 'babel', 'es'],
|
||||
mode: 'javascript'
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
label: 'HTML',
|
||||
alias: [],
|
||||
mode: 'html'
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
label: 'CSS',
|
||||
alias: ['cascade', 'stylesheet'],
|
||||
mode: 'css'
|
||||
},
|
||||
{
|
||||
name: 'php',
|
||||
label: 'PHP',
|
||||
alias: [],
|
||||
mode: 'php'
|
||||
},
|
||||
{
|
||||
name: 'python',
|
||||
label: 'Python',
|
||||
alias: ['py'],
|
||||
mode: 'python'
|
||||
},
|
||||
{
|
||||
name: 'ruby',
|
||||
label: 'Ruby',
|
||||
alias: ['rb'],
|
||||
mode: 'ruby'
|
||||
},
|
||||
{
|
||||
name: 'java',
|
||||
label: 'Java',
|
||||
alias: [],
|
||||
mode: 'java'
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
label: 'C',
|
||||
alias: ['c', 'h', 'clang', 'clang'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'cpp',
|
||||
label: 'C++',
|
||||
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'csharp',
|
||||
label: 'C#',
|
||||
alias: ['cs', 'c#'],
|
||||
mode: 'csharp'
|
||||
},
|
||||
{
|
||||
name: 'swift',
|
||||
label: 'Swift',
|
||||
alias: [],
|
||||
mode: 'swift'
|
||||
},
|
||||
{
|
||||
name: 'golang',
|
||||
label: 'Go',
|
||||
alias: ['go'],
|
||||
mode: 'golang'
|
||||
},
|
||||
|
||||
// Minor
|
||||
{
|
||||
name: 'abap',
|
||||
label: 'ABAP',
|
||||
alias: [],
|
||||
mode: 'abap'
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
label: 'ABC',
|
||||
alias: [],
|
||||
mode: 'abc'
|
||||
},
|
||||
{
|
||||
name: 'actionscript',
|
||||
label: 'ActionScript',
|
||||
alias: ['as'],
|
||||
mode: 'actionscript'
|
||||
},
|
||||
{
|
||||
name: 'ada',
|
||||
label: 'Ada',
|
||||
alias: [],
|
||||
mode: 'ada'
|
||||
},
|
||||
{
|
||||
name: 'apache_conf',
|
||||
label: 'Apache config',
|
||||
alias: ['apache', 'conf'],
|
||||
mode: 'apache_conf'
|
||||
},
|
||||
{
|
||||
name: 'applescript',
|
||||
label: 'AppleScript',
|
||||
alias: ['scpt'],
|
||||
mode: 'applescript'
|
||||
},
|
||||
{
|
||||
name: 'asciidoc',
|
||||
label: 'AsciiDoc',
|
||||
alias: ['ascii', 'doc', 'txt'],
|
||||
mode: 'asciidoc'
|
||||
},
|
||||
{
|
||||
name: 'assembly_x86',
|
||||
label: 'Assembly x86',
|
||||
alias: ['assembly', 'x86', 'asm'],
|
||||
mode: 'assembly_x86'
|
||||
},
|
||||
{
|
||||
name: 'autohotkey',
|
||||
label: 'AutoHotkey',
|
||||
alias: ['ahk'],
|
||||
mode: 'autohotkey'
|
||||
},
|
||||
{
|
||||
name: 'batchfile',
|
||||
label: 'Batch file',
|
||||
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
||||
mode: 'batchfile'
|
||||
},
|
||||
{
|
||||
name: 'cirru',
|
||||
label: 'Cirru',
|
||||
alias: [],
|
||||
mode: 'cirru'
|
||||
},
|
||||
{
|
||||
name: 'clojure',
|
||||
label: 'Clojure',
|
||||
alias: ['clj', 'cljs', 'cljc', 'edn'],
|
||||
mode: 'clojure'
|
||||
},
|
||||
{
|
||||
name: 'cobol',
|
||||
label: 'COBOL',
|
||||
alias: ['cbl', 'cob', 'cpy'],
|
||||
mode: 'cobol'
|
||||
},
|
||||
{
|
||||
name: 'coffee',
|
||||
label: 'CoffeeScript',
|
||||
alias: ['coffee'],
|
||||
mode: 'coffee'
|
||||
},
|
||||
{
|
||||
name: 'coldfusion',
|
||||
label: 'ColdFusion',
|
||||
alias: ['cfm', 'cfc'],
|
||||
mode: 'coldfusion'
|
||||
},
|
||||
{
|
||||
name: 'curly',
|
||||
label: 'Curly',
|
||||
alias: [],
|
||||
mode: 'curly'
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
label: 'D',
|
||||
alias: ['dlang'],
|
||||
mode: 'd'
|
||||
},
|
||||
{
|
||||
name: 'dockerfile',
|
||||
label: 'DockerFile',
|
||||
alias: ['docker'],
|
||||
mode: 'docker'
|
||||
},
|
||||
{
|
||||
name: 'dart',
|
||||
label: 'Dart',
|
||||
alias: [],
|
||||
mode: 'dart'
|
||||
},
|
||||
{
|
||||
name: 'diff',
|
||||
label: 'Diff',
|
||||
alias: [],
|
||||
mode: 'diff'
|
||||
},
|
||||
{
|
||||
name: 'django',
|
||||
label: 'Django',
|
||||
alias: [],
|
||||
mode: 'djt'
|
||||
},
|
||||
{
|
||||
name: 'dot',
|
||||
label: 'DOT',
|
||||
alias: ['gv'],
|
||||
mode: 'dot'
|
||||
},
|
||||
{
|
||||
name: 'eiffel',
|
||||
label: 'Eiffel',
|
||||
alias: [],
|
||||
mode: 'eiffel'
|
||||
},
|
||||
{
|
||||
name: 'ejs',
|
||||
label: 'EJS',
|
||||
alias: [],
|
||||
mode: 'ejs'
|
||||
},
|
||||
{
|
||||
name: 'elixir',
|
||||
label: 'Elixir',
|
||||
alias: ['ex', 'exs'],
|
||||
mode: 'elixir'
|
||||
},
|
||||
{
|
||||
name: 'elm',
|
||||
label: 'Elm',
|
||||
alias: [],
|
||||
mode: 'elm'
|
||||
},
|
||||
{
|
||||
name: 'erlang',
|
||||
label: 'Erlang',
|
||||
alias: ['erl', 'hrl'],
|
||||
mode: 'erlang'
|
||||
},
|
||||
{
|
||||
name: 'forth',
|
||||
label: 'Forth',
|
||||
alias: ['fs', 'fth'],
|
||||
mode: 'forth'
|
||||
},
|
||||
{
|
||||
name: 'freemaker',
|
||||
label: 'Freemaker',
|
||||
alias: ['ftl'],
|
||||
mode: 'ftl'
|
||||
},
|
||||
{
|
||||
name: 'gcode',
|
||||
label: 'G-code',
|
||||
alias: ['mpt', 'mpf', 'nc'],
|
||||
mode: 'gcode'
|
||||
},
|
||||
{
|
||||
name: 'gherkin',
|
||||
label: 'Gherkin',
|
||||
alias: ['cucumber'],
|
||||
mode: 'gherkin'
|
||||
},
|
||||
{
|
||||
name: 'gitignore',
|
||||
label: 'Gitignore',
|
||||
alias: ['git'],
|
||||
mode: 'gitignore'
|
||||
},
|
||||
{
|
||||
name: 'glsl',
|
||||
label: 'GLSL',
|
||||
alias: ['opengl', 'shading'],
|
||||
mode: 'glsl'
|
||||
},
|
||||
{
|
||||
name: 'groovy',
|
||||
label: 'Groovy',
|
||||
alias: [],
|
||||
mode: 'grooby'
|
||||
},
|
||||
{
|
||||
name: 'haml',
|
||||
label: 'Haml',
|
||||
alias: [],
|
||||
mode: 'haml'
|
||||
},
|
||||
{
|
||||
name: 'handlebars',
|
||||
label: 'Handlebars',
|
||||
alias: ['hbs'],
|
||||
mode: 'handlebars'
|
||||
},
|
||||
{
|
||||
name: 'haskell',
|
||||
label: 'Haskell',
|
||||
alias: ['hs', 'lhs'],
|
||||
mode: 'haskell'
|
||||
},
|
||||
{
|
||||
name: 'haxe',
|
||||
label: 'Haxe',
|
||||
alias: ['hx', 'hxml'],
|
||||
mode: 'haxe'
|
||||
},
|
||||
{
|
||||
name: 'html_ruby',
|
||||
label: 'HTML (Ruby)',
|
||||
alias: ['erb', 'rhtml'],
|
||||
mode: 'html_ruby'
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
label: 'JSX',
|
||||
alias: ['es', 'babel', 'js', 'jsx', 'react'],
|
||||
mode: 'jsx'
|
||||
},
|
||||
{
|
||||
name: 'typescript',
|
||||
label: 'TypeScript',
|
||||
alias: ['ts'],
|
||||
mode: 'typescript'
|
||||
},
|
||||
{
|
||||
name: 'ini',
|
||||
label: 'INI file',
|
||||
alias: [],
|
||||
mode: 'ini'
|
||||
},
|
||||
{
|
||||
name: 'io',
|
||||
label: 'Io',
|
||||
alias: [],
|
||||
mode: 'io'
|
||||
},
|
||||
{
|
||||
name: 'jack',
|
||||
label: 'Jack',
|
||||
alias: [],
|
||||
mode: 'jack'
|
||||
},
|
||||
{
|
||||
name: 'jade',
|
||||
label: 'Jade',
|
||||
alias: [],
|
||||
mode: 'jade'
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
label: 'JSON',
|
||||
alias: [],
|
||||
mode: 'json'
|
||||
},
|
||||
{
|
||||
name: 'jsoniq',
|
||||
label: 'JSONiq',
|
||||
alias: ['query'],
|
||||
mode: 'jsoniq'
|
||||
},
|
||||
{
|
||||
name: 'jsp',
|
||||
label: 'JSP',
|
||||
alias: [],
|
||||
mode: 'jsp'
|
||||
},
|
||||
{
|
||||
name: 'julia',
|
||||
label: 'Julia',
|
||||
alias: [],
|
||||
mode: 'julia'
|
||||
},
|
||||
{
|
||||
name: 'latex',
|
||||
label: 'Latex',
|
||||
alias: ['tex'],
|
||||
mode: 'latex'
|
||||
},
|
||||
{
|
||||
name: 'lean',
|
||||
label: 'Lean',
|
||||
alias: [],
|
||||
mode: 'lean'
|
||||
},
|
||||
{
|
||||
name: 'less',
|
||||
label: 'Less',
|
||||
alias: [],
|
||||
mode: 'less'
|
||||
},
|
||||
{
|
||||
name: 'liquid',
|
||||
label: 'Liquid',
|
||||
alias: [],
|
||||
mode: 'liquid'
|
||||
},
|
||||
{
|
||||
name: 'lisp',
|
||||
label: 'Lisp',
|
||||
alias: ['lsp'],
|
||||
mode: 'lisp'
|
||||
},
|
||||
{
|
||||
name: 'livescript',
|
||||
label: 'LiveScript',
|
||||
alias: ['ls'],
|
||||
mode: 'livescript'
|
||||
},
|
||||
{
|
||||
name: 'logiql',
|
||||
label: 'LogiQL',
|
||||
alias: [],
|
||||
mode: 'logiql'
|
||||
},
|
||||
{
|
||||
name: 'lsl',
|
||||
label: 'LSL',
|
||||
alias: [],
|
||||
mode: 'lsl'
|
||||
},
|
||||
{
|
||||
name: 'lua',
|
||||
label: 'Lua',
|
||||
alias: [],
|
||||
mode: 'lua'
|
||||
},
|
||||
{
|
||||
name: 'luapage',
|
||||
label: 'Luapage',
|
||||
alias: [],
|
||||
mode: 'luapage'
|
||||
},
|
||||
{
|
||||
name: 'lucene',
|
||||
label: 'Lucene',
|
||||
alias: [],
|
||||
mode: 'lucene'
|
||||
},
|
||||
{
|
||||
name: 'makefile',
|
||||
label: 'Makefile',
|
||||
alias: [],
|
||||
mode: 'makefile'
|
||||
},
|
||||
{
|
||||
name: 'mask',
|
||||
label: 'Mask',
|
||||
alias: [],
|
||||
mode: 'mask'
|
||||
},
|
||||
{
|
||||
name: 'matlab',
|
||||
label: 'MATLAB',
|
||||
alias: [],
|
||||
mode: 'matlab'
|
||||
},
|
||||
{
|
||||
name: 'maze',
|
||||
label: 'Maze',
|
||||
alias: [],
|
||||
mode: 'maze'
|
||||
},
|
||||
{
|
||||
name: 'mel',
|
||||
label: 'MEL',
|
||||
alias: [],
|
||||
mode: 'mel'
|
||||
},
|
||||
{
|
||||
name: 'mipsassembler',
|
||||
label: 'MIPS assembly',
|
||||
alias: [],
|
||||
mode: 'mipsassembler'
|
||||
},
|
||||
{
|
||||
name: 'mushcode',
|
||||
label: 'MUSHCode',
|
||||
alias: [],
|
||||
mode: 'mushcode'
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
label: 'MySQL',
|
||||
alias: [],
|
||||
mode: 'mysql'
|
||||
},
|
||||
{
|
||||
name: 'nix',
|
||||
label: 'Nix',
|
||||
alias: [],
|
||||
mode: 'nix'
|
||||
},
|
||||
{
|
||||
name: 'objectivec',
|
||||
label: 'Objective C',
|
||||
alias: ['objc'],
|
||||
mode: 'objectivec'
|
||||
},
|
||||
{
|
||||
name: 'ocaml',
|
||||
label: 'OCaml',
|
||||
alias: [],
|
||||
mode: 'ocaml'
|
||||
},
|
||||
{
|
||||
name: 'pascal',
|
||||
label: 'Pascal',
|
||||
alias: [],
|
||||
mode: 'pascal'
|
||||
},
|
||||
{
|
||||
name: 'perl',
|
||||
label: 'Perl',
|
||||
alias: [],
|
||||
mode: 'perl'
|
||||
},
|
||||
{
|
||||
name: 'pgsql',
|
||||
label: 'Postgres SQL',
|
||||
alias: ['postgres'],
|
||||
mode: 'pgsql'
|
||||
},
|
||||
{
|
||||
name: 'powershell',
|
||||
label: 'PowerShell',
|
||||
alias: ['ps1'],
|
||||
mode: 'powershell'
|
||||
},
|
||||
{
|
||||
name: 'praat',
|
||||
label: 'Praat',
|
||||
alias: [],
|
||||
mode: 'praat'
|
||||
},
|
||||
{
|
||||
name: 'prolog',
|
||||
label: 'Prolog',
|
||||
alias: ['pl', 'pro'],
|
||||
mode: 'prolog'
|
||||
},
|
||||
{
|
||||
name: 'properties',
|
||||
label: 'Properties',
|
||||
alias: [],
|
||||
mode: 'properties'
|
||||
},
|
||||
{
|
||||
name: 'protobuf',
|
||||
label: 'Protocol Buffers',
|
||||
alias: ['protocol', 'buffers'],
|
||||
mode: 'protobuf'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
label: 'R',
|
||||
alias: ['rlang'],
|
||||
mode: 'r'
|
||||
},
|
||||
{
|
||||
name: 'rdoc',
|
||||
label: 'RDoc',
|
||||
alias: [],
|
||||
mode: 'rdoc'
|
||||
},
|
||||
{
|
||||
name: 'rust',
|
||||
label: 'Rust',
|
||||
alias: [],
|
||||
mode: 'rust'
|
||||
},
|
||||
{
|
||||
name: 'sass',
|
||||
label: 'Sass',
|
||||
alias: [],
|
||||
mode: 'sass'
|
||||
},
|
||||
{
|
||||
name: 'scad',
|
||||
label: 'SCAD',
|
||||
alias: [],
|
||||
mode: 'scad'
|
||||
},
|
||||
{
|
||||
name: 'scala',
|
||||
label: 'Scala',
|
||||
alias: [],
|
||||
mode: 'scala'
|
||||
},
|
||||
{
|
||||
name: 'scheme',
|
||||
label: 'Scheme',
|
||||
alias: ['scm', 'ss'],
|
||||
mode: 'scheme'
|
||||
},
|
||||
{
|
||||
name: 'scss',
|
||||
label: 'Scss',
|
||||
alias: [],
|
||||
mode: 'scss'
|
||||
},
|
||||
{
|
||||
name: 'sh',
|
||||
label: 'Shell',
|
||||
alias: ['shell'],
|
||||
mode: 'sh'
|
||||
},
|
||||
{
|
||||
name: 'sjs',
|
||||
label: 'StratifiedJS',
|
||||
alias: ['stratified'],
|
||||
mode: 'sjs'
|
||||
},
|
||||
{
|
||||
name: 'smarty',
|
||||
label: 'Smarty',
|
||||
alias: [],
|
||||
mode: 'smarty'
|
||||
},
|
||||
{
|
||||
name: 'snippets',
|
||||
label: 'Snippets',
|
||||
alias: [],
|
||||
mode: 'snippets'
|
||||
},
|
||||
{
|
||||
name: 'soy_template',
|
||||
label: 'Soy Template',
|
||||
alias: ['soy'],
|
||||
mode: 'soy_template'
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
label: 'Space',
|
||||
alias: [],
|
||||
mode: 'space'
|
||||
},
|
||||
{
|
||||
name: 'sql',
|
||||
label: 'SQL',
|
||||
alias: [],
|
||||
mode: 'sql'
|
||||
},
|
||||
{
|
||||
name: 'sqlserver',
|
||||
label: 'SQL Server',
|
||||
alias: [],
|
||||
mode: 'sqlserver'
|
||||
},
|
||||
{
|
||||
name: 'stylus',
|
||||
label: 'Stylus',
|
||||
alias: [],
|
||||
mode: 'stylus'
|
||||
},
|
||||
{
|
||||
name: 'svg',
|
||||
label: 'SVG',
|
||||
alias: [],
|
||||
mode: 'svg'
|
||||
},
|
||||
{
|
||||
name: 'swig',
|
||||
label: 'SWIG',
|
||||
alias: [],
|
||||
mode: 'swig'
|
||||
},
|
||||
{
|
||||
name: 'tcl',
|
||||
label: 'Tcl',
|
||||
alias: [],
|
||||
mode: 'tcl'
|
||||
},
|
||||
{
|
||||
name: 'tex',
|
||||
label: 'TeX',
|
||||
alias: [],
|
||||
mode: 'tex'
|
||||
},
|
||||
{
|
||||
name: 'textile',
|
||||
label: 'Textile',
|
||||
alias: [],
|
||||
mode: 'textile'
|
||||
},
|
||||
{
|
||||
name: 'toml',
|
||||
label: 'TOML',
|
||||
alias: [],
|
||||
mode: 'toml'
|
||||
},
|
||||
{
|
||||
name: 'twig',
|
||||
label: 'Twig',
|
||||
alias: [],
|
||||
mode: 'twig'
|
||||
},
|
||||
{
|
||||
name: 'vala',
|
||||
label: 'Vala',
|
||||
alias: [],
|
||||
mode: 'vala'
|
||||
},
|
||||
{
|
||||
name: 'vbscript',
|
||||
label: 'VBScript',
|
||||
alias: ['vbs', 'vbe'],
|
||||
mode: 'vbscript'
|
||||
},
|
||||
{
|
||||
name: 'velocity',
|
||||
label: 'Velocity',
|
||||
alias: [],
|
||||
mode: 'velocity'
|
||||
},
|
||||
{
|
||||
name: 'verilog',
|
||||
label: 'Verilog',
|
||||
alias: [],
|
||||
mode: 'verilog'
|
||||
},
|
||||
{
|
||||
name: 'vhdl',
|
||||
label: 'VHDL',
|
||||
alias: [],
|
||||
mode: 'vhdl'
|
||||
},
|
||||
{
|
||||
name: 'xml',
|
||||
label: 'XML',
|
||||
alias: [],
|
||||
mode: 'xml'
|
||||
},
|
||||
{
|
||||
name: 'xquery',
|
||||
label: 'XQuery',
|
||||
alias: [],
|
||||
mode: 'xquery'
|
||||
},
|
||||
{
|
||||
name: 'yaml',
|
||||
label: 'YAML',
|
||||
alias: [],
|
||||
mode: 'yaml'
|
||||
}
|
||||
]
|
||||
|
||||
export default modes
|
||||
@@ -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
|
||||
}
|
||||
24
browser/main/Detail/Detail.styl
Normal file
24
browser/main/Detail/Detail.styl
Normal file
@@ -0,0 +1,24 @@
|
||||
.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
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
291
browser/main/Detail/FolderSelect.js
Normal file
291
browser/main/Detail/FolderSelect.js
Normal file
@@ -0,0 +1,291 @@
|
||||
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, data, value } = this.props
|
||||
let splitted = value.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let folderKey = splitted.shift()
|
||||
let options = []
|
||||
data.storageMap.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]
|
||||
|
||||
if (this.state.search.trim().length > 0) {
|
||||
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
|
||||
options = options.filter((option) => filter.test(option.folder.name))
|
||||
}
|
||||
|
||||
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)
|
||||
132
browser/main/Detail/FolderSelect.styl
Normal file
132
browser/main/Detail/FolderSelect.styl
Normal file
@@ -0,0 +1,132 @@
|
||||
.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
|
||||
max-height 450px
|
||||
overflow auto
|
||||
z-index 200
|
||||
background-color white
|
||||
border-radius 2px
|
||||
box-shadow 2px 2px 10px gray
|
||||
|
||||
.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
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.root--search, .root--focus
|
||||
@extend .root
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-input--focus-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-input--focus-borderColor
|
||||
|
||||
.idle-label-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.search-input
|
||||
color white
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.search-optionList
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
box-shadow 2px 2px 10px black
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
305
browser/main/Detail/MarkdownNoteDetail.js
Normal file
305
browser/main/Detail/MarkdownNoteDetail.js
Normal file
@@ -0,0 +1,305 @@
|
||||
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'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import StatusBar from '../StatusBar'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: ''
|
||||
}, props.note)
|
||||
}
|
||||
this.dispatchTimer = null
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.content.focus()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
title = markdown.strip(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 () {
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: 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.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
note: Object.assign({}, newNote)
|
||||
}, () => {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
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) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteKeyDown (e) {
|
||||
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config } = this.props
|
||||
let { note } = this.state
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
<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'
|
||||
data={data}
|
||||
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)}
|
||||
disabled
|
||||
>
|
||||
<i className='fa fa-share-alt fa-fw' />
|
||||
<span styleName='info-right-button-tooltip'
|
||||
style={{right: 20}}
|
||||
>Share Note</span>
|
||||
</button>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v' />
|
||||
<span styleName='info-right-button-tooltip'
|
||||
style={{right: 5}}
|
||||
>More Options</span>
|
||||
</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>
|
||||
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</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)
|
||||
102
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
102
browser/main/Detail/MarkdownNoteDetail.styl
Normal file
@@ -0,0 +1,102 @@
|
||||
$info-height = 75px
|
||||
|
||||
.root
|
||||
absolute top right bottom
|
||||
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-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 .info-right-button-tooltip
|
||||
opacity 1
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
|
||||
.info-right-button-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
top 45px
|
||||
padding 5px
|
||||
opacity 0
|
||||
border-radius 2px
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
top $info-height
|
||||
bottom $statusBar-height
|
||||
|
||||
.body-noteEditor
|
||||
absolute top bottom left right
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.info
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.info-delete
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-confirmButton
|
||||
colorDarkDangerButton()
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-right-button
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
&:active
|
||||
border-color $ui-dark-button--focus-borderColor
|
||||
&:hover .info-right-button-tooltip
|
||||
opacity 1
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
|
||||
.info-right-button-tooltip
|
||||
darkTooltip()
|
||||
653
browser/main/Detail/SnippetNoteDetail.js
Normal file
653
browser/main/Detail/SnippetNoteDetail.js
Normal file
@@ -0,0 +1,653 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetNoteDetail.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
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'
|
||||
import CodeMirror from 'codemirror'
|
||||
import SnippetTab from './SnippetTab'
|
||||
import StatusBar from '../StatusBar'
|
||||
import context from 'browser/lib/context'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
case 'ejs':
|
||||
return 'Embedded Javascript'
|
||||
case 'html_ruby':
|
||||
return 'Embedded Ruby'
|
||||
case 'objectivec':
|
||||
return 'Objective C'
|
||||
case 'text':
|
||||
return 'Plain Text'
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class SnippetNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isMovingNote: false,
|
||||
snippetIndex: 0,
|
||||
note: Object.assign({
|
||||
description: ''
|
||||
}, props.note, {
|
||||
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
let nextNote = Object.assign({
|
||||
description: ''
|
||||
}, nextProps.note, {
|
||||
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
|
||||
})
|
||||
this.setState({
|
||||
snippetIndex: 0,
|
||||
note: nextNote
|
||||
}, () => {
|
||||
let { snippets } = this.state.note
|
||||
snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
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 () {
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = setTimeout(() => {
|
||||
this.saveNow()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
saveNow () {
|
||||
let { note, dispatch } = this.props
|
||||
clearTimeout(this.saveQueue)
|
||||
this.saveQueue = null
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.key, this.state.note)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: 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.key, newStorageKey, newFolderKey)
|
||||
.then((newNote) => {
|
||||
this.setState({
|
||||
isMovingNote: true,
|
||||
note: Object.assign({}, newNote)
|
||||
}, () => {
|
||||
let { dispatch, location } = this.props
|
||||
dispatch({
|
||||
type: 'MOVE_NOTE',
|
||||
originNote: note,
|
||||
note: newNote
|
||||
})
|
||||
hashHistory.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: newNote.storage + '-' + newNote.key
|
||||
}
|
||||
})
|
||||
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) {
|
||||
context.popup([{
|
||||
label: 'Delete',
|
||||
click: (e) => this.handleDeleteMenuClick(e)
|
||||
}])
|
||||
}
|
||||
|
||||
handleDeleteMenuClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabPlusButtonClick (e) {
|
||||
this.addSnippet()
|
||||
}
|
||||
|
||||
handleTabButtonClick (e, index) {
|
||||
this.setState({
|
||||
snippetIndex: index
|
||||
})
|
||||
}
|
||||
|
||||
handleTabDeleteButtonClick (e, index) {
|
||||
if (this.state.note.snippets.length > 1) {
|
||||
if (this.state.note.snippets[index].content.trim().length > 0) {
|
||||
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a snippet',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogIndex === 0) {
|
||||
this.deleteSnippetByIndex(index)
|
||||
}
|
||||
} else {
|
||||
this.deleteSnippetByIndex(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteSnippetByIndex (index) {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets.splice(index, 1)
|
||||
this.state.note.snippets = snippets
|
||||
let snippetIndex = this.state.snippetIndex >= snippets.length
|
||||
? snippets.length - 1
|
||||
: this.state.snippetIndex
|
||||
this.setState({
|
||||
note: this.state.note,
|
||||
snippetIndex
|
||||
})
|
||||
}
|
||||
|
||||
renameSnippetByIndex (index, name) {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].name = name
|
||||
let syntax = CodeMirror.findModeByFileName(name.trim())
|
||||
let mode = syntax != null ? syntax.name : null
|
||||
if (mode != null) snippets[index].mode = mode
|
||||
this.state.note.snippets = snippets
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpNextTab()
|
||||
} else if (e.ctrlKey && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpPrevTab()
|
||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||
e.preventDefault()
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
case 76:
|
||||
{
|
||||
let isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.focus()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 84:
|
||||
{
|
||||
let isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleModeButtonClick (e, index) {
|
||||
let menu = new Menu()
|
||||
CodeMirror.modeInfo.forEach((mode) => {
|
||||
menu.append(new MenuItem({
|
||||
label: mode.name,
|
||||
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
|
||||
}))
|
||||
})
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
handleIndentTypeButtonClick (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'tab',
|
||||
click: (e) => this.handleIndentTypeItemClick(e, 'tab')
|
||||
},
|
||||
{
|
||||
label: 'space',
|
||||
click: (e) => this.handleIndentTypeItemClick(e, 'space')
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleIndentSizeButtonClick (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: '2',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 2)
|
||||
},
|
||||
{
|
||||
label: '4',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 4)
|
||||
},
|
||||
{
|
||||
label: '8',
|
||||
click: (e) => this.handleIndentSizeItemClick(e, 8)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleIndentSizeItemClick (e, indentSize) {
|
||||
let { config, dispatch } = this.props
|
||||
let editor = Object.assign({}, config.editor, {
|
||||
indentSize
|
||||
})
|
||||
ConfigManager.set({
|
||||
editor
|
||||
})
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: {
|
||||
editor
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleIndentTypeItemClick (e, indentType) {
|
||||
let { config, dispatch } = this.props
|
||||
let editor = Object.assign({}, config.editor, {
|
||||
indentType
|
||||
})
|
||||
ConfigManager.set({
|
||||
editor
|
||||
})
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: {
|
||||
editor
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.description.focus()
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
let { note } = this.state
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
content: ''
|
||||
}])
|
||||
let snippetIndex = note.snippets.length - 1
|
||||
|
||||
this.setState({
|
||||
note,
|
||||
snippetIndex
|
||||
}, () => {
|
||||
this.refs['tab-' + snippetIndex].startRenaming()
|
||||
})
|
||||
}
|
||||
|
||||
jumpNextTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
jumpPrevTab () {
|
||||
this.setState({
|
||||
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
|
||||
}, () => {
|
||||
this.focusEditor()
|
||||
})
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, 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 <SnippetTab
|
||||
key={index}
|
||||
ref={'tab-' + index}
|
||||
snippet={snippet}
|
||||
isActive={isActive}
|
||||
onClick={(e) => this.handleTabButtonClick(e, index)}
|
||||
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
|
||||
onRename={(name) => this.renameSnippetByIndex(index, name)}
|
||||
isDeletable={note.snippets.length > 1}
|
||||
/>
|
||||
})
|
||||
|
||||
let viewList = note.snippets.map((snippet, index) => {
|
||||
let isActive = this.state.snippetIndex === index
|
||||
|
||||
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
|
||||
return <div styleName='tabView'
|
||||
key={index}
|
||||
style={{zIndex: isActive ? 5 : 4}}
|
||||
>
|
||||
{snippet.mode === 'markdown'
|
||||
? <MarkdownEditor styleName='tabView-content'
|
||||
value={snippet.content}
|
||||
config={config}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
/>
|
||||
: <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'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<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'
|
||||
data={data}
|
||||
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)}
|
||||
disabled
|
||||
>
|
||||
<i className='fa fa-share-alt fa-fw'/>
|
||||
<span styleName='info-right-button-tooltip'
|
||||
style={{right: 20}}
|
||||
>Share Note</span>
|
||||
</button>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v'/>
|
||||
<span styleName='info-right-button-tooltip'
|
||||
style={{right: 5}}
|
||||
>More Options</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='body'>
|
||||
<div styleName='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'>
|
||||
<div styleName='list'>
|
||||
{tabList}
|
||||
</div>
|
||||
<button styleName='plusButton'
|
||||
onClick={(e) => this.handleTabPlusButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus'/>
|
||||
</button>
|
||||
</div>
|
||||
{viewList}
|
||||
</div>
|
||||
|
||||
<div styleName='override'>
|
||||
<button
|
||||
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
|
||||
>
|
||||
{this.state.note.snippets[this.state.snippetIndex].mode == null
|
||||
? 'Select Syntax...'
|
||||
: this.state.note.snippets[this.state.snippetIndex].mode
|
||||
}
|
||||
<i className='fa fa-caret-down'/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => this.handleIndentTypeButtonClick(e)}
|
||||
>
|
||||
Indent: {config.editor.indentType}
|
||||
<i className='fa fa-caret-down'/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => this.handleIndentSizeButtonClick(e)}
|
||||
>
|
||||
size: {config.editor.indentSize}
|
||||
<i className='fa fa-caret-down'/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</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)
|
||||
167
browser/main/Detail/SnippetNoteDetail.styl
Normal file
167
browser/main/Detail/SnippetNoteDetail.styl
Normal file
@@ -0,0 +1,167 @@
|
||||
$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-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 .info-right-button-tooltip
|
||||
opacity 1
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
.info-right-button-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
top 45px
|
||||
padding 5px
|
||||
opacity 0
|
||||
|
||||
.body
|
||||
absolute left right
|
||||
top $info-height
|
||||
bottom $statusBar-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 .list
|
||||
flex 1
|
||||
display flex
|
||||
overflow hidden
|
||||
border-right $ui-border
|
||||
|
||||
.tabList .plusButton
|
||||
navButtonColor()
|
||||
width 30px
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 110px
|
||||
|
||||
.tabView-content
|
||||
absolute top left right bottom
|
||||
|
||||
.override
|
||||
absolute bottom left
|
||||
height 23px
|
||||
z-index 1
|
||||
button
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 1px 0 0
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.info
|
||||
border-bottom-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.info-delete
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-confirmButton
|
||||
colorDarkDangerButton()
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-delete-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.info-right-button
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
&:active
|
||||
border-color $ui-dark-button--focus-borderColor
|
||||
&:hover .info-right-button-tooltip
|
||||
opacity 1
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
|
||||
.info-right-button-tooltip
|
||||
darkTooltip()
|
||||
|
||||
.body .description
|
||||
border-bottom-color $ui-dark-borderColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color white
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
border-bottom-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.tabList .list
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.tabList .plusButton
|
||||
navDarkButtonColor()
|
||||
131
browser/main/Detail/SnippetTab.js
Normal file
131
browser/main/Detail/SnippetTab.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './SnippetTab.styl'
|
||||
import context from 'browser/lib/context'
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isRenaming: false,
|
||||
name: props.snippet.name
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate (nextProps) {
|
||||
if (nextProps.snippet.name !== this.props.snippet.name) {
|
||||
this.setState({
|
||||
name: nextProps.snippet.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleClick (e) {
|
||||
this.props.onClick(e)
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
context.popup([
|
||||
{
|
||||
label: 'Rename',
|
||||
click: (e) => this.handleRenameClick(e)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
handleRenameClick (e) {
|
||||
this.startRenaming()
|
||||
}
|
||||
|
||||
handleNameInputBlur (e) {
|
||||
this.handleRename()
|
||||
}
|
||||
|
||||
handleNameInputChange (e) {
|
||||
this.setState({
|
||||
name: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleNameInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 13:
|
||||
this.handleRename()
|
||||
break
|
||||
case 27:
|
||||
this.setState({
|
||||
name: this.props.snippet.name,
|
||||
isRenaming: false
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleRename () {
|
||||
this.setState({
|
||||
isRenaming: false
|
||||
}, () => {
|
||||
if (this.props.snippet.name !== this.state.name) {
|
||||
this.props.onRename(this.state.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (e) {
|
||||
this.props.onDelete(e)
|
||||
}
|
||||
|
||||
startRenaming () {
|
||||
this.setState({
|
||||
isRenaming: true
|
||||
}, () => {
|
||||
this.refs.name.focus()
|
||||
this.refs.name.select()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { isActive, snippet, isDeletable } = this.props
|
||||
return (
|
||||
<div styleName={isActive
|
||||
? 'root--active'
|
||||
: 'root'
|
||||
}
|
||||
>
|
||||
{!this.state.isRenaming
|
||||
? <button styleName='button'
|
||||
onClick={(e) => this.handleClick(e)}
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
>
|
||||
{snippet.name.trim().length > 0
|
||||
? snippet.name
|
||||
: <span styleName='button-unnamed'>
|
||||
Unnamed
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
: <input styleName='input'
|
||||
ref='name'
|
||||
value={this.state.name}
|
||||
onChange={(e) => this.handleNameInputChange(e)}
|
||||
onBlur={(e) => this.handleNameInputBlur(e)}
|
||||
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
|
||||
/>
|
||||
}
|
||||
{isDeletable &&
|
||||
<button styleName='deleteButton'
|
||||
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-times'/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SnippetTab.propTypes = {
|
||||
}
|
||||
|
||||
export default CSSModules(SnippetTab, styles)
|
||||
102
browser/main/Detail/SnippetTab.styl
Normal file
102
browser/main/Detail/SnippetTab.styl
Normal file
@@ -0,0 +1,102 @@
|
||||
.root
|
||||
position relative
|
||||
flex 1
|
||||
overflow hidden
|
||||
border-right $ui-border
|
||||
&:last-child
|
||||
border-right none
|
||||
&:hover
|
||||
.deleteButton
|
||||
color $ui-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-active-color
|
||||
.root--active
|
||||
@extend .root
|
||||
min-width 100px
|
||||
.button
|
||||
border-color $brand-color
|
||||
|
||||
.button
|
||||
width 100%
|
||||
height 29px
|
||||
overflow ellipsis
|
||||
text-align left
|
||||
padding-right 30px
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
&:active, &:active:hover
|
||||
border-color $brand-color
|
||||
|
||||
.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
|
||||
|
||||
.input
|
||||
height 29px
|
||||
border $ui-active-color
|
||||
padding 0 5px
|
||||
width 100%
|
||||
outline none
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-dark-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
.button
|
||||
border-color $brand-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
|
||||
&:active
|
||||
color white
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dark-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
&:active
|
||||
color $ui-dark-button--active-color
|
||||
&:active, &:active:hover
|
||||
color $ui-dark-button--active-color
|
||||
|
||||
.input
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color white
|
||||
68
browser/main/Detail/StarButton.js
Normal file
68
browser/main/Detail/StarButton.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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 styleName='icon'
|
||||
className={this.state.isActive || this.props.isActive
|
||||
? 'fa fa-star'
|
||||
: 'fa fa-star-o'
|
||||
}
|
||||
/>
|
||||
<span styleName='tooltip'>Star Note</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StarButton.propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(StarButton, styles)
|
||||
27
browser/main/Detail/StarButton.styl
Normal file
27
browser/main/Detail/StarButton.styl
Normal file
@@ -0,0 +1,27 @@
|
||||
.root
|
||||
position relative
|
||||
padding 0
|
||||
&:hover
|
||||
.icon
|
||||
transform rotate(-72deg)
|
||||
.tooltip
|
||||
opacity 1
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
color $brand-color
|
||||
&:hover
|
||||
color $brand-color !important
|
||||
.icon
|
||||
transform rotate(-72deg)
|
||||
.icon
|
||||
transition transform 0.15s
|
||||
|
||||
.tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
top 45px
|
||||
right 65px
|
||||
padding 5px
|
||||
opacity 0
|
||||
border-radius 2px
|
||||
152
browser/main/Detail/TagSelect.js
Normal file
152
browser/main/Detail/TagSelect.js
Normal file
@@ -0,0 +1,152 @@
|
||||
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 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
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().replace(/ +/g, '_')
|
||||
|
||||
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)
|
||||
87
browser/main/Detail/TagSelect.styl
Normal file
87
browser/main/Detail/TagSelect.styl
Normal file
@@ -0,0 +1,87 @@
|
||||
.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-button--focus-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
|
||||
|
||||
body[data-theme="dark"]
|
||||
.icon
|
||||
color $ui-dark-button-color
|
||||
|
||||
.tag
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-button-color
|
||||
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color $ui-dark-borderColor
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
&:focus
|
||||
border-color $ui-input--focus-borderColor = #369DCD
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor = #DDD
|
||||
103
browser/main/Detail/index.js
Normal file
103
browser/main/Detail/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
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'
|
||||
import StatusBar from '../StatusBar'
|
||||
|
||||
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, data, config } = this.props
|
||||
let note = null
|
||||
if (location.query.key != null) {
|
||||
let splitted = location.query.key.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
|
||||
note = data.noteMap.get(storageKey + '-' + 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>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return (
|
||||
<SnippetNoteDetail
|
||||
note={note}
|
||||
config={config}
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownNoteDetail
|
||||
note={note}
|
||||
config={config}
|
||||
ref='root'
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Detail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
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)
|
||||
230
browser/main/Main.js
Normal file
230
browser/main/Main.js
Normal file
@@ -0,0 +1,230 @@
|
||||
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 _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import InitModal from 'browser/main/modals/InitModal'
|
||||
import mixpanel from 'browser/main/lib/mixpanel'
|
||||
|
||||
function focused () {
|
||||
mixpanel.track('MAIN_FOCUSED')
|
||||
}
|
||||
|
||||
class Main extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let { config } = props
|
||||
|
||||
this.state = {
|
||||
isRightSliderFocused: false,
|
||||
listWidth: config.listWidth,
|
||||
navWidth: config.listWidth,
|
||||
isLeftSliderFocused: false
|
||||
}
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
let { status, config } = this.props
|
||||
|
||||
return {
|
||||
status,
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { dispatch, config } = this.props
|
||||
|
||||
if (config.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('focus', focused)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', focused)
|
||||
}
|
||||
|
||||
handleLeftSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isLeftSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleRightSlideMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isRightSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isRightSliderFocused: 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
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
this.setState({
|
||||
isLeftSliderFocused: false
|
||||
}, () => {
|
||||
let { dispatch } = this.props
|
||||
let navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({listWidth: navWidth})
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
listWidth: navWidth
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.state.isRightSliderFocused) {
|
||||
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
|
||||
})
|
||||
}
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
let navWidth = e.pageX
|
||||
if (navWidth < 80) {
|
||||
navWidth = 80
|
||||
} else if (navWidth > 600) {
|
||||
navWidth = 600
|
||||
}
|
||||
this.setState({
|
||||
navWidth: navWidth
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
'data',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.navWidth - 1}}
|
||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox'/>
|
||||
</div>
|
||||
}
|
||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
ref='body'
|
||||
style={{left: config.isSideNavFolded ? 44 : this.state.navWidth}}
|
||||
>
|
||||
<TopBar style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'config',
|
||||
'data',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isRightSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.listWidth}}
|
||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth + 1}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Main.childContextTypes = {
|
||||
status: PropTypes.shape({
|
||||
updateReady: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
41
browser/main/Main.styl
Normal file
41
browser/main/Main.styl
Normal file
@@ -0,0 +1,41 @@
|
||||
.root
|
||||
absolute top left bottom right
|
||||
|
||||
.body
|
||||
absolute right top bottom
|
||||
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
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
absolute top left bottom right
|
||||
|
||||
.slider
|
||||
background-color $ui-dark-borderColor
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.slider--active
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -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
|
||||
}
|
||||
225
browser/main/NoteList/NoteList.styl
Normal file
225
browser/main/NoteList/NoteList.styl
Normal file
@@ -0,0 +1,225 @@
|
||||
.root
|
||||
absolute left bottom
|
||||
border-top $ui-border
|
||||
top $topBar-height - 1
|
||||
|
||||
.control
|
||||
absolute top left right
|
||||
user-select none
|
||||
height 25px
|
||||
font-size 10px
|
||||
border-bottom $ui-border
|
||||
line-height 25px
|
||||
display flex
|
||||
background-color $ui-backgroundColor
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-sortBy
|
||||
flex 1
|
||||
padding-left 5px
|
||||
|
||||
.control-sortBy-select
|
||||
margin-left 5px
|
||||
padding 0
|
||||
border none
|
||||
background-color transparent
|
||||
font-size 10px
|
||||
|
||||
.control-button
|
||||
width 25px
|
||||
padding 0
|
||||
background-color transparent
|
||||
border none
|
||||
color $ui-inactive-text-color
|
||||
transition 0.15s
|
||||
&:active, &:active:hover
|
||||
color $ui-active-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
.control-button-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-button--active
|
||||
@extend .control-button
|
||||
color $ui-active-color
|
||||
&:hover
|
||||
color $ui-active-color
|
||||
|
||||
.control-button-tooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
top 20px
|
||||
right 5px
|
||||
padding 5px
|
||||
opacity 0
|
||||
white-space nowrap
|
||||
border-radius 2px
|
||||
z-index 1
|
||||
|
||||
.list
|
||||
absolute left right bottom
|
||||
top 24px
|
||||
overflow auto
|
||||
|
||||
.item
|
||||
position relative
|
||||
border-bottom $ui-border
|
||||
padding 2px 5px
|
||||
user-select none
|
||||
cursor pointer
|
||||
transition background-color 0.15s
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
&:active
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-title
|
||||
.item-title-empty
|
||||
.item-title-icon
|
||||
.item-bottom-tagIcon
|
||||
.item-bottom-tagList-empty
|
||||
.item-bottom-time
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
background-color $ui-active-color
|
||||
color white
|
||||
.item-title
|
||||
.item-title-empty
|
||||
.item-title-icon
|
||||
.item-bottom-tagIcon
|
||||
.item-bottom-tagList-empty
|
||||
.item-bottom-time
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-border
|
||||
absolute top bottom left right
|
||||
border-style solid
|
||||
border-width 2px
|
||||
border-color transparent
|
||||
transition 0.15s
|
||||
|
||||
.item-title
|
||||
height 24px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
padding 0
|
||||
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-bottom
|
||||
margin-top 2px
|
||||
height 20px
|
||||
font-size 12px
|
||||
line-height 20px
|
||||
overflow ellipsis
|
||||
display flex
|
||||
|
||||
.item-bottom-tagIcon
|
||||
vertical-align middle
|
||||
color $ui-button-color
|
||||
height 20px
|
||||
line-height 20px
|
||||
|
||||
.item-bottom-tagList
|
||||
flex 1
|
||||
overflow ellipsis
|
||||
line-height 20px
|
||||
|
||||
.item-bottom-tagList-item
|
||||
margin 0 4px
|
||||
padding 0 4px
|
||||
height 20px
|
||||
box-sizing border-box
|
||||
border-radius 3px
|
||||
vertical-align middle
|
||||
border-style solid
|
||||
border-color $ui-button--focus-borderColor
|
||||
border-width 0 0 0 3px
|
||||
background-color $ui-backgroundColor
|
||||
color $ui-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
font-size 10px
|
||||
margin-left 5px
|
||||
|
||||
.item-bottom-time
|
||||
color $ui-inactive-text-color
|
||||
margin-left 5px
|
||||
font-size 10px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color alpha($ui-active-color, 20%)
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
border-color $ui-dark-borderColor
|
||||
.item-title
|
||||
color white
|
||||
.item-bottom-tagList-item
|
||||
background-color transparent
|
||||
color white
|
||||
.item-bottom-tagList-empty
|
||||
color white
|
||||
&:hover
|
||||
background-color $ui-active-color
|
||||
|
||||
.item-title
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-darkinactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.item-bottom-tagIcon
|
||||
color $ui-dark-button-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
border-color $ui-dark-button--focus-borderColor
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
.control
|
||||
background-color $ui-dark-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
.control-sortBy-select
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
|
||||
434
browser/main/NoteList/index.js
Normal file
434
browser/main/NoteList/index.js
Normal file
@@ -0,0 +1,434 @@
|
||||
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'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
function sortByCreatedAt (a, b) {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||
}
|
||||
|
||||
function sortByAlphabetical (a, b) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
function sortByUpdatedAt (a, b) {
|
||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||
}
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.selectNextNoteHandler = () => {
|
||||
console.log('fired next')
|
||||
this.selectNextNote()
|
||||
}
|
||||
this.selectPriorNoteHandler = () => {
|
||||
this.selectPriorNote()
|
||||
}
|
||||
this.focusHandler = () => {
|
||||
this.refs.list.focus()
|
||||
}
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ee.on('list:next', this.selectNextNoteHandler)
|
||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||
ee.on('list:focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.location.pathname !== this.props.location.pathname) {
|
||||
this.resetScroll()
|
||||
}
|
||||
}
|
||||
|
||||
resetScroll () {
|
||||
this.refs.list.scrollTop = 0
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
|
||||
ee.off('list:next', this.selectNextNoteHandler)
|
||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||
ee.off('list:focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
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].storage + '-' + this.notes[0].key
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto scroll
|
||||
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note != null && note.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
if (targetIndex > -1) {
|
||||
let list = this.refs.list
|
||||
let item = list.childNodes[targetIndex]
|
||||
|
||||
if (item == null) return false
|
||||
|
||||
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.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
|
||||
if (targetIndex === 0) {
|
||||
return
|
||||
}
|
||||
targetIndex--
|
||||
if (targetIndex < 0) targetIndex = 0
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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.storage + '-' + note.key === location.query.key
|
||||
})
|
||||
|
||||
if (targetIndex === this.notes.length - 1) {
|
||||
targetIndex = 0
|
||||
} else {
|
||||
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].storage + '-' + this.notes[targetIndex].key
|
||||
}
|
||||
})
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
ee.emit('top:new-note')
|
||||
}
|
||||
|
||||
if (e.keyCode === 68) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:delete')
|
||||
}
|
||||
|
||||
if (e.keyCode === 69) {
|
||||
e.preventDefault()
|
||||
ee.emit('detail:focus')
|
||||
}
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
e.preventDefault()
|
||||
this.selectPriorNote()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
e.preventDefault()
|
||||
this.selectNextNote()
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
let { data, params, location } = this.props
|
||||
|
||||
if (location.pathname.match(/\/home/)) {
|
||||
return data.noteMap.map((note) => note)
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/starred/)) {
|
||||
return data.starredSet.toJS()
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let storageKey = params.storageKey
|
||||
let folderKey = params.folderKey
|
||||
let storage = data.storageMap.get(storageKey)
|
||||
if (storage == null) return []
|
||||
|
||||
let folder = _.find(storage.folders, {key: folderKey})
|
||||
if (folder == null) {
|
||||
let storageNoteSet = data.storageNoteMap
|
||||
.get(storage.key)
|
||||
if (storageNoteSet == null) storageNoteSet = []
|
||||
return storageNoteSet
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let folderNoteKeyList = data.folderNoteMap
|
||||
.get(storage.key + '-' + folder.key)
|
||||
|
||||
return folderNoteKeyList != null
|
||||
? folderNoteKeyList
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
: []
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteContextMenu (e, uniqueKey) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete Note',
|
||||
click: (e) => this.handleDeleteNote(e, uniqueKey)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleDeleteNote (e, uniqueKey) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete a note',
|
||||
detail: 'This work cannot be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (index === 0) {
|
||||
let { dispatch, location } = this.props
|
||||
let splitted = uniqueKey.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
|
||||
dataApi
|
||||
.deleteNote(storageKey, noteKey)
|
||||
.then((data) => {
|
||||
let dispatchHandler = () => {
|
||||
dispatch({
|
||||
type: 'DELETE_NOTE',
|
||||
storageKey: data.storageKey,
|
||||
noteKey: data.noteKey
|
||||
})
|
||||
}
|
||||
|
||||
if (location.query.key === uniqueKey) {
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
} else {
|
||||
dispatchHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
let config = {
|
||||
sortBy: e.target.value
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
handleListStyleButtonClick (e, style) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
let config = {
|
||||
listStyle: style
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location, notes, config } = this.props
|
||||
let sortFunc = config.sortBy === 'CREATED_AT'
|
||||
? sortByCreatedAt
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
this.notes = notes = this.getNotes()
|
||||
.sort(sortFunc)
|
||||
|
||||
let noteList = notes
|
||||
.map((note) => {
|
||||
if (note == null) return null
|
||||
let tagElements = _.isArray(note.tags)
|
||||
? note.tags.map((tag) => {
|
||||
return (
|
||||
<span styleName='item-bottom-tagList-item'
|
||||
key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
let isActive = location.query.key === note.storage + '-' + note.key
|
||||
return (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={note.storage + '-' + note.key}
|
||||
onClick={(e) => this.handleNoteClick(e, note.storage + '-' + note.key)}
|
||||
onContextMenu={(e) => this.handleNoteContextMenu(e, note.storage + '-' + note.key)}
|
||||
>
|
||||
<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>
|
||||
|
||||
{config.listStyle === 'DEFAULT' &&
|
||||
<div styleName='item-bottom'>
|
||||
<i styleName='item-bottom-tagIcon'
|
||||
className='fa fa-tags fa-fw'
|
||||
/>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{tagElements.length > 0
|
||||
? tagElements
|
||||
: <span styleName='item-bottom-tagList-empty'>Not tagged yet</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div styleName='item-bottom-time'>
|
||||
{moment(config.sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='NoteList'
|
||||
styleName='root'
|
||||
style={this.props.style}
|
||||
>
|
||||
<div styleName='control'>
|
||||
<div styleName='control-sortBy'>
|
||||
Sort by
|
||||
<select styleName='control-sortBy-select'
|
||||
value={config.sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option value='UPDATED_AT'>Updated Time</option>
|
||||
<option value='CREATED_AT'>Created Time</option>
|
||||
<option value='ALPHABETICAL'>Alphabetical</option>
|
||||
</select>
|
||||
</div>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||
>
|
||||
<i className='fa fa-th-list'/>
|
||||
<span styleName='control-button-tooltip'>
|
||||
Default Size
|
||||
</span>
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||
>
|
||||
<i className='fa fa-list'/>
|
||||
<span styleName='control-button-tooltip'>
|
||||
Small Size
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div styleName='list'
|
||||
ref='list'
|
||||
tabIndex='-1'
|
||||
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
|
||||
>
|
||||
{noteList}
|
||||
</div>
|
||||
</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)
|
||||
153
browser/main/SideNav/SideNav.styl
Normal file
153
browser/main/SideNav/SideNav.styl
Normal file
@@ -0,0 +1,153 @@
|
||||
.root
|
||||
absolute top left bottom
|
||||
width $sideNav-width
|
||||
border-right $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
|
||||
overflow ellipsis
|
||||
|
||||
.menu
|
||||
margin 0
|
||||
|
||||
.menu-button
|
||||
navButtonColor()
|
||||
height 32px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
width 100%
|
||||
text-align left
|
||||
overflow ellipsis
|
||||
|
||||
.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 32px
|
||||
top 120px
|
||||
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
|
||||
transition opacity 0.15s
|
||||
opacity 1
|
||||
.top-menu-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 30px
|
||||
left 32px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
opacity 0
|
||||
margin-left 0
|
||||
overflow hidden
|
||||
background-color $ui-tooltip-backgroundColor
|
||||
z-index 10
|
||||
color white
|
||||
line-height 30px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
font-size 12px
|
||||
.menu-button, .menu-button--active
|
||||
text-align center
|
||||
&:hover .menu-button-label
|
||||
transition opacity 0.15s
|
||||
opacity 1
|
||||
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
background-color $ui-tooltip-backgroundColor
|
||||
z-index 10
|
||||
color white
|
||||
line-height 32px
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
pointer-events none
|
||||
opacity 0
|
||||
font-size 12px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--folded
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.top
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.top-menu
|
||||
navDarkButtonColor()
|
||||
|
||||
.menu-button
|
||||
navDarkButtonColor()
|
||||
|
||||
.menu-button--active
|
||||
@extend .menu-button
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-button--active-color
|
||||
&:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.storageList-empty
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.navToggle
|
||||
navDarkButtonColor()
|
||||
|
||||
224
browser/main/SideNav/StorageItem.js
Normal file
224
browser/main/SideNav/StorageItem.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StorageItem.styl'
|
||||
import { hashHistory } from 'react-router'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
|
||||
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
class StorageItem extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: true
|
||||
}
|
||||
}
|
||||
|
||||
handleHeaderContextMenu (e) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Add Folder',
|
||||
click: (e) => this.handleAddFolderButtonClick(e)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Unlink Storage',
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleUnlinkStorageClick (e) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Unlink Storage',
|
||||
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
let { storage, dispatch } = this.props
|
||||
dataApi.removeStorage(storage.key)
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: 'REMOVE_STORAGE',
|
||||
storageKey: storage.key
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
this.setState({
|
||||
isOpen: !this.state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
handleAddFolderButtonClick (e) {
|
||||
let { storage } = this.props
|
||||
|
||||
modal.open(CreateFolderModal, {
|
||||
storage
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
handleFolderButtonContextMenu (e, folder) {
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Rename Folder',
|
||||
click: (e) => this.handleRenameFolderClick(e, folder)
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Delete Folder',
|
||||
click: (e) => this.handleFolderDeleteClick(e, folder)
|
||||
}))
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
handleRenameFolderClick (e, folder) {
|
||||
let { storage } = this.props
|
||||
modal.open(RenameFolderModal, {
|
||||
storage,
|
||||
folder
|
||||
})
|
||||
}
|
||||
|
||||
handleFolderDeleteClick (e, folder) {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Delete Folder',
|
||||
detail: 'This work will deletes all notes in the folder and can not be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
let { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteFolder(storage.key, folder.key)
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'DELETE_FOLDER',
|
||||
storage: data.storage,
|
||||
folderKey: data.folderKey
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storage, location, isFolded, data } = this.props
|
||||
let { folderNoteMap } = data
|
||||
let folderList = storage.folders.map((folder) => {
|
||||
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))
|
||||
let noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
let noteCount = noteSet != null
|
||||
? noteSet.size
|
||||
: 0
|
||||
return <button styleName={isActive
|
||||
? 'folderList-item--active'
|
||||
: 'folderList-item'
|
||||
}
|
||||
key={folder.key}
|
||||
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||
onContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
|
||||
>
|
||||
<span styleName='folderList-item-name'
|
||||
style={{borderColor: folder.color}}
|
||||
>
|
||||
{isFolded ? folder.name.substring(0, 1) : folder.name}
|
||||
</span>
|
||||
{!isFolded &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>
|
||||
{folder.name}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
})
|
||||
|
||||
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
|
||||
|
||||
return (
|
||||
<div styleName={isFolded ? 'root--folded' : 'root'}
|
||||
key={storage.key}
|
||||
>
|
||||
<div styleName={isActive
|
||||
? 'header--active'
|
||||
: 'header'
|
||||
}
|
||||
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
|
||||
>
|
||||
<button styleName='header-toggleButton'
|
||||
onMouseDown={(e) => this.handleToggleButtonClick(e)}
|
||||
>
|
||||
<i className={this.state.isOpen
|
||||
? 'fa fa-caret-down'
|
||||
: 'fa fa-caret-right'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!isFolded &&
|
||||
<button styleName='header-addFolderButton'
|
||||
onClick={(e) => this.handleAddFolderButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-plus'/>
|
||||
</button>
|
||||
}
|
||||
|
||||
<button styleName='header-info'
|
||||
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||
>
|
||||
<span styleName='header-info-name'>
|
||||
{isFolded ? storage.name.substring(0, 1) : storage.name}
|
||||
</span>
|
||||
{isFolded &&
|
||||
<span styleName='header-info--folded-tooltip'>
|
||||
{storage.name}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
{this.state.isOpen &&
|
||||
<div styleName='folderList' >
|
||||
{folderList}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageItem.propTypes = {
|
||||
isFolded: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CSSModules(StorageItem, styles)
|
||||
183
browser/main/SideNav/StorageItem.styl
Normal file
183
browser/main/SideNav/StorageItem.styl
Normal file
@@ -0,0 +1,183 @@
|
||||
.root
|
||||
width 100%
|
||||
user-select none
|
||||
.header
|
||||
position relative
|
||||
height 26px
|
||||
width 100%
|
||||
transition 0.15s
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
&:active
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
color $ui-active-color
|
||||
|
||||
.header--active
|
||||
@extend .header
|
||||
.header-info
|
||||
color $ui-button--active-color
|
||||
background-color $ui-button--active-backgroundColor
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
&:hover
|
||||
&:hover:active
|
||||
color white
|
||||
|
||||
.header-toggleButton
|
||||
position absolute
|
||||
left 0
|
||||
width 25px
|
||||
height 26px
|
||||
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 26px
|
||||
padding-left 25px
|
||||
padding-right 10px
|
||||
line-height 26px
|
||||
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
|
||||
|
||||
.header-addFolderButton
|
||||
position absolute
|
||||
right 0
|
||||
width 25px
|
||||
height 26px
|
||||
padding 0
|
||||
border none
|
||||
color $ui-inactive-text-color
|
||||
background-color transparent
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
&:active
|
||||
color $ui-active-color
|
||||
|
||||
.folderList-item
|
||||
display flex
|
||||
width 100%
|
||||
height 26px
|
||||
background-color transparent
|
||||
color $ui-inactive-text-color
|
||||
padding 0
|
||||
margin-bottom 2px
|
||||
text-align left
|
||||
border none
|
||||
overflow ellipsis
|
||||
font-size 14px
|
||||
&:first-child
|
||||
margin-top 0
|
||||
&: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
|
||||
flex 1
|
||||
padding 0 10px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 6px
|
||||
border-style solid
|
||||
border-color transparent
|
||||
|
||||
.folderList-item-noteCount
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 5px
|
||||
font-size 12px
|
||||
|
||||
.folderList-item-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
padding 0 10px
|
||||
left 44px
|
||||
z-index 10
|
||||
pointer-events none
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
height 26px
|
||||
margin-top -26px
|
||||
line-height 26px
|
||||
|
||||
.root--folded
|
||||
@extend .root
|
||||
.header
|
||||
width 100%
|
||||
.header-info
|
||||
overflow ellipsis
|
||||
padding 0 0 0 18px
|
||||
&:hover .header-info--folded-tooltip
|
||||
opacity 1
|
||||
.header-info-path
|
||||
display none
|
||||
.header-toggleButton
|
||||
width 15px
|
||||
.header-info--folded-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
padding 0 10px
|
||||
left 44px
|
||||
z-index 10
|
||||
pointer-events none
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
.header-info--folded-tooltip-path
|
||||
font-size 10px
|
||||
margin 0 5px
|
||||
.folderList-item:hover, .folderList-item--active:hover
|
||||
.folderList-item-tooltip
|
||||
opacity 1
|
||||
.folderList-item-name
|
||||
padding-left 14px
|
||||
body[data-theme="dark"]
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
color $ui-dark-active-color
|
||||
.header--active
|
||||
.header-toggleButton
|
||||
.header-addFolderButton
|
||||
color white
|
||||
&:active
|
||||
color white
|
||||
120
browser/main/SideNav/index.js
Normal file
120
browser/main/SideNav/index.js
Normal file
@@ -0,0 +1,120 @@
|
||||
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 { data, location, config, dispatch } = this.props
|
||||
|
||||
let isFolded = config.isSideNavFolded
|
||||
let isHomeActive = location.pathname.match(/^\/home$/)
|
||||
let isStarredActive = location.pathname.match(/^\/starred$/)
|
||||
|
||||
let storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
key={storage.key}
|
||||
storage={storage}
|
||||
data={data}
|
||||
location={location}
|
||||
isFolded={isFolded}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
})
|
||||
let style = {}
|
||||
if (!isFolded) style.width = this.props.width
|
||||
return (
|
||||
<div className='SideNav'
|
||||
styleName={isFolded ? 'root--folded' : 'root'}
|
||||
tabIndex='1'
|
||||
style={style}
|
||||
>
|
||||
<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-files-o fa-fw'/>
|
||||
<span styleName='menu-button-label'>All Notes</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>
|
||||
<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)
|
||||
55
browser/main/StatusBar/StatusBar.styl
Normal file
55
browser/main/StatusBar/StatusBar.styl
Normal file
@@ -0,0 +1,55 @@
|
||||
.root
|
||||
absolute bottom left right
|
||||
height $statusBar-height - 1
|
||||
background-color $ui-backgroundColor
|
||||
border-top $ui-border
|
||||
display flex
|
||||
|
||||
.blank
|
||||
flex 1
|
||||
|
||||
.help
|
||||
navButtonColor()
|
||||
height 24px
|
||||
width 24px
|
||||
border-width 0 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
.zoom
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
height 24px
|
||||
border-width 0 0 0 1px
|
||||
border-style solid
|
||||
border-color $ui-borderColor
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
.update-icon
|
||||
color $brand-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.help
|
||||
navButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
.update
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
89
browser/main/StatusBar/index.js
Normal file
89
browser/main/StatusBar/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
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 { remote, ipcRenderer } = electron
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
|
||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3]
|
||||
|
||||
class StatusBar extends React.Component {
|
||||
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) {
|
||||
ipcRenderer.send('update-app-confirm')
|
||||
}
|
||||
}
|
||||
|
||||
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, status } = this.context
|
||||
|
||||
return (
|
||||
<div className='StatusBar'
|
||||
styleName='root'
|
||||
>
|
||||
<div styleName='blank' />
|
||||
{status.updateReady
|
||||
? <button onClick={this.updateApp} styleName='update'>
|
||||
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
{/*<button styleName='help'>
|
||||
<i className='fa fa-info-circle' />
|
||||
</button>*/}
|
||||
<button styleName='zoom'
|
||||
onClick={(e) => this.handleZoomButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-search-plus' />
|
||||
{Math.floor(config.zoom * 100)}%
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StatusBar.contextTypes = {
|
||||
status: PropTypes.shape({
|
||||
updateReady: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
config: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
StatusBar.propTypes = {
|
||||
config: PropTypes.shape({
|
||||
zoom: PropTypes.number
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(StatusBar, styles)
|
||||
173
browser/main/TopBar/TopBar.styl
Normal file
173
browser/main/TopBar/TopBar.styl
Normal file
@@ -0,0 +1,173 @@
|
||||
.root
|
||||
position relative
|
||||
background-color $ui-backgroundColor
|
||||
height $topBar-height - 1
|
||||
.root--expanded
|
||||
@extend .root
|
||||
$control-height = 34px
|
||||
|
||||
.control
|
||||
position absolute
|
||||
top 8px
|
||||
left 8px
|
||||
right 8px
|
||||
height $control-height
|
||||
border $ui-border
|
||||
border-radius 20px
|
||||
overflow hidden
|
||||
display flex
|
||||
|
||||
.control-search
|
||||
height 32px
|
||||
flex 1
|
||||
background-color white
|
||||
position relative
|
||||
|
||||
.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-contextButton
|
||||
display block
|
||||
width 20px
|
||||
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 .control-newPostButton-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-newPostButton
|
||||
display block
|
||||
width 36px
|
||||
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 .control-newPostButton-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-newPostButton-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 45px
|
||||
left 385px
|
||||
z-index 10
|
||||
padding 5px
|
||||
line-height normal
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dark-borderColor
|
||||
.control-search
|
||||
background-color $dark-background-color
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.control-search-input
|
||||
input
|
||||
background-color $dark-background-color
|
||||
color $ui-dark-text-color
|
||||
.control-search-optionList
|
||||
color white
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
box-shadow 2px 2px 10px black
|
||||
|
||||
.control-search-optionList-item
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
|
||||
.control-search-optionList-item-folder
|
||||
color $ui-dark-text-color
|
||||
.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
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-contextButton,
|
||||
.control-newPostButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
&:active
|
||||
border-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.control-newPostButton-tooltip
|
||||
darkTooltip()
|
||||
383
browser/main/TopBar/index.js
Normal file
383
browser/main/TopBar/index.js
Normal file
@@ -0,0 +1,383 @@
|
||||
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'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const OSX = window.process.platform === 'darwin'
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
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 { config } = this.props
|
||||
|
||||
switch (config.ui.defaultNote) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
this.createNote('MARKDOWN_NOTE')
|
||||
break
|
||||
case 'SNIPPET_NOTE':
|
||||
this.createNote('SNIPPET_NOTE')
|
||||
break
|
||||
case 'ALWAYS_ASK':
|
||||
default:
|
||||
let { dispatch, location } = this.props
|
||||
let { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
let { data, params } = this.props
|
||||
let storage = data.storageMap.get(params.storageKey)
|
||||
|
||||
// Find first storage
|
||||
if (storage == null) {
|
||||
for (let kv of data.storageMap) {
|
||||
storage = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (storage == null) 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')
|
||||
|
||||
return {
|
||||
storage,
|
||||
folder
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: this.refs.searchInput.value
|
||||
})
|
||||
}
|
||||
|
||||
getOptions () {
|
||||
let { data } = this.props
|
||||
let { search } = this.state
|
||||
let notes = data.noteMap.map((note) => note)
|
||||
if (search.trim().length === 0) return []
|
||||
let searchBlocks = search.split(' ')
|
||||
searchBlocks.forEach((block) => {
|
||||
if (block.match(/^!#.+/)) {
|
||||
let tag = block.match(/^!#(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
notes = notes
|
||||
.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
} else if (block.match(/^!.+/)) {
|
||||
let block = block.match(/^!(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
notes = notes.filter((note) => {
|
||||
if (!_.isArray(note.tags) || !note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return !note.description.match(regExp)
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return !note.content.match(regExp)
|
||||
}
|
||||
return false
|
||||
})
|
||||
} else if (block.match(/^#.+/)) {
|
||||
let tag = block.match(/#(.+)/)[1]
|
||||
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
|
||||
notes = notes
|
||||
.filter((note) => {
|
||||
if (!_.isArray(note.tags)) return false
|
||||
return note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let regExp = new RegExp(_.escapeRegExp(block), 'i')
|
||||
notes = notes.filter((note) => {
|
||||
if (_.isArray(note.tags) && note.tags.some((_tag) => {
|
||||
return _tag.match(regExp)
|
||||
})) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(regExp)
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(regExp)
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleContextButtonClick (e) {
|
||||
let { config } = this.props
|
||||
|
||||
let menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Create Markdown Note',
|
||||
click: (e) => this.createNote('MARKDOWN_NOTE')
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Create Snippet Note',
|
||||
click: (e) => this.createNote('SNIPPET_NOTE')
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
type: 'separator'
|
||||
}))
|
||||
menu.append(new MenuItem({
|
||||
label: 'Change Default Note',
|
||||
submenu: [
|
||||
{
|
||||
type: 'radio',
|
||||
label: 'Markdown Note',
|
||||
checked: config.ui.defaultNote === 'MARKDOWN_NOTE',
|
||||
click: (e) => this.setDefaultNote('MARKDOWN_NOTE')
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
label: 'Snippet Note',
|
||||
checked: config.ui.defaultNote === 'SNIPPET_NOTE',
|
||||
click: (e) => this.setDefaultNote('SNIPPET_NOTE')
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
label: 'Always Ask',
|
||||
checked: config.ui.defaultNote === 'ALWAYS_ASK',
|
||||
click: (e) => this.setDefaultNote('ALWAYS_ASK')
|
||||
}
|
||||
]
|
||||
}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
}
|
||||
|
||||
createNote (noteType) {
|
||||
let { dispatch, location } = this.props
|
||||
if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.')
|
||||
|
||||
let { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
let newNote = noteType === 'MARKDOWN_NOTE'
|
||||
? {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
: {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
|
||||
dataApi
|
||||
.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.storage + '-' + note.key}
|
||||
})
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultNote (defaultNote) {
|
||||
let { config, dispatch } = this.props
|
||||
let ui = Object.assign(config.ui)
|
||||
ui.defaultNote = defaultNote
|
||||
ConfigManager.set({
|
||||
ui
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'SET_UI',
|
||||
config: ConfigManager.get()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config, style, data } = this.props
|
||||
let searchOptionList = this.getOptions()
|
||||
.map((note) => {
|
||||
let storage = data.storageMap.get(note.storage)
|
||||
let folder = _.find(storage.folders, {key: note.folder})
|
||||
return <div styleName='control-search-optionList-item'
|
||||
key={note.storage + '-' + note.key}
|
||||
onClick={(e) => this.handleOptionClick(note.storage + '-' + note.key)(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>
|
||||
<button styleName='control-contextButton'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-caret-down'/>
|
||||
</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
|
||||
}
|
||||
102
browser/main/global.styl
Normal file
102
browser/main/global.styl
Normal file
@@ -0,0 +1,102 @@
|
||||
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
|
||||
body[data-theme="dark"]
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color alpha(black, 60%)
|
||||
|
||||
.CodeMirror
|
||||
font-family inherit !important
|
||||
line-height 1.4em
|
||||
height 100%
|
||||
.CodeMirror-focused .CodeMirror-selected
|
||||
background #B1D7FE
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection
|
||||
background #B1D7FE
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection
|
||||
background #B1D7FE
|
||||
::selection
|
||||
background #B1D7FE
|
||||
@@ -1,67 +1,89 @@
|
||||
import { Provider } from 'react-redux'
|
||||
import MainPage from './MainPage'
|
||||
import Main from './Main'
|
||||
import store from './store'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
require('../styles/main/index.styl')
|
||||
import { openModal } from 'browser/lib/modal'
|
||||
import Tutorial from './modal/Tutorial'
|
||||
import activityRecord from 'browser/lib/activityRecord'
|
||||
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||
import { syncHistoryWithStore } from 'react-router-redux'
|
||||
require('./lib/ipcClient')
|
||||
require('../lib/customMeta')
|
||||
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
const path = require('path')
|
||||
const remote = electron.remote
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
window.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||
remote.getCurrentWindow().toggleDevTools()
|
||||
}
|
||||
})
|
||||
}
|
||||
const { remote, ipcRenderer } = electron
|
||||
const { dialog } = remote
|
||||
|
||||
activityRecord.init()
|
||||
window.addEventListener('online', function () {
|
||||
ipc.send('check-update', 'check-update')
|
||||
document.addEventListener('drop', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
options.silent = false
|
||||
}
|
||||
console.log(options)
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
ipc.on('notify', function (e, payload) {
|
||||
notify(payload.title, {
|
||||
body: payload.body
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('copy-finder', function () {
|
||||
activityRecord.emit('FINDER_COPY')
|
||||
})
|
||||
ipc.on('open-finder', function () {
|
||||
activityRecord.emit('FINDER_OPEN')
|
||||
document.addEventListener('dragover', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
let el = document.getElementById('content')
|
||||
const history = syncHistoryWithStore(hashHistory, store)
|
||||
|
||||
function notify (...args) {
|
||||
return new window.Notification(...args)
|
||||
}
|
||||
|
||||
function 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) {
|
||||
ipcRenderer.send('update-app-confirm')
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<div>
|
||||
<Provider store={store}>
|
||||
<MainPage/>
|
||||
</Provider>
|
||||
</div>
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path='/' component={Main}>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path='home' />
|
||||
<Route path='starred' />
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path=':storageKey'>
|
||||
<IndexRoute />
|
||||
<Route path='folders/:folderKey' />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>
|
||||
), el, function () {
|
||||
let loadingCover = document.getElementById('loadingCover')
|
||||
loadingCover.parentNode.removeChild(loadingCover)
|
||||
let status = JSON.parse(localStorage.getItem('status'))
|
||||
if (status == null) status = {}
|
||||
if (!status.introWatched) {
|
||||
openModal(Tutorial)
|
||||
status.introWatched = true
|
||||
localStorage.setItem('status', JSON.stringify(status))
|
||||
}
|
||||
|
||||
ipcRenderer.on('update-ready', function () {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_AVAILABLE'
|
||||
})
|
||||
notify('Update ready!', {
|
||||
body: 'New Boostnote is ready to be installed.'
|
||||
})
|
||||
updateApp()
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-found', function () {
|
||||
notify('Update found!', {
|
||||
body: 'Preparing to update...'
|
||||
})
|
||||
})
|
||||
|
||||
ipcRenderer.send('update-check', 'check-update')
|
||||
window.addEventListener('online', function () {
|
||||
if (!store.getState().status.updateReady) {
|
||||
ipcRenderer.send('update-check', 'check-update')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
130
browser/main/lib/ConfigManager.js
Normal file
130
browser/main/lib/ConfigManager.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const electron = require('electron')
|
||||
const { ipcRenderer } = electron
|
||||
const consts = require('browser/lib/consts')
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
zoom: 1,
|
||||
isSideNavFolded: false,
|
||||
listWidth: 250,
|
||||
navWidth: 200,
|
||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||
hotkey: {
|
||||
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
||||
},
|
||||
ui: {
|
||||
theme: 'default',
|
||||
disableDirectWrite: false,
|
||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
},
|
||||
editor: {
|
||||
theme: 'default',
|
||||
fontSize: '14',
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentType: 'space',
|
||||
indentSize: '2',
|
||||
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({}, DEFAULT_CONFIG, JSON.parse(config))
|
||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, config.hotkey)
|
||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, config.ui)
|
||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, config.editor)
|
||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, config.preview)
|
||||
if (!validate(config)) throw new Error('INVALID CONFIG')
|
||||
} catch (err) {
|
||||
console.warn('Boostnote resets the malformed configuration.')
|
||||
config = DEFAULT_CONFIG
|
||||
_save(config)
|
||||
}
|
||||
|
||||
if (!isInitialized) {
|
||||
isInitialized = true
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
|
||||
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
|
||||
? config.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (config.editor.theme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function set (updates) {
|
||||
let currentConfig = get()
|
||||
let newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
|
||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||
_save(newConfig)
|
||||
|
||||
if (newConfig.ui.theme === 'dark') {
|
||||
document.body.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
if (editorTheme == null) {
|
||||
editorTheme = document.createElement('link')
|
||||
editorTheme.setAttribute('id', 'editorTheme')
|
||||
editorTheme.setAttribute('rel', 'stylesheet')
|
||||
document.head.appendChild(editorTheme)
|
||||
}
|
||||
let newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
|
||||
? newConfig.editor.theme
|
||||
: 'default'
|
||||
|
||||
if (newTheme !== 'default') {
|
||||
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
|
||||
}
|
||||
|
||||
ipcRenderer.send('config-renew', {
|
||||
config: get()
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
85
browser/main/lib/dataApi/addStorage.js
Normal file
85
browser/main/lib/dataApi/addStorage.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
/**
|
||||
* @param {Object}
|
||||
* name, path, type
|
||||
*
|
||||
* 1. check if BoostnoteJSON can be created
|
||||
* if the file doesn't exist or isn't valid, try to rewrite it.
|
||||
* if the rewriting failed, throw Error
|
||||
* 2. save metadata to localStorage
|
||||
* 3. fetch notes & folders
|
||||
* 4. return `{storage: {...} folders: [folder]}`
|
||||
*/
|
||||
function addStorage (input) {
|
||||
if (!_.isString(input.path)) {
|
||||
return Promise.reject(new Error('Path must be a string.'))
|
||||
}
|
||||
let rawStorages
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
rawStorages = []
|
||||
}
|
||||
let key = keygen()
|
||||
while (rawStorages.some((storage) => storage.key === key)) {
|
||||
key = keygen()
|
||||
}
|
||||
|
||||
let newStorage = {
|
||||
key,
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
path: input.path
|
||||
}
|
||||
|
||||
return Promise.resolve(newStorage)
|
||||
.then(resolveStorageData)
|
||||
.then(function saveMetadataToLocalStorage (resolvedStorage) {
|
||||
newStorage = resolvedStorage
|
||||
rawStorages.push({
|
||||
key: newStorage.key,
|
||||
type: newStorage.type,
|
||||
name: newStorage.name,
|
||||
path: newStorage.path
|
||||
})
|
||||
|
||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
return newStorage
|
||||
})
|
||||
.then(function (storage) {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
let unknownCount = 0
|
||||
notes.forEach((note) => {
|
||||
if (!storage.folders.some((folder) => note.folder === folder.key)) {
|
||||
unknownCount++
|
||||
storage.folders.push({
|
||||
key: note.folder,
|
||||
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
|
||||
name: 'Unknown ' + unknownCount
|
||||
})
|
||||
}
|
||||
})
|
||||
if (unknownCount > 0) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
}
|
||||
return notes
|
||||
})
|
||||
})
|
||||
.then(function returnValue (notes) {
|
||||
return {
|
||||
storage: newStorage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = addStorage
|
||||
63
browser/main/lib/dataApi/createFolder.js
Normal file
63
browser/main/lib/dataApi/createFolder.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {Object} input
|
||||
* ```
|
||||
* {
|
||||
* color: String,
|
||||
* name: String
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function createFolder (storageKey, input) {
|
||||
let rawStorages
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
if (!_.isString(input.name)) throw new Error('Name must be a string.')
|
||||
if (!_.isString(input.color)) throw new Error('Color must be a string.')
|
||||
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(rawStorages, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function createFolder (storage) {
|
||||
let key = keygen()
|
||||
while (storage.folders.some((folder) => folder.key === key)) {
|
||||
key = keygen()
|
||||
}
|
||||
let newFolder = {
|
||||
key,
|
||||
color: input.color,
|
||||
name: input.name
|
||||
}
|
||||
|
||||
storage.folders.push(newFolder)
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
|
||||
return {
|
||||
storage
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createFolder
|
||||
84
browser/main/lib/dataApi/createNote.js
Normal file
84
browser/main/lib/dataApi/createNote.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const sander = require('sander')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function validateInput (input) {
|
||||
if (!_.isArray(input.tags)) input.tags = []
|
||||
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
|
||||
if (!_.isString(input.title)) input.title = ''
|
||||
input.isStarred = !!input.isStarred
|
||||
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
if (!_.isString(input.content)) input.content = ''
|
||||
break
|
||||
case 'SNIPPET_NOTE':
|
||||
if (!_.isString(input.description)) input.description = ''
|
||||
if (!_.isArray(input.snippets)) {
|
||||
input.snippets = [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
|
||||
}
|
||||
}
|
||||
|
||||
function createNote (storageKey, input) {
|
||||
let targetStorage
|
||||
try {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
input = Object.assign({}, input)
|
||||
validateInput(input)
|
||||
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function checkFolderExists (storage) {
|
||||
if (_.find(storage.folders, {key: input.folder}) == null) {
|
||||
throw new Error('Target folder doesn\'t exist.')
|
||||
}
|
||||
return storage
|
||||
})
|
||||
.then(function saveNote (storage) {
|
||||
let key = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
|
||||
key = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
let noteData = Object.assign({}, input, {
|
||||
key,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
storage: storageKey
|
||||
})
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
|
||||
return noteData
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = createNote
|
||||
75
browser/main/lib/dataApi/deleteFolder.js
Normal file
75
browser/main/lib/dataApi/deleteFolder.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const sander = require('sander')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} folderKey
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object,
|
||||
* folderKey: String
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function deleteFolder (storageKey, folderKey) {
|
||||
let rawStorages
|
||||
let targetStorage
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(rawStorages, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function assignNotes (storage) {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(function deleteFolderAndNotes (data) {
|
||||
let { storage, notes } = data
|
||||
storage.folders = storage.folders
|
||||
.filter(function excludeTargetFolder (folder) {
|
||||
return folder.key !== folderKey
|
||||
})
|
||||
|
||||
let targetNotes = notes.filter(function filterTargetNotes (note) {
|
||||
return note.folder === folderKey
|
||||
})
|
||||
|
||||
let deleteAllNotes = targetNotes
|
||||
.map(function deleteNote (note) {
|
||||
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
|
||||
return sander.unlink(notePath)
|
||||
.catch(function (err) {
|
||||
console.warn('Failed to delete', notePath, err)
|
||||
})
|
||||
})
|
||||
return Promise.all(deleteAllNotes)
|
||||
.then(() => storage)
|
||||
})
|
||||
.then(function (storage) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
|
||||
return {
|
||||
storage,
|
||||
folderKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = deleteFolder
|
||||
34
browser/main/lib/dataApi/deleteNote.js
Normal file
34
browser/main/lib/dataApi/deleteNote.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
|
||||
function deleteNote (storageKey, noteKey) {
|
||||
let targetStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function deleteNoteFile (storage) {
|
||||
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
|
||||
|
||||
try {
|
||||
sander.unlinkSync(notePath)
|
||||
} catch (err) {
|
||||
console.warn('Failed to delete note cson', err)
|
||||
}
|
||||
return {
|
||||
noteKey,
|
||||
storageKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = deleteNote
|
||||
20
browser/main/lib/dataApi/index.js
Normal file
20
browser/main/lib/dataApi/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const dataApi = {
|
||||
init: require('./init'),
|
||||
addStorage: require('./addStorage'),
|
||||
renameStorage: require('./renameStorage'),
|
||||
removeStorage: require('./removeStorage'),
|
||||
createFolder: require('./createFolder'),
|
||||
updateFolder: require('./updateFolder'),
|
||||
deleteFolder: require('./deleteFolder'),
|
||||
createNote: require('./createNote'),
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
moveNote: require('./moveNote'),
|
||||
migrateFromV5Storage: require('./migrateFromV5Storage'),
|
||||
|
||||
_migrateFromV6Storage: require('./migrateFromV6Storage'),
|
||||
_resolveStorageData: require('./resolveStorageData'),
|
||||
_resolveStorageNotes: require('./resolveStorageNotes')
|
||||
}
|
||||
|
||||
module.exports = dataApi
|
||||
83
browser/main/lib/dataApi/init.js
Normal file
83
browser/main/lib/dataApi/init.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict'
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
/**
|
||||
* @return {Object} all storages and notes
|
||||
* ```
|
||||
* {
|
||||
* storages: [...],
|
||||
* notes: [...]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This method deals with 3 patterns.
|
||||
* 1. v1
|
||||
* 2. legacy
|
||||
* 3. empty directory
|
||||
*/
|
||||
function init () {
|
||||
let fetchStorages = function () {
|
||||
let rawStorages
|
||||
try {
|
||||
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse cached data from localStorage', e)
|
||||
rawStorages = []
|
||||
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
}
|
||||
return Promise.all(rawStorages
|
||||
.map(resolveStorageData))
|
||||
}
|
||||
|
||||
let fetchNotes = function (storages) {
|
||||
let findNotesFromEachStorage = storages
|
||||
.map((storage) => {
|
||||
return resolveStorageNotes(storage)
|
||||
.then((notes) => {
|
||||
let unknownCount = 0
|
||||
notes.forEach((note) => {
|
||||
if (!storage.folders.some((folder) => note.folder === folder.key)) {
|
||||
unknownCount++
|
||||
storage.folders.push({
|
||||
key: note.folder,
|
||||
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
|
||||
name: 'Unknown ' + unknownCount
|
||||
})
|
||||
}
|
||||
})
|
||||
if (unknownCount > 0) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
|
||||
}
|
||||
return notes
|
||||
})
|
||||
})
|
||||
return Promise.all(findNotesFromEachStorage)
|
||||
.then(function concatNoteGroup (noteGroups) {
|
||||
return noteGroups.reduce(function (sum, group) {
|
||||
return sum.concat(group)
|
||||
}, [])
|
||||
})
|
||||
.then(function returnData (notes) {
|
||||
return {
|
||||
storages,
|
||||
notes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(fetchStorages())
|
||||
.then((storages) => {
|
||||
return storages
|
||||
.filter((storage) => {
|
||||
if (!_.isObject(storage)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
.then(fetchNotes)
|
||||
}
|
||||
module.exports = init
|
||||
110
browser/main/lib/dataApi/migrateFromV5Storage.js
Normal file
110
browser/main/lib/dataApi/migrateFromV5Storage.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const consts = require('browser/lib/consts')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
|
||||
function migrateFromV5Storage (storageKey, data) {
|
||||
let targetStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
targetStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function (storage) {
|
||||
return importAll(storage, data)
|
||||
})
|
||||
}
|
||||
|
||||
function importAll (storage, data) {
|
||||
let oldArticles = data.articles
|
||||
let notes = []
|
||||
data.folders
|
||||
.forEach(function (oldFolder) {
|
||||
let folderKey = keygen()
|
||||
while (storage.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.folders.push(newFolder)
|
||||
|
||||
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
|
||||
articles.forEach((article) => {
|
||||
let noteKey = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
|
||||
noteKey = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
console.error('Failed to read `notes` directory.')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (article.mode === 'markdown') {
|
||||
let newNote = {
|
||||
tags: article.tags,
|
||||
createdAt: article.createdAt,
|
||||
updatedAt: article.updatedAt,
|
||||
folder: folderKey,
|
||||
storage: storage.key,
|
||||
type: 'MARKDOWN_NOTE',
|
||||
isStarred: false,
|
||||
title: article.title,
|
||||
content: '# ' + article.title + '\n\n' + article.content,
|
||||
key: noteKey
|
||||
}
|
||||
notes.push(newNote)
|
||||
} else {
|
||||
let newNote = {
|
||||
tags: article.tags,
|
||||
createdAt: article.createdAt,
|
||||
updatedAt: article.updatedAt,
|
||||
folder: folderKey,
|
||||
storage: 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
notes.forEach(function (note) {
|
||||
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
|
||||
})
|
||||
|
||||
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
|
||||
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = migrateFromV5Storage
|
||||
89
browser/main/lib/dataApi/migrateFromV6Storage.js
Normal file
89
browser/main/lib/dataApi/migrateFromV6Storage.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const _ = require('lodash')
|
||||
const CSON = require('@rokt33r/season')
|
||||
|
||||
function migrateFromV5Storage (storagePath) {
|
||||
var boostnoteJSONPath = path.join(storagePath, 'boostnote.json')
|
||||
return Promise.resolve()
|
||||
.then(function readBoostnoteJSON () {
|
||||
return sander.readFile(boostnoteJSONPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
})
|
||||
.then(function verifyVersion (rawData) {
|
||||
var boostnoteJSONData = JSON.parse(rawData)
|
||||
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
|
||||
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
|
||||
|
||||
return boostnoteJSONData
|
||||
})
|
||||
.then(function setVersion (boostnoteJSONData) {
|
||||
boostnoteJSONData.version = '1.0'
|
||||
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
|
||||
.then(() => boostnoteJSONData)
|
||||
})
|
||||
.then(function fetchNotes (boostnoteJSONData) {
|
||||
var fetchNotesFromEachFolder = boostnoteJSONData.folders
|
||||
.map(function (folder) {
|
||||
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
|
||||
return sander
|
||||
.readFile(folderDataJSONPath, {
|
||||
encoding: 'utf-8'
|
||||
})
|
||||
.then(function (rawData) {
|
||||
var data = JSON.parse(rawData)
|
||||
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
|
||||
return data.notes
|
||||
.map(function setFolderToNote (note) {
|
||||
note.folder = folder.key
|
||||
return note
|
||||
})
|
||||
})
|
||||
.catch(function failedToReadDataJSON (err) {
|
||||
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
|
||||
return []
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.all(fetchNotesFromEachFolder)
|
||||
.then(function flatten (folderNotes) {
|
||||
return folderNotes
|
||||
.reduce(function concatNotes (sum, notes) {
|
||||
return sum.concat(notes)
|
||||
}, [])
|
||||
})
|
||||
.then(function saveNotes (notes) {
|
||||
notes.forEach(function renewKey (note) {
|
||||
var newKey = keygen()
|
||||
while (notes.some((_note) => _note.key === newKey)) {
|
||||
newKey = keygen()
|
||||
}
|
||||
note.key = newKey
|
||||
})
|
||||
|
||||
const noteDirPath = path.join(storagePath, 'notes')
|
||||
notes
|
||||
.map(function saveNote (note) {
|
||||
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
|
||||
})
|
||||
return true
|
||||
})
|
||||
.then(function deleteFolderDir (check) {
|
||||
if (check) {
|
||||
boostnoteJSONData.folders.forEach((folder) => {
|
||||
sander.rimrafSync(path.join(storagePath, folder.key))
|
||||
})
|
||||
}
|
||||
return check
|
||||
})
|
||||
})
|
||||
.catch(function handleError (err) {
|
||||
console.warn(err)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = migrateFromV5Storage
|
||||
|
||||
90
browser/main/lib/dataApi/moveNote.js
Normal file
90
browser/main/lib/dataApi/moveNote.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const sander = require('sander')
|
||||
|
||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
let oldStorage, newStorage
|
||||
try {
|
||||
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Storage doesn\'t exist.')
|
||||
|
||||
oldStorage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (oldStorage == null) throw new Error('Storage doesn\'t exist.')
|
||||
|
||||
newStorage = _.find(cachedStorageList, {key: newStorageKey})
|
||||
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(oldStorage)
|
||||
.then(function saveNote (_oldStorage) {
|
||||
oldStorage = _oldStorage
|
||||
let noteData
|
||||
let notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
|
||||
try {
|
||||
noteData = CSON.readFileSync(notePath)
|
||||
} catch (err) {
|
||||
console.warn('Failed to find note cson', err)
|
||||
throw err
|
||||
}
|
||||
let newNoteKey
|
||||
return Promise.resolve()
|
||||
.then(function resolveNewStorage () {
|
||||
if (storageKey === newStorageKey) {
|
||||
newNoteKey = noteKey
|
||||
return oldStorage
|
||||
}
|
||||
return resolveStorageData(newStorage)
|
||||
.then(function findNewNoteKey (_newStorage) {
|
||||
newStorage = _newStorage
|
||||
newNoteKey = keygen()
|
||||
let isUnique = false
|
||||
while (!isUnique) {
|
||||
try {
|
||||
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
|
||||
newNoteKey = keygen()
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
isUnique = true
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newStorage
|
||||
})
|
||||
})
|
||||
.then(function checkFolderExistsAndPrepareNoteData (newStorage) {
|
||||
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
|
||||
|
||||
noteData.folder = newFolderKey
|
||||
noteData.key = newNoteKey
|
||||
noteData.storage = newStorageKey
|
||||
noteData.updatedAt = new Date()
|
||||
|
||||
return noteData
|
||||
})
|
||||
.then(function writeAndReturn (noteData) {
|
||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||
return noteData
|
||||
})
|
||||
.then(function deleteOldNote (data) {
|
||||
if (storageKey !== newStorageKey) {
|
||||
try {
|
||||
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = moveNote
|
||||
30
browser/main/lib/dataApi/removeStorage.js
Normal file
30
browser/main/lib/dataApi/removeStorage.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @return {key}
|
||||
*/
|
||||
function removeStorage (key) {
|
||||
let rawStorages
|
||||
|
||||
try {
|
||||
rawStorages = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
rawStorages = []
|
||||
}
|
||||
|
||||
rawStorages = rawStorages
|
||||
.filter(function excludeTargetStorage (rawStorage) {
|
||||
return rawStorage.key !== key
|
||||
})
|
||||
|
||||
localStorage.setItem('storages', JSON.stringify(rawStorages))
|
||||
|
||||
return Promise.resolve({
|
||||
storageKey: key
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = removeStorage
|
||||
31
browser/main/lib/dataApi/renameStorage.js
Normal file
31
browser/main/lib/dataApi/renameStorage.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {String} name
|
||||
* @return {Object} Storage meta data
|
||||
*/
|
||||
function renameStorage (key, name) {
|
||||
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
|
||||
|
||||
let cachedStorageList
|
||||
try {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
let targetStorage = _.find(cachedStorageList, {key: key})
|
||||
if (targetStorage == null) return Promise.reject('Storage')
|
||||
|
||||
targetStorage.name = name
|
||||
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
|
||||
|
||||
targetStorage.path
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
}
|
||||
|
||||
module.exports = renameStorage
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user