diff --git a/.eslintrc b/.eslintrc index 9177344c..a1646659 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,5 +12,10 @@ "react/no-find-dom-node": "warn", "react/no-render-return-value": "warn", "react/no-deprecated": "warn" + }, + "globals": { + "FileReader": true, + "localStorage": true, + "fetch": true } } diff --git a/.gitignore b/.gitignore index 1c447c6d..9f75dd1b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ node_modules/* /compiled /secret *.log +.vscode +.idea \ No newline at end of file diff --git a/.snapcraft/travis_snapcraft.cfg b/.snapcraft/travis_snapcraft.cfg new file mode 100644 index 00000000..9c97b30a Binary files /dev/null and b/.snapcraft/travis_snapcraft.cfg differ diff --git a/.travis.yml b/.travis.yml index 37322896..013169e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,20 @@ language: node_js node_js: - - 'stable' - - 'lts/*' - -script: npm run lint && npm run test + - stable + - lts/* +script: + - npm run lint && npm run test + - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi' +after_success: + - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv + -in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d +sudo: required +services: + - docker +deploy: + 'on': + branch: master + provider: script + script: if [ ${TRAVIS_NODE_VERSION} = "stable" ];then docker run -v $(pwd):$(pwd) -t snapcore/snapcraft sh -c "apt update -qq + && cd $(pwd) && snapcraft && snapcraft push *.snap --release edge"; fi + skip_cleanup: true diff --git a/Backers.md b/Backers.md index 49d1a0ea..18d221bf 100644 --- a/Backers.md +++ b/Backers.md @@ -1,9 +1,72 @@ -Become a [backer](https://salt.bountysource.com/teams/boostnote) and support Boostnote! -You can support Boostnote from $ 5 a month! +

Sponsors & Backers

-# Backers -[Kazu Yokomizo](https://twitter.com/kazup_bot) +Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider: -kolchan11 +- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio) -RonWalker22 +--- + +## Backers via OpenCollective + +### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259) +- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/. + +### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257) +- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/. + +### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258) +- Get your name and Url (or E-mail) on Readme.md on GitHub. + +### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176) +- [Ralph03](https://opencollective.com/ralph03) + +- [Nikolas Dan](https://opencollective.com/nikolas-dan) + +### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175) +- [Yeojong Kim](https://twitter.com/yeojoy) + +- [Scotia Draven](https://opencollective.com/scotia-draven) + +- [A. J. Vargas](https://opencollective.com/aj-vargas) + +### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors +- Ryosuke Tamura - $30 + +- tatoosh11 - $10 + +- Alexander Borovkov - $10 + +- spoonhoop - $5 + +- Drew Williams - $2 + +- Andy Shaw - $2 + +- mysafesky -$2 + +--- + +## Backers via Bountysource +https://salt.bountysource.com/teams/boostnote + +- Kuzz - $65 + +- Intense Raiden - $45 + +- ravy22 - $25 + +- trentpolack - $20 + +- hikariru - $10 + +- kolchan11 - $10 + +- RonWalker22 - $10 + +- hocchuc - $5 + +- Adam - $5 + +- Steve - $5 + +- evmin - $5 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index a4ec1b43..be4b7e4f 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1 +1,10 @@ -Please paste some **screenshots** with the **developer tool** open if you report a bug. + + + diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 66e647d6..a354cee1 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -1,9 +1,11 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' 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' +import fs from 'fs' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -57,6 +59,7 @@ export default class CodeEditor extends React.Component { tabSize: this.props.indentSize, indentWithTabs: this.props.indentType !== 'space', keyMap: this.props.keyMap, + scrollPastEnd: this.props.scrollPastEnd, inputStyle: 'textarea', dragDrop: false, extraKeys: { @@ -66,7 +69,7 @@ export default class CodeEditor extends React.Component { if (cm.somethingSelected()) cm.indentSelection('add') else { const tabs = cm.getOption('indentWithTabs') - if (line.trimLeft() === '- ' || line.trimLeft() === '* ' || line.trimLeft() === '+ ') { + if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) { cm.execCommand('goLineStart') if (tabs) { cm.execCommand('insertTab') @@ -102,15 +105,25 @@ export default class CodeEditor extends React.Component { this.editor.on('change', this.changeHandler) this.editor.on('paste', this.pasteHandler) - let editorTheme = document.getElementById('editorTheme') + const 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) + CodeMirror.Vim.map('ZZ', ':q', 'normal') + } + + 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') + const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) } @@ -145,6 +158,10 @@ export default class CodeEditor extends React.Component { this.editor.setOption('lineNumbers', this.props.displayLineNumbers) } + if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { + this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) + } + if (needRefresh) { this.editor.refresh() } @@ -190,7 +207,7 @@ export default class CodeEditor extends React.Component { } setValue (value) { - let cursor = this.editor.getCursor() + const cursor = this.editor.getCursor() this.editor.setValue(value) this.editor.setCursor(cursor) } @@ -207,9 +224,7 @@ export default class CodeEditor extends React.Component { } insertImageMd (imageMd) { - const textarea = this.editor.getInputField() - const cm = this.editor - cm.replaceSelection(`${textarea.value.substr(0, textarea.selectionStart)}${imageMd}${textarea.value.substr(textarea.selectionEnd)}`) + this.editor.replaceSelection(imageMd) } handlePaste (editor, e) { @@ -217,7 +232,7 @@ export default class CodeEditor extends React.Component { if (!dataTransferItem.type.match('image')) return const blob = dataTransferItem.getAsFile() - let reader = new FileReader() + const reader = new FileReader() let base64data reader.readAsDataURL(blob) @@ -227,16 +242,18 @@ export default class CodeEditor extends React.Component { 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 imageDir = path.join(storagePath, 'images') + if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) + const imagePath = path.join(imageDir, `${imageName}.png`) + fs.writeFile(imagePath, binaryData, 'binary') const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` this.insertImageMd(imageMd) } } render () { - let { className, fontFamily, fontSize } = this.props + const { className, fontSize } = this.props + let fontFamily = this.props.className fontFamily = _.isString(fontFamily) && fontFamily.length > 0 ? [fontFamily].concat(defaultEditorFontFamily) : defaultEditorFontFamily diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 88485207..6deb7bed 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -1,11 +1,11 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './MarkdownEditor.styl' import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import eventEmitter from 'browser/main/lib/eventEmitter' import { findStorage } from 'browser/lib/findStorage' -const _ = require('lodash') class MarkdownEditor extends React.Component { constructor (props) { @@ -70,9 +70,9 @@ class MarkdownEditor extends React.Component { } handleContextMenu (e) { - let { config } = this.props + const { config } = this.props if (config.editor.switchPreview === 'RIGHTCLICK') { - let newStatus = this.state.status === 'PREVIEW' + const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW' this.setState({ @@ -91,9 +91,9 @@ class MarkdownEditor extends React.Component { handleBlur (e) { if (this.state.isLocked) return this.setState({ keyPressed: new Set() }) - let { config } = this.props + const { config } = this.props if (config.editor.switchPreview === 'BLUR') { - let cursorPosition = this.refs.code.editor.getCursor() + const cursorPosition = this.refs.code.editor.getCursor() this.setState({ status: 'PREVIEW' }, () => { @@ -109,7 +109,7 @@ class MarkdownEditor extends React.Component { } handlePreviewMouseUp (e) { - let { config } = this.props + const { config } = this.props if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) { this.setState({ status: 'CODE' @@ -123,15 +123,15 @@ class MarkdownEditor extends React.Component { handleCheckboxClick (e) { e.preventDefault() e.stopPropagation() - let idMatch = /checkbox-([0-9]+)/ - let checkedMatch = /\[x\]/i - let uncheckedMatch = /\[ \]/ + const idMatch = /checkbox-([0-9]+)/ + const checkedMatch = /\[x\]/i + const uncheckedMatch = /\[ \]/ if (idMatch.test(e.target.getAttribute('id'))) { - let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 - let lines = this.refs.code.value + const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 + const lines = this.refs.code.value .split('\n') - let targetLine = lines[lineIndex] + const targetLine = lines[lineIndex] if (targetLine.match(checkedMatch)) { lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]') @@ -163,12 +163,12 @@ class MarkdownEditor extends React.Component { } handleKeyDown (e) { - let { config } = this.props + const { 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) } + const 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' && @@ -207,14 +207,14 @@ class MarkdownEditor extends React.Component { } render () { - let { className, value, config, storageKey } = this.props + const { className, value, config, storageKey } = this.props let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 let editorIndentSize = parseInt(config.editor.indentSize, 10) if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 - let previewStyle = {} + const previewStyle = {} if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' const storage = findStorage(storageKey) @@ -243,6 +243,7 @@ class MarkdownEditor extends React.Component { indentType={config.editor.indentType} indentSize={editorIndentSize} displayLineNumbers={config.editor.displayLineNumbers} + scrollPastEnd={config.editor.scrollPastEnd} storageKey={storageKey} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} @@ -260,6 +261,7 @@ class MarkdownEditor extends React.Component { codeBlockFontFamily={config.editor.fontFamily} lineNumber={config.preview.lineNumber} indentSize={editorIndentSize} + scrollPastEnd={config.editor.scrollPastEnd} ref='preview' onContextMenu={(e) => this.handleContextMenu(e)} tabIndex='0' @@ -267,6 +269,7 @@ class MarkdownEditor extends React.Component { onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseDown={(e) => this.handlePreviewMouseDown(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)} + showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} /> diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 86a63f2f..c9d3d963 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,4 +1,5 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' @@ -10,6 +11,7 @@ import eventEmitter from 'browser/main/lib/eventEmitter' import fs from 'fs' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' +import mdurl from 'mdurl' const { remote } = require('electron') const { app } = remote @@ -32,19 +34,27 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) { font-weight: normal; text-rendering: optimizeLegibility; } +@font-face { + font-family: 'Lato'; + src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */ + url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */ + url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype'); + font-style: normal; + font-weight: 700; + text-rendering: optimizeLegibility; +} ${markdownStyle} body { - font-family: ${fontFamily.join(', ')}; + font-family: '${fontFamily.join("','")}'; font-size: ${fontSize}px; } code { - font-family: ${codeBlockFontFamily.join(', ')}; + font-family: '${codeBlockFontFamily.join("','")}'; background-color: rgba(0,0,0,0.04); - color: #CC305F; } .lineNumber { ${lineNumber && 'display: block !important;'} - font-family: ${codeBlockFontFamily.join(', ')}; + font-family: '${codeBlockFontFamily.join("','")}'; } .clipboardButton { @@ -92,7 +102,7 @@ const OSX = global.process.platform === 'darwin' const defaultFontFamily = ['helvetica', 'arial', 'sans-serif'] if (!OSX) { - defaultFontFamily.unshift('\'Microsoft YaHei\'') + defaultFontFamily.unshift('Microsoft YaHei') defaultFontFamily.unshift('meiryo') } const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] @@ -117,10 +127,10 @@ export default class MarkdownPreview extends React.Component { e.preventDefault() e.stopPropagation() - let anchor = e.target.closest('a') - let href = anchor.getAttribute('href') + const anchor = e.target.closest('a') + const href = anchor.getAttribute('href') if (_.isString(href) && href.match(/^#/)) { - let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length)) + const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length)) if (targetElement != null) { this.getWindow().scrollTo(0, targetElement.offsetTop) } @@ -134,10 +144,12 @@ export default class MarkdownPreview extends React.Component { } handleContextMenu (e) { + if (!this.props.onContextMenu) return this.props.onContextMenu(e) } handleMouseDown (e) { + if (!this.props.onMouseDown) return if (e.target != null) { switch (e.target.tagName) { case 'A': @@ -149,6 +161,7 @@ export default class MarkdownPreview extends React.Component { } handleMouseUp (e) { + if (!this.props.onMouseUp) return if (e.target != null && e.target.tagName === 'A') { return null } @@ -184,6 +197,16 @@ export default class MarkdownPreview extends React.Component { }) } + fixDecodedURI (node) { + if (node && node.children.length === 1 && typeof node.children[0] === 'string') { + const { innerText, href } = node + + node.innerText = mdurl.decode(href) === innerText + ? href + : innerText + } + } + componentDidMount () { this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) @@ -224,6 +247,7 @@ export default class MarkdownPreview extends React.Component { prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockTheme !== this.props.codeBlockTheme || prevProps.lineNumber !== this.props.lineNumber || + prevProps.showCopyNotification !== this.props.showCopyNotification || prevProps.theme !== this.props.theme) { this.applyStyle() this.rewriteIframe() @@ -231,12 +255,13 @@ export default class MarkdownPreview extends React.Component { } applyStyle () { - let { fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props + const { fontSize, lineNumber, codeBlockTheme } = this.props + let { fontFamily, codeBlockFontFamily } = this.props fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 - ? [fontFamily].concat(defaultFontFamily) + ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) : defaultFontFamily codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 - ? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily) + ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) : defaultCodeBlockFontFamily this.setCodeTheme(codeBlockTheme) @@ -247,7 +272,9 @@ export default class MarkdownPreview extends React.Component { theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' ? theme : 'elegant' - this.getWindow().document.getElementById('codeTheme').href = `${appPath}/node_modules/codemirror/theme/${theme}.css` + this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized') + ? `${appPath}/node_modules/codemirror/theme/solarized.css` + : `${appPath}/node_modules/codemirror/theme/${theme}.css` } rewriteIframe () { @@ -262,7 +289,8 @@ export default class MarkdownPreview extends React.Component { el.removeEventListener('click', this.linkClickHandler) }) - let { value, theme, indentSize, codeBlockTheme, storagePath } = this.props + const { theme, indentSize, showCopyNotification, storagePath } = this.props + let { value, codeBlockTheme } = this.props this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) @@ -279,6 +307,7 @@ export default class MarkdownPreview extends React.Component { }) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { + this.fixDecodedURI(el) el.addEventListener('click', this.anchorClickHandler) }) @@ -304,25 +333,32 @@ export default class MarkdownPreview extends React.Component { let syntax = CodeMirror.findModeByName(el.className) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') CodeMirror.requireMode(syntax.mode, () => { - let content = htmlTextHelper.decodeEntities(el.innerHTML) + const content = htmlTextHelper.decodeEntities(el.innerHTML) const copyIcon = document.createElement('i') copyIcon.innerHTML = '' copyIcon.onclick = (e) => { copy(content) - this.notify('Saved to Clipboard!', { - body: 'Paste it wherever you want!', - silent: true - }) + if (showCopyNotification) { + this.notify('Saved to Clipboard!', { + body: 'Paste it wherever you want!', + silent: true + }) + } } el.parentNode.appendChild(copyIcon) el.innerHTML = '' - el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` + if (codeBlockTheme.indexOf('solarized') === 0) { + const [refThema, color] = codeBlockTheme.split(' ') + el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror` + } else { + el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` + } CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize }) }) }) - let opts = {} + const opts = {} // if (this.props.theme === 'dark') { // opts['font-color'] = '#DDD' // opts['line-color'] = '#DDD' @@ -332,7 +368,7 @@ export default class MarkdownPreview extends React.Component { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => { Raphael.setWindow(this.getWindow()) try { - let diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) + const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) el.innerHTML = '' diagram.drawSVG(el, opts) _.forEach(el.querySelectorAll('a'), (el) => { @@ -348,7 +384,7 @@ export default class MarkdownPreview extends React.Component { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => { Raphael.setWindow(this.getWindow()) try { - let diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) + const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) el.innerHTML = '' diagram.drawSVG(el, {theme: 'simple'}) _.forEach(el.querySelectorAll('a'), (el) => { @@ -371,11 +407,11 @@ export default class MarkdownPreview extends React.Component { } scrollTo (targetRow) { - let blocks = this.getWindow().document.querySelectorAll('body>[data-line]') + const blocks = this.getWindow().document.querySelectorAll('body>[data-line]') for (let index = 0; index < blocks.length; index++) { let block = blocks[index] - let row = parseInt(block.getAttribute('data-line')) + const row = parseInt(block.getAttribute('data-line')) if (row > targetRow || index === blocks.length - 1) { block = blocks[index - 1] block != null && this.getWindow().scrollTo(0, block.offsetTop) @@ -405,7 +441,7 @@ export default class MarkdownPreview extends React.Component { } render () { - let { className, style, tabIndex } = this.props + const { className, style, tabIndex } = this.props return ( \n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)' }) .then((note) => { store.dispatch({ @@ -184,6 +183,12 @@ class InitModal extends React.Component { }) } + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + render () { if (this.state.isLoading) { return
@@ -196,17 +201,12 @@ class InitModal extends React.Component { tabIndex='-1' onKeyDown={(e) => this.handleKeyDown(e)} > - -
-
Initialize Storage
-
- this.handleCloseButtonClick(e)} />
- Welcome! + Welcome to Boostnote!
- Please select a directory for Boostnote storage. + Please select a directory for data storage.
Loading... - : 'Let\'s Go!' + : 'CREATE' }
diff --git a/browser/main/modals/InitModal.styl b/browser/main/modals/InitModal.styl index 73b7308a..62e02b68 100644 --- a/browser/main/modals/InitModal.styl +++ b/browser/main/modals/InitModal.styl @@ -1,7 +1,11 @@ .root modal() - max-width 540px + background-color #fff + max-width 100vw + max-height 100vh overflow hidden + margin 0 + padding 150px 0 position relative .root--loading @extend .root @@ -13,14 +17,6 @@ .loadingMessage color $ui-text-color margin 15px auto 35px -.header - height 50px - font-size 18px - line-height 50px - padding 0 15px - background-color $ui-backgroundColor - border-bottom solid 1px $ui-borderColor - color $ui-text-color .body padding 30px @@ -32,20 +28,20 @@ color $ui-text-color .body-description - font-size 14px + font-size 16px color $ui-text-color text-align center margin-bottom 25px .body-path margin 0 auto 25px - width 280px + width 330px .body-path-input - height 30px + height 40px vertical-align middle - width 250px - font-size 12px + width 300px + font-size 14px border-style solid border-width 1px 0 1px 1px border-color $border-color @@ -54,7 +50,10 @@ padding 0 5px .body-path-button - height 30px + height 42px + width 30px + font-size 16px + font-weight 600 border none border-top-right-radius 2px border-bottom-right-radius 2px @@ -69,6 +68,8 @@ .body-control-createButton colorPrimaryButton() + font-size 14px + font-weight 600 border none border-radius 2px height 40px diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index 4e794832..346fe920 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -26,7 +26,7 @@ class NewNoteModal extends React.Component { handleMarkdownNoteButtonClick (e) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') - let { storage, folder, dispatch, location } = this.props + const { storage, folder, dispatch, location } = this.props dataApi .createNote(storage, { type: 'MARKDOWN_NOTE', @@ -58,7 +58,7 @@ class NewNoteModal extends React.Component { handleSnippetNoteButtonClick (e) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') - let { storage, folder, dispatch, location } = this.props + const { storage, folder, dispatch, location } = this.props dataApi .createNote(storage, { diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index b12a20cf..748ab88c 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -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 @@ -63,3 +64,20 @@ body[data-theme="dark"] .description color $ui-inactive-text-color +body[data-theme="solarized-dark"] + .root + background-color transparent + + .header + color $ui-solarized-dark-text-color + + .control-button + border-color $ui-solarized-dark-borderColor + color $ui-solarized-dark-text-color + background-color transparent + &:focus + colorDarkPrimaryButton() + + .description + color $ui-solarized-dark-text-color + diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index 11114272..ae54218c 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -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 @@ -62,10 +67,17 @@ text-align right :global .alert - font-size 12px - line-height 30px - padding 0 5px - float right + display inline-block + position absolute + top 60px + right 15px + font-size 14px + .success + color #1EC38B + .error + color red + + .group-control-leftButton colorDefaultButton() @@ -77,14 +89,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 @@ -101,7 +115,6 @@ line-height 1.2 .note-for-keymap - margin-left: 10px font-size: 12px .code-mirror @@ -114,6 +127,12 @@ colorDarkControl() border-color $ui-dark-borderColor background-color $ui-dark-backgroundColor color $ui-dark-text-color + +colorSolarizedDarkControl() + border none + background-color $ui-solarized-dark-button-backgroundColor + color $ui-solarized-dark-text-color + body[data-theme="dark"] .root @@ -141,3 +160,33 @@ body[data-theme="dark"] .group-section-control select, .group-section-control-input colorDarkControl() + + +body[data-theme="solarized-dark"] + .root + color $ui-solarized-dark-text-color + + .group-header + color $ui-solarized-dark-text-color + border-color $ui-solarized-dark-borderColor + + .group-header2 + color $ui-solarized-dark-text-color + + .group-section-control-input + border-color $ui-solarized-dark-borderColor + + .group-control + border-color $ui-solarized-dark-borderColor + .group-control-leftButton + colorDarkDefaultButton() + border-color $ui-solarized-dark-borderColor + .group-control-rightButton + colorSolarizedDarkPrimaryButton() + .group-hint + colorSolarizedDarkControl() + .group-section-control + select, .group-section-control-input + colorSolarizedDarkControl() + + diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js new file mode 100644 index 00000000..3dccd27b --- /dev/null +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -0,0 +1,49 @@ +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './Crowdfunding.styl' + +const electron = require('electron') +const { shell } = electron + +class Crowdfunding extends React.Component { + constructor (props) { + super(props) + + this.state = { + } + } + + handleLinkClick (e) { + shell.openExternal(e.currentTarget.href) + e.preventDefault() + } + + render () { + return ( +
+
Crowdfunding
+

Dear all,

+
+

Thanks for your using!

+

Boostnote is used in about 200 countries and regions, it is a awesome developer community.

+
+

To continue supporting this growth, and to satisfy community expectations,

+

we would like to invest more time in this project.

+
+

If you like this project and see its potential, you can help!

+
+

Thanks,

+

Boostnote maintainers.

+
+ +
+ ) + } +} + +Crowdfunding.propTypes = { +} + +export default CSSModules(Crowdfunding, styles) diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl new file mode 100644 index 00000000..930c33f0 --- /dev/null +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -0,0 +1,36 @@ +@import('./Tab') + +.root + padding 15px + white-space pre + line-height 1.4 + color alpha($ui-text-color, 90%) + width 100% + font-size 14px +p + font-size 16px + +.cf-link + width 250px + height 35px + border-radius 2px + border none + background-color alpha(#1EC38B, 90%) + &:hover + background-color #1EC38B + transition 0.2s + a + text-decoration none + color white + font-weight 600 + font-size 16px + +body[data-theme="dark"] + p + color $ui-dark-text-color + +body[data-theme="solarized-dark"] + .root + color $ui-solarized-dark-text-color + p + color $ui-solarized-dark-text-color \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/FolderItem.js b/browser/main/modals/PreferencesModal/FolderItem.js new file mode 100644 index 00000000..9d1cd08f --- /dev/null +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -0,0 +1,304 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import ReactDOM from 'react-dom' +import styles from './FolderItem.styl' +import dataApi from 'browser/main/lib/dataApi' +import store from 'browser/main/store' +import { SketchPicker } from 'react-color' +import { SortableElement, SortableHandle } from 'react-sortable-hoc' + +class FolderItem extends React.Component { + constructor (props) { + super(props) + + this.state = { + status: 'IDLE', + folder: { + showColumnPicker: false, + colorPickerPos: { left: 0, top: 0 }, + color: props.color, + name: props.name + } + } + } + + handleEditChange (e) { + const { folder } = this.state + + folder.name = this.refs.nameInput.value + this.setState({ + folder + }) + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + confirm () { + const { storage, folder } = this.props + dataApi + .updateFolder(storage.key, folder.key, { + color: this.state.folder.color, + name: this.state.folder.name + }) + .then((data) => { + store.dispatch({ + type: 'UPDATE_FOLDER', + storage: data.storage + }) + this.setState({ + status: 'IDLE' + }) + }) + } + + handleColorButtonClick (e) { + const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } }) + this.setState({ folder }, function () { + // After the color picker has been painted, re-calculate its position + // by comparing its dimensions to the host dimensions. + const { hostBoundingBox } = this.props + const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker) + const colorPickerBox = colorPickerNode.getBoundingClientRect() + const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom + const folder = Object.assign({}, this.state.folder, { + colorPickerPos: { + left: 25, + top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics + } + }) + this.setState({ folder }) + }) + } + + handleColorChange (color) { + const folder = Object.assign({}, this.state.folder, { color: color.hex }) + this.setState({ folder }) + } + + handleColorPickerClose (event) { + const folder = Object.assign({}, this.state.folder, { showColumnPicker: false }) + this.setState({ folder }) + } + + handleCancelButtonClick (e) { + this.setState({ + status: 'IDLE' + }) + } + + handleFolderItemBlur (e) { + let el = e.relatedTarget + while (el != null) { + if (el === this.refs.root) { + return false + } + el = el.parentNode + } + this.confirm() + } + + renderEdit (e) { + const popover = { position: 'absolute', zIndex: 2 } + const cover = { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0 + } + const pickerStyle = Object.assign({}, { + position: 'absolute' + }, this.state.folder.colorPickerPos) + return ( +
this.handleFolderItemBlur(e)} + tabIndex='-1' + ref='root' + > +
+ + this.handleEditChange(e)} + /> +
+
+ + +
+
+ ) + } + + handleDeleteConfirmButtonClick (e) { + const { storage, folder } = this.props + dataApi + .deleteFolder(storage.key, folder.key) + .then((data) => { + store.dispatch({ + type: 'DELETE_FOLDER', + storage: data.storage, + folderKey: data.folderKey + }) + }) + } + + renderDelete () { + return ( +
+
+ Are you sure to delete this folder? +
+
+ + +
+
+ ) + } + + handleEditButtonClick (e) { + const { folder: propsFolder } = this.props + const { folder: stateFolder } = this.state + const folder = Object.assign({}, stateFolder, propsFolder) + this.setState({ + status: 'EDIT', + folder + }, () => { + this.refs.nameInput.select() + }) + } + + handleDeleteButtonClick (e) { + this.setState({ + status: 'DELETE' + }) + } + + renderIdle () { + const { folder } = this.props + return ( +
this.handleEditButtonClick(e)} + > +
+ {folder.name} + ({folder.key}) +
+
+ + +
+
+ ) + } + + render () { + switch (this.state.status) { + case 'DELETE': + return this.renderDelete() + case 'EDIT': + return this.renderEdit() + case 'IDLE': + default: + return this.renderIdle() + } + } +} + +FolderItem.propTypes = { + hostBoundingBox: PropTypes.shape({ + bottom: PropTypes.number, + height: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + width: PropTypes.number + }), + storage: PropTypes.shape({ + key: PropTypes.string + }), + folder: PropTypes.shape({ + key: PropTypes.string, + color: PropTypes.string, + name: PropTypes.string + }) +} + +class Handle extends React.Component { + render () { + return ( +
+ +
+ ) + } +} + +class SortableFolderItemComponent extends React.Component { + render () { + const StyledHandle = CSSModules(Handle, this.props.styles) + const DragHandle = SortableHandle(StyledHandle) + + const StyledFolderItem = CSSModules(FolderItem, this.props.styles) + + return ( +
+ + +
+ ) + } +} + +export default CSSModules(SortableElement(SortableFolderItemComponent), styles) diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl new file mode 100644 index 00000000..acc4cbfb --- /dev/null +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -0,0 +1,128 @@ +.folderItem + height 35px + box-sizing border-box + padding 2.5px 15px + &:hover + background-color darken(white, 3%) + +.folderItem-drag-handle + height 35px + border none + padding 0 10px + line-height 35px + float left + cursor row-resize + +.folderItem-left + height 30px + border-left solid 2px transparent + padding 0 10px + line-height 30px + float left +.folderItem-left-danger + color $danger-color + font-weight bold + +.folderItem-left-key + color $ui-inactive-text-color + font-size 13px + margin 0 5px + border none + +.folderItem-left-colorButton + colorDefaultButton() + height 25px + width 25px + line-height 23px + padding 0 + box-sizing border-box + vertical-align middle + border $ui-border + border-radius 2px + margin-right 5px + margin-left -15px + +.folderItem-left-nameInput + height 25px + box-sizing border-box + vertical-align middle + border $ui-border + border-radius 2px + padding 0 5px + outline none + +.folderItem-right + float right + +.folderItem-right-button + vertical-align middle + height 25px + margin-top 2.5px + colorDefaultButton() + border-radius 2px + border $ui-border + margin-right 5px + padding 0 5px + &:last-child + margin-right 0 + +.folderItem-right-confirmButton + @extend .folderItem-right-button + border none + colorPrimaryButton() + +.folderItem-right-dangerButton + @extend .folderItem-right-button + border none + colorDangerButton() + +body[data-theme="dark"] + .folderItem + &:hover + background-color lighten($ui-dark-button--hover-backgroundColor, 5%) + + .folderItem-left-danger + color $danger-color + font-weight bold + + .folderItem-left-key + color $ui-dark-inactive-text-color + + .folderItem-left-colorButton + colorDarkDefaultButton() + border-color $ui-dark-borderColor + + .folderItem-right-button + colorDarkDefaultButton() + border-color $ui-dark-borderColor + + .folderItem-right-confirmButton + colorDarkPrimaryButton() + + .folderItem-right-dangerButton + colorDarkDangerButton() + + + +body[data-theme="solarized-dark"] + .folderItem + &:hover + background-color $ui-solarized-dark-button-backgroundColor + + .folderItem-left-danger + color $danger-color + + .folderItem-left-key + color $ui-dark-inactive-text-color + + .folderItem-left-colorButton + colorSolarizedDarkPrimaryButton() + + .folderItem-right-button + colorSolarizedDarkPrimaryButton() + + .folderItem-right-confirmButton + colorSolarizedDarkPrimaryButton() + + .folderItem-right-dangerButton + colorSolarizedDarkPrimaryButton() diff --git a/browser/main/modals/PreferencesModal/FolderList.js b/browser/main/modals/PreferencesModal/FolderList.js new file mode 100644 index 00000000..8585f641 --- /dev/null +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import dataApi from 'browser/main/lib/dataApi' +import styles from './FolderList.styl' +import store from 'browser/main/store' +import FolderItem from './FolderItem' +import { SortableContainer } from 'react-sortable-hoc' + +class FolderList extends React.Component { + render () { + const { storage, hostBoundingBox } = this.props + + const folderList = storage.folders.map((folder, index) => { + return + }) + + return ( +
+ {folderList.length > 0 + ? folderList + :
No Folders
+ } +
+ ) + } +} + +FolderList.propTypes = { + hostBoundingBox: PropTypes.shape({ + bottom: PropTypes.number, + height: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + width: PropTypes.number + }), + storage: PropTypes.shape({ + key: PropTypes.string + }), + folder: PropTypes.shape({ + key: PropTypes.number, + color: PropTypes.string, + name: PropTypes.string + }) +} + +class SortableFolderListComponent extends React.Component { + constructor (props) { + super(props) + this.onSortEnd = ({oldIndex, newIndex}) => { + const { storage } = this.props + dataApi + .reorderFolder(storage.key, oldIndex, newIndex) + .then((data) => { + store.dispatch({ + type: 'REORDER_FOLDER', + storage: data.storage + }) + this.setState() + }) + } + } + + render () { + const StyledFolderList = CSSModules(FolderList, this.props.styles) + const SortableFolderList = SortableContainer(StyledFolderList) + + return ( + + ) + } +} + +export default CSSModules(SortableFolderListComponent, styles) diff --git a/browser/main/modals/PreferencesModal/FolderList.styl b/browser/main/modals/PreferencesModal/FolderList.styl new file mode 100644 index 00000000..e69de29b diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index f75e41b8..9a15e79f 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -1,8 +1,10 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' import store from 'browser/main/store' +import _ from 'lodash' const electron = require('electron') const ipc = electron.ipcRenderer @@ -40,7 +42,7 @@ class HotkeyTab extends React.Component { } handleSaveButtonClick (e) { - let newConfig = { + const newConfig = { hotkey: this.state.config.hotkey } @@ -50,6 +52,7 @@ class HotkeyTab extends React.Component { type: 'SET_UI', config: newConfig }) + this.clearMessage() } handleHintToggleButtonClick (e) { @@ -59,7 +62,7 @@ class HotkeyTab extends React.Component { } handleHotkeyChange (e) { - let { config } = this.state + const { config } = this.state config.hotkey = { toggleFinder: this.refs.toggleFinder.value, toggleMain: this.refs.toggleMain.value @@ -69,14 +72,22 @@ class HotkeyTab extends React.Component { }) } + clearMessage () { + _.debounce(() => { + this.setState({ + keymapAlert: null + }) + }, 2000)() + } + render () { - let keymapAlert = this.state.keymapAlert - let keymapAlertElement = keymapAlert != null + const keymapAlert = this.state.keymapAlert + const keymapAlertElement = keymapAlert != null ?

