mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 02:06:29 +00:00
Compare commits
311 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1a75a13e9 | ||
|
|
ee6f4de183 | ||
|
|
475885b3ef | ||
|
|
2d2b2d4c6c | ||
|
|
4d00454539 | ||
|
|
bf590b5614 | ||
|
|
3ef33c065c | ||
|
|
8e89fb8b92 | ||
|
|
8e81cfcf89 | ||
|
|
515736262d | ||
|
|
bd266dc602 | ||
|
|
9b3306157c | ||
|
|
f8e6a939ca | ||
|
|
8c48ee6fc1 | ||
|
|
5e476054d7 | ||
|
|
2fc8547384 | ||
|
|
2af2d71540 | ||
|
|
6aaf9d9eb2 | ||
|
|
42a9caf5a3 | ||
|
|
e6c1d7a383 | ||
|
|
02100bbc0a | ||
|
|
ce7c5f5d40 | ||
|
|
69f1ad6eb3 | ||
|
|
8320fb5024 | ||
|
|
4b79bca6bf | ||
|
|
4a5fd41249 | ||
|
|
4e90a93b30 | ||
|
|
9861fbf7c8 | ||
|
|
c762b9ae00 | ||
|
|
cc667a6edf | ||
|
|
419c57ed3f | ||
|
|
601f0b0de8 | ||
|
|
7b1c6c10b7 | ||
|
|
5a85c257cf | ||
|
|
beceb851c2 | ||
|
|
31485d3387 | ||
|
|
fc552e030a | ||
|
|
bf4c9f920a | ||
|
|
4ebd503664 | ||
|
|
0907bc80ef | ||
|
|
2cf46a3332 | ||
|
|
41868f28e6 | ||
|
|
964b7b62de | ||
|
|
0d34a03fe0 | ||
|
|
a62faa471c | ||
|
|
66f3ce2cb2 | ||
|
|
e313b5e59d | ||
|
|
bb26d9a0a8 | ||
|
|
0ca41fbdb4 | ||
|
|
2cfb883bad | ||
|
|
ec8c8bb669 | ||
|
|
0b54f01107 | ||
|
|
67be198bee | ||
|
|
32a4a1aae1 | ||
|
|
dac7372839 | ||
|
|
521c261a37 | ||
|
|
27367488c2 | ||
|
|
0d5c3b1be6 | ||
|
|
a67d5ffacb | ||
|
|
687440a7c7 | ||
|
|
bafdc24a6d | ||
|
|
047f9c93c5 | ||
|
|
116fafc117 | ||
|
|
060c92091c | ||
|
|
5802525b73 | ||
|
|
c3580caabc | ||
|
|
08c027acc5 | ||
|
|
4468792346 | ||
|
|
1b16c68cf9 | ||
|
|
b99c1e3b32 | ||
|
|
eb2994e3c2 | ||
|
|
d88dd26186 | ||
|
|
59fcc58e9c | ||
|
|
acba61f36a | ||
|
|
a3a55a8bb4 | ||
|
|
22929d84fc | ||
|
|
9ea9d30947 | ||
|
|
7f08428fe2 | ||
|
|
0d80a7d961 | ||
|
|
2899264b54 | ||
|
|
923de0aa0d | ||
|
|
2b729dad15 | ||
|
|
b9b5bae78a | ||
|
|
a9acde07d1 | ||
|
|
a46b8d3079 | ||
|
|
8b92e2cbb7 | ||
|
|
881f5a5110 | ||
|
|
4e986a6384 | ||
|
|
ce5e1babb7 | ||
|
|
b85790d2fa | ||
|
|
6bc3e7fcf1 | ||
|
|
1ca968201d | ||
|
|
f2a03e4cc7 | ||
|
|
a752730718 | ||
|
|
9d20fd91ec | ||
|
|
70a6a3acb8 | ||
|
|
7f52eed4d5 | ||
|
|
105119e1a4 | ||
|
|
169e30e029 | ||
|
|
a5fa3e9e7a | ||
|
|
8985062d34 | ||
|
|
56eb9c76ae | ||
|
|
e5b6762bf3 | ||
|
|
e8bccaef88 | ||
|
|
afdb038244 | ||
|
|
56942d55eb | ||
|
|
9d742c8435 | ||
|
|
6ee4e48de2 | ||
|
|
184f3dc04b | ||
|
|
2027f60014 | ||
|
|
076edd375f | ||
|
|
ab1aa56059 | ||
|
|
46f7dfdfeb | ||
|
|
fcaa5e21cf | ||
|
|
1e202db50f | ||
|
|
9405b95825 | ||
|
|
f05e256afc | ||
|
|
731ffd4a22 | ||
|
|
8f4566b7e1 | ||
|
|
95aec54f60 | ||
|
|
f14ce0d68e | ||
|
|
cc1c7f3820 | ||
|
|
2df901288a | ||
|
|
821a7c780e | ||
|
|
6e2e48fa64 | ||
|
|
2864ac88f5 | ||
|
|
4a292d6518 | ||
|
|
e934182e86 | ||
|
|
d8fa73287b | ||
|
|
35938c09e8 | ||
|
|
9eaa90c691 | ||
|
|
049835d426 | ||
|
|
af91c40406 | ||
|
|
4940ad6825 | ||
|
|
d02b740300 | ||
|
|
9cb443dc2f | ||
|
|
1c7cba2951 | ||
|
|
473b80710d | ||
|
|
2247c0835d | ||
|
|
b7b715ba3d | ||
|
|
6c43fb2325 | ||
|
|
a6fe3c27d4 | ||
|
|
d47ff96b13 | ||
|
|
a0def654bd | ||
|
|
4873b40e49 | ||
|
|
0a758f20a7 | ||
|
|
5e58d457a3 | ||
|
|
0f745361ad | ||
|
|
bf6cae9a0e | ||
|
|
ab640a7676 | ||
|
|
820171e19e | ||
|
|
f1e9d0ab81 | ||
|
|
0646484c83 | ||
|
|
a27b79c213 | ||
|
|
773a9b4b7f | ||
|
|
07b838ef7b | ||
|
|
85217a7171 | ||
|
|
886d7b7227 | ||
|
|
6987b762dd | ||
|
|
f32ac81f84 | ||
|
|
87ea66bb92 | ||
|
|
ff6fd62932 | ||
|
|
76728448ff | ||
|
|
3b7225e0fa | ||
|
|
d6280f4397 | ||
|
|
8df867046f | ||
|
|
331c822816 | ||
|
|
6219173945 | ||
|
|
6207e02e7f | ||
|
|
537ba537dc | ||
|
|
3e919241e6 | ||
|
|
2324327e7e | ||
|
|
b8374494ea | ||
|
|
a480ca7b55 | ||
|
|
bfd67fb7f1 | ||
|
|
e6bd6a5077 | ||
|
|
350af844ca | ||
|
|
a49d53179a | ||
|
|
e68069fd2f | ||
|
|
a193ba3e6c | ||
|
|
9034b3ab54 | ||
|
|
f39b7594ab | ||
|
|
f79734391e | ||
|
|
e54f516418 | ||
|
|
e86464535d | ||
|
|
e8553caa65 | ||
|
|
044e6b7180 | ||
|
|
0266770657 | ||
|
|
2d6f7c08e8 | ||
|
|
0a27819a7f | ||
|
|
3185c25ee1 | ||
|
|
820802cdc2 | ||
|
|
2c169e6f15 | ||
|
|
49be1320f9 | ||
|
|
6bc4ecce48 | ||
|
|
0290d23832 | ||
|
|
e001c97e01 | ||
|
|
976d1bb4f3 | ||
|
|
dee6495b08 | ||
|
|
4a77f250f1 | ||
|
|
c697f19642 | ||
|
|
f7d1f9e949 | ||
|
|
27b9952f8e | ||
|
|
961dab4a97 | ||
|
|
930b6bc927 | ||
|
|
ed7d8258cf | ||
|
|
762425125f | ||
|
|
03bdf0c653 | ||
|
|
7002026190 | ||
|
|
b35395e6bd | ||
|
|
b74fb76d03 | ||
|
|
72d2df465b | ||
|
|
2592c943f7 | ||
|
|
7b3a3ba200 | ||
|
|
8369832585 | ||
|
|
d94a674343 | ||
|
|
260611ffd6 | ||
|
|
690549b57f | ||
|
|
5d9aeb4c04 | ||
|
|
eddfdea2ca | ||
|
|
a40385f87f | ||
|
|
0123526c98 | ||
|
|
275e3317a3 | ||
|
|
d0b835a825 | ||
|
|
f450260ff8 | ||
|
|
5f7b119e5c | ||
|
|
bb9eb494e9 | ||
|
|
6ea2b5e1d9 | ||
|
|
b21baf1ce5 | ||
|
|
4c3fd461e4 | ||
|
|
8365a60d5d | ||
|
|
804ac1aa96 | ||
|
|
49fd2cfc4d | ||
|
|
2bb2a52f27 | ||
|
|
8d71b28afa | ||
|
|
86eac7054d | ||
|
|
dccb92d72b | ||
|
|
2e628de9c6 | ||
|
|
2d243abc12 | ||
|
|
ae08bf4d7a | ||
|
|
cbff5fb585 | ||
|
|
2650cc2f1c | ||
|
|
ec560ceab1 | ||
|
|
ad17cb8837 | ||
|
|
4f98d9641a | ||
|
|
17535ccd4c | ||
|
|
110c1ea337 | ||
|
|
7e087bfbab | ||
|
|
85e0d4b922 | ||
|
|
265262ccbf | ||
|
|
345008e9b6 | ||
|
|
849aa05d76 | ||
|
|
5ff5ec6a71 | ||
|
|
684c3f64aa | ||
|
|
4ff73ede59 | ||
|
|
7302d83f60 | ||
|
|
f9836fd66e | ||
|
|
c0f05695fe | ||
|
|
e50a1172d6 | ||
|
|
7a4234e73c | ||
|
|
4b9640e5a3 | ||
|
|
4b58054100 | ||
|
|
8951dbf4ed | ||
|
|
5047bc94eb | ||
|
|
75fb29c594 | ||
|
|
bd75f7fd2c | ||
|
|
8b8cb3c9b4 | ||
|
|
b479b21c37 | ||
|
|
525c490704 | ||
|
|
2e3599d005 | ||
|
|
f005bb1e46 | ||
|
|
21f96febdb | ||
|
|
805829be78 | ||
|
|
a7bd3f253f | ||
|
|
81e8a290f0 | ||
|
|
f46a967b6e | ||
|
|
b78d9534aa | ||
|
|
bdefaf7427 | ||
|
|
43550c7dec | ||
|
|
1b07d393f2 | ||
|
|
44fc356775 | ||
|
|
5623c68170 | ||
|
|
d5f82943cf | ||
|
|
96a020341d | ||
|
|
99b5f75763 | ||
|
|
a128ff7cd8 | ||
|
|
59329cc5da | ||
|
|
ecd5daaf55 | ||
|
|
514232d720 | ||
|
|
93abfe3202 | ||
|
|
4d9e0e3bd7 | ||
|
|
75d9bf8e38 | ||
|
|
ccb9752254 | ||
|
|
0f2ca19c6c | ||
|
|
d5a77aade4 | ||
|
|
3c89e6a128 | ||
|
|
a36be79977 | ||
|
|
617f0e0f9c | ||
|
|
0db1a94105 | ||
|
|
f29f97a00f | ||
|
|
032b5a59f2 | ||
|
|
1e5bc395ac | ||
|
|
fef4afe660 | ||
|
|
679ed1eacf | ||
|
|
78a9320017 | ||
|
|
927981bf30 | ||
|
|
70ea2f6c14 | ||
|
|
a6779414ff | ||
|
|
b62fcf523a | ||
|
|
d0bbea5a97 | ||
|
|
212d7027fc |
33
.boostnoterc.sample
Normal file
33
.boostnoterc.sample
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"amaEnabled": true,
|
||||
"editor": {
|
||||
"fontFamily": "Monaco, Consolas",
|
||||
"fontSize": "14",
|
||||
"indentSize": "2",
|
||||
"indentType": "space",
|
||||
"keyMap": "vim",
|
||||
"switchPreview": "BLUR",
|
||||
"theme": "monokai"
|
||||
},
|
||||
"hotkey": {
|
||||
"toggleFinder": "Cmd + Alt + S",
|
||||
"toggleMain": "Cmd + Alt + L"
|
||||
},
|
||||
"isSideNavFolded": false,
|
||||
"listStyle": "DEFAULT",
|
||||
"listWidth": 174,
|
||||
"navWidth": 200,
|
||||
"preview": {
|
||||
"codeBlockTheme": "dracula",
|
||||
"fontFamily": "Lato",
|
||||
"fontSize": "14",
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
"disableDirectWrite": false,
|
||||
"theme": "default"
|
||||
},
|
||||
"zoom": 1
|
||||
}
|
||||
10
.eslintrc
10
.eslintrc
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"extends": ["standard", "standard-jsx"],
|
||||
"extends": ["standard", "standard-jsx", "plugin:react/recommended"],
|
||||
"plugins": ["react"],
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"prefer-const": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-lone-blocks": "warn"
|
||||
"no-lone-blocks": "warn",
|
||||
"react/prop-types": 0,
|
||||
"react/no-string-refs": 0,
|
||||
"react/no-find-dom-node": "warn",
|
||||
"react/no-render-return-value": "warn",
|
||||
"react/no-deprecated": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,26 +1,6 @@
|
||||
language: node_js
|
||||
# To fix the npm version in 4.x
|
||||
# refs: https://github.com/travis-ci/travis-ci/issues/4653#issuecomment-194051953
|
||||
before_install: 'if [[ `npm -v` != 4* ]]; then npm i -g npm@4; fi'
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
install:
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then yarn; fi'
|
||||
node_js:
|
||||
- 'stable'
|
||||
- 'lts/*'
|
||||
script:
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" && ${TRAVIS_NODE_VERSION} = "stable" ]]; then
|
||||
npm run lint;
|
||||
npm run test;
|
||||
./script/e2e-runner.sh;
|
||||
fi'
|
||||
- os: linux
|
||||
node_js:
|
||||
- 'stable'
|
||||
- 'lts/*'
|
||||
script:
|
||||
- 'npm run lint'
|
||||
- 'npm run test'
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" && ${TRAVIS_NODE_VERSION} = "stable" ]]; then ./script/e2e-runner.sh; fi'
|
||||
node_js:
|
||||
- 'stable'
|
||||
- 'lts/*'
|
||||
|
||||
script: npm run lint && npm run test
|
||||
|
||||
@@ -3,3 +3,7 @@ You can support Boostnote from $ 5 a month!
|
||||
|
||||
# Backers
|
||||
[Kazu Yokomizo](https://twitter.com/kazup_bot)
|
||||
|
||||
kolchan11
|
||||
|
||||
RonWalker22
|
||||
|
||||
1
ISSUE_TEMPLATE.md
Normal file
1
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1 @@
|
||||
Please paste some **screenshots** with the **developer tool** open if you report a bug.
|
||||
@@ -3,6 +3,7 @@ import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import path from 'path'
|
||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
@@ -39,6 +40,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
}
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
@@ -98,14 +100,25 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
this.editor.on('blur', this.blurHandler)
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.on('paste', this.pasteHandler)
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||
|
||||
CodeMirror.Vim.defineEx('quit', 'q', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('q!', 'q!', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
|
||||
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
||||
}
|
||||
|
||||
quitEditor () {
|
||||
document.querySelector('textarea').blur()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.editor.off('blur', this.blurHandler)
|
||||
this.editor.off('change', this.changeHandler)
|
||||
this.editor.off('paste', this.pasteHandler)
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||
}
|
||||
@@ -192,15 +205,39 @@ export default class CodeEditor extends React.Component {
|
||||
const imagePath = e.dataTransfer.files[0].path
|
||||
const filename = path.basename(imagePath)
|
||||
|
||||
copyImage(imagePath, this.props.storageKey).then((imagePathInTheStorage) => {
|
||||
const imageMd = ``
|
||||
copyImage(imagePath, this.props.storageKey).then((imagePath) => {
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
})
|
||||
}
|
||||
|
||||
insertImageMd (imageMd) {
|
||||
const textarea = this.editor.getInputField()
|
||||
const cm = this.editor
|
||||
cm.setValue(cm.getValue() + imageMd)
|
||||
cm.replaceSelection(`${textarea.value.substr(0, textarea.selectionStart)}${imageMd}${textarea.value.substr(textarea.selectionEnd)}`)
|
||||
}
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const dataTransferItem = e.clipboardData.items[0]
|
||||
if (!dataTransferItem.type.match('image')) return
|
||||
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
let reader = new FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imagePath = path.join(`${storagePath}`, 'images', `${imageName}.png`)
|
||||
|
||||
require('fs').writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -4,6 +4,8 @@ import styles from './MarkdownEditor.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
const _ = require('lodash')
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -79,7 +81,6 @@ class MarkdownEditor extends React.Component {
|
||||
if (newStatus === 'CODE') {
|
||||
this.refs.code.focus()
|
||||
} else {
|
||||
this.refs.code.blur()
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
@@ -162,15 +163,18 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
let { config } = this.props
|
||||
if (this.state.status !== 'CODE') return false
|
||||
const keyPressed = this.state.keyPressed
|
||||
keyPressed.add(e.keyCode)
|
||||
this.setState({ keyPressed })
|
||||
let isNoteHandlerKey = (el) => { return keyPressed.has(el) }
|
||||
// These conditions are for ctrl-e and ctrl-w
|
||||
if (keyPressed.size === this.escapeFromEditor.length &&
|
||||
!this.state.isLocked && this.state.status === 'CODE' &&
|
||||
this.escapeFromEditor.every(isNoteHandlerKey)) {
|
||||
document.activeElement.blur()
|
||||
this.handleContextMenu()
|
||||
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
|
||||
}
|
||||
if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) {
|
||||
this.addMdAroundWord('**')
|
||||
@@ -213,6 +217,8 @@ class MarkdownEditor extends React.Component {
|
||||
let previewStyle = {}
|
||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||
|
||||
const storage = findStorage(storageKey)
|
||||
|
||||
return (
|
||||
<div className={className == null
|
||||
? 'MarkdownEditor'
|
||||
@@ -260,6 +266,7 @@ class MarkdownEditor extends React.Component {
|
||||
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
|
||||
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -51,12 +51,12 @@ code {
|
||||
color: rgba(147,147,149,0.8);;
|
||||
fill: rgba(147,147,149,1);;
|
||||
border-radius: 50%;
|
||||
margin: 7px;
|
||||
margin: 0px 10px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.printHandler = () => this.handlePrint()
|
||||
|
||||
this.linkClickHandler = this.handlelinkClick.bind(this)
|
||||
}
|
||||
|
||||
handlePreviewAnchorClick (e) {
|
||||
@@ -160,6 +163,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.exportAsDocument('md')
|
||||
}
|
||||
|
||||
handlePrint () {
|
||||
this.refs.root.contentWindow.print()
|
||||
}
|
||||
|
||||
exportAsDocument (fileType) {
|
||||
const options = {
|
||||
filters: [
|
||||
@@ -196,6 +203,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('print', this.printHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@@ -206,6 +214,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('print', this.printHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
@@ -249,7 +258,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
let { value, theme, indentSize, codeBlockTheme } = this.props
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
let { value, theme, indentSize, codeBlockTheme, storagePath } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
|
||||
@@ -273,6 +286,16 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||
el.src = markdown.normalizeLinkText(el.src)
|
||||
if (!/\/:storage/.test(el.src)) return
|
||||
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
@@ -373,6 +396,14 @@ export default class MarkdownPreview extends React.Component {
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handlelinkClick (e) {
|
||||
const noteHash = e.target.href.split('/').pop()
|
||||
const regexIsNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsNoteLink.test(noteHash)) {
|
||||
eventEmitter.emit('list:jump', noteHash)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, style, tabIndex } = this.props
|
||||
return (
|
||||
@@ -394,5 +425,6 @@ MarkdownPreview.propTypes = {
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
value: PropTypes.string,
|
||||
storagePath: PropTypes.string
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const ModalEscButton = ({
|
||||
handleEscButtonClick
|
||||
}) => (
|
||||
<button styleName='escButton' onClick={handleEscButtonClick}>
|
||||
<div styleName='esc-mark'>x</div>
|
||||
<div styleName='esc-mark'>×</div>
|
||||
<div styleName='esc-text'>esc</div>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -11,4 +11,6 @@
|
||||
height top-bar-height
|
||||
|
||||
.esc-mark
|
||||
font-size 15px
|
||||
font-size 28px
|
||||
margin-top -5px
|
||||
margin-bottom -7px
|
||||
@@ -4,7 +4,9 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { isArray } from 'lodash'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import { getTodoStatus } from 'browser/lib/getTodoStatus'
|
||||
import styles from './NoteItem.styl'
|
||||
import TodoProcess from './TodoProcess'
|
||||
|
||||
/**
|
||||
* @description Tag element component.
|
||||
@@ -39,18 +41,16 @@ const TagElementList = (tags) => {
|
||||
* @param {boolean} isActive
|
||||
* @param {Object} note
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
* @param {string} dateDisplay
|
||||
*/
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
|
||||
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleDragStart }) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -70,6 +70,10 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
|
||||
{note.isStarred
|
||||
? <i styleName='item-star' className='fa fa-star' /> : ''
|
||||
}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
@@ -91,10 +95,10 @@ NoteItem.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isrequired,
|
||||
tags: PropTypes.array,
|
||||
isStarred: PropTypes.bool.isRequired
|
||||
isStarred: PropTypes.bool.isRequired,
|
||||
isTrashed: PropTypes.bool.isRequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
handleDragStart: PropTypes.func.isRequired,
|
||||
handleDragEnd: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ $control-height = 30px
|
||||
.item-wrapper
|
||||
padding 15px 0
|
||||
border-bottom $ui-border
|
||||
position relative
|
||||
|
||||
.item--active
|
||||
@extend .item
|
||||
@@ -66,7 +67,7 @@ $control-height = 30px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title
|
||||
font-size 13px
|
||||
font-size 14px
|
||||
position relative
|
||||
top -12px
|
||||
left 20px
|
||||
@@ -92,11 +93,12 @@ $control-height = 30px
|
||||
flex 1
|
||||
overflow ellipsis
|
||||
line-height 20px
|
||||
padding-top 2px
|
||||
padding-top 7px
|
||||
padding-left 2px
|
||||
margin-right 27px
|
||||
|
||||
.item-bottom-tagList-item
|
||||
font-size 10px
|
||||
font-size 11px
|
||||
margin-right 8px
|
||||
padding 0
|
||||
height 20px
|
||||
@@ -109,14 +111,14 @@ $control-height = 30px
|
||||
|
||||
.item-bottom-time
|
||||
color $ui-inactive-text-color
|
||||
font-size 10px
|
||||
font-size 11px
|
||||
padding-left 2px
|
||||
padding-bottom 2px
|
||||
|
||||
.item-star
|
||||
position absolute
|
||||
top 34px
|
||||
left 5px
|
||||
right -20px
|
||||
bottom 2px
|
||||
width 34px
|
||||
height 34px
|
||||
color alpha($ui-favorite-star-button-color, 60%)
|
||||
|
||||
@@ -10,17 +10,15 @@ import styles from './NoteItemSimple.styl'
|
||||
* @param {boolean} isActive
|
||||
* @param {Object} note
|
||||
* @param {Function} handleNoteClick
|
||||
* @param {Function} handleNoteContextMenu
|
||||
* @param {Function} handleDragStart
|
||||
*/
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenum, handleDragStart }) => (
|
||||
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleDragStart }) => (
|
||||
<div styleName={isActive
|
||||
? 'item-simple--active'
|
||||
: 'item-simple'
|
||||
}
|
||||
key={`${note.storage}-${note.key}`}
|
||||
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
|
||||
onDragStart={e => handleDragStart(e, note)}
|
||||
draggable='true'
|
||||
>
|
||||
@@ -46,7 +44,6 @@ NoteItemSimple.propTypes = {
|
||||
title: PropTypes.string.isrequired
|
||||
}),
|
||||
handleNoteClick: PropTypes.func.isRequired,
|
||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||
handleDragStart: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ $control-height = 30px
|
||||
|
||||
.item-simple
|
||||
position relative
|
||||
padding 0 25px
|
||||
padding 0 20px
|
||||
user-select none
|
||||
cursor pointer
|
||||
background-color $ui-noteList-backgroundColor
|
||||
@@ -40,7 +40,7 @@ $control-height = 30px
|
||||
background-color $ui-button--active-backgroundColor
|
||||
|
||||
.item-simple-title
|
||||
font-size 12px
|
||||
font-size 13px
|
||||
height 40px
|
||||
box-sizing border-box
|
||||
line-height 24px
|
||||
@@ -48,6 +48,7 @@ $control-height = 30px
|
||||
overflow ellipsis
|
||||
color $ui-inactive-text-color
|
||||
border-bottom $ui-border
|
||||
position relative
|
||||
|
||||
.item-simple-title-icon
|
||||
font-size 12px
|
||||
|
||||
55
browser/components/RealtimeNotification.js
Normal file
55
browser/components/RealtimeNotification.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RealtimeNotification.styl'
|
||||
|
||||
const electron = require('electron')
|
||||
const { shell } = electron
|
||||
|
||||
class RealtimeNotification extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
notifications: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchNotifications()
|
||||
}
|
||||
|
||||
fetchNotifications () {
|
||||
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
|
||||
fetch(notificationsUrl)
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
.then(json => {
|
||||
this.setState({notifications: json.notifications})
|
||||
})
|
||||
}
|
||||
|
||||
handleLinkClick (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { notifications } = this.state
|
||||
const link = notifications.length > 0
|
||||
? <a styleName='notification-link' href={notifications[0].linkUrl}
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>
|
||||
{notifications[0].text}
|
||||
</a>
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div styleName='notification-area'>{link}</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RealtimeNotification.propTypes = {}
|
||||
|
||||
export default CSSModules(RealtimeNotification, styles)
|
||||
35
browser/components/RealtimeNotification.styl
Normal file
35
browser/components/RealtimeNotification.styl
Normal file
@@ -0,0 +1,35 @@
|
||||
.notification-area
|
||||
z-index 1000
|
||||
font-size 12px
|
||||
position absolute
|
||||
bottom 0px
|
||||
background-color #EBEBEB
|
||||
width 100vw
|
||||
height 30px
|
||||
text-align center
|
||||
|
||||
.notification-link
|
||||
text-decoration none
|
||||
color #282A36
|
||||
border 1px solid #6FA8E6
|
||||
background-color alpha(#6FA8E6, 0.2)
|
||||
padding 3px 9px
|
||||
border-radius 2px
|
||||
position absolute
|
||||
bottom 4px
|
||||
margin-left -10%
|
||||
transition 0.2s
|
||||
&:hover
|
||||
color #1378BD
|
||||
|
||||
body[data-theme="dark"]
|
||||
.notification-area
|
||||
background-color #1E2124
|
||||
|
||||
.notification-link
|
||||
color #fff
|
||||
border 1px solid alpha(#5CB85C, 0.6)
|
||||
background-color alpha(#5CB85C, 0.2)
|
||||
transition 0.2s
|
||||
&:hover
|
||||
color #5CB85C
|
||||
@@ -15,7 +15,7 @@ import styles from './SideNavFilter.styl'
|
||||
*/
|
||||
const SideNavFilter = ({
|
||||
isFolded, isHomeActive, handleAllNotesButtonClick,
|
||||
isStarredActive, handleStarredButtonClick
|
||||
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick
|
||||
}) => (
|
||||
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
|
||||
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
|
||||
@@ -30,6 +30,12 @@ const SideNavFilter = ({
|
||||
<i className='fa fa-star fa-fw' />
|
||||
<span styleName='menu-button-label'>Starred</span>
|
||||
</button>
|
||||
<button styleName={isTrashedActive ? 'menu-button--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
>
|
||||
<i className='fa fa-trash fa-fw' />
|
||||
<span styleName='menu-button-label'>Trash</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -38,7 +44,9 @@ SideNavFilter.propTypes = {
|
||||
isHomeActive: PropTypes.bool.isRequired,
|
||||
handleAllNotesButtonClick: PropTypes.func.isRequired,
|
||||
isStarredActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired
|
||||
isTrashedActive: PropTypes.bool.isRequired,
|
||||
handleStarredButtonClick: PropTypes.func.isRequired,
|
||||
handleTrashdButtonClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(SideNavFilter, styles)
|
||||
|
||||
33
browser/components/TodoProcess.js
Normal file
33
browser/components/TodoProcess.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @fileoverview Percentage of todo achievement.
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TodoProcess.styl'
|
||||
|
||||
const TodoProcess = ({
|
||||
todoStatus: {
|
||||
total: totalTodo,
|
||||
completed: completedTodo
|
||||
}
|
||||
}) => (
|
||||
<div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}>
|
||||
<div styleName='todo-process-text'>
|
||||
<i className='fa fa-fw fa-check-square-o' />
|
||||
{completedTodo} of {totalTodo}
|
||||
</div>
|
||||
<div styleName='todo-process-bar'>
|
||||
<div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoProcess.propTypes = {
|
||||
todoStatus: {
|
||||
total: PropTypes.number.isRequired,
|
||||
completed: PropTypes.number.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSModules(TodoProcess, styles)
|
||||
45
browser/components/TodoProcess.styl
Normal file
45
browser/components/TodoProcess.styl
Normal file
@@ -0,0 +1,45 @@
|
||||
.todo-process
|
||||
font-size 12px
|
||||
display flex
|
||||
padding-top 15px
|
||||
width 85%
|
||||
|
||||
.todo-process-text
|
||||
display inline-block
|
||||
padding-right 10px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
color $ui-inactive-text-color
|
||||
i
|
||||
color $ui-inactive-text-color
|
||||
padding-right 5px
|
||||
|
||||
.todo-process-bar
|
||||
display inline-block
|
||||
margin auto
|
||||
height 4px
|
||||
border-radius 10px
|
||||
background-color #DADFE1
|
||||
border-radius 2px
|
||||
flex-grow 1
|
||||
border 1px solid alpha(#6C7A89, 10%)
|
||||
|
||||
.todo-process-bar--inner
|
||||
height 100%
|
||||
border-radius 5px
|
||||
background-color #6C7A89
|
||||
transition 0.3s
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.todo-process
|
||||
color $ui-dark-text-color
|
||||
|
||||
.todo-process-bar
|
||||
background-color #363A3D
|
||||
|
||||
.todo-process-text
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.todo-process-bar--inner
|
||||
background-color: alpha(#939395, 50%)
|
||||
@@ -64,7 +64,7 @@ $list-width = 250px
|
||||
|
||||
.result-nav-storageList
|
||||
absolute bottom left right
|
||||
top 80px + 32px + 10px + 10px
|
||||
top 110px + 32px + 10px + 10px
|
||||
overflow-y auto
|
||||
|
||||
.result-list
|
||||
|
||||
@@ -5,6 +5,7 @@ import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard } = electron
|
||||
@@ -106,6 +107,8 @@ class NoteDetail extends React.Component {
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
|
||||
const storage = findStorage(note.storage)
|
||||
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
let tabList = note.snippets.map((snippet, index) => {
|
||||
let isActive = this.state.snippetIndex === index
|
||||
@@ -143,6 +146,7 @@ class NoteDetail extends React.Component {
|
||||
config={config}
|
||||
value={snippet.content}
|
||||
ref={'code-' + index}
|
||||
storageKey={note.storage}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
@@ -192,6 +196,7 @@ class NoteDetail extends React.Component {
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
value={note.content}
|
||||
storagePath={storage.path}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.root
|
||||
absolute top bottom left right
|
||||
bottom 30px
|
||||
left $note-detail-left-margin
|
||||
right $note-detail-right-margin
|
||||
height 100%
|
||||
|
||||
@@ -70,7 +70,6 @@ class NoteList extends React.Component {
|
||||
dateDisplay={dateDisplay}
|
||||
key={key}
|
||||
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
|
||||
handleNoteContextMenu={() => ''}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -95,7 +95,7 @@ class FinderMain extends React.Component {
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.refs.detail.saveToClipboard()
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('COPY_FINDER')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
|
||||
hideFinder()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
21
browser/lib/RcParser.js
Normal file
21
browser/lib/RcParser.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path'
|
||||
import sander from 'sander'
|
||||
|
||||
const BOOSTNOTERC = '.boostnoterc'
|
||||
const homePath = global.process.env.HOME || global.process.env.USERPROFILE
|
||||
const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
|
||||
|
||||
export function parse (boostnotercPath = _boostnotercPath) {
|
||||
if (!sander.existsSync(boostnotercPath)) return {}
|
||||
try {
|
||||
return JSON.parse(sander.readFileSync(boostnotercPath).toString())
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
console.warn('Your .boostnoterc is broken so it\'s not used.')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parse
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import moment from 'moment'
|
||||
* @param {mixed}
|
||||
* @return {string}
|
||||
*/
|
||||
export function getLastUpdated (date) {
|
||||
export function formatDate (date) {
|
||||
const m = moment(date)
|
||||
if (!m.isValid()) {
|
||||
throw Error('Invalid argument.')
|
||||
|
||||
14
browser/lib/findStorage.js
Normal file
14
browser/lib/findStorage.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const _ = require('lodash')
|
||||
|
||||
export function findStorage (storageKey) {
|
||||
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
const storage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (storage === undefined) throw new Error('Target storage doesn\'t exist.')
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
export default {
|
||||
findStorage
|
||||
}
|
||||
25
browser/lib/getTodoStatus.js
Normal file
25
browser/lib/getTodoStatus.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export function getTodoStatus (content) {
|
||||
let splitted = content.split('\n')
|
||||
let numberOfTodo = 0
|
||||
let numberOfCompletedTodo = 0
|
||||
|
||||
splitted.forEach((line) => {
|
||||
let trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: numberOfTodo,
|
||||
completed: numberOfCompletedTodo
|
||||
}
|
||||
}
|
||||
|
||||
export function getTodoPercentageOfCompleted (content) {
|
||||
const state = getTodoStatus(content)
|
||||
return Math.floor(state.completed / state.total * 100)
|
||||
}
|
||||
@@ -59,6 +59,15 @@ md.use(math, {
|
||||
})
|
||||
md.use(require('markdown-it-imsize'))
|
||||
md.use(require('markdown-it-footnote'))
|
||||
md.use(require('markdown-it-multimd-table'))
|
||||
md.use(require('markdown-it-named-headers'), {
|
||||
slugify: (header) => {
|
||||
return encodeURI(header.trim()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
|
||||
.replace(/\s+/g, '-'))
|
||||
.replace(/\-+$/, '')
|
||||
}
|
||||
})
|
||||
// Override task item
|
||||
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
@@ -156,11 +165,17 @@ function strip (input) {
|
||||
return output
|
||||
}
|
||||
|
||||
function normalizeLinkText (linkText) {
|
||||
return md.normalizeLinkText(linkText)
|
||||
}
|
||||
|
||||
const markdown = {
|
||||
render: function markdown (content) {
|
||||
if (!_.isString(content)) content = ''
|
||||
return md.render(content)
|
||||
const renderedContent = md.render(content)
|
||||
return renderedContent
|
||||
},
|
||||
strip
|
||||
strip,
|
||||
normalizeLinkText
|
||||
}
|
||||
export default markdown
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export default function searchFromNotes (data, search) {
|
||||
let notes = data.noteMap.map((note) => note)
|
||||
export default function searchFromNotes (notes, search) {
|
||||
if (search.trim().length === 0) return []
|
||||
let searchBlocks = search.split(' ')
|
||||
searchBlocks.forEach((block) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.root
|
||||
position relative
|
||||
border solid 1px transparent
|
||||
line-height 34px
|
||||
line-height 26px
|
||||
vertical-align middle
|
||||
border-radius 2px
|
||||
transition 0.15s
|
||||
|
||||
19
browser/main/Detail/InfoButton.js
Normal file
19
browser/main/Detail/InfoButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoButton.styl'
|
||||
|
||||
const InfoButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-infoButton'
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className='fa fa-info infoButton' styleName='info-button' />
|
||||
</button>
|
||||
)
|
||||
|
||||
InfoButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoButton, styles)
|
||||
21
browser/main/Detail/InfoButton.styl
Normal file
21
browser/main/Detail/InfoButton.styl
Normal file
@@ -0,0 +1,21 @@
|
||||
.control-infoButton
|
||||
float right
|
||||
topBarButtonLight()
|
||||
|
||||
.control-infoPanel
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
z-index 200
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.infoButton
|
||||
padding 0px
|
||||
margin 15px 0
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-infoButton
|
||||
topBarButtonDark()
|
||||
103
browser/main/Detail/InfoPanel.js
Normal file
103
browser/main/Detail/InfoPanel.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
|
||||
const InfoPanel = ({
|
||||
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Storage
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{storageName}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Folder
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{folderName}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Created
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Updated
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{updatedAt}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Note Link
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input value={noteLink} onClick={(e) => { e.target.select() }} />
|
||||
</div>
|
||||
</div>
|
||||
{type === 'SNIPPET_NOTE'
|
||||
? ''
|
||||
: <div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Words
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{wordCount}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Letters
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{letterCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--unable'>
|
||||
<i className='fa fa-file-pdf-o fa-fw' />
|
||||
<p>.pdf</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
InfoPanel.propTypes = {
|
||||
storageName: PropTypes.string.isRequired,
|
||||
folderName: PropTypes.string.isRequired,
|
||||
noteLink: PropTypes.string.isRequired,
|
||||
updatedAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired,
|
||||
wordCount: PropTypes.number,
|
||||
letterCount: PropTypes.number,
|
||||
type: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanel, styles)
|
||||
124
browser/main/Detail/InfoPanel.styl
Normal file
124
browser/main/Detail/InfoPanel.styl
Normal file
@@ -0,0 +1,124 @@
|
||||
.control-infoPanel
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
z-index 200
|
||||
line-height normal
|
||||
border-radius 4px
|
||||
opacity 0
|
||||
transition 0.2s
|
||||
|
||||
.control-infoButton-panel
|
||||
z-index 200
|
||||
margin-top 45px
|
||||
margin-left -210px
|
||||
position absolute
|
||||
padding 20px 20px 0 20px
|
||||
width 340px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border 1px solid $border-color
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 45px
|
||||
margin-left -230px
|
||||
position absolute
|
||||
padding 20px 20px 0 20px
|
||||
width 320px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border 1px solid $border-color
|
||||
|
||||
.group-section
|
||||
display flex
|
||||
line-height 30px
|
||||
font-size 13px
|
||||
|
||||
.group-section-label
|
||||
width 70px
|
||||
height 20px
|
||||
text-align left
|
||||
margin-right 50px
|
||||
font-size 13px
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.group-section-control
|
||||
flex 1
|
||||
font-size 13px
|
||||
color $ui-text-color
|
||||
|
||||
.group-section-control input
|
||||
width 160px
|
||||
height 25px
|
||||
|
||||
.group-section-control text
|
||||
color #EA4447
|
||||
font-weight 600
|
||||
font-size 14px
|
||||
width 70px
|
||||
height 25px
|
||||
background-color rgba(226,33,113,0.1)
|
||||
border none
|
||||
outline none
|
||||
border-radius 2px
|
||||
margin-right 5px
|
||||
padding 2px 5px
|
||||
|
||||
[id=export-wrap]
|
||||
height 90px
|
||||
display flex
|
||||
justify-content center
|
||||
margin 20px 0 10px 0
|
||||
button
|
||||
outline none
|
||||
font-size 48px
|
||||
color #A0A0A0
|
||||
background-color transparent
|
||||
border none
|
||||
margin 0 5px
|
||||
border-radius 5px
|
||||
&:hover
|
||||
transition 0.2s
|
||||
background-color alpha($ui-button--hover-backgroundColor, 30%)
|
||||
color $ui-inactive-text-color
|
||||
p
|
||||
font-size 13px
|
||||
color #A0A0A0
|
||||
font-weight light
|
||||
&:hover
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.export--enable
|
||||
cursor pointer
|
||||
|
||||
.export--unable
|
||||
cursor not-allowed
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.group-section-label
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.group-section-control
|
||||
color $ui-dark-text-color
|
||||
|
||||
.group-section-control input
|
||||
background-color alpha($ui-dark-borderColor, 80%)
|
||||
color $ui-dark-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-dark-borderColor, 20%)
|
||||
color $ui-dark-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
70
browser/main/Detail/InfoPanelTrashed.js
Normal file
70
browser/main/Detail/InfoPanelTrashed.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoPanel.styl'
|
||||
|
||||
const InfoPanelTrashed = ({
|
||||
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
|
||||
}) => (
|
||||
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Storage
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{storageName}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Folder
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<text>Trash</text>{folderName}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Created
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{createdAt}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Updated
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
{updatedAt}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='export-wrap'>
|
||||
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
|
||||
<i className='fa fa-file-code-o fa-fw' />
|
||||
<p>.md</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
|
||||
<i className='fa fa-file-text-o fa-fw' />
|
||||
<p>.txt</p>
|
||||
</button>
|
||||
|
||||
<button styleName='export--unable'>
|
||||
<i className='fa fa-file-pdf-o fa-fw' />
|
||||
<p>.pdf</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
InfoPanelTrashed.propTypes = {
|
||||
storageName: PropTypes.string.isRequired,
|
||||
folderName: PropTypes.string.isRequired,
|
||||
updatedAt: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
exportAsMd: PropTypes.func.isRequired,
|
||||
exportAsTxt: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(InfoPanelTrashed, styles)
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Component for show updated date of the detail.
|
||||
*/
|
||||
import React, { PropTypes } from 'react'
|
||||
import { getLastUpdated } from 'browser/lib/date-formatter'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './LastUpdatedString.styl'
|
||||
|
||||
const LastUpdatedString = ({ date }) => {
|
||||
let text = ''
|
||||
|
||||
try {
|
||||
text = `Last updated at ${getLastUpdated(date)}`
|
||||
} catch (e) {
|
||||
text = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<p styleName='info-right-date'>{text}</p>
|
||||
)
|
||||
}
|
||||
|
||||
LastUpdatedString.propTypes = {
|
||||
date: PropTypes.string
|
||||
}
|
||||
|
||||
export default CSSModules(LastUpdatedString, styles)
|
||||
@@ -1,10 +0,0 @@
|
||||
.info-right-date
|
||||
display inline
|
||||
line-height 24px
|
||||
padding-right 25px
|
||||
font-size 11px
|
||||
color $ui-button-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.info-right-date
|
||||
color $ui-dark-button-color
|
||||
@@ -14,6 +14,13 @@ import StatusBar from '../StatusBar'
|
||||
import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -52,7 +59,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
note: Object.assign({}, nextProps.note)
|
||||
}, () => {
|
||||
this.refs.content.reload()
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,30 +72,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
}
|
||||
|
||||
getPercentageOfCompleteTodo (noteContent) {
|
||||
let splitted = noteContent.split('\n')
|
||||
let numberOfTodo = 0
|
||||
let numberOfCompletedTodo = 0
|
||||
|
||||
splitted.forEach((line) => {
|
||||
let trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
return Math.floor(numberOfCompletedTodo / numberOfTodo * 100)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.content = this.refs.content.value
|
||||
note.tags = this.refs.tags.value
|
||||
note.title = markdown.strip(findNoteTitle(note.content))
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||
note.updatedAt = new Date()
|
||||
|
||||
this.setState({
|
||||
@@ -117,7 +106,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('EDIT_NOTE')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -156,7 +145,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_STAR')
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
@@ -171,14 +160,26 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (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) {
|
||||
exportAsMd () {
|
||||
ee.emit('export:save-md')
|
||||
}
|
||||
|
||||
exportAsTxt () {
|
||||
ee.emit('export:save-text')
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
if (isTrashed) {
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -191,9 +192,31 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
this.refs.content.reload()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
@@ -208,7 +231,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
getToggleLockButton () {
|
||||
return this.state.isLocked ? 'fa-lock' : 'fa-unlock-alt'
|
||||
return this.state.isLocked ? 'fa-lock' : 'fa-unlock'
|
||||
}
|
||||
|
||||
handleDeleteKeyDown (e) {
|
||||
@@ -228,76 +251,123 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.focus()
|
||||
}
|
||||
|
||||
handleInfoButtonClick (e) {
|
||||
const infoPanel = document.querySelector('.infoPanel')
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config } = this.props
|
||||
let { data, config, location } = this.props
|
||||
let { note } = this.state
|
||||
let storageKey = note.storage
|
||||
let folderKey = note.folder
|
||||
|
||||
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]
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
<TodoListPercentage
|
||||
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
{(() => {
|
||||
const faClassName = `fa ${this.getToggleLockButton()}`
|
||||
const lockButtonComponent =
|
||||
<button styleName='control-lockButton'
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||
>
|
||||
<i className={faClassName} styleName='lock-button' />
|
||||
<span styleName='control-lockButton-tooltip'>
|
||||
{this.state.isLocked ? 'Unlock' : 'Lock'}
|
||||
</span>
|
||||
</button>
|
||||
return (
|
||||
this.state.isLockButtonShown ? lockButtonComponent : ''
|
||||
)
|
||||
})()}
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-window-maximize' styleName='fullScreen-button' />
|
||||
</button>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.exportAsMd}
|
||||
exportAsTxt={this.exportAsTxt}
|
||||
wordCount={note.content.split(' ').length}
|
||||
letterCount={note.content.replace(/\r?\n/g, '').length}
|
||||
type={note.type}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
>
|
||||
<div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
<TodoListPercentage
|
||||
percentageOfTodo={this.getPercentageOfCompleteTodo(note.content)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
{(() => {
|
||||
const faClassName = `fa ${this.getToggleLockButton()}`
|
||||
const lockButtonComponent =
|
||||
<button styleName='control-lockButton'
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
|
||||
>
|
||||
<i className={faClassName} styleName='lock-button' />
|
||||
<span styleName='control-lockButton-tooltip'>
|
||||
{this.state.isLocked ? 'Unlock' : 'Lock'}
|
||||
</span>
|
||||
</button>
|
||||
return (
|
||||
this.state.isLockButtonShown ? lockButtonComponent : ''
|
||||
)
|
||||
})()}
|
||||
<button styleName='control-trashButton'
|
||||
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||
>
|
||||
<svg height='14px' id='Capa_1' style={{enableBackground: 'new 0 0 753.23 753.23'}} width='14px' version='1.1' viewBox='0 0 753.23 753.23' x='0px' y='0px' xmlSpace='preserve'>
|
||||
<g>
|
||||
<g id='_x34__19_'>
|
||||
<g>
|
||||
<path d='M494.308,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077c0-12.993-10.545-23.539-23.538-23.539
				s-23.538,10.545-23.538,23.539v282.461C470.77,648.531,481.314,659.077,494.308,659.077z M635.538,94.154h-141.23V47.077
				C494.308,21.067,473.24,0,447.23,0H306c-26.01,0-47.077,21.067-47.077,47.077v47.077h-141.23
				c-26.01,0-47.077,21.067-47.077,47.077v47.077c0,25.986,21.067,47.077,47.077,47.077v423.692
				c0,51.996,42.157,94.153,94.154,94.153h329.539c51.996,0,94.153-42.157,94.153-94.153V235.385
				c26.01,0,47.077-21.091,47.077-47.077V141.23C682.615,115.221,661.548,94.154,635.538,94.154z M306,70.615
				c0-12.993,10.545-23.539,23.538-23.539h94.154c12.993,0,23.538,10.545,23.538,23.539v23.539c-22.809,0-141.23,0-141.23,0V70.615z
				 M588.461,659.077c0,25.986-21.066,47.076-47.076,47.076H211.846c-26.01,0-47.077-21.09-47.077-47.076V235.385h423.692V659.077z
				 M612,188.308H141.23c-12.993,0-23.538-10.545-23.538-23.539s10.545-23.539,23.538-23.539H612
				c12.993,0,23.538,10.545,23.538,23.539S624.993,188.308,612,188.308z M258.923,659.077c12.993,0,23.539-10.546,23.539-23.539
				V353.077c0-12.993-10.545-23.539-23.539-23.539s-23.539,10.545-23.539,23.539v282.461
				C235.384,648.531,245.93,659.077,258.923,659.077z M376.615,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077
				c0-12.993-10.545-23.539-23.538-23.539s-23.539,10.545-23.539,23.539v282.461C353.077,648.531,363.622,659.077,376.615,659.077z' />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<MarkdownEditor
|
||||
|
||||
@@ -27,13 +27,9 @@
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
.control-trashButton
|
||||
float right
|
||||
topBarButtonLight()
|
||||
|
||||
.control-fullScreenButton
|
||||
float right
|
||||
padding 7px
|
||||
padding 0 0 2px 0
|
||||
topBarButtonLight()
|
||||
|
||||
.body
|
||||
@@ -58,8 +54,5 @@ body[data-theme="dark"]
|
||||
.control-lockButton-tooltip
|
||||
darkTooltip()
|
||||
|
||||
.control-trashButton
|
||||
topBarButtonDark()
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
@@ -15,26 +15,26 @@ $info-margin-under-border = 27px
|
||||
float left
|
||||
padding 0 5px
|
||||
margin 0px 2px
|
||||
|
||||
.info-left-top
|
||||
display inline-block
|
||||
height $info-height
|
||||
line-height $info-height
|
||||
|
||||
.info-left-top-folderSelect
|
||||
display inline-block
|
||||
padding 0 3px
|
||||
height 34px
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
line-height 26px
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
border-radius 3px
|
||||
|
||||
.info-left-button
|
||||
width 34px
|
||||
height 34px
|
||||
navButtonColor()
|
||||
color $ui-favorite-star-button-color
|
||||
font-size 14px
|
||||
line-height 0
|
||||
margin 13px 2px
|
||||
padding 0
|
||||
border-radius 17px
|
||||
@@ -44,7 +44,7 @@ $info-margin-under-border = 27px
|
||||
border-color $ui-favorite-star-button-color
|
||||
&:active, &:active:hover
|
||||
background-color $ui-favorite-star-button-color
|
||||
color $ui-button--active-color
|
||||
color $ui-button--color
|
||||
|
||||
.info-right
|
||||
position absolute
|
||||
@@ -54,6 +54,27 @@ $info-margin-under-border = 27px
|
||||
bottom 1px
|
||||
padding-left 30px
|
||||
|
||||
.undo-button
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
font-size 14px
|
||||
margin 15px 7px
|
||||
border none
|
||||
color $ui-button-color
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
fill $ui-button-color
|
||||
background-color transparent
|
||||
cursor pointer
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
background-color alpha($ui-button--hover-backgroundColor, 60%)
|
||||
.control-lockButton-tooltip
|
||||
opacity 1
|
||||
|
||||
body[data-theme="dark"]
|
||||
.info
|
||||
border-color $ui-dark-borderColor
|
||||
@@ -73,3 +94,6 @@ body[data-theme="dark"]
|
||||
|
||||
.info-right
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
|
||||
.undo-button
|
||||
topBarButtonDark()
|
||||
@@ -17,6 +17,11 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import _ from 'lodash'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import TrashButton from './TrashButton'
|
||||
import InfoButton from './InfoButton'
|
||||
import InfoPanel from './InfoPanel'
|
||||
import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
|
||||
function pass (name) {
|
||||
switch (name) {
|
||||
@@ -53,7 +58,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
let nextNote = Object.assign({
|
||||
description: ''
|
||||
@@ -68,7 +73,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
snippets.forEach((snippet, index) => {
|
||||
this.refs['code-' + index].reload()
|
||||
})
|
||||
this.refs.tags.reset()
|
||||
if (this.refs.tags) this.refs.tags.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,7 +85,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
handleChange (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.tags = this.refs.tags.value
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
@@ -111,7 +116,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('EDIT_NOTE')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,7 +155,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_STAR')
|
||||
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
|
||||
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
@@ -165,14 +170,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
handleDeleteButtonClick (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) {
|
||||
handleTrashButtonClick (e) {
|
||||
let { note } = this.state
|
||||
const { isTrashed } = note
|
||||
|
||||
if (isTrashed) {
|
||||
let dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Confirm note deletion',
|
||||
detail: 'This will permanently remove this note.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
if (dialogueButtonIndex === 1) return
|
||||
let { note, dispatch } = this.props
|
||||
dataApi
|
||||
.deleteNote(note.storage, note.key)
|
||||
@@ -185,9 +194,30 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
ee.once('list:moved', dispatchHandler)
|
||||
ee.emit('list:next')
|
||||
})
|
||||
} else {
|
||||
note.isTrashed = true
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
ee.emit('list:next')
|
||||
}
|
||||
|
||||
handleUndoButtonClick (e) {
|
||||
let { note } = this.state
|
||||
|
||||
note.isTrashed = false
|
||||
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
ee.emit('list:next')
|
||||
})
|
||||
}
|
||||
|
||||
handleFullScreenButton (e) {
|
||||
@@ -241,7 +271,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
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: Object.assign(this.state.note, {snippets: snippets})})
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
@@ -254,7 +284,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
return (e) => {
|
||||
let snippets = this.state.note.snippets.slice()
|
||||
snippets[index].mode = name
|
||||
this.state.note.snippets = snippets
|
||||
this.setState({note: Object.assign(this.state.note, {snippets: snippets})})
|
||||
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
@@ -268,7 +298,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
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: Object.assign(this.state.note, {snippets: snippets})})
|
||||
this.setState({
|
||||
note: this.state.note
|
||||
}, () => {
|
||||
@@ -432,10 +462,27 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
handleInfoButtonClick (e) {
|
||||
const infoPanel = document.querySelector('.infoPanel')
|
||||
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
|
||||
}
|
||||
|
||||
showWarning () {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Sorry!',
|
||||
detail: 'md/text import is available only a markdown note.',
|
||||
buttons: ['OK']
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { data, config } = this.props
|
||||
let { data, config, location } = this.props
|
||||
let { note } = this.state
|
||||
|
||||
let storageKey = note.storage
|
||||
let folderKey = note.folder
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
@@ -473,6 +520,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
: <CodeEditor styleName='tabView-content'
|
||||
mode={snippet.mode}
|
||||
@@ -490,54 +538,91 @@ class SnippetNoteDetail extends React.Component {
|
||||
</div>
|
||||
})
|
||||
|
||||
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]
|
||||
|
||||
const trashTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<i styleName='undo-button'
|
||||
className='fa fa-undo fa-fw'
|
||||
onClick={(e) => this.handleUndoButtonClick(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const detailTopBar = <div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-window-maximize' styleName='fullScreen-button' />
|
||||
</button>
|
||||
<InfoButton
|
||||
onClick={(e) => this.handleInfoButtonClick(e)}
|
||||
/>
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
noteLink={`[${note.title}](${location.query.key})`}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
exportAsTxt={this.showWarning}
|
||||
type={note.type}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='info'>
|
||||
<div styleName='info-left'>
|
||||
<StarButton styleName='info-left-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<button styleName='control-trashButton'
|
||||
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||
>
|
||||
<svg height='14px' id='Capa_1' style={{enableBackground: 'new 0 0 753.23 753.23'}} width='14px' version='1.1' viewBox='0 0 753.23 753.23' x='0px' y='0px' xmlSpace='preserve'>
|
||||
<g>
|
||||
<g id='_x34__19_'>
|
||||
<g>
|
||||
<path d='M494.308,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077c0-12.993-10.545-23.539-23.538-23.539
				s-23.538,10.545-23.538,23.539v282.461C470.77,648.531,481.314,659.077,494.308,659.077z M635.538,94.154h-141.23V47.077
				C494.308,21.067,473.24,0,447.23,0H306c-26.01,0-47.077,21.067-47.077,47.077v47.077h-141.23
				c-26.01,0-47.077,21.067-47.077,47.077v47.077c0,25.986,21.067,47.077,47.077,47.077v423.692
				c0,51.996,42.157,94.153,94.154,94.153h329.539c51.996,0,94.153-42.157,94.153-94.153V235.385
				c26.01,0,47.077-21.091,47.077-47.077V141.23C682.615,115.221,661.548,94.154,635.538,94.154z M306,70.615
				c0-12.993,10.545-23.539,23.538-23.539h94.154c12.993,0,23.538,10.545,23.538,23.539v23.539c-22.809,0-141.23,0-141.23,0V70.615z
				 M588.461,659.077c0,25.986-21.066,47.076-47.076,47.076H211.846c-26.01,0-47.077-21.09-47.077-47.076V235.385h423.692V659.077z
				 M612,188.308H141.23c-12.993,0-23.538-10.545-23.538-23.539s10.545-23.539,23.538-23.539H612
				c12.993,0,23.538,10.545,23.538,23.539S624.993,188.308,612,188.308z M258.923,659.077c12.993,0,23.539-10.546,23.539-23.539
				V353.077c0-12.993-10.545-23.539-23.539-23.539s-23.539,10.545-23.539,23.539v282.461
				C235.384,648.531,245.93,659.077,258.923,659.077z M376.615,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077
				c0-12.993-10.545-23.539-23.538-23.539s-23.539,10.545-23.539,23.539v282.461C353.077,648.531,363.622,659.077,376.615,659.077z' />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button styleName='control-fullScreenButton'
|
||||
onMouseDown={(e) => this.handleFullScreenButton(e)}
|
||||
>
|
||||
<i className='fa fa-arrows-alt' styleName='fullScreen-button' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
<div styleName='body'>
|
||||
<div styleName='description'>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
.body .description
|
||||
absolute top left right
|
||||
height 80px
|
||||
height 50px
|
||||
|
||||
.body .description textarea
|
||||
outline none
|
||||
@@ -27,14 +27,14 @@
|
||||
height 100%
|
||||
width 100%
|
||||
resize none
|
||||
border none
|
||||
padding 10px
|
||||
border 1px solid $ui-borderColor
|
||||
padding 2px 5px
|
||||
line-height 1.6
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
|
||||
.tabList
|
||||
absolute left right
|
||||
top 80px
|
||||
top 55px
|
||||
height 30px
|
||||
display flex
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
@@ -50,29 +50,26 @@
|
||||
|
||||
.tabView
|
||||
absolute left right bottom
|
||||
top 130px
|
||||
top 100px
|
||||
|
||||
.tabView-content
|
||||
absolute top left right bottom
|
||||
|
||||
.override
|
||||
absolute bottom left
|
||||
bottom 30px
|
||||
left 60px
|
||||
height 23px
|
||||
z-index 1
|
||||
z-index 101
|
||||
button
|
||||
navButtonColor()
|
||||
height 24px
|
||||
&:active .update-icon
|
||||
color white
|
||||
|
||||
.control-trashButton
|
||||
float right
|
||||
topBarButtonLight()
|
||||
|
||||
.control-fullScreenButton
|
||||
float right
|
||||
padding 7px
|
||||
padding 0 0 2px 0
|
||||
topBarButtonLight()
|
||||
|
||||
body[data-theme="dark"]
|
||||
@@ -87,6 +84,7 @@ body[data-theme="dark"]
|
||||
.body .description textarea
|
||||
background-color $ui-dark-noteDetail-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
border 1px solid $ui-dark-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -105,8 +103,5 @@ body[data-theme="dark"]
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-trashButton
|
||||
topBarButtonDark()
|
||||
|
||||
.control-fullScreenButton
|
||||
topBarButtonDark()
|
||||
|
||||
@@ -57,7 +57,7 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_TAG')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.root
|
||||
display inline-block
|
||||
top 19px
|
||||
user-select none
|
||||
height 23px
|
||||
vertical-align middle
|
||||
width 300px
|
||||
overflow-x scroll
|
||||
@@ -14,7 +14,6 @@
|
||||
display inline-block
|
||||
margin 1px 3px
|
||||
padding 0
|
||||
vertical-align middle
|
||||
height 20px
|
||||
background-color alpha($ui-tag-backgroundColor, 10%)
|
||||
border-radius 3px
|
||||
@@ -49,9 +48,9 @@
|
||||
|
||||
.newTag
|
||||
display inline-block
|
||||
margin 0 2px
|
||||
margin 2px 0 15px 2px
|
||||
vertical-align middle
|
||||
height 24px
|
||||
height 18px
|
||||
box-sizing borde-box
|
||||
border none
|
||||
background-color transparent
|
||||
|
||||
19
browser/main/Detail/TrashButton.js
Normal file
19
browser/main/Detail/TrashButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TrashButton.styl'
|
||||
|
||||
const TrashButton = ({
|
||||
onClick
|
||||
}) => (
|
||||
<button styleName='control-trashButton'
|
||||
onClick={(e) => onClick(e)}
|
||||
>
|
||||
<i className='fa fa-trash trashButton' styleName='info-button' />
|
||||
</button>
|
||||
)
|
||||
|
||||
TrashButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TrashButton, styles)
|
||||
11
browser/main/Detail/TrashButton.styl
Normal file
11
browser/main/Detail/TrashButton.styl
Normal file
@@ -0,0 +1,11 @@
|
||||
.control-trashButton
|
||||
float right
|
||||
topBarButtonLight()
|
||||
|
||||
.trashButton
|
||||
padding 0px
|
||||
margin 15px 0
|
||||
|
||||
body[data-theme="dark"]
|
||||
.control-trashButton
|
||||
topBarButtonDark()
|
||||
@@ -17,7 +17,7 @@ class Detail extends React.Component {
|
||||
this.refs.root != null && this.refs.root.focus()
|
||||
}
|
||||
this.deleteHandler = () => {
|
||||
this.refs.root != null && this.refs.root.handleDeleteButtonClick()
|
||||
this.refs.root != null && this.refs.root.handleTrashButtonClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class Detail extends React.Component {
|
||||
tabIndex='0'
|
||||
>
|
||||
<div styleName='empty'>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new post</div>
|
||||
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
|
||||
</div>
|
||||
<StatusBar
|
||||
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
|
||||
|
||||
@@ -14,6 +14,7 @@ import InitModal from 'browser/main/modals/InitModal'
|
||||
import mixpanel from 'browser/main/lib/mixpanel'
|
||||
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import RealtimeNotification from 'browser/components/RealtimeNotification'
|
||||
|
||||
function focused () {
|
||||
mixpanel.track('MAIN_FOCUSED')
|
||||
@@ -172,8 +173,8 @@ class Main extends React.Component {
|
||||
}
|
||||
|
||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||
this.state.noteDetailWidth = noteDetail.style.left
|
||||
this.state.mainBodyWidth = mainBody.style.left
|
||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
||||
this.setState({mainBodyWidth: mainBody.style.left})
|
||||
noteDetail.style.left = '0px'
|
||||
mainBody.style.left = '0px'
|
||||
noteList.style.display = 'none'
|
||||
@@ -255,6 +256,7 @@ class Main extends React.Component {
|
||||
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
|
||||
/>
|
||||
</div>
|
||||
<RealtimeNotification />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
68
browser/main/NewNoteButton/NewNoteButton.styl
Normal file
68
browser/main/NewNoteButton/NewNoteButton.styl
Normal file
@@ -0,0 +1,68 @@
|
||||
.root
|
||||
position relative
|
||||
background-color $ui-noteList-backgroundColor
|
||||
height $topBar-height - 1
|
||||
margin-left: auto;
|
||||
width: 64px;
|
||||
margin-right: -15px;
|
||||
|
||||
.root--expanded
|
||||
@extend .root
|
||||
|
||||
$control-height = 34px
|
||||
|
||||
.control
|
||||
position absolute
|
||||
top 13px
|
||||
left 8px
|
||||
right 8px
|
||||
height $control-height
|
||||
overflow hidden
|
||||
display flex
|
||||
|
||||
.control-newNoteButton
|
||||
display block
|
||||
width 32px
|
||||
height $control-height - 2
|
||||
navButtonColor()
|
||||
font-size 16px
|
||||
line-height 28px
|
||||
padding 0
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover .control-newNoteButton-tooltip
|
||||
opacity 1
|
||||
|
||||
.control-newNoteButton-tooltip
|
||||
tooltip()
|
||||
position fixed
|
||||
pointer-events none
|
||||
top 50px
|
||||
left 433px
|
||||
z-index 200
|
||||
padding 5px
|
||||
line-height normal
|
||||
border-radius 2px
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
.control-newNoteButton
|
||||
color $ui-inactive-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
border-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.control-newNoteButton-tooltip
|
||||
darkTooltip()
|
||||
106
browser/main/NewNoteButton/index.js
Normal file
106
browser/main/NewNoteButton/index.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NewNoteButton.styl'
|
||||
import _ from 'lodash'
|
||||
import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import { hashHistory } from 'react-router'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
|
||||
const OSX = window.process.platform === 'darwin'
|
||||
|
||||
class NewNoteButton extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
|
||||
this.newNoteHandler = () => {
|
||||
this.handleNewNoteButtonClick()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
eventEmitter.on('top:new-note', this.newNoteHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
eventEmitter.off('top:new-note', this.newNoteHandler)
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { config, location, dispatch } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
const { data, params } = this.props
|
||||
let storage = data.storageMap.get(params.storageKey)
|
||||
|
||||
// Find first storage
|
||||
if (storage == null) {
|
||||
for (let kv of data.storageMap) {
|
||||
storage = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (storage == null) this.showMessageBox('No storage to create a note')
|
||||
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
|
||||
if (folder == null) this.showMessageBox('No folder to create a note')
|
||||
|
||||
return {
|
||||
storage,
|
||||
folder
|
||||
}
|
||||
}
|
||||
|
||||
showMessageBox (message) {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: message,
|
||||
buttons: ['OK']
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, style } = this.props
|
||||
return (
|
||||
<div className='NewNoteButton'
|
||||
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
||||
style={style}
|
||||
>
|
||||
<div styleName='control'>
|
||||
<button styleName='control-newNoteButton'
|
||||
onClick={(e) => this.handleNewNoteButtonClick(e)}>
|
||||
<i className='fa fa-pencil-square-o' />
|
||||
<span styleName='control-newNoteButton-tooltip'>
|
||||
Make a Note {OSX ? '⌘' : '^'} + n
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewNoteButton.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
config: PropTypes.shape({
|
||||
isSideNavFolded: PropTypes.bool
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(NewNoteButton, styles)
|
||||
@@ -2,6 +2,7 @@ $control-height = 30px
|
||||
|
||||
.root
|
||||
absolute left bottom
|
||||
bottom 30px
|
||||
top $topBar-height - 1
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
@@ -17,7 +18,7 @@ $control-height = 30px
|
||||
|
||||
.control-sortBy
|
||||
flex 1
|
||||
padding-left 25px
|
||||
padding-left 22px
|
||||
|
||||
.control-sortBy-select
|
||||
appearance: none;
|
||||
@@ -28,11 +29,14 @@ $control-height = 30px
|
||||
background-color transparent
|
||||
outline none
|
||||
cursor pointer
|
||||
font-size 10px
|
||||
font-size 11px
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-text-color
|
||||
|
||||
.control-button-area
|
||||
margin-right 12px
|
||||
|
||||
.control-button
|
||||
width 25px
|
||||
padding 0
|
||||
|
||||
@@ -13,6 +13,7 @@ import fs from 'fs'
|
||||
import { hashHistory } from 'react-router'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import { findNoteTitle } from 'browser/lib/findNoteTitle'
|
||||
import stripgtags from 'striptags'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem, dialog } = remote
|
||||
@@ -47,10 +48,7 @@ class NoteList extends React.Component {
|
||||
this.alertIfSnippet()
|
||||
}
|
||||
this.importFromFileHandler = this.importFromFile.bind(this)
|
||||
|
||||
this.jumpToTopHandler = () => {
|
||||
this.jumpToTop()
|
||||
}
|
||||
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
@@ -62,9 +60,8 @@ class NoteList extends React.Component {
|
||||
ee.on('list:prior', this.selectPriorNoteHandler)
|
||||
ee.on('list:focus', this.focusHandler)
|
||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.on('list:top', this.jumpToTopHandler)
|
||||
ee.on('list:jumpToTop', this.jumpToTopHandler)
|
||||
ee.on('import:file', this.importFromFileHandler)
|
||||
ee.on('list:jump', this.jumpNoteByHash)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -84,9 +81,8 @@ class NoteList extends React.Component {
|
||||
ee.off('list:prior', this.selectPriorNoteHandler)
|
||||
ee.off('list:focus', this.focusHandler)
|
||||
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.off('list:top', this.jumpToTopHandler)
|
||||
ee.off('list:jumpToTop', this.jumpToTopHandler)
|
||||
ee.off('import:file', this.importFromFileHandler)
|
||||
ee.off('list:jump', this.jumpNoteByHash)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
@@ -179,6 +175,31 @@ class NoteList extends React.Component {
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
jumpNoteByHashHandler (event, noteHash) {
|
||||
// first argument event isn't used.
|
||||
if (this.notes === null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note.storage + '-' + note.key === noteHash
|
||||
})
|
||||
|
||||
if (targetIndex < 0) targetIndex = 0
|
||||
|
||||
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
|
||||
|
||||
@@ -226,7 +247,12 @@ class NoteList extends React.Component {
|
||||
if (searchInputText === '') {
|
||||
router.push('/home')
|
||||
}
|
||||
return searchFromNotes(this.props.data, searchInputText)
|
||||
return searchFromNotes(this.notes, searchInputText)
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/trashed/)) {
|
||||
return data.trashedSet.toJS()
|
||||
.map((uniqueKey) => data.noteMap.get(uniqueKey))
|
||||
}
|
||||
|
||||
let storageKey = params.storageKey
|
||||
@@ -264,49 +290,6 @@ class NoteList extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -350,23 +333,6 @@ class NoteList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
jumpToTop () {
|
||||
if (this.notes === null || this.notes.length === 0) {
|
||||
return
|
||||
}
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
|
||||
const targetIndex = 0
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleDragStart (e, note) {
|
||||
const noteData = JSON.stringify(note)
|
||||
e.dataTransfer.setData('note', noteData)
|
||||
@@ -382,6 +348,22 @@ class NoteList extends React.Component {
|
||||
properties: ['openFile', 'multiSelections']
|
||||
}
|
||||
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => {
|
||||
this.addNotesFromFiles(filepaths)
|
||||
})
|
||||
}
|
||||
|
||||
handleDrop (e) {
|
||||
e.preventDefault()
|
||||
const { location } = this.props
|
||||
const filepaths = Array.from(e.dataTransfer.files).map(file => { return file.path })
|
||||
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
|
||||
}
|
||||
|
||||
// Add notes to the current folder
|
||||
addNotesFromFiles (filepaths) {
|
||||
const { dispatch, location } = this.props
|
||||
|
||||
const targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return note !== null && `${note.storage}-${note.key}` === location.query.key
|
||||
})
|
||||
@@ -389,28 +371,26 @@ class NoteList extends React.Component {
|
||||
const storageKey = this.notes[targetIndex].storage
|
||||
const folderKey = this.notes[targetIndex].folder
|
||||
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, (filepaths) => {
|
||||
if (filepaths === undefined) return
|
||||
filepaths.forEach((filepath) => {
|
||||
fs.readFile(filepath, (err, data) => {
|
||||
if (err) throw Error('File reading error: ', err)
|
||||
const content = data.toString()
|
||||
const newNote = {
|
||||
content: content,
|
||||
folder: folderKey,
|
||||
title: markdown.strip(findNoteTitle(content)),
|
||||
type: 'MARKDOWN_NOTE'
|
||||
}
|
||||
dataApi.createNote(storageKey, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: `${note.storage}-${note.key}`}
|
||||
})
|
||||
if (filepaths === undefined) return
|
||||
filepaths.forEach((filepath) => {
|
||||
fs.readFile(filepath, (err, data) => {
|
||||
if (err) throw Error('File reading error: ', err)
|
||||
const content = data.toString()
|
||||
const newNote = {
|
||||
content: content,
|
||||
folder: folderKey,
|
||||
title: markdown.strip(findNoteTitle(content)),
|
||||
type: 'MARKDOWN_NOTE'
|
||||
}
|
||||
dataApi.createNote(storageKey, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: `${note.storage}-${note.key}`}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -426,6 +406,10 @@ class NoteList extends React.Component {
|
||||
: sortByUpdatedAt
|
||||
this.notes = notes = this.getNotes()
|
||||
.sort(sortFunc)
|
||||
.filter((note) => {
|
||||
// this is for the trash box
|
||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
||||
})
|
||||
|
||||
let noteList = notes
|
||||
.map(note => {
|
||||
@@ -449,7 +433,6 @@ class NoteList extends React.Component {
|
||||
dateDisplay={dateDisplay}
|
||||
key={key}
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
/>
|
||||
)
|
||||
@@ -461,7 +444,6 @@ class NoteList extends React.Component {
|
||||
note={note}
|
||||
key={key}
|
||||
handleNoteClick={this.handleNoteClick.bind(this)}
|
||||
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
/>
|
||||
)
|
||||
@@ -471,6 +453,7 @@ class NoteList extends React.Component {
|
||||
<div className='NoteList'
|
||||
styleName='root'
|
||||
style={this.props.style}
|
||||
onDrop={(e) => this.handleDrop(e)}
|
||||
>
|
||||
<div styleName='control'>
|
||||
<div styleName='control-sortBy'>
|
||||
@@ -479,27 +462,29 @@ class NoteList extends React.Component {
|
||||
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>
|
||||
<option value='UPDATED_AT'>Last Updated</option>
|
||||
<option value='CREATED_AT'>Creation Time</option>
|
||||
<option value='ALPHABETICAL'>Alphabetically</option>
|
||||
</select>
|
||||
</div>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||
>
|
||||
<i className='fa fa-th-large' />
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||
>
|
||||
<i className='fa fa-list-ul' />
|
||||
</button>
|
||||
<div styleName='control-button-area'>
|
||||
<button styleName={config.listStyle === 'DEFAULT'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
|
||||
>
|
||||
<i className='fa fa-th-large' />
|
||||
</button>
|
||||
<button styleName={config.listStyle === 'SMALL'
|
||||
? 'control-button--active'
|
||||
: 'control-button'
|
||||
}
|
||||
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
|
||||
>
|
||||
<i className='fa fa-list-ul' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='list'
|
||||
ref='list'
|
||||
|
||||
@@ -114,7 +114,7 @@ class StorageItem extends React.Component {
|
||||
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.',
|
||||
detail: 'This will delete all notes in the folder and can not be undone.',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
})
|
||||
|
||||
@@ -180,14 +180,20 @@ class StorageItem extends React.Component {
|
||||
|
||||
render () {
|
||||
let { storage, location, isFolded, data, dispatch } = this.props
|
||||
let { folderNoteMap } = data
|
||||
let { folderNoteMap, trashedSet } = 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
|
||||
let noteCount = 0
|
||||
if (noteSet) {
|
||||
let trashedNoteCount = 0
|
||||
const noteKeys = noteSet.map(noteKey => { return noteKey })
|
||||
trashedSet.toJS().forEach(trashedKey => {
|
||||
if (noteKeys.some(noteKey => { return noteKey === trashedKey })) trashedNoteCount++
|
||||
})
|
||||
noteCount = noteSet.size - trashedNoteCount
|
||||
}
|
||||
return (
|
||||
<StorageItemChild
|
||||
key={folder.key}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.root
|
||||
width 100%
|
||||
user-select none
|
||||
padding-top 20px
|
||||
|
||||
.header
|
||||
position relative
|
||||
|
||||
@@ -33,12 +33,18 @@ class SideNav extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleTrashedButtonClick (e) {
|
||||
let { router } = this.context
|
||||
router.push('/trashed')
|
||||
}
|
||||
|
||||
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 isTrashedActive = !!location.pathname.match(/^\/trashed$/)
|
||||
|
||||
let storageList = data.storageMap.map((storage, key) => {
|
||||
return <StorageItem
|
||||
@@ -62,8 +68,8 @@ class SideNav extends React.Component {
|
||||
<button styleName='top-menu'
|
||||
onClick={(e) => this.handleMenuButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-navicon fa-fw' />
|
||||
<span styleName='top-menu-label'>Menu</span>
|
||||
<i className='fa fa-wrench fa-fw' />
|
||||
<span styleName='top-menu-label'>Preferences</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +78,9 @@ class SideNav extends React.Component {
|
||||
isHomeActive={isHomeActive}
|
||||
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
|
||||
isStarredActive={isStarredActive}
|
||||
isTrashedActive={isTrashedActive}
|
||||
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
||||
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
||||
/>
|
||||
|
||||
<div styleName='storageList'>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
.root
|
||||
absolute bottom left right
|
||||
height $statusBar-height
|
||||
bottom 16px
|
||||
z-index 100
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
display flex
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './StatusBar.styl'
|
||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||
import LastUpdatedString from '../Detail/LastUpdatedString'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote, ipcRenderer } = electron
|
||||
@@ -60,16 +59,12 @@ class StatusBar extends React.Component {
|
||||
{Math.floor(config.zoom * 100)}%
|
||||
</button>
|
||||
|
||||
<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
|
||||
}
|
||||
|
||||
<LastUpdatedString date={this.props.date} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,26 +19,25 @@ $control-height = 34px
|
||||
|
||||
.control-search
|
||||
height 32px
|
||||
width 1px
|
||||
flex 1
|
||||
background-color white
|
||||
position relative
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-inactive-text-color
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
display block
|
||||
absolute top bottom right
|
||||
left 30px
|
||||
width 100%
|
||||
padding-left 12px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
input
|
||||
width 100%
|
||||
height 100%
|
||||
outline none
|
||||
border none
|
||||
color $ui-text-color
|
||||
font-size 16px
|
||||
padding-bottom 2px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
|
||||
.control-search-optionList
|
||||
@@ -92,9 +91,7 @@ $control-height = 34px
|
||||
width 32px
|
||||
height $control-height - 2
|
||||
navButtonColor()
|
||||
border $ui-border
|
||||
border-radius 32px
|
||||
font-size 14px
|
||||
font-size 16px
|
||||
line-height 28px
|
||||
padding 0
|
||||
&:active
|
||||
@@ -132,6 +129,7 @@ body[data-theme="dark"]
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
@@ -2,12 +2,12 @@ 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'
|
||||
import NewNoteButton from 'browser/main/NewNoteButton'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
|
||||
const OSX = window.process.platform === 'darwin'
|
||||
|
||||
@@ -21,71 +21,19 @@ class TopBar extends React.Component {
|
||||
isSearching: false
|
||||
}
|
||||
|
||||
this.newNoteHandler = () => {
|
||||
this.handleNewPostButtonClick()
|
||||
}
|
||||
|
||||
this.focusSearchHandler = () => {
|
||||
this.handleOnSearchFocus()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ee.on('top:new-note', this.newNoteHandler)
|
||||
ee.on('top:focus-search', this.focusSearchHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('top:new-note', this.newNoteHandler)
|
||||
ee.off('top:focus-search', this.focusSearchHandler)
|
||||
}
|
||||
|
||||
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) window.alert('No storage to create a note')
|
||||
let folder = _.find(storage.folders, {key: params.folderKey})
|
||||
if (folder == null) folder = storage.folders[0]
|
||||
if (folder == null) window.alert('No folder to create a note')
|
||||
|
||||
return {
|
||||
storage,
|
||||
folder
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { router } = this.context
|
||||
router.push('/searched')
|
||||
@@ -94,22 +42,6 @@ class TopBar extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleOptionClick (uniqueKey) {
|
||||
return (e) => {
|
||||
this.setState({
|
||||
isSearching: false
|
||||
}, () => {
|
||||
let { location } = this.props
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchFocus (e) {
|
||||
this.setState({
|
||||
isSearching: true
|
||||
@@ -134,60 +66,6 @@ class TopBar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
createNote (noteType) {
|
||||
let { dispatch, location } = this.props
|
||||
if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.')
|
||||
|
||||
let { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
let newNote = noteType === 'MARKDOWN_NOTE'
|
||||
? {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
: {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder.key,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}]
|
||||
}
|
||||
|
||||
dataApi
|
||||
.createNote(storage.key, newNote)
|
||||
.then((note) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: {key: note.storage + '-' + note.key}
|
||||
})
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultNote (defaultNote) {
|
||||
let { config, dispatch } = this.props
|
||||
let ui = Object.assign(config.ui)
|
||||
ui.defaultNote = defaultNote
|
||||
ConfigManager.set({
|
||||
ui
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'SET_UI',
|
||||
config: ConfigManager.get()
|
||||
})
|
||||
}
|
||||
|
||||
handleOnSearchFocus () {
|
||||
if (this.state.isSearching) {
|
||||
this.refs.search.childNodes[0].blur()
|
||||
@@ -197,7 +75,7 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config, style, data } = this.props
|
||||
let { config, style, data, location } = this.props
|
||||
return (
|
||||
<div className='TopBar'
|
||||
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
|
||||
@@ -205,7 +83,6 @@ class TopBar extends React.Component {
|
||||
>
|
||||
<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)}
|
||||
@@ -230,14 +107,17 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
</div>
|
||||
<button styleName='control-newPostButton'
|
||||
onClick={(e) => this.handleNewPostButtonClick(e)}>
|
||||
<i className='fa fa-plus' />
|
||||
<span styleName='control-newPostButton-tooltip'>
|
||||
Make a Note {OSX ? '⌘' : '^'} + n
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{location.pathname === '/trashed' ? ''
|
||||
: <NewNoteButton
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
])}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ textarea.block-input
|
||||
fullsize()
|
||||
|
||||
modalZIndex= 1000
|
||||
modalBackColor = transparentify(white, 65%)
|
||||
modalBackColor = white
|
||||
.ace_focus
|
||||
outline-color rgb(59, 153, 252)
|
||||
outline-offset 0px
|
||||
@@ -86,12 +86,12 @@ modalBackColor = transparentify(white, 65%)
|
||||
body[data-theme="dark"]
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color alpha(black, 60%)
|
||||
background-color $ui-dark-backgroundColor
|
||||
|
||||
.CodeMirror
|
||||
font-family inherit !important
|
||||
line-height 1.4em
|
||||
height 100%
|
||||
height 96%
|
||||
.CodeMirror > div > textarea
|
||||
margin-bottom -1em
|
||||
.CodeMirror-focused .CodeMirror-selected
|
||||
|
||||
@@ -23,6 +23,16 @@ document.addEventListener('dragover', function (e) {
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const className = e.target.className
|
||||
if (!className && typeof (className) !== 'string') return
|
||||
const isInfoButton = className.includes('infoButton')
|
||||
const isInfoPanel = e.target.offsetParent.className.includes('infoPanel')
|
||||
if (isInfoButton || isInfoPanel) return
|
||||
const infoPanel = document.querySelector('.infoPanel')
|
||||
if (infoPanel) infoPanel.style.display = 'none'
|
||||
})
|
||||
|
||||
let el = document.getElementById('content')
|
||||
const history = syncHistoryWithStore(hashHistory, store)
|
||||
|
||||
@@ -51,6 +61,7 @@ ReactDOM.render((
|
||||
<Route path='home' />
|
||||
<Route path='starred' />
|
||||
<Route path='searched' />
|
||||
<Route path='trashed' />
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home' />
|
||||
<Route path=':storageKey'>
|
||||
|
||||
@@ -3,29 +3,34 @@ const AMA = require('aws-sdk-mobile-analytics')
|
||||
const ConfigManager = require('browser/main/lib/ConfigManager')
|
||||
|
||||
AWS.config.region = 'us-east-1'
|
||||
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
||||
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
})
|
||||
|
||||
const mobileAnalyticsClient = new AMA.Manager({
|
||||
appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
appTitle: 'xxxxxxxxxx'
|
||||
})
|
||||
if (process.env.NODE_ENV === 'production' && ConfigManager.default.get().amaEnabled) {
|
||||
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
|
||||
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
})
|
||||
const mobileAnalyticsClient = new AMA.Manager({
|
||||
appId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
appTitle: 'xxxxxxxxxx'
|
||||
})
|
||||
}
|
||||
|
||||
function initAwsMobileAnalytics () {
|
||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
||||
AWS.config.credentials.get((err) => {
|
||||
if (!err) {
|
||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
||||
recordDynamicCustomEvent('APP_STARTED')
|
||||
recordStaticCustomEvent()
|
||||
}
|
||||
})
|
||||
recordStaticCustomEvent()
|
||||
}
|
||||
|
||||
function recordDynamitCustomEvent (type) {
|
||||
function recordDynamicCustomEvent (type) {
|
||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
||||
mobileAnalyticsClient.recordEvent(type)
|
||||
}
|
||||
|
||||
function recordStaticCustomEvent () {
|
||||
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
|
||||
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
|
||||
uiColorTheme: ConfigManager.default.get().ui.theme
|
||||
})
|
||||
@@ -33,5 +38,5 @@ function recordStaticCustomEvent () {
|
||||
|
||||
module.exports = {
|
||||
initAwsMobileAnalytics,
|
||||
recordDynamitCustomEvent
|
||||
recordDynamicCustomEvent
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import _ from 'lodash'
|
||||
import RcParser from 'browser/lib/RcParser'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const win = global.process.platform === 'win32'
|
||||
const electron = require('electron')
|
||||
const { ipcRenderer } = electron
|
||||
const consts = require('browser/lib/consts')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
@@ -14,6 +18,7 @@ export const DEFAULT_CONFIG = {
|
||||
navWidth: 200,
|
||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||
amaEnabled: true,
|
||||
hotkey: {
|
||||
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
||||
@@ -24,18 +29,18 @@ export const DEFAULT_CONFIG = {
|
||||
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
},
|
||||
editor: {
|
||||
theme: 'default',
|
||||
theme: 'base16-light',
|
||||
keyMap: 'sublime',
|
||||
fontSize: '14',
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
|
||||
indentType: 'space',
|
||||
indentSize: '2',
|
||||
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
fontFamily: 'Lato',
|
||||
codeBlockTheme: 'elegant',
|
||||
fontFamily: win ? 'Segoe UI' : 'Lato',
|
||||
codeBlockTheme: 'dracula',
|
||||
lineNumber: true
|
||||
}
|
||||
}
|
||||
@@ -55,17 +60,17 @@ function _save (config) {
|
||||
}
|
||||
|
||||
function get () {
|
||||
let config = window.localStorage.getItem('config')
|
||||
const rawStoredConfig = window.localStorage.getItem('config')
|
||||
const storedConfig = Object.assign({}, DEFAULT_CONFIG, JSON.parse(rawStoredConfig))
|
||||
let config = storedConfig
|
||||
|
||||
try {
|
||||
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)
|
||||
const boostnotercConfig = RcParser.parse()
|
||||
config = assignConfigValues(storedConfig, boostnotercConfig)
|
||||
|
||||
if (!validate(config)) throw new Error('INVALID CONFIG')
|
||||
} catch (err) {
|
||||
console.warn('Boostnote resets the malformed configuration.')
|
||||
console.warn('Boostnote resets the invalid configuration.')
|
||||
config = DEFAULT_CONFIG
|
||||
_save(config)
|
||||
}
|
||||
@@ -124,6 +129,15 @@ function set (updates) {
|
||||
})
|
||||
}
|
||||
|
||||
function assignConfigValues (originalConfig, rcConfig) {
|
||||
let config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
||||
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||
return config
|
||||
}
|
||||
|
||||
export default {
|
||||
get,
|
||||
set,
|
||||
|
||||
@@ -2,6 +2,7 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const _ = require('lodash')
|
||||
const sander = require('sander')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
/**
|
||||
* @description To copy an image and return the path.
|
||||
@@ -12,17 +13,17 @@ const sander = require('sander')
|
||||
function copyImage (filePath, storageKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
|
||||
const storage = _.find(cachedStorageList, {key: storageKey})
|
||||
if (storage === undefined) throw new Error('Target storage doesn\'t exist.')
|
||||
const targetStorage = storage
|
||||
const targetStorage = findStorage(storageKey)
|
||||
|
||||
const inputImage = fs.createReadStream(filePath)
|
||||
const imageName = path.basename(filePath)
|
||||
const outputImage = fs.createWriteStream(path.join(targetStorage.path, 'images', imageName))
|
||||
const imageExt = path.extname(filePath)
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const basename = `${imageName}${imageExt}`
|
||||
const imageDir = path.join(targetStorage.path, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const outputImage = fs.createWriteStream(path.join(imageDir, basename))
|
||||
inputImage.pipe(outputImage)
|
||||
resolve(`${encodeURI(targetStorage.path)}/images/${encodeURI(imageName)}`)
|
||||
resolve(basename)
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
@@ -29,11 +30,7 @@ function createFolder (storageKey, input) {
|
||||
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.')
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ const _ = require('lodash')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
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
|
||||
input.isTrashed = !!input.isTrashed
|
||||
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
@@ -37,11 +39,7 @@ function createNote (storageKey, input) {
|
||||
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.')
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
|
||||
const resolveStorageNotes = require('./resolveStorageNotes')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const sander = require('sander')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
@@ -21,11 +22,7 @@ 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.')
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@ const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const sander = require('sander')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
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.')
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
@@ -4,17 +4,13 @@ const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const keygen = require('browser/lib/keygen')
|
||||
const sander = require('sander')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
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})
|
||||
oldStorage = findStorage(storageKey)
|
||||
newStorage = findStorage(newStorageKey)
|
||||
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const _ = require('lodash')
|
||||
const resolveStorageData = require('./resolveStorageData')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {String} name
|
||||
|
||||
@@ -2,6 +2,7 @@ const resolveStorageData = require('./resolveStorageData')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const CSON = require('@rokt33r/season')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
function validateInput (input) {
|
||||
let validatedInput = {}
|
||||
@@ -21,6 +22,10 @@ function validateInput (input) {
|
||||
validatedInput.isStarred = !!input.isStarred
|
||||
}
|
||||
|
||||
if (input.isTrashed != null) {
|
||||
validatedInput.isTrashed = !!input.isTrashed
|
||||
}
|
||||
|
||||
validatedInput.type = input.type
|
||||
switch (input.type) {
|
||||
case 'MARKDOWN_NOTE':
|
||||
@@ -64,11 +69,7 @@ function updateNote (storageKey, noteKey, input) {
|
||||
if (input == null) throw new Error('No input found.')
|
||||
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.')
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
@@ -101,6 +102,7 @@ function updateNote (storageKey, noteKey, input) {
|
||||
noteData.createdAt = new Date()
|
||||
noteData.updatedAt = new Date()
|
||||
noteData.isStarred = false
|
||||
noteData.isTrashed = false
|
||||
noteData.tags = []
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class CreateFolderModal extends React.Component {
|
||||
}
|
||||
|
||||
confirm () {
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_FOLDER')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_FOLDER')
|
||||
if (this.state.name.trim().length > 0) {
|
||||
let { storage } = this.props
|
||||
let input = {
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
padding 0 40px
|
||||
|
||||
.header
|
||||
height 70px
|
||||
height 80px
|
||||
margin-bottom 10px
|
||||
margin-top 20px
|
||||
font-size 18px
|
||||
@@ -15,23 +14,27 @@
|
||||
background-color $ui-backgroundColor
|
||||
color $ui-text-color
|
||||
|
||||
.title
|
||||
font-size 36px
|
||||
font-weight 600
|
||||
|
||||
.control-folder-label
|
||||
text-align left
|
||||
font-size 12px
|
||||
font-size 14px
|
||||
color $ui-text-color
|
||||
|
||||
.control-folder-input
|
||||
display block
|
||||
height 30px
|
||||
width 420px
|
||||
height 40px
|
||||
width 490px
|
||||
padding 0 5px
|
||||
margin 10px auto 15px
|
||||
margin 10px 0
|
||||
border 1px solid #C9C9C9 // TODO: use variable.
|
||||
border-radius 2px
|
||||
background-color transparent
|
||||
outline none
|
||||
vertical-align middle
|
||||
font-size 14px
|
||||
font-size 16px
|
||||
&:disabled
|
||||
background-color $ui-input--disabled-backgroundColor
|
||||
&:focus, &:active
|
||||
@@ -39,14 +42,13 @@
|
||||
|
||||
.control-confirmButton
|
||||
display block
|
||||
float right
|
||||
height 30px
|
||||
width 100px
|
||||
height 35px
|
||||
width 140px
|
||||
border none
|
||||
border-radius 2px
|
||||
padding 0 25px
|
||||
margin 20px auto
|
||||
font-size 12px
|
||||
font-size 14px
|
||||
colorPrimaryButton()
|
||||
|
||||
body[data-theme="dark"]
|
||||
@@ -56,7 +58,6 @@ body[data-theme="dark"]
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
padding 0 40px
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
export default class DeleteArticleModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.confirmHandler = (e) => this.handleYesButtonClick()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ReactDOM.findDOMNode(this.refs.no).focus()
|
||||
ipc.on('modal-confirm', this.confirmHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipc.removeListener('modal-confirm', this.confirmHandler)
|
||||
}
|
||||
|
||||
handleNoButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handleYesButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='DeleteArticleModal modal'>
|
||||
<div className='title'><i className='fa fa-fw fa-trash' /> Delete an article.</div>
|
||||
|
||||
<div className='message'>Do you really want to delete?</div>
|
||||
|
||||
<div className='control'>
|
||||
<button ref='no' onClick={(e) => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close' /> No</button>
|
||||
<button ref='yes' onClick={(e) => this.handleYesButtonClick(e)} className='danger'><i className='fa fa-fw fa-check' /> Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeleteArticleModal.propTypes = {
|
||||
action: PropTypes.object,
|
||||
articleKey: PropTypes.string,
|
||||
close: PropTypes.func
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class InitModal extends React.Component {
|
||||
} else {
|
||||
return dataApi
|
||||
.createFolder(data.storage.key, {
|
||||
color: '#6AA5E9',
|
||||
color: '#1278BD',
|
||||
name: 'Default'
|
||||
})
|
||||
.then((_data) => {
|
||||
@@ -122,19 +122,6 @@ class InitModal extends React.Component {
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
let defaultMarkdownNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: data.storage.folders[0].key,
|
||||
title: 'Welcome to Boostnote :)',
|
||||
content: '# Welcome to Boostnote :)\nThis is a markdown note.\n\nClick to edit this note.'
|
||||
})
|
||||
.then((note) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
let defaultSnippetNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
@@ -145,12 +132,12 @@ class InitModal extends React.Component {
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Hello World</h1>\n</body>\n</html>'
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: 'var html = document.getElementById(\'hello\').innerHTML\n\nconsole.log(html)'
|
||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -160,6 +147,19 @@ class InitModal extends React.Component {
|
||||
note: note
|
||||
})
|
||||
})
|
||||
let defaultMarkdownNote = dataApi
|
||||
.createNote(data.storage.key, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: data.storage.folders[0].key,
|
||||
title: 'Welcome to Boostnote!',
|
||||
content: '# Welcome to Boostnote! \n### _Click to edit this note._\n\n---\n\nBoostnote is an *open source* note-taking app. \nRepository is published on [GitHub](https://github.com/BoostIO/Boostnote), and tweeting everyday on [@Boostnoteapp](https://twitter.com/boostnoteapp)!\n\n## Features \n- [x] No Internet and Registration Required. \n- [ ] Quick search and copy the content of note. `macOS: Cmd + Alt + S / windows: Ctrl + Alt + S` \n- [ ] Markdown & Snippet note. \n- [ ] Available for `vim` and `emacs` mode. \n- [ ] Choose your favorite theme on UI, Editor and Code Block! \n--- \n\n- Copy Codeblock on Markdown Preview.\n```javascript\nvar boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)\n```'
|
||||
})
|
||||
.then((note) => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.resolve(defaultSnippetNote)
|
||||
.then(defaultMarkdownNote)
|
||||
|
||||
@@ -24,8 +24,8 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_ALLNOTE')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
let { storage, folder, dispatch, location } = this.props
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
@@ -56,8 +56,8 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamitCustomEvent('ADD_ALLNOTE')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
let { storage, folder, dispatch, location } = this.props
|
||||
|
||||
dataApi
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
font-size 18px
|
||||
line-height 50px
|
||||
padding 0 15px
|
||||
background-color $ui-backgroundColor
|
||||
border-bottom solid 1px $ui-borderColor
|
||||
color $ui-text-color
|
||||
margin-bottom 20px
|
||||
|
||||
.title
|
||||
font-size 36px
|
||||
font-weight 600
|
||||
|
||||
.control
|
||||
padding 25px 15px 15px
|
||||
padding 25px 0px
|
||||
text-align center
|
||||
|
||||
.control-button
|
||||
width 220px
|
||||
width 240px
|
||||
height 220px
|
||||
margin 0 15px
|
||||
border $ui-border
|
||||
@@ -30,8 +33,8 @@
|
||||
colorPrimaryButton()
|
||||
|
||||
.control-button-icon
|
||||
font-size 50px
|
||||
margin-bottom 15px
|
||||
font-size 48px
|
||||
margin-bottom 25px
|
||||
|
||||
.control-button-label
|
||||
font-size 18px
|
||||
@@ -49,8 +52,6 @@ body[data-theme="dark"]
|
||||
modalDark()
|
||||
|
||||
.header
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
.control-button
|
||||
|
||||
@@ -31,10 +31,15 @@
|
||||
|
||||
.group-section-control
|
||||
flex 1
|
||||
margin-left 5px
|
||||
|
||||
.group-section-control select
|
||||
outline none
|
||||
border 1px solid $ui-borderColor
|
||||
font-size 16px
|
||||
height 30px
|
||||
width 250px
|
||||
margin-bottom 5px
|
||||
background-color transparent
|
||||
|
||||
.group-section-control-input
|
||||
@@ -77,14 +82,16 @@
|
||||
margin-right 10px
|
||||
|
||||
.group-control-rightButton
|
||||
float right
|
||||
position absolute
|
||||
top 10px
|
||||
right 20px
|
||||
colorPrimaryButton()
|
||||
border none
|
||||
border-radius 2px
|
||||
font-size $tab--button-font-size
|
||||
height 35px
|
||||
width 100px
|
||||
margin-right 10px
|
||||
height 40px
|
||||
width 120px
|
||||
padding 0 15px
|
||||
|
||||
.group-hint
|
||||
border $ui-border
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './InfoTab.styl'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import store from 'browser/main/store'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
|
||||
const electron = require('electron')
|
||||
const { shell, remote } = electron
|
||||
@@ -11,6 +14,7 @@ class InfoTab extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: this.props.config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +23,29 @@ class InfoTab extends React.Component {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
handleConfigChange (e) {
|
||||
const newConfig = { amaEnabled: this.refs.amaEnabled.checked }
|
||||
|
||||
this.setState({ config: newConfig })
|
||||
}
|
||||
|
||||
handleSaveButtonClick (e) {
|
||||
let newConfig = {
|
||||
amaEnabled: this.state.config.amaEnabled
|
||||
}
|
||||
|
||||
if (!newConfig.amaEnabled) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
|
||||
}
|
||||
|
||||
ConfigManager.set(newConfig)
|
||||
|
||||
store.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: newConfig
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
@@ -54,7 +81,7 @@ class InfoTab extends React.Component {
|
||||
<li>
|
||||
<a href='https://github.com/BoostIO/Boostnote/issues'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>GitHub Issues</a> : We'd love to hear your feedback 🙌
|
||||
>GitHub Issues</a> : We'd love to hear your feedback 🙌
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md'
|
||||
@@ -68,6 +95,18 @@ class InfoTab extends React.Component {
|
||||
License: GPL v3
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<div styleName='policy'>Data collection policy</div>
|
||||
<div>We collect only the number of DAU for Boostnote and **DO NOT collect** any detail information such as your note content.</div>
|
||||
<div>You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
|
||||
<div>These data are only used for Boostnote improvements.</div>
|
||||
<input onChange={(e) => this.handleConfigChange(e)}
|
||||
checked={this.state.config.amaEnabled}
|
||||
ref='amaEnabled'
|
||||
type='checkbox'
|
||||
/>
|
||||
Enable to send analytics to our servers<br />
|
||||
<button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>Save</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,15 @@
|
||||
a
|
||||
color #4E8EC6
|
||||
text-decoration none
|
||||
|
||||
|
||||
.policy
|
||||
width 100%
|
||||
font-size 20px
|
||||
margin-bottom 10px
|
||||
|
||||
.policy-submit
|
||||
margin-top 10px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color alpha($tab--dark-text-color, 80%)
|
||||
color alpha($tab--dark-text-color, 80%)
|
||||
|
||||
@@ -4,9 +4,10 @@ top-bar--height = 50px
|
||||
|
||||
.root
|
||||
modal()
|
||||
max-width 800px
|
||||
min-height 500px
|
||||
height 80%
|
||||
max-width 100vw
|
||||
min-height 100vh
|
||||
height 100vh
|
||||
width 100vw
|
||||
overflow hidden
|
||||
position relative
|
||||
|
||||
@@ -25,22 +26,22 @@ top-bar--height = 50px
|
||||
top top-bar--height
|
||||
left 0
|
||||
width 140px
|
||||
margin-left 30px
|
||||
margin-left 10px
|
||||
margin-top 20px
|
||||
background-color $ui-backgroundColor
|
||||
|
||||
.nav-button
|
||||
font-size 14px
|
||||
text-align left
|
||||
width 100px
|
||||
margin 4px 0
|
||||
padding 5px 0
|
||||
width 120px
|
||||
margin 5px 0
|
||||
padding 7px 0
|
||||
padding-left 10px
|
||||
border none
|
||||
border-radius 2px
|
||||
background-color transparent
|
||||
color $ui-text-color
|
||||
font-size 14px
|
||||
font-size 16px
|
||||
|
||||
.nav-button--active
|
||||
@extend .nav-button
|
||||
|
||||
@@ -298,8 +298,8 @@ class StorageItem extends React.Component {
|
||||
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: 'Unlink Storage',
|
||||
detail: 'This work just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
|
||||
buttons: ['Confirm', 'Cancel']
|
||||
detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.',
|
||||
buttons: ['Unlink', 'Cancel']
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import dataApi from 'browser/main/lib/dataApi'
|
||||
import StorageItem from './StorageItem'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const { shell, remote } = electron
|
||||
|
||||
function browseFolder () {
|
||||
let dialog = remote.dialog
|
||||
@@ -50,6 +50,11 @@ class StoragesTab extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleLinkClick (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
renderList () {
|
||||
let { data, boundingBox } = this.props
|
||||
|
||||
@@ -161,7 +166,10 @@ class StoragesTab extends React.Component {
|
||||
<option value='FILESYSTEM'>File System</option>
|
||||
</select>
|
||||
<div styleName='addStorage-body-section-type-description'>
|
||||
3rd party cloud integration(such as Google Drive and Dropbox) will be available soon.
|
||||
3rd party cloud integration:
|
||||
<a href='https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>Cloud-Syncing</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ $tab--button-font-size = 14px
|
||||
$tab--dark-text-color = #E5E5E5
|
||||
|
||||
.header
|
||||
font-size 24px
|
||||
margin-bottom 30px
|
||||
font-size 36px
|
||||
margin-bottom 60px
|
||||
|
||||
body[data-theme="dark"]
|
||||
.header
|
||||
|
||||
@@ -90,9 +90,8 @@ class UiTab extends React.Component {
|
||||
<div styleName='group'>
|
||||
<div styleName='group-header'>UI</div>
|
||||
|
||||
<div styleName='group-header2'>Theme</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
Color Theme
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.theme}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -192,7 +191,7 @@ class UiTab extends React.Component {
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
Switching Preview
|
||||
Switch to Preview
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.editor.switchPreview}
|
||||
@@ -200,7 +199,7 @@ class UiTab extends React.Component {
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
>
|
||||
<option value='BLUR'>When Editor Blurred</option>
|
||||
<option value='RIGHTCLICK'>When Right Clicking</option>
|
||||
<option value='RIGHTCLICK'>On Right Click</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +217,7 @@ class UiTab extends React.Component {
|
||||
<option value='vim'>vim</option>
|
||||
<option value='emacs'>emacs</option>
|
||||
</select>
|
||||
<span styleName='note-for-keymap'>Please reload boostnote after you change the keymap</span>
|
||||
<span styleName='note-for-keymap'>Please restart boostnote after you change the keymap</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -271,7 +270,7 @@ class UiTab extends React.Component {
|
||||
ref='previewLineNumber'
|
||||
type='checkbox'
|
||||
/>
|
||||
Code block line numbering
|
||||
Show line numbers for preview code blocks
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,12 @@ class Preferences extends React.Component {
|
||||
|
||||
switch (this.state.currentTab) {
|
||||
case 'INFO':
|
||||
return <InfoTab />
|
||||
return (
|
||||
<InfoTab
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
case 'HOTKEY':
|
||||
return (
|
||||
<HotkeyTab
|
||||
@@ -116,7 +121,7 @@ class Preferences extends React.Component {
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='top-bar'>
|
||||
<p>Your menu for Boostnote</p>
|
||||
<p>Your preferences for Boostnote</p>
|
||||
</div>
|
||||
<ModalEscButton handleEscButtonClick={(e) => this.handleEscButtonClick(e)} />
|
||||
<div styleName='nav'>
|
||||
|
||||
@@ -11,7 +11,8 @@ function defaultDataMap () {
|
||||
starredSet: new Set(),
|
||||
storageNoteMap: new Map(),
|
||||
folderNoteMap: new Map(),
|
||||
tagNoteMap: new Map()
|
||||
tagNoteMap: new Map(),
|
||||
trashedSet: new Set()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +35,10 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
}
|
||||
|
||||
let storageNoteList = state.storageNoteMap.get(note.storage)
|
||||
if (storageNoteList == null) {
|
||||
storageNoteList = new Set(storageNoteList)
|
||||
@@ -78,6 +83,15 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
} else {
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Update storageNoteMap if oldNote doesn't exist
|
||||
if (oldNote == null) {
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
@@ -163,6 +177,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(originKey)
|
||||
}
|
||||
|
||||
if (originNote.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(originKey)
|
||||
}
|
||||
|
||||
// From storageNoteMap
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
let noteSet = state.storageNoteMap.get(originNote.storage)
|
||||
@@ -199,6 +218,15 @@ function data (state = defaultDataMap(), action) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet.add(uniqueKey)
|
||||
} else {
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Update storageNoteMap if oldNote doesn't exist
|
||||
if (oldNote == null) {
|
||||
state.storageNoteMap = new Map(state.storageNoteMap)
|
||||
@@ -283,6 +311,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(uniqueKey)
|
||||
}
|
||||
|
||||
if (targetNote.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(uniqueKey)
|
||||
}
|
||||
|
||||
// From folderNoteMap
|
||||
let folderKey = targetNote.storage + '-' + targetNote.folder
|
||||
state.folderNoteMap = new Map(state.folderNoteMap)
|
||||
@@ -348,6 +381,11 @@ function data (state = defaultDataMap(), action) {
|
||||
state.starredSet.delete(noteKey)
|
||||
}
|
||||
|
||||
if (note.isTrashed) {
|
||||
state.trashedSet = new Set(state.trashedSet)
|
||||
state.trashedSet.delete(noteKey)
|
||||
}
|
||||
|
||||
// Delete key from tag map
|
||||
state.tagNoteMap = new Map(state.tagNoteMap)
|
||||
note.tags.forEach((tag) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ $danger-color = #c9302c
|
||||
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||
|
||||
// Layouts
|
||||
$statusBar-height = 24px
|
||||
$statusBar-height = 36px
|
||||
$sideNav-width = 200px
|
||||
$sideNav--folded-width = 44px
|
||||
$topBar-height = 60px
|
||||
@@ -150,18 +150,20 @@ modal()
|
||||
position relative
|
||||
z-index $modal-z-index
|
||||
width 100%
|
||||
margin-left 80px
|
||||
margin-right 80px
|
||||
margin-bottom 80px
|
||||
margin-top 100px
|
||||
background-color $modal-background
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
box-shadow 0 0 1px rgba(76,86,103,.15), 0 2px 18px rgba(31,37,50,.22)
|
||||
|
||||
topBarButtonLight()
|
||||
width 34px
|
||||
height 34px
|
||||
border-radius 17px
|
||||
font-size 14px
|
||||
margin 13px 7px
|
||||
padding-top 7px
|
||||
margin 15px 7px
|
||||
border none
|
||||
color $ui-button-color
|
||||
fill $ui-button-color
|
||||
@@ -169,7 +171,7 @@ topBarButtonLight()
|
||||
&:active
|
||||
border-color $ui-button--active-backgroundColor
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
background-color alpha($ui-button--hover-backgroundColor, 60%)
|
||||
.control-lockButton-tooltip
|
||||
opacity 1
|
||||
|
||||
@@ -182,7 +184,7 @@ $ui-dark-noteDetail-backgroundColor = #2D3033
|
||||
$ui-dark-tag-backgroundColor = #3A404C
|
||||
$dark-background-color = lighten($ui-dark-backgroundColor, 10%)
|
||||
$ui-dark-text-color = #DDDDDD
|
||||
$ui-dark-button--active-color = white
|
||||
$ui-dark-button--active-color = #f4f4f4
|
||||
$ui-dark-button--active-backgroundColor = #3A404C
|
||||
$ui-dark-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%)
|
||||
$ui-dark-button--focus-borderColor = lighten(#369DCD, 25%)
|
||||
@@ -232,10 +234,12 @@ topBarButtonDark()
|
||||
color $ui-dark-topbar-button-color
|
||||
&:hover
|
||||
background-color $dark-default-button-background--hover
|
||||
color $ui-dark-tooltip-text-color
|
||||
&:active
|
||||
border-color $ui-dark-button--focus-borderColor
|
||||
&:active:hover
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-tooltip-text-color
|
||||
&:focus
|
||||
border-color $ui-button--focus-borderColor
|
||||
|
||||
@@ -259,4 +263,3 @@ modalDark()
|
||||
background-color $ui-dark-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
box-shadow 2px 2px 10px black
|
||||
|
||||
@@ -69,3 +69,21 @@ Pull requestをすることはその変化分のコードの著作権をMaisin&C
|
||||
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
|
||||
もし、このアプリケーションに料金が発生する時は、Boostnote専用のCloud storageの提供やMobile appとの連動、何か特殊なプレミアム機能の提供など形になります。
|
||||
現在考えられているのは、GPL v3の場合、他のライセンスとの互換が不可能であるため、もしより自由なLicense(BSD, MIT)に変える時に改めて著作権者としてライセンスし直す選択肢を残すイメージです。
|
||||
|
||||
---
|
||||
|
||||
# Contributing to Boostnote (Simplified Chinese)
|
||||
|
||||
### 当您创建一个issue的时候
|
||||
我们对您的issue格式没有要求,但是我们有一个请求:
|
||||
|
||||
**如果可能,请在开发者模式打开的情况下,为我们提供屏幕截图**
|
||||
|
||||
(您可以通过`Ctrl+Shift+I`打开开发者模式)。
|
||||
感谢您对我们的支持。
|
||||
|
||||
### 关于您提供的Pull Request的著作权(版权)问题
|
||||
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给Maisin&Co。
|
||||
|
||||
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
|
||||
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Build
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), and [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md).
|
||||
|
||||
## Environments
|
||||
* npm: 4.x
|
||||
@@ -43,6 +44,8 @@ You can build the program by using `grunt`. However, we don't recommend this bec
|
||||
|
||||
So, we've prepared a separate script which just makes an executable file.
|
||||
|
||||
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# How to debug Boostnote (Electron app)
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), and [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md)
|
||||
|
||||
Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome.
|
||||
|
||||
You can toggle the `Developer Tools` like this:
|
||||
|
||||
@@ -37,6 +37,8 @@ Gruntを使います。
|
||||
|
||||
それで、実行ファイルを作るスクリプトを用意しておきました。
|
||||
|
||||
このビルドはnpm v5.3.0では動かないのでv5.2.0で動かす必要があります。
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Testing for Boostnote
|
||||
## e2eテスト
|
||||
Boostnoteには[ava](https://github.com/avajs/ava)と[spectron](https://github.com/electron/spectron)で書かれたe2eテストがあります。
|
||||
|
||||
### 実行方法
|
||||
以下のe2eテストのコマンドがあります。
|
||||
|
||||
```
|
||||
$ yarn run test:e2e
|
||||
```
|
||||
|
||||
もう一つのテストコマンドと分けた理由は、travisCIの都合です。
|
||||
|
||||
### travisCIでは
|
||||
travisCIではmasterブランチで飲みe2eテストを実行するようにしています。もし興味がある方は `.travis.yml`をご覧ください。
|
||||
@@ -37,6 +37,8 @@ yarn run hot
|
||||
|
||||
그래서, 실행파일만을 만드는 스크립트를 준비해 뒀습니다.
|
||||
|
||||
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
@@ -43,6 +43,8 @@ $ yarn run dev-start
|
||||
|
||||
Мы подготовили отдельный скрипт, который просто создает исполняемый файл:
|
||||
|
||||
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Тестирование для Boostnote
|
||||
## Тестирование e2e
|
||||
Существуют тесты e2e для Boostnote, написанные на [ava](https://github.com/avajs/ava) и [spectron](https://github.com/electron/spectron).
|
||||
|
||||
### Как запустить
|
||||
Для тестирование e2e существует команда:
|
||||
|
||||
```
|
||||
$ yarn run test:e2e
|
||||
```
|
||||
|
||||
Причина, по которой я использую другую команду тестирования - это удобство travisCI.
|
||||
|
||||
### TravisCI
|
||||
Я установил тесты e2e, запущенные на travisCI, только в ветке master. Если вас это интересует, ознакомьтесь с файлом .travis.yml
|
||||
|
||||
---
|
||||
|
||||
Special thanks:
|
||||
Translated by @AlexanderBelkevich
|
||||
@@ -1,15 +0,0 @@
|
||||
# Testing for Boostnote
|
||||
## e2e testing
|
||||
There is e2e tests for Boostnote written in [ava](https://github.com/avajs/ava) and [spectron](https://github.com/electron/spectron).
|
||||
|
||||
### How to run
|
||||
There is a command for e2e testing bellow:
|
||||
|
||||
```
|
||||
$ yarn run test:e2e
|
||||
```
|
||||
|
||||
The reason why I seperate aother test command is because of convenience of travisCI.
|
||||
|
||||
### On travisCI
|
||||
I set e2e tests running on travisCI only master branch. If you're interested in it, please take a look at .travis.yml
|
||||
56
docs/zh_CN/build.md
Normal file
56
docs/zh_CN/build.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 构建Boostnote
|
||||
|
||||
## 环境
|
||||
* npm: 4.x
|
||||
* node: 7.x
|
||||
|
||||
因为`$ grand pre-build`的问题,您只能使用`npm v4.x`而不能使用`npm v5.x`。
|
||||
|
||||
## 开发
|
||||
|
||||
我们使用Webpack HMR来开发Boostnote。
|
||||
在代码根目录下运行下列指令可以以默认配置运行Boostnote。
|
||||
|
||||
### 首先使用yarn安装所需的依赖包。
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### 接着编译并且运行Boostnote。
|
||||
|
||||
```
|
||||
$ yarn run dev-start
|
||||
```
|
||||
|
||||
这个指令相当于在两个终端内同时运行`yarn run webpack`和`yarn run hot`。
|
||||
|
||||
如果出现错误`Failed to load resource: net::ERR_CONNECTION_REFUSED`,请尝试重新运行Boostnote。
|
||||

|
||||
|
||||
### 然后您就可以进行开发了
|
||||
|
||||
当您对代码作出更改的时候,`webpack`会自动抓取并应用所有代码更改。
|
||||
|
||||
> ### 提示
|
||||
> 在如下情况中,您可能需要重新运行Boostnote才能应用代码更改
|
||||
> 1. 当您在修改了一个组件的构造函数的时候When editing a constructor method of a component
|
||||
> 2. 当您新建了一个CSS类的时候(其实这和第1项是相同的,因为每个CSS类都需在组件的构造函数中被重写)
|
||||
|
||||
## 部署
|
||||
|
||||
我们使用Grunt来自动部署Boostnote。
|
||||
因为部署需要协同设计(codesign)与验证码(authenticode),所以您可以但我们不建议通过`grunt`来部署。
|
||||
所以我们准备了一个脚本文件来生成执行文件。
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
您只能使用`npm v5.2.0`而不能使用`npm v5.3.0`。
|
||||
|
||||
接下来您就可以在`dist`目录中找到可执行文件。
|
||||
|
||||
> ### 提示
|
||||
> 因为此可执行文件并没有被注册,所以自动更新不可用。
|
||||
> 如果需要,您也可将协同设计(codesign)与验证码(authenticode)使用于这个可执行文件中。
|
||||
15
docs/zh_CN/debug.md
Normal file
15
docs/zh_CN/debug.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 在Boostnote上Debug
|
||||
|
||||
Boostnote基于Electron,所以Boostnote上的开发者工具和Google Chrome相同。
|
||||
|
||||
您可以像这样或者按下快捷键`Ctrl+Shift+I`打开开发者工具:
|
||||

|
||||
|
||||
开发者工具大概形如这样:
|
||||

|
||||
|
||||
您可以在`console`选项卡中找到运行错误,
|
||||
也可以像这样在`debugger`选项卡中设置断点去分步Debug:
|
||||

|
||||
|
||||
关于具体如何使用开发者工具,详见[Chrome 官档](https://developer.chrome.com/devtools)。(如果您在中国大陆,您可能需要一个VPN才能正常访问)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user