{keymapAlert.message}

: null - let { config } = this.state + const { config } = this.state return (
@@ -94,7 +105,7 @@ class HotkeyTab extends React.Component {
-
Toggle Finder(popup)
+
Toggle Finder (Quick search)
this.handleHotkeyChange(e)} diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js index 35cd53a2..2a1db828 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -4,6 +4,7 @@ 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' +import _ from 'lodash' const electron = require('electron') const { shell, remote } = electron @@ -36,8 +37,21 @@ class InfoTab extends React.Component { if (!newConfig.amaEnabled) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA') + this.setState({ + amaMessage: 'We hope we will gain your trust' + }) + } else { + this.setState({ + amaMessage: 'Thank\'s for trust us' + }) } + _.debounce(() => { + this.setState({ + amaMessage: '' + }) + }, 3000)() + ConfigManager.set(newConfig) store.dispatch({ @@ -46,10 +60,49 @@ class InfoTab extends React.Component { }) } + infoMessage () { + const { amaMessage } = this.state + return amaMessage ?

{amaMessage}

: null + } + render () { return (
-
Info
+ +
Community
+ + +
+ +
Info
@@ -62,31 +115,17 @@ class InfoTab extends React.Component {
+ -
+ +
+
Data collection policy
-

We collect only the number of DAU for Boostnote and DO NOT collect any detail information

-

such as your note content. You can see how it works on this.handleLinkClick(e)}>GitHub.

-

These data are only used for Boostnote improvements.

+
We collect only the number of DAU for Boostnote and **DO NOT collect** any detail information such as your note content.
+
You can see how it works on this.handleLinkClick(e)}>GitHub.
+
This data is only used for Boostnote improvements.
this.handleConfigChange(e)} checked={this.state.config.amaEnabled} ref='amaEnabled' @@ -107,6 +148,7 @@ class InfoTab extends React.Component { /> Enable to send analytics to our servers
+ {this.infoMessage()}
) } diff --git a/browser/main/modals/PreferencesModal/InfoTab.styl b/browser/main/modals/PreferencesModal/InfoTab.styl index 6158c634..cc04a10f 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.styl +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -42,13 +42,29 @@ color #4E8EC6 text-decoration none +.separate-line + margin 40px 0 + .policy + width 100% font-size 20px margin-bottom 10px .policy-submit margin-top 10px +.policy-confirm + margin-top 10px + font-size 12px + body[data-theme="dark"] .root color alpha($tab--dark-text-color, 80%) + + +body[data-theme="solarized-dark"] + .root + color $ui-solarized-dark-text-color +.list + a + color $ui-solarized-dark-active-color diff --git a/browser/main/modals/PreferencesModal/PreferencesModal.styl b/browser/main/modals/PreferencesModal/PreferencesModal.styl index a0a670b5..4a280a38 100644 --- a/browser/main/modals/PreferencesModal/PreferencesModal.styl +++ b/browser/main/modals/PreferencesModal/PreferencesModal.styl @@ -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 @@ -24,23 +25,23 @@ top-bar--height = 50px absolute top left right top top-bar--height left 0 - width 140px - margin-left 30px + width 170px + 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 150px + 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 @@ -55,7 +56,7 @@ top-bar--height = 50px .content absolute left right bottom top top-bar--height - left 140px + left 170px margin-top 10px overflow-y auto @@ -85,3 +86,29 @@ body[data-theme="dark"] background-color $dark-primary-button-background--active &:hover color white + + +body[data-theme="solarized-dark"] + .root + background-color transparent + .top-bar + background-color transparent + border-color $ui-solarized-dark-borderColor + p + color $ui-solarized-dark-text-color + .nav + background-color transparent + border-color $ui-solarized-dark-borderColor + .nav-button + background-color transparent + color $ui-solarized-dark-text-color + &:hover + color $ui-solarized-dark-text-color + + .nav-button--active + @extend .nav-button + color $ui-solarized-dark-button--active-color + background-color $ui-solarized-dark-button--active-backgroundColor + &:hover + color white + diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index d3586f35..f2092835 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -1,265 +1,14 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' +import PropTypes from 'prop-types' +import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StorageItem.styl' import consts from 'browser/lib/consts' import dataApi from 'browser/main/lib/dataApi' import store from 'browser/main/store' +import FolderList from './FolderList' const { shell, remote } = require('electron') const { dialog } = remote -import { SketchPicker } from 'react-color' - -class UnstyledFolderItem extends React.Component { - constructor (props) { - super(props) - - this.state = { - status: 'IDLE', - folder: { - showColumnPicker: false, - colorPickerPos: { left: 0, top: 0 }, - color: props.color, - name: props.name - } - } - } - - handleEditChange (e) { - let { folder } = this.state - - folder.name = this.refs.nameInput.value - this.setState({ - folder - }) - } - - handleConfirmButtonClick (e) { - this.confirm() - } - - confirm () { - let { storage, folder } = this.props - dataApi - .updateFolder(storage.key, folder.key, { - color: this.state.folder.color, - name: this.state.folder.name - }) - .then((data) => { - store.dispatch({ - type: 'UPDATE_FOLDER', - storage: data.storage - }) - this.setState({ - status: 'IDLE' - }) - }) - } - - handleColorButtonClick (e) { - const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } }) - this.setState({ folder }, function () { - // After the color picker has been painted, re-calculate its position - // by comparing its dimensions to the host dimensions. - const { hostBoundingBox } = this.props - const colorPickerNode = ReactDOM.findDOMNode(this.refs.colorPicker) - const colorPickerBox = colorPickerNode.getBoundingClientRect() - const offsetTop = hostBoundingBox.bottom - colorPickerBox.bottom - const folder = Object.assign({}, this.state.folder, { - colorPickerPos: { - left: 25, - top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics - } - }) - this.setState({ folder }) - }) - } - - handleColorChange (color) { - const folder = Object.assign({}, this.state.folder, { color: color.hex }) - this.setState({ folder }) - } - - handleColorPickerClose (event) { - const folder = Object.assign({}, this.state.folder, { showColumnPicker: false }) - this.setState({ folder }) - } - - handleCancelButtonClick (e) { - this.setState({ - status: 'IDLE' - }) - } - - handleFolderItemBlur (e) { - let el = e.relatedTarget - while (el != null) { - if (el === this.refs.root) { - return false - } - el = el.parentNode - } - this.confirm() - } - - renderEdit (e) { - const popover = { position: 'absolute', zIndex: 2 } - const cover = { - position: 'fixed', - top: 0, - right: 0, - bottom: 0, - left: 0 - } - const pickerStyle = Object.assign({}, { - position: 'absolute' - }, this.state.folder.colorPickerPos) - return ( -
this.handleFolderItemBlur(e)} - tabIndex='-1' - ref='root' - > -
- - this.handleEditChange(e)} - /> -
-
- - -
-
- ) - } - - handleDeleteConfirmButtonClick (e) { - let { storage, folder } = this.props - dataApi - .deleteFolder(storage.key, folder.key) - .then((data) => { - store.dispatch({ - type: 'DELETE_FOLDER', - storage: data.storage, - folderKey: data.folderKey - }) - }) - } - - renderDelete () { - return ( -
-
- Are you sure to delete this folder? -
-
- - -
-
- ) - } - - handleEditButtonClick (e) { - let { folder: propsFolder } = this.props - let { folder: stateFolder } = this.state - const folder = Object.assign({}, stateFolder, propsFolder) - this.setState({ - status: 'EDIT', - folder - }, () => { - this.refs.nameInput.select() - }) - } - - handleDeleteButtonClick (e) { - this.setState({ - status: 'DELETE' - }) - } - - renderIdle () { - let { folder } = this.props - return ( -
this.handleEditButtonClick(e)} - > -
- {folder.name} - ({folder.key}) -
-
- - -
-
- - ) - } - - render () { - switch (this.state.status) { - case 'DELETE': - return this.renderDelete() - case 'EDIT': - return this.renderEdit() - case 'IDLE': - default: - return this.renderIdle() - } - } -} - -const FolderItem = CSSModules(UnstyledFolderItem, styles) class StorageItem extends React.Component { constructor (props) { @@ -271,8 +20,8 @@ class StorageItem extends React.Component { } handleNewFolderButtonClick (e) { - let { storage } = this.props - let input = { + const { storage } = this.props + const input = { name: 'Untitled', color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] } @@ -290,12 +39,12 @@ class StorageItem extends React.Component { } handleExternalButtonClick () { - let { storage } = this.props + const { storage } = this.props shell.showItemInFolder(storage.path) } handleUnlinkButtonClick (e) { - let index = dialog.showMessageBox(remote.getCurrentWindow(), { + const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: 'Unlink Storage', detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.', @@ -303,7 +52,7 @@ class StorageItem extends React.Component { }) if (index === 0) { - let { storage } = this.props + const { storage } = this.props dataApi.removeStorage(storage.key) .then(() => { store.dispatch({ @@ -318,7 +67,7 @@ class StorageItem extends React.Component { } handleLabelClick (e) { - let { storage } = this.props + const { storage } = this.props this.setState({ isLabelEditing: true, name: storage.name @@ -333,7 +82,7 @@ class StorageItem extends React.Component { } handleLabelBlur (e) { - let { storage } = this.props + const { storage } = this.props dataApi .renameStorage(storage.key, this.state.name) .then((_storage) => { @@ -348,14 +97,8 @@ class StorageItem extends React.Component { } render () { - let { storage, hostBoundingBox } = this.props - let folderList = storage.folders.map((folder) => { - return - }) + const { storage, hostBoundingBox } = this.props + return (
@@ -404,12 +147,9 @@ class StorageItem extends React.Component {
-
- {folderList.length > 0 - ? folderList - :
No Folders
- } -
+
) } @@ -426,11 +166,6 @@ StorageItem.propTypes = { }), storage: PropTypes.shape({ key: PropTypes.string - }), - folder: PropTypes.shape({ - key: PropTypes.string, - color: PropTypes.string, - name: PropTypes.string }) } diff --git a/browser/main/modals/PreferencesModal/StorageItem.styl b/browser/main/modals/PreferencesModal/StorageItem.styl index 538edfb8..13759007 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.styl +++ b/browser/main/modals/PreferencesModal/StorageItem.styl @@ -63,75 +63,6 @@ z-index 10 white-space nowrap -.folderList-item - height 35px - box-sizing border-box - padding 2.5px 15px - &:hover - background-color darken(white, 3%) -.folderList-item-left - height 30px - border-left solid 2px transparent - padding 0 10px - line-height 30px - float left -.folderList-item-left-danger - color $danger-color - font-weight bold - -.folderList-item-left-key - color $ui-inactive-text-color - font-size 10px - margin 0 5px - border none - -.folderList-item-left-colorButton - colorDefaultButton() - height 25px - width 25px - line-height 23px - padding 0 - box-sizing border-box - vertical-align middle - border $ui-border - border-radius 2px - margin-right 5px - margin-left -15px - -.folderList-item-left-nameInput - height 25px - box-sizing border-box - vertical-align middle - border $ui-border - border-radius 2px - padding 0 5px - outline none - -.folderList-item-right - float right - -.folderList-item-right-button - vertical-align middle - height 25px - margin-top 2.5px - colorDefaultButton() - border-radius 2px - border $ui-border - margin-right 5px - padding 0 5px - &:last-child - margin-right 0 - -.folderList-item-right-confirmButton - @extend .folderList-item-right-button - border none - colorPrimaryButton() - -.folderList-item-right-dangerButton - @extend .folderList-item-right-button - border none - colorDangerButton() - body[data-theme="dark"] .header border-color $ui-dark-borderColor @@ -153,28 +84,3 @@ body[data-theme="dark"] top 25px z-index 10 white-space nowrap - - .folderList-item - &:hover - background-color lighten($ui-dark-button--hover-backgroundColor, 5%) - - .folderList-item-left-danger - color $danger-color - font-weight bold - - .folderList-item-left-key - color $ui-dark-inactive-text-color - - .folderList-item-left-colorButton - colorDarkDefaultButton() - border-color $ui-dark-borderColor - - .folderList-item-right-button - colorDarkDefaultButton() - border-color $ui-dark-borderColor - - .folderList-item-right-confirmButton - colorDarkPrimaryButton() - - .folderList-item-right-dangerButton - colorDarkDangerButton() diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js index dc1f96f0..5c328f8a 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -1,16 +1,17 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StoragesTab.styl' import dataApi from 'browser/main/lib/dataApi' import StorageItem from './StorageItem' const electron = require('electron') -const remote = electron.remote +const { shell, remote } = electron function browseFolder () { - let dialog = remote.dialog + const dialog = remote.dialog - let defaultPath = remote.app.getPath('home') + const defaultPath = remote.app.getPath('home') return new Promise((resolve, reject) => { dialog.showOpenDialog({ title: 'Select Directory', @@ -50,11 +51,16 @@ class StoragesTab extends React.Component { }) } + handleLinkClick (e) { + shell.openExternal(e.currentTarget.href) + e.preventDefault() + } + renderList () { - let { data, boundingBox } = this.props + const { data, boundingBox } = this.props if (!boundingBox) { return null } - let storageList = data.storageMap.map((storage) => { + const storageList = data.storageMap.map((storage) => { return { if (targetPath.length > 0) { - let { newStorage } = this.state + const { newStorage } = this.state newStorage.path = targetPath this.setState({ newStorage @@ -97,7 +103,7 @@ class StoragesTab extends React.Component { } handleAddStorageChange (e) { - let { newStorage } = this.state + const { newStorage } = this.state newStorage.name = this.refs.addStorageName.value newStorage.path = this.refs.addStoragePath.value this.setState({ @@ -112,7 +118,7 @@ class StoragesTab extends React.Component { path: this.state.newStorage.path }) .then((data) => { - let { dispatch } = this.props + const { dispatch } = this.props dispatch({ type: 'ADD_STORAGE', storage: data.storage, @@ -161,7 +167,10 @@ class StoragesTab extends React.Component {
- 3rd party cloud integration(such as Google Drive and Dropbox) will be available soon. + 3rd party cloud integration: + this.handleLinkClick(e)} + >Cloud-Syncing-and-Backup
diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index 966a8eab..230f0aed 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -158,3 +158,44 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor + + + +body[data-theme="solarized-dark"] + .root + color $ui-solarized-dark-text-color + + .folderList-item + border-bottom $ui-solarized-dark-borderColor + + .folderList-empty + color $ui-solarized-dark-text-color + + .list-empty + color $ui-solarized-dark-text-color + .list-control-addStorageButton + border-color $ui-solarized-dark-button-backgroundColor + background-color $ui-solarized-dark-button-backgroundColor + color $ui-solarized-dark-text-color + + .addStorage-header + color $ui-solarized-dark-text-color + border-color $ui-solarized-dark-borderColor + + .addStorage-body-section-name-input + border-color $$ui-solarized-dark-borderColor + + .addStorage-body-section-type-description + color $ui-solarized-dark-text-color + + .addStorage-body-section-path-button + colorPrimaryButton() + .addStorage-body-control + border-color $ui-solarized-dark-borderColor + + .addStorage-body-control-createButton + colorDarkPrimaryButton() + .addStorage-body-control-cancelButton + colorDarkDefaultButton() + border-color $ui-solarized-dark-borderColor + diff --git a/browser/main/modals/PreferencesModal/Tab.styl b/browser/main/modals/PreferencesModal/Tab.styl index 7280407b..e5fc48da 100644 --- a/browser/main/modals/PreferencesModal/Tab.styl +++ b/browser/main/modals/PreferencesModal/Tab.styl @@ -10,8 +10,12 @@ $tab--button-font-size = 14px $tab--dark-text-color = #E5E5E5 .header - font-size 24px - margin-bottom 30px + font-size 36px + margin-bottom 60px + +.header--sub + font-size 36px + margin-bottom 20px body[data-theme="dark"] .header diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 61aca0dc..0baedf4c 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -1,4 +1,5 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' @@ -9,6 +10,11 @@ import CodeMirror from 'codemirror' const OSX = global.process.platform === 'darwin' +import _ from 'lodash' + +const electron = require('electron') +const ipc = electron.ipcRenderer + class UiTab extends React.Component { constructor (props) { super(props) @@ -18,8 +24,27 @@ class UiTab extends React.Component { } } - componentWillMount () { - CodeMirror.autoLoadMode(ReactCodeMirror, 'javascript') + componentDidMount () { + CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') + this.handleSettingDone = () => { + this.setState({UiAlert: { + type: 'success', + message: 'Successfully applied!' + }}) + } + this.handleSettingError = (err) => { + this.setState({UiAlert: { + type: 'error', + message: err.message != null ? err.message : 'Error occurs!' + }}) + } + ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) + } + + componentWillUnmount () { + ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) } handleUIChange (e) { @@ -36,6 +61,7 @@ class UiTab extends React.Component { const newConfig = { ui: { theme: this.refs.uiTheme.value, + showCopyNotification: this.refs.showCopyNotification.checked, disableDirectWrite: this.refs.uiD2w != null ? this.refs.uiD2w.checked : false @@ -48,20 +74,25 @@ class UiTab extends React.Component { indentSize: this.refs.editorIndentSize.value, displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, switchPreview: this.refs.editorSwitchPreview.value, - keyMap: this.refs.editorKeyMap.value + keyMap: this.refs.editorKeyMap.value, + scrollPastEnd: this.refs.scrollPastEnd.checked }, preview: { fontSize: this.refs.previewFontSize.value, fontFamily: this.refs.previewFontFamily.value, codeBlockTheme: this.refs.previewCodeBlockTheme.value, - lineNumber: this.refs.previewLineNumber.checked + lineNumber: this.refs.previewLineNumber.checked, + latexInlineOpen: this.refs.previewLatexInlineOpen.value, + latexInlineClose: this.refs.previewLatexInlineClose.value, + latexBlockOpen: this.refs.previewLatexBlockOpen.value, + latexBlockClose: this.refs.previewLatexBlockClose.value } } const newCodemirrorTheme = this.refs.editorTheme.value if (newCodemirrorTheme !== codemirrorTheme) { - checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme}.css`) + checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`) } this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }) @@ -80,9 +111,25 @@ class UiTab extends React.Component { type: 'SET_UI', config: newConfig }) + this.clearMessage() + } + + clearMessage () { + _.debounce(() => { + this.setState({ + UiAlert: null + }) + }, 2000)() } render () { + const UiAlert = this.state.UiAlert + const UiAlertElement = UiAlert != null + ?

+ {UiAlert.message} +

+ : null + const themes = consts.THEMES const { config, codemirrorTheme } = this.state const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};' @@ -91,19 +138,30 @@ class UiTab extends React.Component {
UI
-
Theme
-
+ Color Theme
+
+ +
{ global.process.platform === 'win32' ?
@@ -137,7 +195,7 @@ class UiTab extends React.Component { }
- + (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} />
@@ -219,7 +277,7 @@ class UiTab extends React.Component { - Please restart boostnote after you change the keymap +

⚠️ Please restart boostnote after you change the keymap

@@ -234,6 +292,18 @@ class UiTab extends React.Component { +
+ +
+ +
Preview
@@ -286,13 +356,64 @@ class UiTab extends React.Component { Show line numbers for preview code blocks
+
+
+ LaTeX Inline Open Delimiter +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ LaTeX Inline Close Delimiter +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ LaTeX Block Open Delimiter +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ LaTeX Block Close Delimiter +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
-
+
+ {UiAlertElement}
diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index c7b9a68d..2fff0364 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -1,13 +1,16 @@ -import React, { PropTypes } from 'react' +import PropTypes from 'prop-types' +import React from 'react' import ReactDOM from 'react-dom' import { connect } from 'react-redux' import HotkeyTab from './HotkeyTab' import UiTab from './UiTab' import InfoTab from './InfoTab' +import Crowdfunding from './Crowdfunding' import StoragesTab from './StoragesTab' import ModalEscButton from 'browser/components/ModalEscButton' import CSSModules from 'browser/lib/CSSModules' import styles from './PreferencesModal.styl' +import RealtimeNotification from 'browser/components/RealtimeNotification' class Preferences extends React.Component { constructor (props) { @@ -40,7 +43,7 @@ class Preferences extends React.Component { renderContent () { const { boundingBox } = this.state - let { dispatch, config, data } = this.props + const { dispatch, config, data } = this.props switch (this.state.currentTab) { case 'INFO': @@ -64,6 +67,10 @@ class Preferences extends React.Component { config={config} /> ) + case 'CROWDFUNDING': + return ( + + ) case 'STORAGES': default: return ( @@ -88,17 +95,18 @@ class Preferences extends React.Component { } render () { - let content = this.renderContent() + const content = this.renderContent() - let tabs = [ + const tabs = [ {target: 'STORAGES', label: 'Storages'}, {target: 'HOTKEY', label: 'Hotkey'}, {target: 'UI', label: 'UI'}, - {target: 'INFO', label: 'Info'} + {target: 'INFO', label: 'Community / Info'}, + {target: 'CROWDFUNDING', label: 'Crowdfunding'} ] - let navButtons = tabs.map((tab) => { - let isActive = this.state.currentTab === tab.target + const navButtons = tabs.map((tab) => { + const isActive = this.state.currentTab === tab.target return (