diff --git a/.babelrc b/.babelrc index 270349d2..3a366286 100644 --- a/.babelrc +++ b/.babelrc @@ -7,7 +7,7 @@ "test": { "presets": ["env" ,"react", "es2015"], "plugins": [ - [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ] + [ "babel-plugin-webpack-alias", { "config": "/webpack.config.js" } ] ] } } diff --git a/.eslintrc b/.eslintrc index 1709c9d8..67b6c8fe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,6 @@ { - "extends": ["standard", "standard-jsx", "plugin:react/recommended"], - "plugins": ["react"], + "extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"], + "plugins": ["react", "prettier"], "rules": { "no-useless-escape": 0, "prefer-const": ["warn", { @@ -13,12 +13,15 @@ "react/no-string-refs": 0, "react/no-find-dom-node": "warn", "react/no-render-return-value": "warn", - "react/no-deprecated": "warn" + "react/no-deprecated": "warn", + "prettier/prettier": ["error"] }, "globals": { "FileReader": true, "localStorage": true, - "fetch": true + "fetch": true, + "Image": true, + "MutationObserver": true }, "env": { "jest": true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ea304082 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +issuehunt: BoostIo/Boostnote diff --git a/.gitignore b/.gitignore index ace5316c..f494f3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ node_modules/* /secret *.log .idea -.vscode \ No newline at end of file +.vscode +package-lock.json +config.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..515c6cd5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": false, + "jsxSingleQuote": true +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index d9267f77..71b65671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: node_js node_js: - - 7 + - 8 script: - npm run lint && npm run test - - yarn jest - - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi' + - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && 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 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 5554c4b8..8285bec5 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -5,19 +5,19 @@ Let us know what is currently happening. Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug. -If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile. +If your issue is regarding the new Boost Note.next, please open an issue in the new repo 👉 https://github.com/BoostIO/BoostNote.next/issues. --> # Expected behavior # Steps to reproduce 1. @@ -26,8 +26,8 @@ Please be thorough, issues we can reproduce are easier to fix! # Environment -- Version : -- OS Version and name : +- Boostnote version: +- OS version and name: + ## Description + ## Issue fixed + @@ -20,6 +23,7 @@ your PR will be reviewed faster if we know exactly what it does. Change :white_circle: to :radio_button: in all the options that apply --> + ## Type of changes - :white_circle: Bug fix (Change that fixed an issue) @@ -34,3 +38,5 @@ Change :white_circle: to :radio_button: in all the options that apply - :white_circle: I have written test for my code and it has been tested - :white_circle: All existing tests have been passed - :white_circle: I have attached a screenshot/video to visualize my change if possible +- :white_circle: This PR will modify the UI or affects the UX +- :white_circle: This PR will add/update/delete a keybinding diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index e4c06e96..59fabaeb 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,54 +2,69 @@ import PropTypes from 'prop-types' import React from 'react' import _ from 'lodash' import CodeMirror from 'codemirror' +import hljs from 'highlight.js' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import convertModeName from 'browser/lib/convertModeName' -import { - options, - TableEditor, - Alignment -} from '@susisu/mte-kernel' +import { options, TableEditor, Alignment } from '@susisu/mte-kernel' import TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' -import crypto from 'crypto' -import consts from 'browser/lib/consts' + +import { isMarkdownTitleURL } from 'browser/lib/utils' import styles from '../components/CodeEditor.styl' -import fs from 'fs' const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') -import TurndownService from 'turndown' + .buildEditorContextMenu +import { createTurndownService } from '../lib/turndown' +import { languageMaps } from '../lib/CMLanguageList' +import snippetManager from '../lib/SnippetManager' import { - gfm -} from 'turndown-plugin-gfm' + generateInEditor, + tocExistsInEditor +} from 'browser/lib/markdown-toc-generator' +import markdownlint from 'markdownlint' +import Jsonlint from 'jsonlint-mod' +import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' +import prettier from 'prettier' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' const buildCMRulers = (rulers, enableRulers) => - (enableRulers ? rulers.map(ruler => ({ - column: ruler - })) : []) + enableRulers + ? rulers.map(ruler => ({ + column: ruler + })) + : [] -function translateHotkey (hotkey) { - return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') +function translateHotkey(hotkey) { + return hotkey + .replace(/\s*\+\s*/g, '-') + .replace(/Command/g, 'Cmd') + .replace(/Control/g, 'Ctrl') } export default class CodeEditor extends React.Component { - constructor (props) { + constructor(props) { super(props) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, { leading: false, trailing: true }) - this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject) - this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject) + this.changeHandler = (editor, changeObject) => + this.handleChange(editor, changeObject) + this.highlightHandler = (editor, changeObject) => + this.handleHighlight(editor, changeObject) this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } + const debouncedDeletionOfAttachments = _.debounce( + attachmentManagement.deleteAttachmentsNotPresentInNote, + 30000 + ) this.blurHandler = (editor, e) => { ipcRenderer.send('editor:focused', false) if (e == null) return null @@ -61,16 +76,14 @@ export default class CodeEditor extends React.Component { el = el.parentNode } this.props.onBlur != null && this.props.onBlur(e) - - const { - storageKey, - noteKey - } = this.props - attachmentManagement.deleteAttachmentsNotPresentInNote( - this.editor.getValue(), - storageKey, - noteKey - ) + const { storageKey, noteKey } = this.props + if (this.props.deleteUnusedAttachments === true) { + debouncedDeletionOfAttachments( + this.editor.getValue(), + storageKey, + noteKey + ) + } } this.pasteHandler = (editor, e) => { e.preventDefault() @@ -83,11 +96,13 @@ export default class CodeEditor extends React.Component { this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null this.scrollToLineHandeler = this.scrollToLine.bind(this) + this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this) + this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this) this.formatTable = () => this.handleFormatTable() if (props.switchPreview !== 'RIGHTCLICK') { - this.contextMenuHandler = function (editor, event) { + this.contextMenuHandler = function(editor, event) { const menu = buildEditorContextMenu(editor, event) if (menu != null) { setTimeout(() => menu.popup(remote.getCurrentWindow()), 30) @@ -97,27 +112,27 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() - this.turndownService = new TurndownService() + this.turndownService = createTurndownService() } - handleSearch (msg) { + handleSearch(msg) { const cm = this.editor const component = this if (component.searchState) cm.removeOverlay(component.searchState) - if (msg.length < 3) return + if (msg.length < 1) return - cm.operation(function () { + cm.operation(function() { component.searchState = makeOverlay(msg, 'searching') cm.addOverlay(component.searchState) - function makeOverlay (query, style) { + function makeOverlay(query, style) { query = new RegExp( query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi' ) return { - token: function (stream) { + token: function(stream) { query.lastIndex = stream.pos var match = query.exec(stream.string) if (match && match.index === stream.pos) { @@ -134,24 +149,27 @@ export default class CodeEditor extends React.Component { }) } - handleFormatTable () { - this.tableEditor.formatAll(options({ - textWidthOptions: {} - })) + handleFormatTable() { + this.tableEditor.formatAll( + options({ + textWidthOptions: {} + }) + ) } - handleEditorActivity () { + handleEditorActivity() { if (!this.textEditorInterface.transaction) { this.updateTableEditorState() } } - updateDefaultKeyMap () { + updateDefaultKeyMap() { const { hotkey } = this.props - const expandSnippet = this.expandSnippet.bind(this) + const self = this + const expandSnippet = snippetManager.expandSnippet this.defaultKeyMap = CodeMirror.normalizeKeyMap({ - Tab: function (cm) { + Tab: function(cm) { const cursor = cm.getCursor() const line = cm.getLine(cursor.line) const cursorPosition = cursor.ch @@ -168,14 +186,16 @@ export default class CodeEditor extends React.Component { } cm.execCommand('goLineEnd') } else if ( - !charBeforeCursor.match(/\t|\s|\r|\n/) && + !charBeforeCursor.match(/\t|\s|\r|\n|\$/) && cursor.ch > 1 ) { // text expansion on tab key if the char before is alphabet - const snippets = JSON.parse( - fs.readFileSync(consts.SNIPPET_FILE, 'utf8') + const wordBeforeCursor = self.getWordBeforeCursor( + line, + cursor.line, + cursor.ch ) - if (expandSnippet(line, cursor, cm, snippets) === false) { + if (expandSnippet(wordBeforeCursor, cursor, cm) === false) { if (tabs) { cm.execCommand('insertTab') } else { @@ -191,12 +211,20 @@ export default class CodeEditor extends React.Component { } } }, - 'Cmd-Left': function (cm) { + 'Cmd-Left': function(cm) { cm.execCommand('goLineLeft') }, - 'Cmd-T': function (cm) { + 'Cmd-T': function(cm) { // Do nothing }, + [translateHotkey(hotkey.insertDate)]: function(cm) { + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleDateString()) + }, + [translateHotkey(hotkey.insertDateTime)]: function(cm) { + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleString()) + }, Enter: 'boostNewLineAndIndentContinueMarkdownList', 'Ctrl-C': cm => { if (cm.getOption('keyMap').substr(0, 3) === 'vim') { @@ -204,13 +232,48 @@ export default class CodeEditor extends React.Component { } return CodeMirror.Pass }, + [translateHotkey(hotkey.prettifyMarkdown)]: cm => { + // Default / User configured prettier options + const currentConfig = JSON.parse(self.props.prettierConfig) + + // Parser type will always need to be markdown so we override the option before use + currentConfig.parser = 'markdown' + + // Get current cursor position + const cursorPos = cm.getCursor() + currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos) + + // Prettify contents of editor + const formattedTextDetails = prettier.formatWithCursor( + cm.doc.getValue(), + currentConfig + ) + + const formattedText = formattedTextDetails.formatted + const formattedCursorPos = formattedTextDetails.cursorOffset + cm.doc.setValue(formattedText) + + // Reset Cursor position to be at the same markdown as was before prettifying + const newCursorPos = cm.doc.posFromIndex(formattedCursorPos) + cm.doc.setCursor(newCursorPos) + }, + [translateHotkey(hotkey.sortLines)]: cm => { + const selection = cm.doc.getSelection() + const appendLineBreak = /\n$/.test(selection) + + const sorted = _.split(selection.trim(), '\n').sort() + const sortedString = + _.join(sorted, '\n') + (appendLineBreak ? '\n' : '') + + cm.doc.replaceSelection(sortedString) + }, [translateHotkey(hotkey.pasteSmartly)]: cm => { this.handlePaste(cm, true) } }) } - updateTableEditorState () { + updateTableEditorState() { const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) if (active) { if (this.extraKeysMode !== 'editor') { @@ -226,26 +289,11 @@ export default class CodeEditor extends React.Component { } } - componentDidMount () { - const { rulers, enableRulers } = this.props + componentDidMount() { + const { rulers, enableRulers, enableMarkdownLint, RTL } = this.props eventEmitter.on('line:jump', this.scrollToLineHandeler) - const defaultSnippet = [ - { - id: crypto.randomBytes(16).toString('hex'), - name: 'Dummy text', - prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - } - ] - if (!fs.existsSync(consts.SNIPPET_FILE)) { - fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) - } - + snippetManager.init() this.updateDefaultKeyMap() this.value = this.props.value @@ -254,7 +302,7 @@ export default class CodeEditor extends React.Component { value: this.props.value, linesHighlighted: this.props.linesHighlighted, lineNumbers: this.props.displayLineNumbers, - lineWrapping: true, + lineWrapping: this.props.lineWrapping, theme: this.props.theme, indentUnit: this.props.indentSize, tabSize: this.props.indentSize, @@ -263,24 +311,42 @@ export default class CodeEditor extends React.Component { scrollPastEnd: this.props.scrollPastEnd, inputStyle: 'textarea', dragDrop: false, + direction: RTL ? 'rtl' : 'ltr', + rtlMoveVisually: RTL, foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false, + gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ], autoCloseBrackets: { codeBlock: { pairs: this.props.codeBlockMatchingPairs, + closeBefore: this.props.codeBlockMatchingCloseBefore, triples: this.props.codeBlockMatchingTriples, explode: this.props.codeBlockExplodingPairs }, markdown: { pairs: this.props.matchingPairs, + closeBefore: this.props.matchingCloseBefore, triples: this.props.matchingTriples, explode: this.props.explodingPairs } }, - extraKeys: this.defaultKeyMap + extraKeys: this.defaultKeyMap, + prettierConfig: this.props.prettierConfig }) - this.setMode(this.props.mode) + document.querySelector( + '.CodeMirror-lint-markers' + ).style.display = enableMarkdownLint ? 'inline-block' : 'none' + + if (!this.props.mode && this.props.value && this.props.autoDetect) { + this.autoDetectLanguage(this.props.value) + } else { + this.setMode(this.props.mode) + } this.editor.on('focus', this.focusHandler) this.editor.on('blur', this.blurHandler) @@ -307,7 +373,7 @@ export default class CodeEditor extends React.Component { this.textEditorInterface = new TextEditorInterface(this.editor) this.tableEditor = new TableEditor(this.textEditorInterface) if (this.props.spellCheck) { - this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) + this.editor.addPanel(this.createSpellCheckPanel(), { position: 'bottom' }) } eventEmitter.on('code:format-table', this.formatTable) @@ -317,13 +383,13 @@ export default class CodeEditor extends React.Component { }) this.editorKeyMap = CodeMirror.normalizeKeyMap({ - 'Tab': () => { + Tab: () => { this.tableEditor.nextCell(this.tableEditorOptions) }, 'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) }, - 'Enter': () => { + Enter: () => { this.tableEditor.nextRow(this.tableEditorOptions) }, 'Ctrl-Enter': () => { @@ -442,61 +508,12 @@ export default class CodeEditor extends React.Component { this.initialHighlighting() } - expandSnippet (line, cursor, cm, snippets) { - const wordBeforeCursor = this.getWordBeforeCursor( - line, - cursor.line, - cursor.ch - ) - const templateCursorString = ':{}' - for (let i = 0; i < snippets.length; i++) { - if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { - if (snippets[i].content.indexOf(templateCursorString) !== -1) { - const snippetLines = snippets[i].content.split('\n') - let cursorLineNumber = 0 - let cursorLinePosition = 0 - - let cursorIndex - for (let j = 0; j < snippetLines.length; j++) { - cursorIndex = snippetLines[j].indexOf(templateCursorString) - - if (cursorIndex !== -1) { - cursorLineNumber = j - cursorLinePosition = cursorIndex - - break - } - } - - cm.replaceRange( - snippets[i].content.replace(templateCursorString, ''), - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) - cm.setCursor({ - line: cursor.line + cursorLineNumber, - ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length - }) - } else { - cm.replaceRange( - snippets[i].content, - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) - } - return true - } - } - - return false - } - - getWordBeforeCursor (line, lineNumber, cursorPosition) { + getWordBeforeCursor(line, lineNumber, cursorPosition) { let wordBeforeCursor = '' const originCursorPosition = cursorPosition - const emptyChars = /\t|\s|\r|\n/ + const emptyChars = /\t|\s|\r|\n|\$/ - // to prevent the word to expand is long that will crash the whole app + // to prevent the word is long that will crash the whole app // the safeStop is there to stop user to expand words that longer than 20 chars const safeStop = 20 @@ -506,7 +523,7 @@ export default class CodeEditor extends React.Component { if (!emptyChars.test(currentChar)) { wordBeforeCursor = currentChar + wordBeforeCursor } else if (wordBeforeCursor.length >= safeStop) { - throw new Error('Your snippet trigger is too long !') + throw new Error('Stopped after 20 loops for safety reason !') } else { break } @@ -528,11 +545,11 @@ export default class CodeEditor extends React.Component { } } - quitEditor () { + quitEditor() { document.querySelector('textarea').blur() } - componentWillUnmount () { + componentWillUnmount() { this.editor.off('focus', this.focusHandler) this.editor.off('blur', this.blurHandler) this.editor.off('change', this.changeHandler) @@ -547,11 +564,13 @@ export default class CodeEditor extends React.Component { eventEmitter.off('code:format-table', this.formatTable) } - componentDidUpdate (prevProps, prevState) { + componentDidUpdate(prevProps, prevState) { let needRefresh = false const { rulers, - enableRulers + enableRulers, + enableMarkdownLint, + customMarkdownLintConfig } = this.props if (prevProps.mode !== this.props.mode) { this.setMode(this.props.mode) @@ -569,6 +588,25 @@ export default class CodeEditor extends React.Component { if (prevProps.keyMap !== this.props.keyMap) { needRefresh = true } + if (prevProps.RTL !== this.props.RTL) { + this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr') + this.editor.setOption('rtlMoveVisually', this.props.RTL) + } + if ( + prevProps.enableMarkdownLint !== enableMarkdownLint || + prevProps.customMarkdownLintConfig !== customMarkdownLintConfig + ) { + if (!enableMarkdownLint) { + this.editor.setOption('lint', { default: false }) + document.querySelector('.CodeMirror-lint-markers').style.display = + 'none' + } else { + this.editor.setOption('lint', this.getCodeEditorLintConfig()) + document.querySelector('.CodeMirror-lint-markers').style.display = + 'inline-block' + } + needRefresh = true + } if ( prevProps.enableRulers !== enableRulers || @@ -589,24 +627,36 @@ export default class CodeEditor extends React.Component { this.editor.setOption('lineNumbers', this.props.displayLineNumbers) } + if (prevProps.lineWrapping !== this.props.lineWrapping) { + this.editor.setOption('lineWrapping', this.props.lineWrapping) + } + if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } - if (prevProps.matchingPairs !== this.props.matchingPairs || + if ( + prevProps.matchingPairs !== this.props.matchingPairs || + prevProps.matchingCloseBefore !== this.props.matchingCloseBefore || prevProps.matchingTriples !== this.props.matchingTriples || prevProps.explodingPairs !== this.props.explodingPairs || prevProps.codeBlockMatchingPairs !== this.props.codeBlockMatchingPairs || - prevProps.codeBlockMatchingTriples !== this.props.codeBlockMatchingTriples || - prevProps.codeBlockExplodingPairs !== this.props.codeBlockExplodingPairs) { + prevProps.codeBlockMatchingCloseBefore !== + this.props.codeBlockMatchingCloseBefore || + prevProps.codeBlockMatchingTriples !== + this.props.codeBlockMatchingTriples || + prevProps.codeBlockExplodingPairs !== this.props.codeBlockExplodingPairs + ) { const autoCloseBrackets = { codeBlock: { pairs: this.props.codeBlockMatchingPairs, + closeBefore: this.props.codeBlockMatchingCloseBefore, triples: this.props.codeBlockMatchingTriples, explode: this.props.codeBlockExplodingPairs }, markdown: { pairs: this.props.matchingPairs, + closeBefore: this.props.matchingCloseBefore, triples: this.props.matchingTriples, explode: this.props.explodingPairs } @@ -650,26 +700,120 @@ export default class CodeEditor extends React.Component { const elem = document.getElementById('editor-bottom-panel') elem.parentNode.removeChild(elem) } else { - this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) + this.editor.addPanel(this.createSpellCheckPanel(), { + position: 'bottom' + }) } } + if ( + prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments + ) { + this.editor.setOption( + 'deleteUnusedAttachments', + this.props.deleteUnusedAttachments + ) + } if (needRefresh) { this.editor.refresh() } } - setMode (mode) { - let syntax = CodeMirror.findModeByName(convertModeName(mode)) + getCodeEditorLintConfig() { + const { mode } = this.props + const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown' + + return checkMarkdownNoteIsOpen + ? { + getAnnotations: this.validatorOfMarkdown, + async: true + } + : false + } + + validatorOfMarkdown(text, updateLinting) { + const { customMarkdownLintConfig } = this.props + let lintConfigJson + try { + Jsonlint.parse(customMarkdownLintConfig) + lintConfigJson = JSON.parse(customMarkdownLintConfig) + } catch (err) { + eventEmitter.emit('APP_SETTING_ERROR') + return + } + const lintOptions = { + strings: { + content: text + }, + config: lintConfigJson + } + + return markdownlint(lintOptions, (err, result) => { + if (!err) { + const foundIssues = [] + const splitText = text.split('\n') + result.content.map(item => { + let ruleNames = '' + item.ruleNames.map((ruleName, index) => { + ruleNames += ruleName + ruleNames += index === item.ruleNames.length - 1 ? ': ' : '/' + }) + const lineNumber = item.lineNumber - 1 + foundIssues.push({ + from: CodeMirror.Pos(lineNumber, 0), + to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length), + message: ruleNames + item.ruleDescription, + severity: 'warning' + }) + }) + updateLinting(foundIssues) + } + }) + } + + setMode(mode) { + let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') this.editor.setOption('mode', syntax.mime) CodeMirror.autoLoadMode(this.editor, syntax.mode) } - handleChange (editor, changeObject) { + handleChange(editor, changeObject) { spellcheck.handleChange(editor, changeObject) + // The current note contains an toc. We'll check for changes on headlines. + // origin is undefined when markdownTocGenerator replace the old tod + if (tocExistsInEditor(editor) && changeObject.origin !== undefined) { + let requireTocUpdate + + // Check if one of the changed lines contains a headline + for (let line = 0; line < changeObject.text.length; line++) { + if ( + this.linePossibleContainsHeadline( + editor.getLine(changeObject.from.line + line) + ) + ) { + requireTocUpdate = true + break + } + } + + if (!requireTocUpdate) { + // Check if one of the removed lines contains a headline + for (let line = 0; line < changeObject.removed.length; line++) { + if (this.linePossibleContainsHeadline(changeObject.removed[line])) { + requireTocUpdate = true + break + } + } + } + + if (requireTocUpdate) { + generateInEditor(editor) + } + } + this.updateHighlight(editor, changeObject) this.value = editor.getValue() @@ -678,15 +822,21 @@ export default class CodeEditor extends React.Component { } } - incrementLines (start, linesAdded, linesRemoved, editor) { - let highlightedLines = editor.options.linesHighlighted + linePossibleContainsHeadline(currentLine) { + // We can't check if the line start with # because when some write text before + // the # we also need to update the toc + return currentLine.includes('# ') + } + + incrementLines(start, linesAdded, linesRemoved, editor) { + const highlightedLines = editor.options.linesHighlighted const totalHighlightedLines = highlightedLines.length - let offset = linesAdded - linesRemoved + const offset = linesAdded - linesRemoved // Store new items to be added as we're changing the lines - let newLines = [] + const newLines = [] let i = totalHighlightedLines @@ -699,7 +849,7 @@ export default class CodeEditor extends React.Component { highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1) // Lines that need to be relocated - if (lineNumber >= (start + linesRemoved)) { + if (lineNumber >= start + linesRemoved) { newLines.push(lineNumber + offset) } } @@ -713,22 +863,30 @@ export default class CodeEditor extends React.Component { } } - handleHighlight (editor, changeObject) { + handleHighlight(editor, changeObject) { const lines = editor.options.linesHighlighted if (!lines.includes(changeObject)) { lines.push(changeObject) - editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + editor.addLineClass( + changeObject, + 'text', + 'CodeMirror-activeline-background' + ) } else { lines.splice(lines.indexOf(changeObject), 1) - editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background') + editor.removeLineClass( + changeObject, + 'text', + 'CodeMirror-activeline-background' + ) } if (this.props.onChange) { this.props.onChange(editor) } } - updateHighlight (editor, changeObject) { + updateHighlight(editor, changeObject) { const linesAdded = changeObject.text.length - 1 const linesRemoved = changeObject.removed.length - 1 @@ -759,25 +917,28 @@ export default class CodeEditor extends React.Component { this.incrementLines(start, linesAdded, linesRemoved, editor) } - moveCursorTo (row, col) {} + moveCursorTo(row, col) {} - scrollToLine (event, num) { + scrollToLine(event, num) { const cursor = { line: num, ch: 1 } this.editor.setCursor(cursor) + const top = this.editor.charCoords({ line: num, ch: 0 }, 'local').top + const middleHeight = this.editor.getScrollerElement().offsetHeight / 2 + this.editor.scrollTo(null, top - middleHeight - 5) } - focus () { + focus() { this.editor.focus() } - blur () { + blur() { this.editor.blur() } - reload () { + reload() { // Change event shouldn't be fired when switch note this.editor.off('change', this.changeHandler) this.value = this.props.value @@ -788,18 +949,30 @@ export default class CodeEditor extends React.Component { this.editor.refresh() } - setValue (value) { + setValue(value) { const cursor = this.editor.getCursor() this.editor.setValue(value) this.editor.setCursor(cursor) } - handleDropImage (dropEvent) { + /** + * Update content of one line + * @param {Number} lineNumber + * @param {String} content + */ + setLineContent(lineNumber, content) { + const prevContent = this.editor.getLine(lineNumber) + const prevContentLength = prevContent ? prevContent.length : 0 + this.editor.replaceRange( + content, + { line: lineNumber, ch: 0 }, + { line: lineNumber, ch: prevContentLength } + ) + } + + handleDropImage(dropEvent) { dropEvent.preventDefault() - const { - storageKey, - noteKey - } = this.props + const { storageKey, noteKey } = this.props attachmentManagement.handleAttachmentDrop( this, storageKey, @@ -808,35 +981,44 @@ export default class CodeEditor extends React.Component { ) } - insertAttachmentMd (imageMd) { + insertAttachmentMd(imageMd) { this.editor.replaceSelection(imageMd) } - handlePaste (editor, forceSmartPaste) { + autoDetectLanguage(content) { + const res = hljs.highlightAuto(content, Object.keys(languageMaps)) + this.setMode(languageMaps[res.language]) + } + + handlePaste(editor, forceSmartPaste) { const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props - const isURL = str => { - const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ - return matcher.test(str) - } + const isURL = str => + /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) const isInLinkTag = editor => { const startCursor = editor.getCursor('start') - const prevChar = editor.getRange({ - line: startCursor.line, - ch: startCursor.ch - 2 - }, { - line: startCursor.line, - ch: startCursor.ch - }) + const prevChar = editor.getRange( + { + line: startCursor.line, + ch: startCursor.ch - 2 + }, + { + line: startCursor.line, + ch: startCursor.ch + } + ) const endCursor = editor.getCursor('end') - const nextChar = editor.getRange({ - line: endCursor.line, - ch: endCursor.ch - }, { - line: endCursor.line, - ch: endCursor.ch + 1 - }) + const nextChar = editor.getRange( + { + line: endCursor.line, + ch: endCursor.ch + }, + { + line: endCursor.line, + ch: endCursor.ch + 1 + } + ) return prevChar === '](' && nextChar === ')' } @@ -848,7 +1030,7 @@ export default class CodeEditor extends React.Component { return true } - let line = line = cursor.line - 1 + let line = (line = cursor.line - 1) while (line >= 0) { token = editor.getTokenAt({ ch: 3, @@ -880,6 +1062,12 @@ export default class CodeEditor extends React.Component { if (isInFencedCodeBlock(editor)) { this.handlePasteText(editor, pastedTxt) + } else if ( + fetchUrlTitle && + isMarkdownTitleURL(pastedTxt) && + !isInLinkTag(editor) + ) { + this.handlePasteUrl(editor, pastedTxt) } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this.handlePasteUrl(editor, pastedTxt) } else if (attachmentManagement.isAttachmentLink(pastedTxt)) { @@ -891,7 +1079,7 @@ export default class CodeEditor extends React.Component { } else { const image = clipboard.readImage() if (!image.isEmpty()) { - attachmentManagement.handlePastNativeImage( + attachmentManagement.handlePasteNativeImage( this, storageKey, noteKey, @@ -908,16 +1096,30 @@ export default class CodeEditor extends React.Component { this.handlePasteText(editor, pastedTxt) } } + + if (!this.props.mode && this.props.autoDetect) { + this.autoDetectLanguage(editor.doc.getValue()) + } } - handleScroll (e) { + handleScroll(e) { if (this.props.onScroll) { this.props.onScroll(e) } } - handlePasteUrl (editor, pastedTxt) { - const taggedUrl = `<${pastedTxt}>` + handlePasteUrl(editor, pastedTxt) { + let taggedUrl = `<${pastedTxt}>` + let urlToFetch = pastedTxt + let titleMark = '' + + if (isMarkdownTitleURL(pastedTxt)) { + const pastedTxtSplitted = pastedTxt.split(' ') + titleMark = `${pastedTxtSplitted[0]} ` + urlToFetch = pastedTxtSplitted[1] + taggedUrl = `<${urlToFetch}>` + } + editor.replaceSelection(taggedUrl) const isImageReponse = response => { @@ -929,22 +1131,23 @@ export default class CodeEditor extends React.Component { const replaceTaggedUrl = replacement => { const value = editor.getValue() const cursor = editor.getCursor() - const newValue = value.replace(taggedUrl, replacement) + const newValue = value.replace(taggedUrl, titleMark + replacement) const newCursor = Object.assign({}, cursor, { - ch: cursor.ch + newValue.length - value.length + ch: cursor.ch + newValue.length - (value.length - titleMark.length) }) + editor.setValue(newValue) editor.setCursor(newCursor) } - fetch(pastedTxt, { + fetch(urlToFetch, { method: 'get' }) .then(response => { if (isImageReponse(response)) { - return this.mapImageResponse(response, pastedTxt) + return this.mapImageResponse(response, urlToFetch) } else { - return this.mapNormalResponse(response, pastedTxt) + return this.mapNormalResponse(response, urlToFetch) } }) .then(replacement => { @@ -955,16 +1158,16 @@ export default class CodeEditor extends React.Component { }) } - handlePasteHtml (editor, pastedHtml) { + handlePasteHtml(editor, pastedHtml) { const markdown = this.turndownService.turndown(pastedHtml) editor.replaceSelection(markdown) } - handlePasteText (editor, pastedTxt) { + handlePasteText(editor, pastedTxt) { editor.replaceSelection(pastedTxt) } - mapNormalResponse (response, pastedTxt) { + mapNormalResponse(response, pastedTxt) { return this.decodeResponse(response).then(body => { return new Promise((resolve, reject) => { try { @@ -972,10 +1175,12 @@ export default class CodeEditor extends React.Component { body, 'text/html' ) - const escapePipe = (str) => { + const escapePipe = str => { return str.replace('|', '\\|') } - const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})` + const linkWithTitle = `[${escapePipe( + parsedBody.title + )}](${pastedTxt})` resolve(linkWithTitle) } catch (e) { reject(e) @@ -984,7 +1189,7 @@ export default class CodeEditor extends React.Component { }) } - initialHighlighting () { + initialHighlighting() { if (this.editor.options.linesHighlighted == null) { return } @@ -998,16 +1203,20 @@ export default class CodeEditor extends React.Component { // make sure that we skip the invalid lines althrough this case should not be happened. continue } - this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background') + this.editor.addLineClass( + lineNumber, + 'text', + 'CodeMirror-activeline-background' + ) } } - restartHighlighting () { + restartHighlighting() { this.editor.options.linesHighlighted = this.props.linesHighlighted this.initialHighlighting() } - mapImageResponse (response, pastedTxt) { + mapImageResponse(response, pastedTxt) { return new Promise((resolve, reject) => { try { const url = response.url @@ -1020,7 +1229,7 @@ export default class CodeEditor extends React.Component { }) } - decodeResponse (response) { + decodeResponse(response) { const headers = response.headers const _charset = headers.has('content-type') ? this.extractContentTypeCharset(headers.get('content-type')) @@ -1028,11 +1237,11 @@ export default class CodeEditor extends React.Component { return response.arrayBuffer().then(buff => { return new Promise((resolve, reject) => { try { - const charset = _charset !== undefined && - iconv.encodingExists(_charset) - ? _charset - : 'utf-8' - resolve(iconv.decode(new Buffer(buff), charset).toString()) + const charset = + _charset !== undefined && iconv.encodingExists(_charset) + ? _charset + : 'utf-8' + resolve(iconv.decode(Buffer.from(buff), charset).toString()) } catch (e) { reject(e) } @@ -1040,52 +1249,50 @@ export default class CodeEditor extends React.Component { }) } - extractContentTypeCharset (contentType) { + extractContentTypeCharset(contentType) { return contentType .split(';') .filter(str => { - return str.trim().toLowerCase().startsWith('charset') + return str + .trim() + .toLowerCase() + .startsWith('charset') }) .map(str => { return str.replace(/['"]/g, '').split('=')[1] })[0] } - render () { - const { - className, - fontSize - } = this.props - const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) - const width = this.props.width - return (< - div className={ - className == null ? 'CodeEditor' : `CodeEditor ${className}` - } - ref='root' - tabIndex='-1' - style={ - { - fontFamily, - fontSize: fontSize, - width: width - } - } - onDrop={ - e => this.handleDropImage(e) - } + render() { + const { className, fontSize, fontFamily, width, height } = this.props + const normalisedFontFamily = normalizeEditorFontFamily(fontFamily) + + return ( +
this.handleDropImage(e)} /> ) } - createSpellCheckPanel () { + createSpellCheckPanel() { const panel = document.createElement('div') panel.className = 'panel bottom' panel.id = 'editor-bottom-panel' const dropdown = document.createElement('select') dropdown.title = 'Spellcheck' dropdown.className = styles['spellcheck-select'] - dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value)) + dropdown.addEventListener('change', e => + spellcheck.setLanguage(this.editor, dropdown.value) + ) const options = spellcheck.getAvailableDictionaries() for (const op of options) { const option = document.createElement('option') @@ -1107,7 +1314,12 @@ CodeEditor.propTypes = { onBlur: PropTypes.func, onChange: PropTypes.func, readOnly: PropTypes.bool, - spellCheck: PropTypes.bool + autoDetect: PropTypes.bool, + spellCheck: PropTypes.bool, + enableMarkdownLint: PropTypes.bool, + customMarkdownLintConfig: PropTypes.string, + deleteUnusedAttachments: PropTypes.bool, + RTL: PropTypes.bool } CodeEditor.defaultProps = { @@ -1118,5 +1330,11 @@ CodeEditor.defaultProps = { fontFamily: 'Monaco, Consolas', indentSize: 4, indentType: 'space', - spellCheck: false + autoDetect: false, + spellCheck: false, + enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, + customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig, + prettierConfig: DEFAULT_CONFIG.editor.prettierConfig, + deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments, + RTL: false } diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl index 7a254935..1aa0e466 100644 --- a/browser/components/CodeEditor.styl +++ b/browser/components/CodeEditor.styl @@ -3,4 +3,3 @@ .spellcheck-select border: none - text-decoration underline wavy red diff --git a/browser/components/ColorPicker.js b/browser/components/ColorPicker.js new file mode 100644 index 00000000..4d4e80e4 --- /dev/null +++ b/browser/components/ColorPicker.js @@ -0,0 +1,77 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { SketchPicker } from 'react-color' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ColorPicker.styl' + +const componentHeight = 330 + +class ColorPicker extends React.Component { + constructor(props) { + super(props) + + this.state = { + color: this.props.color || '#939395' + } + + this.onColorChange = this.onColorChange.bind(this) + this.handleConfirm = this.handleConfirm.bind(this) + } + + componentWillReceiveProps(nextProps) { + this.onColorChange(nextProps.color) + } + + onColorChange(color) { + this.setState({ + color + }) + } + + handleConfirm() { + this.props.onConfirm(this.state.color) + } + + render() { + const { onReset, onCancel, targetRect } = this.props + const { color } = this.state + + const clientHeight = document.body.clientHeight + const alignX = targetRect.right + 4 + let alignY = targetRect.top + if (targetRect.top + componentHeight > clientHeight) { + alignY = targetRect.bottom - componentHeight + } + + return ( +
+
+ +
+ + + +
+
+ ) + } +} + +ColorPicker.propTypes = { + color: PropTypes.string, + targetRect: PropTypes.object, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onReset: PropTypes.func.isRequired +} + +export default CSSModules(ColorPicker, styles) diff --git a/browser/components/ColorPicker.styl b/browser/components/ColorPicker.styl new file mode 100644 index 00000000..fbc1212a --- /dev/null +++ b/browser/components/ColorPicker.styl @@ -0,0 +1,39 @@ +.colorPicker + position fixed + z-index 2 + display flex + flex-direction column + +.cover + position fixed + top 0 + right 0 + bottom 0 + left 0 + +.footer + display flex + justify-content center + z-index 2 + align-items center + & > button + button + margin-left 10px + +.btn-cancel, +.btn-confirm, +.btn-reset + vertical-align middle + height 25px + margin-top 2.5px + border-radius 2px + border none + padding 0 5px + background-color $default-button-background + &:hover + background-color $default-button-background--hover +.btn-confirm + background-color #1EC38B + &:hover + background-color darken(#1EC38B, 25%) + + diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 1596b2e5..5108ddc6 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' @@ -10,7 +11,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' class MarkdownEditor extends React.Component { - constructor (props) { + constructor(props) { super(props) // char codes for ctrl + w @@ -20,194 +21,246 @@ class MarkdownEditor extends React.Component { this.supportMdSelectionBold = [16, 17, 186] this.state = { - status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW', + status: + props.config.editor.switchPreview === 'RIGHTCLICK' + ? props.config.editor.delfaultStatus + : 'CODE', renderValue: props.value, keyPressed: new Set(), - isLocked: false + isLocked: props.isLocked } - this.lockEditorCode = () => this.handleLockEditor() + this.lockEditorCode = this.handleLockEditor.bind(this) + this.focusEditor = this.focusEditor.bind(this) + + this.previewRef = React.createRef() } - componentDidMount () { + componentDidMount() { this.value = this.refs.code.value eventEmitter.on('editor:lock', this.lockEditorCode) + eventEmitter.on('editor:focus', this.focusEditor) } - componentDidUpdate () { + componentDidUpdate() { this.value = this.refs.code.value } - componentWillReceiveProps (props) { + UNSAFE_componentWillReceiveProps(props) { if (props.value !== this.props.value) { this.queueRendering(props.value) } } - componentWillUnmount () { + componentWillUnmount() { this.cancelQueue() eventEmitter.off('editor:lock', this.lockEditorCode) + eventEmitter.off('editor:focus', this.focusEditor) } - queueRendering (value) { + focusEditor() { + this.setState( + { + status: 'CODE' + }, + () => { + if (this.refs.code == null) { + return + } + this.refs.code.focus() + } + ) + } + + queueRendering(value) { clearTimeout(this.renderTimer) this.renderTimer = setTimeout(() => { this.renderPreview(value) }, 500) } - cancelQueue () { + cancelQueue() { clearTimeout(this.renderTimer) } - renderPreview (value) { + renderPreview(value) { this.setState({ renderValue: value }) } - setValue (value) { + setValue(value) { this.refs.code.setValue(value) } - handleChange (e) { + handleChange(e) { this.value = this.refs.code.value this.props.onChange(e) } - handleContextMenu (e) { + handleContextMenu(e) { + if (this.state.isLocked) return const { config } = this.props if (config.editor.switchPreview === 'RIGHTCLICK') { const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW' - this.setState({ - status: newStatus - }, () => { - if (newStatus === 'CODE') { - this.refs.code.focus() - } else { - this.refs.preview.focus() - } - eventEmitter.emit('topbar:togglelockbutton', this.state.status) + this.setState( + { + status: newStatus + }, + () => { + if (newStatus === 'CODE') { + this.refs.code.focus() + } else { + this.previewRef.current.focus() + } + eventEmitter.emit('topbar:togglelockbutton', this.state.status) - const newConfig = Object.assign({}, config) - newConfig.editor.delfaultStatus = newStatus - ConfigManager.set(newConfig) - }) + const newConfig = Object.assign({}, config) + newConfig.editor.delfaultStatus = newStatus + ConfigManager.set(newConfig) + } + ) } } - handleBlur (e) { + handleBlur(e) { if (this.state.isLocked) return this.setState({ keyPressed: new Set() }) const { config } = this.props - if (config.editor.switchPreview === 'BLUR' || - (config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE') + if ( + config.editor.switchPreview === 'BLUR' || + (config.editor.switchPreview === 'DBL_CLICK' && + this.state.status === 'CODE') ) { const cursorPosition = this.refs.code.editor.getCursor() - this.setState({ - status: 'PREVIEW' - }, () => { - this.refs.preview.focus() - this.refs.preview.scrollTo(cursorPosition.line) - }) + this.setState( + { + status: 'PREVIEW' + }, + () => { + this.previewRef.current.focus() + this.previewRef.current.scrollToRow(cursorPosition.line) + } + ) eventEmitter.emit('topbar:togglelockbutton', this.state.status) } } - handleDoubleClick (e) { + handleDoubleClick(e) { if (this.state.isLocked) return - this.setState({keyPressed: new Set()}) + this.setState({ keyPressed: new Set() }) const { config } = this.props if (config.editor.switchPreview === 'DBL_CLICK') { - this.setState({ - status: 'CODE' - }, () => { - this.refs.code.focus() - eventEmitter.emit('topbar:togglelockbutton', this.state.status) - }) + this.setState( + { + status: 'CODE' + }, + () => { + this.refs.code.focus() + eventEmitter.emit('topbar:togglelockbutton', this.state.status) + } + ) } } - handlePreviewMouseDown (e) { + handlePreviewMouseDown(e) { this.previewMouseDownedAt = new Date() } - handlePreviewMouseUp (e) { + handlePreviewMouseUp(e) { const { config } = this.props - if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) { - this.setState({ - status: 'CODE' - }, () => { - this.refs.code.focus() - }) + if ( + config.editor.switchPreview === 'BLUR' && + new Date() - this.previewMouseDownedAt < 200 + ) { + this.setState( + { + status: 'CODE' + }, + () => { + this.refs.code.focus() + } + ) eventEmitter.emit('topbar:togglelockbutton', this.state.status) } } - handleCheckboxClick (e) { + handleCheckboxClick(e) { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { - const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 - const lines = this.refs.code.value - .split('\n') + const lineIndex = + parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 + const lines = this.refs.code.value.split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } - focus () { + focus() { if (this.state.status === 'PREVIEW') { - this.setState({ - status: 'CODE' - }, () => { - this.refs.code.focus() - }) + this.setState( + { + status: 'CODE' + }, + () => { + this.refs.code.focus() + } + ) } else { this.refs.code.focus() } eventEmitter.emit('topbar:togglelockbutton', this.state.status) } - reload () { + reload() { this.refs.code.reload() this.cancelQueue() this.renderPreview(this.props.value) } - handleKeyDown (e) { + handleKeyDown(e) { const { config } = this.props if (this.state.status !== 'CODE') return false const keyPressed = this.state.keyPressed keyPressed.add(e.keyCode) this.setState({ keyPressed }) - const 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' && - this.escapeFromEditor.every(isNoteHandlerKey)) { + if ( + keyPressed.size === this.escapeFromEditor.length && + !this.state.isLocked && + this.state.status === 'CODE' && + this.escapeFromEditor.every(isNoteHandlerKey) + ) { this.handleContextMenu() if (config.editor.switchPreview === 'BLUR') document.activeElement.blur() } - if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) { + if ( + keyPressed.size === this.supportMdSelectionBold.length && + this.supportMdSelectionBold.every(isNoteHandlerKey) + ) { this.addMdAroundWord('**') } } - addMdAroundWord (mdElement) { + addMdAroundWord(mdElement) { if (this.refs.code.editor.getSelection()) { return this.addMdAroundSelection(mdElement) } @@ -215,47 +268,63 @@ class MarkdownEditor extends React.Component { const word = this.refs.code.editor.findWordAt(currentCaret) const cmDoc = this.refs.code.editor.getDoc() cmDoc.replaceRange(mdElement, word.anchor) - cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length }) - } - - addMdAroundSelection (mdElement) { - this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`) - } - - handleDropImage (dropEvent) { - dropEvent.preventDefault() - const { storageKey, noteKey } = this.props - - this.setState({ - status: 'CODE' - }, () => { - this.refs.code.focus() - - this.refs.code.editor.execCommand('goDocEnd') - this.refs.code.editor.execCommand('goLineEnd') - this.refs.code.editor.execCommand('newlineAndIndent') - - attachmentManagement.handleAttachmentDrop( - this.refs.code, - storageKey, - noteKey, - dropEvent - ) + cmDoc.replaceRange(mdElement, { + line: word.head.line, + ch: word.head.ch + mdElement.length }) } - handleKeyUp (e) { + addMdAroundSelection(mdElement) { + this.refs.code.editor.replaceSelection( + `${mdElement}${this.refs.code.editor.getSelection()}${mdElement}` + ) + } + + handleDropImage(dropEvent) { + dropEvent.preventDefault() + const { storageKey, noteKey } = this.props + + this.setState( + { + status: 'CODE' + }, + () => { + this.refs.code.focus() + + this.refs.code.editor.execCommand('goDocEnd') + this.refs.code.editor.execCommand('goLineEnd') + this.refs.code.editor.execCommand('newlineAndIndent') + + attachmentManagement.handleAttachmentDrop( + this.refs.code, + storageKey, + noteKey, + dropEvent + ) + } + ) + } + + handleKeyUp(e) { const keyPressed = this.state.keyPressed keyPressed.delete(e.keyCode) this.setState({ keyPressed }) } - handleLockEditor () { + handleLockEditor() { this.setState({ isLocked: !this.state.isLocked }) } - render () { - const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props + render() { + const { + className, + value, + config, + storageKey, + noteKey, + linesHighlighted, + RTL + } = this.props let editorFontSize = parseInt(config.editor.fontSize, 10) if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 @@ -263,23 +332,24 @@ class MarkdownEditor extends React.Component { if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 const previewStyle = {} - if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' + if (this.props.ignorePreviewPointerEvents) + previewStyle.pointerEvents = 'none' const storage = findStorage(storageKey) return ( -
this.handleContextMenu(e)} + onContextMenu={e => this.handleContextMenu(e)} tabIndex='-1' - onKeyDown={(e) => this.handleKeyDown(e)} - onKeyUp={(e) => this.handleKeyUp(e)} + onKeyDown={e => this.handleKeyDown(e)} + onKeyUp={e => this.handleKeyUp(e)} > - this.handleChange(e)} - onBlur={(e) => this.handleBlur(e)} + onChange={e => this.handleChange(e)} + onBlur={e => this.handleBlur(e)} spellCheck={config.editor.spellcheck} enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} switchPreview={config.editor.switchPreview} + enableMarkdownLint={config.editor.enableMarkdownLint} + customMarkdownLintConfig={config.editor.customMarkdownLintConfig} + prettierConfig={config.editor.prettierConfig} + deleteUnusedAttachments={config.editor.deleteUnusedAttachments} + RTL={RTL} /> - this.handleContextMenu(e)} - onDoubleClick={(e) => this.handleDoubleClick(e)} + mermaidHTMLLabel={config.preview.mermaidHTMLLabel} + onContextMenu={e => this.handleContextMenu(e)} + onDoubleClick={e => this.handleDoubleClick(e)} tabIndex='0' value={this.state.renderValue} - onMouseUp={(e) => this.handlePreviewMouseUp(e)} - onMouseDown={(e) => this.handlePreviewMouseDown(e)} - onCheckboxClick={(e) => this.handleCheckboxClick(e)} + onMouseUp={e => this.handlePreviewMouseUp(e)} + onMouseDown={e => this.handlePreviewMouseDown(e)} + onCheckboxClick={e => this.handleCheckboxClick(e)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} noteKey={noteKey} customCSS={config.preview.customCSS} allowCustomCSS={config.preview.allowCustomCSS} lineThroughCheckbox={config.preview.lineThroughCheckbox} - onDrop={(e) => this.handleDropImage(e)} + onDrop={e => this.handleDropImage(e)} + RTL={RTL} />
) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 68484b04..9ddea318 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' +import { connect } from 'react-redux' import Markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' @@ -8,9 +9,10 @@ import consts from 'browser/lib/consts' import Raphael from 'raphael' import flowchart from 'flowchart' import mermaidRender from './render/MermaidRender' -import SequenceDiagram from 'js-sequence-diagrams' +import SequenceDiagram from '@rokt33r/js-sequence-diagrams' import Chart from 'chart.js' import eventEmitter from 'browser/main/lib/eventEmitter' +import config from 'browser/main/lib/ConfigManager' import htmlTextHelper from 'browser/lib/htmlTextHelper' import convertModeName from 'browser/lib/convertModeName' import copy from 'copy-to-clipboard' @@ -18,15 +20,17 @@ import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' -import context from 'browser/lib/context' -import i18n from 'browser/lib/i18n' -import fs from 'fs' import { render } from 'react-dom' import Carousel from 'react-image-carousel' +import { push } from 'connected-react-router' import ConfigManager from '../main/lib/ConfigManager' +import uiThemes from 'browser/lib/ui-themes' +import i18n from 'browser/lib/i18n' const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') +const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder') + .buildMarkdownPreviewContextMenu const { app } = remote const path = require('path') @@ -34,8 +38,6 @@ const fileUrl = require('file-url') const dialog = remote.dialog -const uri2path = require('file-uri-to-path') - const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const appPath = fileUrl( process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve() @@ -46,16 +48,30 @@ const CSS_FILES = [ `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css` ] -function buildStyle ( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS -) { +/** + * @param {Object} opts + * @param {String} opts.fontFamily + * @param {Numberl} opts.fontSize + * @param {String} opts.codeBlockFontFamily + * @param {String} opts.theme + * @param {Boolean} [opts.lineNumber] Should show line number + * @param {Boolean} [opts.scrollPastEnd] + * @param {Boolean} [opts.allowCustomCSS] Should add custom css + * @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy + * @returns {String} + */ +function buildStyle(opts) { + const { + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS, + RTL + } = opts return ` @font-face { font-family: 'Lato'; @@ -85,12 +101,23 @@ function buildStyle ( url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); } + ${markdownStyle} body { font-family: '${fontFamily.join("','")}'; font-size: ${fontSize}px; - ${scrollPastEnd && 'padding-bottom: 90vh;'} + + ${ + scrollPastEnd + ? ` + padding-bottom: 90vh; + box-sizing: border-box; + ` + : '' + } + ${RTL ? 'direction: rtl;' : ''} + ${RTL ? 'text-align: right;' : ''} } @media print { body { @@ -100,7 +127,84 @@ body { code { font-family: '${codeBlockFontFamily.join("','")}'; background-color: rgba(0,0,0,0.04); + text-align: left; + direction: ltr; } + +p code, +li code, +td code +{ + padding: 2px; + border-width: 1px; + border-style: solid; + border-radius: 5px; +} +[data-theme="default"] p code, +[data-theme="default"] li code, +[data-theme="default"] td code +{ + background-color: #F4F4F4; + border-color: #d9d9d9; + color: inherit; +} +[data-theme="white"] p code, +[data-theme="white"] li code, +[data-theme="white"] td code +{ + background-color: #F4F4F4; + border-color: #d9d9d9; + color: inherit; +} +[data-theme="dark"] p code, +[data-theme="dark"] li code, +[data-theme="dark"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} +[data-theme="dracula"] p code, +[data-theme="dracula"] li code, +[data-theme="dracula"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} +[data-theme="monokai"] p code, +[data-theme="monokai"] li code, +[data-theme="monokai"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} +[data-theme="nord"] p code, +[data-theme="nord"] li code, +[data-theme="nord"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} +[data-theme="solarized-dark"] p code, +[data-theme="solarized-dark"] li code, +[data-theme="solarized-dark"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} +[data-theme="vulcan"] p code, +[data-theme="vulcan"] li code, +[data-theme="vulcan"] td code +{ + background-color: #444444; + border-color: #555; + color: #FFFFFF; +} + .lineNumber { ${lineNumber && 'display: block !important;'} font-family: '${codeBlockFontFamily.join("','")}'; @@ -130,14 +234,22 @@ h1, h2 { border: none; } +h3 { + margin: 1em 0 0.8em; +} + +h4, h5, h6 { + margin: 1.1em 0 0.5em; +} + h1 { - padding-bottom: 4px; + padding: 0.2em 0 0.2em; margin: 1em 0 8px; } h2 { - padding-bottom: 0.2em; - margin: 1em 0 0.37em; + padding: 0.2em 0 0.2em; + margin: 1em 0 0.7em; } body p { @@ -160,21 +272,33 @@ ${allowCustomCSS ? customCSS : ''} const scrollBarStyle = ` ::-webkit-scrollbar { + ${config.get().ui.showScrollBar ? '' : 'display: none;'} width: 12px; } ::-webkit-scrollbar-thumb { + ${config.get().ui.showScrollBar ? '' : 'display: none;'} background-color: rgba(0, 0, 0, 0.15); } + +::-webkit-scrollbar-track-piece { + background-color: inherit; +} ` const scrollBarDarkStyle = ` ::-webkit-scrollbar { + ${config.get().ui.showScrollBar ? '' : 'display: none;'} width: 12px; } ::-webkit-scrollbar-thumb { + ${config.get().ui.showScrollBar ? '' : 'display: none;'} background-color: rgba(0, 0, 0, 0.3); } + +::-webkit-scrollbar-track-piece { + background-color: inherit; +} ` const OSX = global.process.platform === 'darwin' @@ -192,8 +316,21 @@ const defaultCodeBlockFontFamily = [ 'source-code-pro', 'monospace' ] -export default class MarkdownPreview extends React.Component { - constructor (props) { + +// return the line number of the line that used to generate the specified element +// return -1 if the line is not found +function getSourceLineNumberByElement(element) { + let isHasLineNumber = element.dataset.line !== undefined + let parent = element + while (!isHasLineNumber && parent.parentElement !== null) { + parent = parent.parentElement + isHasLineNumber = parent.dataset.line !== undefined + } + return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1 +} + +class MarkdownPreview extends React.Component { + constructor(props) { super(props) this.contextMenuHandler = e => this.handleContextMenu(e) @@ -208,14 +345,16 @@ export default class MarkdownPreview extends React.Component { this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsHtmlHandler = () => this.handleSaveAsHtml() + this.saveAsPdfHandler = () => this.handleSaveAsPdf() this.printHandler = () => this.handlePrint() + this.resizeHandler = _.throttle(this.handleResize.bind(this), 100) this.linkClickHandler = this.handleLinkClick.bind(this) this.initMarkdown = this.initMarkdown.bind(this) this.initMarkdown() } - initMarkdown () { + initMarkdown() { const { smartQuotes, sanitize, breaks } = this.props this.markdown = new Markdown({ typographer: smartQuotes, @@ -224,64 +363,61 @@ export default class MarkdownPreview extends React.Component { }) } - handleCheckboxClick (e) { + handleCheckboxClick(e) { this.props.onCheckboxClick(e) } - handleScroll (e) { + handleScroll(e) { if (this.props.onScroll) { this.props.onScroll(e) } } - handleContextMenu (event) { - // If a contextMenu handler was passed to us, use it instead of the self-defined one -> return - if (_.isFunction(this.props.onContextMenu)) { + handleContextMenu(event) { + const menu = buildMarkdownPreviewContextMenu(this, event) + const switchPreview = ConfigManager.get().editor.switchPreview + if (menu != null && switchPreview !== 'RIGHTCLICK') { + menu.popup(remote.getCurrentWindow()) + } else if (_.isFunction(this.props.onContextMenu)) { this.props.onContextMenu(event) - return - } - // No contextMenu was passed to us -> execute our own link-opener - if (event.target.tagName.toLowerCase() === 'a') { - const href = event.target.href - const isLocalFile = href.startsWith('file:') - if (isLocalFile) { - const absPath = uri2path(href) - try { - if (fs.lstatSync(absPath).isFile()) { - context.popup([ - { - label: i18n.__('Show in explorer'), - click: (e) => shell.showItemInFolder(absPath) - } - ]) - } - } catch (e) { - console.log('Error while evaluating if the file is locally available', e) - } - } } } - handleDoubleClick (e) { + handleDoubleClick(e) { if (this.props.onDoubleClick != null) this.props.onDoubleClick(e) } - handleMouseDown (e) { + handleMouseDown(e) { const config = ConfigManager.get() - if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') { + const clickElement = e.target + const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV" + const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked. + + if ( + config.editor.switchPreview === 'RIGHTCLICK' && + e.buttons === 2 && + config.editor.type === 'SPLIT' + ) { eventEmitter.emit('topbar:togglemodebutton', 'CODE') } - if (e.target != null) { - switch (e.target.tagName) { - case 'A': - case 'INPUT': - return null + if (e.ctrlKey) { + if (config.editor.type === 'SPLIT') { + if (lineNumber !== -1) { + eventEmitter.emit('line:jump', lineNumber) + } + } else { + if (lineNumber !== -1) { + eventEmitter.emit('editor:focus') + eventEmitter.emit('line:jump', lineNumber) + } } } - if (this.props.onMouseDown != null) this.props.onMouseDown(e) + + if (this.props.onMouseDown != null && targetTag === 'BODY') + this.props.onMouseDown(e) } - handleMouseUp (e) { + handleMouseUp(e) { if (!this.props.onMouseUp) return if (e.target != null && e.target.tagName === 'A') { return null @@ -289,74 +425,106 @@ export default class MarkdownPreview extends React.Component { if (this.props.onMouseUp != null) this.props.onMouseUp(e) } - handleSaveAsText () { + handleSaveAsText() { this.exportAsDocument('txt') } - handleSaveAsMd () { + handleSaveAsMd() { this.exportAsDocument('md') } - handleSaveAsHtml () { - this.exportAsDocument('html', (noteContent, exportTasks) => { - const { - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - codeBlockTheme, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS - } = this.getStyleParams() + htmlContentFormatter(noteContent, exportTasks, targetDir) { + const { + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + codeBlockTheme, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS, + RTL + } = this.getStyleParams() - const inlineStyles = buildStyle( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS + const inlineStyles = buildStyle({ + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS, + RTL + }) + let body = this.refs.root.contentWindow.document.body.innerHTML + body = attachmentManagement.fixLocalURLS(body, this.props.storagePath) + const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES] + files.forEach(file => { + if (global.process.platform === 'win32') { + file = file.replace('file:///', '') + } else { + file = file.replace('file://', '') + } + exportTasks.push({ + src: file, + dst: 'css' + }) + }) + + let styles = '' + files.forEach(file => { + styles += `` + }) + + return ` + + + + + + ${styles} + + ${body} + ` + } + + handleSaveAsHtml() { + this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => + Promise.resolve( + this.htmlContentFormatter(noteContent, exportTasks, targetDir) ) - let body = this.markdown.render(noteContent) - const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] - files.forEach(file => { - if (global.process.platform === 'win32') { - file = file.replace('file:///', '') - } else { - file = file.replace('file://', '') - } - exportTasks.push({ - src: file, - dst: 'css' + ) + } + + handleSaveAsPdf() { + this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { + const printout = new remote.BrowserWindow({ + show: false, + webPreferences: { webSecurity: false, javascript: false } + }) + printout.loadURL( + 'data:text/html;charset=UTF-8,' + + this.htmlContentFormatter(noteContent, exportTasks, targetDir) + ) + return new Promise((resolve, reject) => { + printout.webContents.on('did-finish-load', () => { + printout.webContents.printToPDF({}, (err, data) => { + if (err) reject(err) + else resolve(data) + printout.destroy() + }) }) }) - - let styles = '' - files.forEach(file => { - styles += `` - }) - - return ` - - - - - ${styles} - - ${body} - ` }) } - handlePrint () { + handlePrint() { this.refs.root.contentWindow.print() } - exportAsDocument (fileType, contentFormatter) { + exportAsDocument(fileType, contentFormatter) { const options = { filters: [{ name: 'Documents', extensions: [fileType] }], properties: ['openFile', 'createDirectory'] @@ -372,7 +540,8 @@ export default class MarkdownPreview extends React.Component { .then(res => { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'info', - message: `Exported to ${filename}` + message: `Exported to ${filename}`, + buttons: [i18n.__('Ok')] }) }) .catch(err => { @@ -386,7 +555,7 @@ export default class MarkdownPreview extends React.Component { }) } - fixDecodedURI (node) { + fixDecodedURI(node) { if ( node && node.children.length === 1 && @@ -398,21 +567,41 @@ export default class MarkdownPreview extends React.Component { } } - getScrollBarStyle () { - const { theme } = this.props - - switch (theme) { - case 'dark': - case 'solarized-dark': - case 'monokai': - case 'dracula': - return scrollBarDarkStyle - default: - return scrollBarStyle + /** + * @description Convert special characters between three ``` + * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ``` + * @returns {string} HTML in which special characters between three ``` have been converted + */ + escapeHtmlCharactersInCodeTag(splitWithCodeTag) { + for (let index = 0; index < splitWithCodeTag.length; index++) { + const codeTagRequired = + splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1 + if (codeTagRequired) { + splitWithCodeTag.splice(index + 1, 0, '```') + } } + let inCodeTag = false + let result = '' + for (let content of splitWithCodeTag) { + if (content === '```') { + inCodeTag = !inCodeTag + } else if (inCodeTag) { + content = escapeHtmlCharacters(content) + } + result += content + } + return result } - componentDidMount () { + getScrollBarStyle() { + const { theme } = this.props + + return uiThemes.some(item => item.name === theme && item.isDark) + ? scrollBarDarkStyle + : scrollBarStyle + } + + componentDidMount() { const { onDrop } = this.props this.refs.root.setAttribute('sandbox', 'allow-scripts') @@ -462,13 +651,15 @@ export default class MarkdownPreview extends React.Component { 'scroll', this.scrollHandler ) + this.refs.root.contentWindow.addEventListener('resize', this.resizeHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) + eventEmitter.on('export:save-pdf', this.saveAsPdfHandler) eventEmitter.on('print', this.printHandler) } - componentWillUnmount () { + componentWillUnmount() { const { onDrop } = this.props this.refs.root.contentWindow.document.body.removeEventListener( @@ -499,23 +690,31 @@ export default class MarkdownPreview extends React.Component { 'scroll', this.scrollHandler ) + this.refs.root.contentWindow.removeEventListener( + 'resize', + this.resizeHandler + ) eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler) + eventEmitter.off('export:save-pdf', this.saveAsPdfHandler) eventEmitter.off('print', this.printHandler) } - componentDidUpdate (prevProps) { - if (prevProps.value !== this.props.value) this.rewriteIframe() + componentDidUpdate(prevProps) { + // actual rewriteIframe function should be called only once + let needsRewriteIframe = false + if (prevProps.value !== this.props.value) needsRewriteIframe = true if ( prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || + prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel || prevProps.smartArrows !== this.props.smartArrows || prevProps.breaks !== this.props.breaks || prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() - this.rewriteIframe() + needsRewriteIframe = true } if ( prevProps.fontFamily !== this.props.fontFamily || @@ -527,14 +726,24 @@ export default class MarkdownPreview extends React.Component { prevProps.theme !== this.props.theme || prevProps.scrollPastEnd !== this.props.scrollPastEnd || prevProps.allowCustomCSS !== this.props.allowCustomCSS || - prevProps.customCSS !== this.props.customCSS + prevProps.customCSS !== this.props.customCSS || + prevProps.RTL !== this.props.RTL ) { this.applyStyle() + needsRewriteIframe = true + } + + if (needsRewriteIframe) { this.rewriteIframe() } + + // Should scroll to top after selecting another note + if (prevProps.noteKey !== this.props.noteKey) { + this.scrollTo(0, 0) + } } - getStyleParams () { + getStyleParams() { const { fontSize, lineNumber, @@ -542,22 +751,24 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } = this.props let { fontFamily, codeBlockFontFamily } = this.props - fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 - ? fontFamily - .split(',') - .map(fontName => fontName.trim()) - .concat(defaultFontFamily) - : defaultFontFamily - codeBlockFontFamily = _.isString(codeBlockFontFamily) && - codeBlockFontFamily.trim().length > 0 - ? codeBlockFontFamily - .split(',') - .map(fontName => fontName.trim()) - .concat(defaultCodeBlockFontFamily) - : defaultCodeBlockFontFamily + fontFamily = + _.isString(fontFamily) && fontFamily.trim().length > 0 + ? fontFamily + .split(',') + .map(fontName => fontName.trim()) + .concat(defaultFontFamily) + : defaultFontFamily + codeBlockFontFamily = + _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 + ? codeBlockFontFamily + .split(',') + .map(fontName => fontName.trim()) + .concat(defaultCodeBlockFontFamily) + : defaultCodeBlockFontFamily return { fontFamily, @@ -568,11 +779,12 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } } - applyStyle () { + applyStyle() { const { fontFamily, fontSize, @@ -582,13 +794,14 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } = this.getStyleParams() this.getWindow().document.getElementById( 'codeTheme' - ).href = this.GetCodeThemeLink(codeBlockTheme) - this.getWindow().document.getElementById('style').innerHTML = buildStyle( + ).href = this.getCodeThemeLink(codeBlockTheme) + this.getWindow().document.getElementById('style').innerHTML = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, @@ -596,21 +809,20 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS - ) + customCSS, + RTL + }) } - GetCodeThemeLink (theme) { - theme = consts.THEMES.some(_theme => _theme === theme) && - theme !== 'default' - ? theme - : 'elegant' - return theme.startsWith('solarized') - ? `${appPath}/node_modules/codemirror/theme/solarized.css` - : `${appPath}/node_modules/codemirror/theme/${theme}.css` + getCodeThemeLink(name) { + const theme = consts.THEMES.find(theme => theme.name === name) + + return theme != null + ? theme.path + : `${appPath}/node_modules/codemirror/theme/elegant.css` } - rewriteIframe () { + rewriteIframe() { _.forEach( this.refs.root.contentWindow.document.querySelectorAll( 'input[type="checkbox"]' @@ -632,11 +844,17 @@ export default class MarkdownPreview extends React.Component { indentSize, showCopyNotification, storagePath, - noteKey + noteKey, + sanitize, + mermaidHTMLLabel } = this.props let { value, codeBlockTheme } = this.props this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) + if (sanitize === 'NONE') { + const splitWithCodeTag = value.split('```') + value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag) + } const renderedHTML = this.markdown.render(value) attachmentManagement.migrateAttachments(value, storagePath, noteKey) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS( @@ -660,9 +878,11 @@ export default class MarkdownPreview extends React.Component { } ) - codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme) - ? codeBlockTheme - : 'default' + codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme) + + const codeBlockThemeClassName = codeBlockTheme + ? codeBlockTheme.className + : 'cm-s-default' _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.code code'), @@ -675,6 +895,8 @@ export default class MarkdownPreview extends React.Component { copyIcon.innerHTML = '' copyIcon.onclick = e => { + e.preventDefault() + e.stopPropagation() copy(content) if (showCopyNotification) { this.notify('Saved to Clipboard!', { @@ -683,14 +905,11 @@ export default class MarkdownPreview extends React.Component { }) } } + el.parentNode.appendChild(copyIcon) el.innerHTML = '' - if (codeBlockTheme.indexOf('solarized') === 0) { - const [refThema, color] = codeBlockTheme.split(' ') - el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` - } else { - el.parentNode.className += ` cm-s-${codeBlockTheme}` - } + el.parentNode.className += ` ${codeBlockThemeClassName}` + CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize }) @@ -749,7 +968,10 @@ export default class MarkdownPreview extends React.Component { el => { try { const format = el.attributes.getNamedItem('data-format').value - const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML) + const chartConfig = + format === 'yaml' + ? yaml.load(el.innerHTML) + : JSON.parse(el.innerHTML) el.innerHTML = '' const canvas = document.createElement('canvas') @@ -761,6 +983,7 @@ export default class MarkdownPreview extends React.Component { canvas.height = height.value + 'vh' } + // eslint-disable-next-line no-unused-vars const chart = new Chart(canvas, chartConfig) } catch (e) { el.className = 'chart-error' @@ -771,7 +994,12 @@ export default class MarkdownPreview extends React.Component { _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), el => { - mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) + mermaidRender( + el, + htmlTextHelper.decodeEntities(el.innerHTML), + theme, + mermaidHTMLLabel + ) } ) @@ -793,26 +1021,133 @@ export default class MarkdownPreview extends React.Component { autoplay = 0 } - render( - , - el + render(, el) + } + ) + + const markdownPreviewIframe = document.querySelector('.MarkdownPreview') + const rect = markdownPreviewIframe.getBoundingClientRect() + const config = { attributes: true, subtree: true } + const imgObserver = new MutationObserver(mutationList => { + for (const mu of mutationList) { + if (mu.target.className === 'carouselContent-enter-done') { + this.setImgOnClickEventHelper(mu.target, rect) + break + } + } + }) + + const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll( + 'img' + ) + for (const img of imgList) { + const parentEl = img.parentElement + this.setImgOnClickEventHelper(img, rect) + imgObserver.observe(parentEl, config) + } + + const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll( + 'a' + ) + for (const a of aList) { + a.removeEventListener('click', this.linkClickHandler) + a.addEventListener('click', this.linkClickHandler) + } + } + + setImgOnClickEventHelper(img, rect) { + img.onclick = () => { + const widthMagnification = document.body.clientWidth / img.width + const heightMagnification = document.body.clientHeight / img.height + const baseOnWidth = widthMagnification < heightMagnification + const magnification = baseOnWidth + ? widthMagnification + : heightMagnification + + const zoomImgWidth = img.width * magnification + const zoomImgHeight = img.height * magnification + const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2 + const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2 + const originalImgTop = img.y + rect.top + const originalImgLeft = img.x + rect.left + const originalImgRect = { + top: `${originalImgTop}px`, + left: `${originalImgLeft}px`, + width: `${img.width}px`, + height: `${img.height}px` + } + const zoomInImgRect = { + top: `${baseOnWidth ? zoomImgTop : 0}px`, + left: `${baseOnWidth ? 0 : zoomImgLeft}px`, + width: `${zoomImgWidth}px`, + height: `${zoomImgHeight}px` + } + const animationSpeed = 300 + + const zoomImg = document.createElement('img') + zoomImg.src = img.src + zoomImg.style = ` + position: absolute; + top: ${baseOnWidth ? zoomImgTop : 0}px; + left: ${baseOnWidth ? 0 : zoomImgLeft}px; + width: ${zoomImgWidth}; + height: ${zoomImgHeight}px; + ` + zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed) + + const overlay = document.createElement('div') + overlay.style = ` + background-color: rgba(0,0,0,0.5); + cursor: zoom-out; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: ${document.body.clientHeight}px; + z-index: 100; + ` + overlay.onclick = () => { + zoomImg.style = ` + position: absolute; + top: ${originalImgTop}px; + left: ${originalImgLeft}px; + width: ${img.width}px; + height: ${img.height}px; + ` + const zoomOutImgAnimation = zoomImg.animate( + [zoomInImgRect, originalImgRect], + animationSpeed ) + zoomOutImgAnimation.onfinish = () => overlay.remove() + } + + overlay.appendChild(zoomImg) + document.body.appendChild(overlay) + } + } + + handleResize() { + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'), + el => { + el.setAttribute('height', el.clientWidth / el.getAttribute('ratio')) } ) } - focus () { + focus() { this.refs.root.focus() } - getWindow () { + getWindow() { return this.refs.root.contentWindow } - scrollTo (targetRow) { + /** + * @public + * @param {Number} targetRow + */ + scrollToRow(targetRow) { const blocks = this.getWindow().document.querySelectorAll( 'body>[data-line]' ) @@ -822,18 +1157,27 @@ export default class MarkdownPreview extends React.Component { 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) + block != null && this.scrollTo(0, block.offsetTop) break } } } - preventImageDroppedHandler (e) { + /** + * `document.body.scrollTo` + * @param {Number} x + * @param {Number} y + */ + scrollTo(x, y) { + this.getWindow().document.body.scrollTo(x, y) + } + + preventImageDroppedHandler(e) { e.preventDefault() e.stopPropagation() } - notify (title, options) { + notify(title, options) { if (global.process.platform === 'win32') { options.icon = path.join( 'file://', @@ -844,24 +1188,35 @@ export default class MarkdownPreview extends React.Component { return new window.Notification(title, options) } - handleLinkClick (e) { + handleLinkClick(e) { e.preventDefault() e.stopPropagation() - const href = e.target.href - const linkHash = href.split('/').pop() + const rawHref = e.target.getAttribute('href') + const { dispatch } = this.props + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() - const regexNoteInternalLink = /main.html#(.+)/ - if (regexNoteInternalLink.test(linkHash)) { - const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) - const targetElement = this.refs.root.contentWindow.document.getElementById( - targetId - ) + const parser = document.createElement('a') + parser.href = rawHref + const isStartWithHash = rawHref[0] === '#' + const { href, hash } = parser - if (targetElement != null) { - this.getWindow().scrollTo(0, targetElement.offsetTop) + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 + + const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html + const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`) + if (isStartWithHash || regexNoteInternalLink.test(rawHref)) { + const posOfHash = linkHash.indexOf('#') + if (posOfHash > -1) { + const extractedId = linkHash.slice(posOfHash + 1) + const targetId = mdurl.encode(extractedId) + const targetElement = this.getWindow().document.getElementById(targetId) + + if (targetElement != null) { + this.scrollTo(0, targetElement.offsetTop) + } + return } - return } // this will match the new uuid v4 hash and the old hash @@ -892,11 +1247,29 @@ export default class MarkdownPreview extends React.Component { return } + const regexIsTagLink = /^:tag:([\w]+)$/ + if (regexIsTagLink.test(rawHref)) { + const tag = rawHref.match(regexIsTagLink)[1] + dispatch(push(`/tags/${encodeURIComponent(tag)}`)) + return + } + // other case - shell.openExternal(href) + this.openExternal(href) } - render () { + openExternal(href) { + try { + const success = + shell.openExternal(href) || shell.openExternal(decodeURI(href)) + if (!success) console.error('failed to open url ' + href) + } catch (e) { + // URI Error threw from decodeURI + console.error(e) + } + } + + render() { 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)' + content: + '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n\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({ @@ -132,23 +138,23 @@ class Main extends React.Component { .then(() => data.storage) }) .then(storage => { - hashHistory.push('/storages/' + storage.key) + store.dispatch(push('/storages/' + storage.key)) }) .catch(err => { throw err }) } - componentDidMount () { + componentDidMount() { const { dispatch, config } = this.props - const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula'] + this.refreshTheme = setInterval(() => { + const conf = ConfigManager.get() + chooseTheme(conf) + }, 5 * 1000) - if (supportedThemes.indexOf(config.ui.theme) !== -1) { - document.body.setAttribute('data-theme', config.ui.theme) - } else { - document.body.setAttribute('data-theme', 'default') - } + chooseTheme(config) + applyTheme(config.ui.theme) if (getLocales().indexOf(config.ui.language) !== -1) { i18n.setLocale(config.ui.language) @@ -169,30 +175,56 @@ class Main extends React.Component { } }) + // eslint-disable-next-line no-undef delete CodeMirror.keyMap.emacs['Ctrl-V'] eventEmitter.on('editor:fullscreen', this.toggleFullScreen) + eventEmitter.on( + 'menubar:togglemenubar', + this.toggleMenuBarVisible.bind(this) + ) + eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this)) } - componentWillUnmount () { + componentWillUnmount() { eventEmitter.off('editor:fullscreen', this.toggleFullScreen) + eventEmitter.off( + 'menubar:togglemenubar', + this.toggleMenuBarVisible.bind(this) + ) + eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this)) + clearInterval(this.refreshTheme) } - handleLeftSlideMouseDown (e) { + changeRoutePush(event, destination) { + const { dispatch } = this.props + dispatch(push(destination)) + } + + toggleMenuBarVisible() { + const { config } = this.props + const { ui } = config + + const newUI = Object.assign(ui, { showMenuBar: !ui.showMenuBar }) + const newConfig = Object.assign(config, newUI) + ConfigManager.set(newConfig) + } + + handleLeftSlideMouseDown(e) { e.preventDefault() this.setState({ isLeftSliderFocused: true }) } - handleRightSlideMouseDown (e) { + handleRightSlideMouseDown(e) { e.preventDefault() this.setState({ isRightSliderFocused: true }) } - handleMouseUp (e) { + handleMouseUp(e) { // Change width of NoteList component. if (this.state.isRightSliderFocused) { this.setState( @@ -232,7 +264,7 @@ class Main extends React.Component { } } - handleMouseMove (e) { + handleMouseMove(e) { if (this.state.isRightSliderFocused) { const offset = this.refs.body.getBoundingClientRect().left let newListWidth = e.pageX - offset @@ -258,7 +290,7 @@ class Main extends React.Component { } } - handleFullScreenButton (e) { + handleFullScreenButton(e) { this.setState({ fullScreen: !this.state.fullScreen }, () => { const noteDetail = document.querySelector('.NoteDetail') const noteList = document.querySelector('.NoteList') @@ -272,7 +304,7 @@ class Main extends React.Component { }) } - hideLeftLists (noteDetail, noteList, mainBody) { + hideLeftLists(noteDetail, noteList, mainBody) { this.setState({ noteDetailWidth: noteDetail.style.left }) this.setState({ mainBodyWidth: mainBody.style.left }) noteDetail.style.left = '0px' @@ -280,13 +312,13 @@ class Main extends React.Component { noteList.style.display = 'none' } - showLeftLists (noteDetail, noteList, mainBody) { + showLeftLists(noteDetail, noteList, mainBody) { noteDetail.style.left = this.state.noteDetailWidth mainBody.style.left = this.state.mainBodyWidth noteList.style.display = 'inline' } - render () { + render() { const { config } = this.props // the width of the navigation bar when it is folded/collapsed @@ -300,10 +332,16 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > - {!config.isSideNavFolded && + {!config.isSideNavFolded && (
-
} +
+ )}
@@ -340,7 +379,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} /> @@ -362,7 +401,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} ignorePreviewPointerEvents={this.state.isRightSliderFocused} diff --git a/browser/main/NewNoteButton/NewNoteButton.styl b/browser/main/NewNoteButton/NewNoteButton.styl index 75a9061c..57b0fc03 100644 --- a/browser/main/NewNoteButton/NewNoteButton.styl +++ b/browser/main/NewNoteButton/NewNoteButton.styl @@ -72,14 +72,13 @@ body[data-theme="dark"] .control-newNoteButton-tooltip darkTooltip() -body[data-theme="solarized-dark"] - .root, .root--expanded - background-color $ui-solarized-dark-noteList-backgroundColor +apply-theme(theme) + body[data-theme={theme}] + .root, .root--expanded + background-color get-theme-var(theme, 'noteList-backgroundColor') -body[data-theme="monokai"] - .root, .root--expanded - background-color $ui-monokai-noteList-backgroundColor +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="dracula"] - .root, .root--expanded - background-color $ui-dracula-noteList-backgroundColor \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index c34443be..3b931496 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -15,33 +15,48 @@ const { dialog } = remote const OSX = window.process.platform === 'darwin' class NewNoteButton extends React.Component { - constructor (props) { + constructor(props) { super(props) - this.state = { - } + this.state = {} - this.newNoteHandler = () => { - this.handleNewNoteButtonClick() - } + this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this) } - componentDidMount () { - eventEmitter.on('top:new-note', this.newNoteHandler) + componentDidMount() { + eventEmitter.on('top:new-note', this.handleNewNoteButtonClick) } - componentWillUnmount () { - eventEmitter.off('top:new-note', this.newNoteHandler) + componentWillUnmount() { + eventEmitter.off('top:new-note', this.handleNewNoteButtonClick) } - handleNewNoteButtonClick (e) { - const { location, params, dispatch, config } = this.props + handleNewNoteButtonClick(e) { + const { + location, + dispatch, + match: { params }, + config + } = this.props const { storage, folder } = this.resolveTargetFolder() - if (config.ui.defaultNote === 'MARKDOWN_NOTE') { - createMarkdownNote(storage.key, folder.key, dispatch, location, params, config) + createMarkdownNote( + storage.key, + folder.key, + dispatch, + location, + params, + config + ) } else if (config.ui.defaultNote === 'SNIPPET_NOTE') { - createSnippetNote(storage.key, folder.key, dispatch, location, params, config) + createSnippetNote( + storage.key, + folder.key, + dispatch, + location, + params, + config + ) } else { modal.open(NewNoteModal, { storage: storage.key, @@ -54,10 +69,12 @@ class NewNoteButton extends React.Component { } } - resolveTargetFolder () { - const { data, params } = this.props + resolveTargetFolder() { + const { + data, + match: { params } + } = this.props let storage = data.storageMap.get(params.storageKey) - // Find first storage if (storage == null) { for (const kv of data.storageMap) { @@ -66,9 +83,12 @@ class NewNoteButton extends React.Component { } } - if (storage == null) this.showMessageBox(i18n.__('No storage to create a note')) - const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0] - if (folder == null) this.showMessageBox(i18n.__('No folder to create a note')) + if (storage == null) + this.showMessageBox(i18n.__('No storage to create a note')) + const folder = + _.find(storage.folders, { key: params.folderKey }) || storage.folders[0] + if (folder == null) + this.showMessageBox(i18n.__('No folder to create a note')) return { storage, @@ -76,7 +96,7 @@ class NewNoteButton extends React.Component { } } - showMessageBox (message) { + showMessageBox(message) { dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: message, @@ -84,17 +104,20 @@ class NewNoteButton extends React.Component { }) } - render () { + render() { const { config, style } = this.props return ( -
- - + +
-
this.handleNoteListKeyDown(e)} + onKeyDown={e => this.handleNoteListKeyDown(e)} onKeyUp={this.handleNoteListKeyUp} + onBlur={this.handleNoteListBlur} > {noteList}
diff --git a/browser/main/SideNav/ListButton.js b/browser/main/SideNav/ListButton.js index b5bc1488..dc9ba1cc 100644 --- a/browser/main/SideNav/ListButton.js +++ b/browser/main/SideNav/ListButton.js @@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './SwitchButton.styl' import i18n from 'browser/lib/i18n' -const ListButton = ({ - onClick, isTagActive -}) => ( - diff --git a/browser/main/SideNav/PreferenceButton.js b/browser/main/SideNav/PreferenceButton.js index 187171f4..25499463 100644 --- a/browser/main/SideNav/PreferenceButton.js +++ b/browser/main/SideNav/PreferenceButton.js @@ -4,11 +4,9 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './PreferenceButton.styl' import i18n from 'browser/lib/i18n' -const PreferenceButton = ({ - onClick -}) => ( - ) diff --git a/browser/main/SideNav/PreferenceButton.styl b/browser/main/SideNav/PreferenceButton.styl index 54513cb6..c404a604 100644 --- a/browser/main/SideNav/PreferenceButton.styl +++ b/browser/main/SideNav/PreferenceButton.styl @@ -1,52 +1,47 @@ -.top-menu-preference - navButtonColor() - position absolute - top 22px - right 10px - width 2em - background-color transparent - &:hover - color $ui-button-default--active-backgroundColor - background-color transparent - .tooltip - opacity 1 - &:active, &:active:hover - color $ui-button-default--active-backgroundColor - -body[data-theme="white"] - .top-menu-preference - navWhiteButtonColor() - background-color transparent - &:hover - color #0B99F1 - background-color transparent - &:active, &:active:hover - color #0B99F1 - background-color transparent - -body[data-theme="dark"] - .top-menu-preference - navDarkButtonColor() - background-color transparent - &:active - background-color alpha($ui-dark-button--active-backgroundColor, 20%) - background-color transparent - &:hover - background-color alpha($ui-dark-button--active-backgroundColor, 20%) - background-color transparent - - - -.tooltip - tooltip() - position absolute - pointer-events none - top 26px - left -20px - z-index 200 - padding 5px - line-height normal - border-radius 2px - opacity 0 - transition 0.1s - white-space nowrap +.top-menu-preference + navButtonColor() + width 2em + background-color transparent + &:hover + color $ui-button-default--active-backgroundColor + background-color transparent + .tooltip + opacity 1 + &:active, &:active:hover + color $ui-button-default--active-backgroundColor + +body[data-theme="white"] + .top-menu-preference + navWhiteButtonColor() + background-color transparent + &:hover + color #0B99F1 + background-color transparent + &:active, &:active:hover + color #0B99F1 + background-color transparent + +body[data-theme="dark"] + .top-menu-preference + navDarkButtonColor() + background-color transparent + &:active + background-color alpha($ui-dark-button--active-backgroundColor, 20%) + background-color transparent + &:hover + background-color alpha($ui-dark-button--active-backgroundColor, 20%) + background-color transparent + +.tooltip + tooltip() + position absolute + pointer-events none + top 26px + left -20px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + white-space nowrap diff --git a/browser/main/SideNav/SearchButton.js b/browser/main/SideNav/SearchButton.js new file mode 100644 index 00000000..72d3cc78 --- /dev/null +++ b/browser/main/SideNav/SearchButton.js @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './SearchButton.styl' +import i18n from 'browser/lib/i18n' + +const SearchButton = ({ onClick, isActive }) => ( + +) + +SearchButton.propTypes = { + onClick: PropTypes.func.isRequired, + isActive: PropTypes.bool +} + +export default CSSModules(SearchButton, styles) diff --git a/browser/main/SideNav/SearchButton.styl b/browser/main/SideNav/SearchButton.styl new file mode 100644 index 00000000..76d4b806 --- /dev/null +++ b/browser/main/SideNav/SearchButton.styl @@ -0,0 +1,55 @@ +.top-menu-search + navButtonColor() + position relative + margin-right 6px + top 3px + width 2em + background-color transparent + &:hover + color $ui-button-default--active-backgroundColor + background-color transparent + .tooltip + opacity 1 + &:active, &:active:hover + color $ui-button-default--active-backgroundColor + +.icon-search + width 16px + +body[data-theme="white"] + .top-menu-search + navWhiteButtonColor() + background-color transparent + &:hover + color #0B99F1 + background-color transparent + &:active, &:active:hover + color #0B99F1 + background-color transparent + +body[data-theme="dark"] + .top-menu-search + navDarkButtonColor() + background-color transparent + &:active + background-color alpha($ui-dark-button--active-backgroundColor, 20%) + background-color transparent + &:hover + background-color alpha($ui-dark-button--active-backgroundColor, 20%) + background-color transparent + + + +.tooltip + tooltip() + position absolute + pointer-events none + top 26px + left -20px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + white-space nowrap diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 9fa6d4fa..b2a8a0d2 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -9,16 +9,47 @@ flex-direction column .top - padding-bottom 15px + display flex + align-items top + justify-content space-between + padding-bottom 10px + margin 14px 14px 4px .switch-buttons background-color transparent border 0 - margin 24px auto 4px 14px display flex + align-items center text-align center +.extra-buttons + position relative + display flex + align-items center +.search + position relative + flex 1 + display flex + max-height 0 + overflow hidden + transition max-height .4s + margin -5px 10px 0 + .search-input + flex 1 + height 2em + vertical-align middle + font-size 14px + border solid 1px $border-color + border-radius 2px + padding 2px 6px + outline none + .search-clear + width 10px + position absolute + right 8px + top 9px + cursor pointer .top-menu-label margin-left 5px @@ -68,8 +99,15 @@ background-color #2E3235 .switch-buttons display none + .extra-buttons > button:first-of-type // hide search icon + display none .top height 60px + align-items center + margin 0 + justify-content center + position relative + left -4px .top-menu position static width $sideNav--folded-width @@ -98,32 +136,52 @@ .top-menu-preference position absolute left 7px + .search + height 28px + .search-input + display none + .search-clear + display none + .search-folded + width 16px + padding-left 4px + margin-bottom 8px + cursor pointer body[data-theme="white"] .root, .root--folded background-color #f9f9f9 color $ui-text-color + .search .search-input + background-color #f9f9f9 + color $ui-text-color body[data-theme="dark"] .root, .root--folded border-right 1px solid $ui-dark-borderColor background-color $ui-dark-backgroundColor color $ui-dark-text-color + .search .search-input + background-color $ui-dark-backgroundColor + color $ui-dark-text-color + border-color $ui-dark-borderColor .top border-color $ui-dark-borderColor -body[data-theme="solarized-dark"] - .root, .root--folded - background-color $ui-solarized-dark-backgroundColor - border-right 1px solid $ui-solarized-dark-borderColor +apply-theme(theme) + body[data-theme={theme}] + .root, .root--folded + background-color get-theme-var(theme, 'backgroundColor') + border-right 1px solid get-theme-var(theme, 'borderColor') -body[data-theme="monokai"] - .root, .root--folded - background-color $ui-monokai-backgroundColor - border-right 1px solid $ui-monokai-borderColor + .search .search-input + background-color get-theme-var(theme, 'backgroundColor') + color get-theme-var(theme, 'text-color') + border-color get-theme-var(theme, 'borderColor') -body[data-theme="dracula"] - .root, .root--folded - background-color $ui-dracula-backgroundColor - border-right 1px solid $ui-dracula-borderColor \ No newline at end of file +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index e336f3ce..a152fc00 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StorageItem.styl' -import { hashHistory } from 'react-router' import modal from 'browser/main/lib/modal' import CreateFolderModal from 'browser/main/modals/CreateFolderModal' import RenameFolderModal from 'browser/main/modals/RenameFolderModal' @@ -12,6 +11,7 @@ import _ from 'lodash' import { SortableElement } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +import { push } from 'connected-react-router' const { remote } = require('electron') const { dialog } = remote @@ -19,7 +19,7 @@ const escapeStringRegexp = require('escape-string-regexp') const path = require('path') class StorageItem extends React.Component { - constructor (props) { + constructor(props) { super(props) const { storage } = this.props @@ -30,11 +30,11 @@ class StorageItem extends React.Component { } } - handleHeaderContextMenu (e) { + handleHeaderContextMenu(e) { context.popup([ { label: i18n.__('Add Folder'), - click: (e) => this.handleAddFolderButtonClick(e) + click: e => this.handleAddFolderButtonClick(e) }, { type: 'separator' @@ -44,11 +44,11 @@ class StorageItem extends React.Component { submenu: [ { label: i18n.__('Export as txt'), - click: (e) => this.handleExportStorageClick(e, 'txt') + click: e => this.handleExportStorageClick(e, 'txt') }, { label: i18n.__('Export as md'), - click: (e) => this.handleExportStorageClick(e, 'md') + click: e => this.handleExportStorageClick(e, 'md') } ] }, @@ -57,75 +57,74 @@ class StorageItem extends React.Component { }, { label: i18n.__('Unlink Storage'), - click: (e) => this.handleUnlinkStorageClick(e) + click: e => this.handleUnlinkStorageClick(e) } ]) } - handleUnlinkStorageClick (e) { + handleUnlinkStorageClick(e) { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Unlink Storage'), - detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'), + detail: i18n.__( + "This work will just detatches a storage from Boostnote. (Any data won't be deleted.)" + ), buttons: [i18n.__('Confirm'), i18n.__('Cancel')] }) if (index === 0) { const { storage, dispatch } = this.props - dataApi.removeStorage(storage.key) + dataApi + .removeStorage(storage.key) .then(() => { dispatch({ type: 'REMOVE_STORAGE', storageKey: storage.key }) }) - .catch((err) => { + .catch(err => { throw err }) } } - handleExportStorageClick (e, fileType) { + handleExportStorageClick(e, fileType) { const options = { properties: ['openDirectory', 'createDirectory'], buttonLabel: i18n.__('Select directory'), title: i18n.__('Select a folder to export the files to'), multiSelections: false } - dialog.showOpenDialog(remote.getCurrentWindow(), options, - (paths) => { - if (paths && paths.length === 1) { - const { storage, dispatch } = this.props - dataApi - .exportStorage(storage.key, fileType, paths[0]) - .then(data => { - dispatch({ - type: 'EXPORT_STORAGE', - storage: data.storage, - fileType: data.fileType - }) - }) - } - }) + dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { + if (paths && paths.length === 1) { + const { storage, dispatch } = this.props + dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => { + dispatch({ + type: 'EXPORT_STORAGE', + storage: data.storage, + fileType: data.fileType + }) + }) + } + }) } - handleToggleButtonClick (e) { + handleToggleButtonClick(e) { const { storage, dispatch } = this.props const isOpen = !this.state.isOpen - dataApi.toggleStorage(storage.key, isOpen) - .then((storage) => { - dispatch({ - type: 'EXPAND_STORAGE', - storage, - isOpen - }) + dataApi.toggleStorage(storage.key, isOpen).then(storage => { + dispatch({ + type: 'EXPAND_STORAGE', + storage, + isOpen }) + }) this.setState({ isOpen: isOpen }) } - handleAddFolderButtonClick (e) { + handleAddFolderButtonClick(e) { const { storage } = this.props modal.open(CreateFolderModal, { @@ -133,23 +132,32 @@ class StorageItem extends React.Component { }) } - handleHeaderInfoClick (e) { - const { storage } = this.props - hashHistory.push('/storages/' + storage.key) + handleHeaderInfoClick(e) { + const { storage, dispatch } = this.props + dispatch(push('/storages/' + storage.key)) } - handleFolderButtonClick (folderKey) { - return (e) => { - const { storage } = this.props - hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey) + handleFolderButtonClick(folderKey) { + return e => { + const { storage, dispatch } = this.props + dispatch(push('/storages/' + storage.key + '/folders/' + folderKey)) } } - handleFolderButtonContextMenu (e, folder) { + handleFolderMouseEnter(e, tooltipRef, isFolded) { + if (isFolded) { + const buttonEl = e.currentTarget + const tooltipEl = tooltipRef.current + + tooltipEl.style.top = buttonEl.getBoundingClientRect().y + 'px' + } + } + + handleFolderButtonContextMenu(e, folder) { context.popup([ { label: i18n.__('Rename Folder'), - click: (e) => this.handleRenameFolderClick(e, folder) + click: e => this.handleRenameFolderClick(e, folder) }, { type: 'separator' @@ -159,11 +167,11 @@ class StorageItem extends React.Component { submenu: [ { label: i18n.__('Export as txt'), - click: (e) => this.handleExportFolderClick(e, folder, 'txt') + click: e => this.handleExportFolderClick(e, folder, 'txt') }, { label: i18n.__('Export as md'), - click: (e) => this.handleExportFolderClick(e, folder, 'md') + click: e => this.handleExportFolderClick(e, folder, 'md') } ] }, @@ -172,12 +180,12 @@ class StorageItem extends React.Component { }, { label: i18n.__('Delete Folder'), - click: (e) => this.handleFolderDeleteClick(e, folder) + click: e => this.handleFolderDeleteClick(e, folder) } ]) } - handleRenameFolderClick (e, folder) { + handleRenameFolderClick(e, folder) { const { storage } = this.props modal.open(RenameFolderModal, { storage, @@ -185,20 +193,19 @@ class StorageItem extends React.Component { }) } - handleExportFolderClick (e, folder, fileType) { + handleExportFolderClick(e, folder, fileType) { const options = { properties: ['openDirectory', 'createDirectory'], buttonLabel: i18n.__('Select directory'), title: i18n.__('Select a folder to export the files to'), multiSelections: false } - dialog.showOpenDialog(remote.getCurrentWindow(), options, - (paths) => { + dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { if (paths && paths.length === 1) { const { storage, dispatch } = this.props dataApi .exportFolder(storage.key, folder.key, fileType, paths[0]) - .then((data) => { + .then(data => { dispatch({ type: 'EXPORT_FOLDER', storage: data.storage, @@ -224,66 +231,74 @@ class StorageItem extends React.Component { }) } - handleFolderDeleteClick (e, folder) { + handleFolderDeleteClick(e, folder) { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Delete Folder'), - detail: i18n.__('This will delete all notes in the folder and can not be undone.'), + detail: i18n.__( + 'This will delete all notes in the folder and can not be undone.' + ), buttons: [i18n.__('Confirm'), i18n.__('Cancel')] }) if (index === 0) { const { storage, dispatch } = this.props - dataApi - .deleteFolder(storage.key, folder.key) - .then((data) => { - dispatch({ - type: 'DELETE_FOLDER', - storage: data.storage, - folderKey: data.folderKey - }) + dataApi.deleteFolder(storage.key, folder.key).then(data => { + dispatch({ + type: 'DELETE_FOLDER', + storage: data.storage, + folderKey: data.folderKey }) + }) } } - handleDragEnter (e, key) { + handleDragEnter(e, key) { e.preventDefault() - if (this.state.draggedOver === key) { return } + if (this.state.draggedOver === key) { + return + } this.setState({ draggedOver: key }) } - handleDragLeave (e) { + handleDragLeave(e) { e.preventDefault() - if (this.state.draggedOver === null) { return } + if (this.state.draggedOver === null) { + return + } this.setState({ draggedOver: null }) } - dropNote (storage, folder, dispatch, location, noteData) { - noteData = noteData.filter((note) => folder.key !== note.folder) + dropNote(storage, folder, dispatch, location, noteData) { + noteData = noteData.filter(note => folder.key !== note.folder) if (noteData.length === 0) return Promise.all( - noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key)) + noteData.map(note => + dataApi.moveNote(note.storage, note.key, storage.key, folder.key) + ) ) - .then((createdNoteData) => { - createdNoteData.forEach((newNote) => { - dispatch({ - type: 'MOVE_NOTE', - originNote: noteData.find((note) => note.content === newNote.oldContent), - note: newNote + .then(createdNoteData => { + createdNoteData.forEach(newNote => { + dispatch({ + type: 'MOVE_NOTE', + originNote: noteData.find( + note => note.content === newNote.oldContent + ), + note: newNote + }) }) }) - }) - .catch((err) => { - console.error(`error on delete notes: ${err}`) - }) + .catch(err => { + console.error(`error on delete notes: ${err}`) + }) } - handleDrop (e, storage, folder, dispatch, location) { + handleDrop(e, storage, folder, dispatch, location) { e.preventDefault() if (this.state.draggedOver !== null) { this.setState({ @@ -294,21 +309,38 @@ class StorageItem extends React.Component { this.dropNote(storage, folder, dispatch, location, noteData) } - render () { + render() { const { storage, location, isFolded, data, dispatch } = this.props const { folderNoteMap, trashedSet } = data const SortableStorageItemChild = SortableElement(StorageItemChild) const folderList = storage.folders.map((folder, index) => { - const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key) - const isActive = !!(location.pathname.match(folderRegex)) + const folderRegex = new RegExp( + escapeStringRegexp(path.sep) + + 'storages' + + escapeStringRegexp(path.sep) + + storage.key + + escapeStringRegexp(path.sep) + + 'folders' + + escapeStringRegexp(path.sep) + + folder.key + ) + const isActive = !!location.pathname.match(folderRegex) + const tooltipRef = React.createRef(null) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) let noteCount = 0 if (noteSet) { let trashedNoteCount = 0 - const noteKeys = noteSet.map(noteKey => { return noteKey }) + const noteKeys = noteSet.map(noteKey => { + return noteKey + }) trashedSet.toJS().forEach(trashedKey => { - if (noteKeys.some(noteKey => { return noteKey === trashedKey })) trashedNoteCount++ + if ( + noteKeys.some(noteKey => { + return noteKey === trashedKey + }) + ) + trashedNoteCount++ }) noteCount = noteSet.size - trashedNoteCount } @@ -317,73 +349,84 @@ class StorageItem extends React.Component { key={folder.key} index={index} isActive={isActive || folder.key === this.state.draggedOver} - handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)} - handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} + tooltipRef={tooltipRef} + handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)} + handleMouseEnter={e => + this.handleFolderMouseEnter(e, tooltipRef, isFolded) + } + handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)} folderName={folder.name} folderColor={folder.color} isFolded={isFolded} noteCount={noteCount} - handleDrop={(e) => { + handleDrop={e => { this.handleDrop(e, storage, folder, dispatch, location) }} - handleDragEnter={(e) => { + handleDragEnter={e => { this.handleDragEnter(e, folder.key) }} - handleDragLeave={(e) => { + handleDragLeave={e => { this.handleDragLeave(e, folder) }} /> ) }) - const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$')) + const isActive = location.pathname.match( + new RegExp( + escapeStringRegexp(path.sep) + + 'storages' + + escapeStringRegexp(path.sep) + + storage.key + + '$' + ) + ) return ( -
-
this.handleHeaderContextMenu(e)} +
+
this.handleHeaderContextMenu(e)} > - - {!isFolded && - - } + )} -
- {this.state.isOpen && -
- {folderList} -
- } + {this.state.isOpen &&
{folderList}
}
) } diff --git a/browser/main/SideNav/StorageItem.styl b/browser/main/SideNav/StorageItem.styl index a06ecb11..375a989f 100644 --- a/browser/main/SideNav/StorageItem.styl +++ b/browser/main/SideNav/StorageItem.styl @@ -132,55 +132,57 @@ body[data-theme="white"] background-color alpha($ui-button--active-backgroundColor, 40%) color $ui-text-color -body[data-theme="dark"] - .header--active - background-color $ui-dark-button--active-backgroundColor - transition color background-color 0.15s +apply-theme(theme) + body[data-theme={theme}] + .header--active + background-color get-theme-var(theme, 'button--active-backgroundColor') + transition color background-color 0.15s + + .header--active + .header-toggleButton + color get-theme-var(theme, 'text-color') + + .header--active + .header-info + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') + &:active + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') + + .header--active + .header-addFolderButton + color get-theme-var(theme, 'text-color') - .header--active .header-toggleButton - color $ui-dark-text-color + &:hover + transition 0.2s + color get-theme-var(theme, 'text-color') + background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%) + &:active, &:active:hover + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') - .header--active .header-info - color $ui-dark-text-color - background-color $ui-dark-button--active-backgroundColor - &:active - color $ui-dark-text-color - background-color $ui-dark-button--active-backgroundColor + background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%) + &:hover + transition 0.2s + color get-theme-var(theme, 'text-color') + background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%) + &:active, &:active:hover + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') - .header--active .header-addFolderButton - color $ui-dark-text-color - - .header-toggleButton - &:hover - transition 0.2s - color $ui-dark-text-color - background-color alpha($ui-dark-button--active-backgroundColor, 60%) - &:active, &:active:hover - color $ui-dark-text-color - background-color $ui-dark-button--active-backgroundColor - - .header-info - background-color alpha($ui-dark-button--active-backgroundColor, 20%) - &:hover - transition 0.2s - color $ui-dark-text-color - background-color alpha($ui-dark-button--active-backgroundColor, 20%) - &:active, &:active:hover - color $ui-dark-text-color - background-color $ui-dark-button--active-backgroundColor - - .header-addFolderButton - &:hover - transition 0.2s - color $ui-dark-text-color - background-color alpha($ui-dark-button--active-backgroundColor, 60%) - &:active, &:active:hover - color $ui-dark-text-color - background-color $ui-dark-button--active-backgroundColor - - + &:hover + transition 0.2s + color get-theme-var(theme, 'text-color') + background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%) + &:active, &:active:hover + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') +apply-theme('dark') +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/SideNav/SwitchButton.styl b/browser/main/SideNav/SwitchButton.styl index 36099140..2184bc69 100644 --- a/browser/main/SideNav/SwitchButton.styl +++ b/browser/main/SideNav/SwitchButton.styl @@ -1,60 +1,60 @@ -.non-active-button - color $ui-inactive-text-color - font-size 16px - border 0 - background-color transparent - transition 0.2s - display flex - text-align center - margin-right 4px - position relative - &:hover - color alpha(#239F86, 60%) - .tooltip - opacity 1 - -.active-button - @extend .non-active-button - color $ui-button-default--active-backgroundColor - -.tooltip - tooltip() - position absolute - pointer-events none - top 22px - left -2px - z-index 200 - padding 5px - line-height normal - border-radius 2px - opacity 0 - transition 0.1s - white-space nowrap - -body[data-theme="white"] - .non-active-button - color $ui-inactive-text-color - &:hover - color alpha(#0B99F1, 60%) - - .tag-title - p - color $ui-text-color - - .non-active-button - &:hover - color alpha(#0B99F1, 60%) - - .active-button - @extend .non-active-button - color #0B99F1 - -body[data-theme="dark"] - .non-active-button - color alpha($ui-dark-text-color, 60%) - &:hover - color alpha(#0B99F1, 60%) - - .tag-title - p +.non-active-button + color $ui-inactive-text-color + font-size 16px + border 0 + background-color transparent + transition 0.2s + display flex + text-align center + margin-right 4px + position relative + &:hover + color alpha(#239F86, 60%) + .tooltip + opacity 1 + +.active-button + @extend .non-active-button + color $ui-button-default--active-backgroundColor + +.tooltip + tooltip() + position absolute + pointer-events none + top 22px + left -2px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + white-space nowrap + +body[data-theme="white"] + .non-active-button + color $ui-inactive-text-color + &:hover + color alpha(#0B99F1, 60%) + + .tag-title + p + color $ui-text-color + + .non-active-button + &:hover + color alpha(#0B99F1, 60%) + + .active-button + @extend .non-active-button + color #0B99F1 + +body[data-theme="dark"] + .non-active-button + color alpha($ui-dark-text-color, 60%) + &:hover + color alpha(#0B99F1, 60%) + + .tag-title + p color alpha($ui-dark-text-color, 60%) \ No newline at end of file diff --git a/browser/main/SideNav/TagButton.js b/browser/main/SideNav/TagButton.js index d91ae2c4..268e0d7e 100644 --- a/browser/main/SideNav/TagButton.js +++ b/browser/main/SideNav/TagButton.js @@ -4,14 +4,17 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './SwitchButton.styl' import i18n from 'browser/lib/i18n' -const TagButton = ({ - onClick, isTagActive -}) => ( - diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index b98d859d..c2f04116 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -1,10 +1,12 @@ import PropTypes from 'prop-types' import React from 'react' +import { push } from 'connected-react-router' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' import styles from './SideNav.styl' import { openModal } from 'browser/main/lib/modal' import PreferencesModal from '../modals/PreferencesModal' +import RenameTagModal from 'browser/main/modals/RenameTagModal' import ConfigManager from 'browser/main/lib/ConfigManager' import StorageItem from './StorageItem' import TagListItem from 'browser/components/TagListItem' @@ -13,39 +15,71 @@ import StorageList from 'browser/components/StorageList' import NavToggleButton from 'browser/components/NavToggleButton' import EventEmitter from 'browser/main/lib/eventEmitter' import PreferenceButton from './PreferenceButton' +import SearchButton from './SearchButton' import ListButton from './ListButton' import TagButton from './TagButton' -import {SortableContainer} from 'react-sortable-hoc' +import { SortableContainer } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' import { remote } from 'electron' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' +import ColorPicker from 'browser/components/ColorPicker' +import { every, sortBy } from 'lodash' -function matchActiveTags (tags, activeTags) { - return _.every(activeTags, v => tags.indexOf(v) >= 0) +function matchActiveTags(tags, activeTags) { + return every(activeTags, v => tags.indexOf(v) >= 0) } class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 + constructor(props) { + super(props) - componentDidMount () { + this.state = { + colorPicker: { + show: false, + color: null, + tagName: null, + targetRect: null, + showSearch: false, + searchText: '' + } + } + + this.dismissColorPicker = this.dismissColorPicker.bind(this) + this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this) + this.handleColorPickerReset = this.handleColorPickerReset.bind(this) + this.handleSearchButtonClick = this.handleSearchButtonClick.bind(this) + this.handleSearchInputChange = this.handleSearchInputChange.bind(this) + this.handleSearchInputClear = this.handleSearchInputClear.bind(this) + } + + componentDidMount() { EventEmitter.on('side:preferences', this.handleMenuButtonClick) } - componentWillUnmount () { + componentWillUnmount() { EventEmitter.off('side:preferences', this.handleMenuButtonClick) } - deleteTag (tag) { - const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), { - ype: 'warning', - message: i18n.__('Confirm tag deletion'), - detail: i18n.__('This will permanently remove this tag.'), - buttons: [i18n.__('Confirm'), i18n.__('Cancel')] - }) + deleteTag(tag) { + const selectedButton = remote.dialog.showMessageBox( + remote.getCurrentWindow(), + { + type: 'warning', + message: i18n.__('Confirm tag deletion'), + detail: i18n.__('This will permanently remove this tag.'), + buttons: [i18n.__('Confirm'), i18n.__('Cancel')] + } + ) if (selectedButton === 0) { - const { data, dispatch, location, params } = this.props + const { + data, + dispatch, + location, + match: { params } + } = this.props const notes = data.noteMap .map(note => note) @@ -59,44 +93,68 @@ class SideNav extends React.Component { return note }) - Promise - .all(notes.map(note => dataApi.updateNote(note.storage, note.key, note))) - .then(updatedNotes => { - updatedNotes.forEach(note => { - dispatch({ - type: 'UPDATE_NOTE', - note - }) + Promise.all( + notes.map(note => dataApi.updateNote(note.storage, note.key, note)) + ).then(updatedNotes => { + updatedNotes.forEach(note => { + dispatch({ + type: 'UPDATE_NOTE', + note }) - - if (location.pathname.match('/tags')) { - const tags = params.tagname.split(' ') - const index = tags.indexOf(tag) - if (index !== -1) { - tags.splice(index, 1) - - this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`) - } - } }) + + if (location.pathname.match('/tags')) { + const tags = params.tagname.split(' ') + const index = tags.indexOf(tag) + if (index !== -1) { + tags.splice(index, 1) + + dispatch( + push( + `/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}` + ) + ) + } + } + }) } } - handleMenuButtonClick (e) { + handleMenuButtonClick(e) { openModal(PreferencesModal) } - handleHomeButtonClick (e) { - const { router } = this.context - router.push('/home') + handleSearchButtonClick(e) { + const { showSearch } = this.state + this.setState({ + showSearch: !showSearch, + searchText: '' + }) } - handleStarredButtonClick (e) { - const { router } = this.context - router.push('/starred') + handleSearchInputClear(e) { + this.setState({ + searchText: '' + }) } - handleTagContextMenu (e, tag) { + handleSearchInputChange(e) { + this.setState({ + searchText: e.target.value + }) + } + + handleHomeButtonClick(e) { + const { dispatch } = this.props + dispatch(push('/home')) + } + + handleStarredButtonClick(e) { + const { dispatch } = this.props + dispatch(push('/starred')) + } + + handleTagContextMenu(e, tag) { const menu = [] menu.push({ @@ -104,47 +162,139 @@ class SideNav extends React.Component { click: this.deleteTag.bind(this, tag) }) + menu.push({ + label: i18n.__('Customize Color'), + click: this.displayColorPicker.bind( + this, + tag, + e.target.getBoundingClientRect() + ) + }) + + menu.push({ + label: i18n.__('Rename Tag'), + click: this.handleRenameTagClick.bind(this, tag) + }) + context.popup(menu) } - handleToggleButtonClick (e) { - const { dispatch, config } = this.props + dismissColorPicker() { + this.setState({ + colorPicker: { + show: false + } + }) + } - ConfigManager.set({isSideNavFolded: !config.isSideNavFolded}) + displayColorPicker(tagName, rect) { + const { config } = this.props + this.setState({ + colorPicker: { + show: true, + color: config.coloredTags[tagName], + tagName, + targetRect: rect + } + }) + } + + handleRenameTagClick(tagName) { + const { data, dispatch } = this.props + + openModal(RenameTagModal, { + tagName, + data, + dispatch + }) + } + + handleColorPickerConfirm(color) { + const { + dispatch, + config: { coloredTags } + } = this.props + const { + colorPicker: { tagName } + } = this.state + const newColoredTags = Object.assign({}, coloredTags, { + [tagName]: color.hex + }) + + const config = { coloredTags: newColoredTags } + ConfigManager.set(config) + dispatch({ + type: 'SET_CONFIG', + config + }) + this.dismissColorPicker() + } + + handleColorPickerReset() { + const { + dispatch, + config: { coloredTags } + } = this.props + const { + colorPicker: { tagName } + } = this.state + const newColoredTags = Object.assign({}, coloredTags) + + delete newColoredTags[tagName] + + const config = { coloredTags: newColoredTags } + ConfigManager.set(config) + dispatch({ + type: 'SET_CONFIG', + config + }) + this.dismissColorPicker() + } + + handleToggleButtonClick(e) { + const { dispatch, config } = this.props + const { showSearch, searchText } = this.state + + ConfigManager.set({ isSideNavFolded: !config.isSideNavFolded }) dispatch({ type: 'SET_IS_SIDENAV_FOLDED', isFolded: !config.isSideNavFolded }) - } - handleTrashedButtonClick (e) { - const { router } = this.context - router.push('/trashed') - } - - handleSwitchFoldersButtonClick () { - const { router } = this.context - router.push('/home') - } - - handleSwitchTagsButtonClick () { - const { router } = this.context - router.push('/alltags') - } - - onSortEnd (storage) { - return ({oldIndex, newIndex}) => { - const { dispatch } = this.props - dataApi - .reorderFolder(storage.key, oldIndex, newIndex) - .then((data) => { - dispatch({ type: 'REORDER_FOLDER', storage: data.storage }) - }) + if (showSearch && searchText.length === 0) { + this.setState({ + showSearch: false + }) } } - SideNavComponent (isFolded, storageList) { - const { location, data, config } = this.props + handleTrashedButtonClick(e) { + const { dispatch } = this.props + dispatch(push('/trashed')) + } + + handleSwitchFoldersButtonClick() { + const { dispatch } = this.props + dispatch(push('/home')) + } + + handleSwitchTagsButtonClick() { + const { dispatch } = this.props + dispatch(push('/alltags')) + } + + onSortEnd(storage) { + return ({ oldIndex, newIndex }) => { + const { dispatch } = this.props + dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => { + dispatch({ type: 'REORDER_FOLDER', storage: data.storage }) + }) + } + } + + SideNavComponent(isFolded) { + const { location, data, config, dispatch } = this.props + const { showSearch, searchText } = this.state const isHomeActive = !!location.pathname.match(/^\/home$/) const isStarredActive = !!location.pathname.match(/^\/starred$/) @@ -153,25 +303,62 @@ class SideNav extends React.Component { let component // TagsMode is not selected - if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) { + if ( + !location.pathname.match('/tags') && + !location.pathname.match('/alltags') + ) { + let storageMap = data.storageMap + if (showSearch && searchText.length > 0) { + storageMap = storageMap.map(storage => { + const folders = storage.folders.filter( + folder => + folder.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1 + ) + return Object.assign({}, storage, { folders }) + }) + } + + const storageList = storageMap.map((storage, key) => { + const SortableStorageItem = SortableContainer(StorageItem) + return ( + + ) + }) + component = (
this.handleHomeButtonClick(e)} + handleAllNotesButtonClick={e => this.handleHomeButtonClick(e)} isStarredActive={isStarredActive} isTrashedActive={isTrashedActive} - handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)} - handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)} - counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size} + handleStarredButtonClick={e => this.handleStarredButtonClick(e)} + handleTrashedButtonClick={e => this.handleTrashedButtonClick(e)} + counterTotalNote={ + data.noteMap._map.size - data.trashedSet._set.size + } counterStarredNote={data.starredSet._set.size} counterDelNote={data.trashedSet._set.size} - handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)} + handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind( + this + )} /> - +
) } else { @@ -183,21 +370,26 @@ class SideNav extends React.Component {
-
-
- {this.tagListComponent(data)} -
+
{this.tagListComponent(data)}
+
) } @@ -205,80 +397,89 @@ class SideNav extends React.Component { return component } - tagListComponent () { + tagListComponent() { const { data, location, config } = this.props + const { colorPicker, showSearch, searchText } = this.state const activeTags = this.getActiveTags(location.pathname) const relatedTags = this.getRelatedTags(activeTags, data.noteMap) - let tagList = _.sortBy(data.tagNoteMap.map( - (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) - ).filter( - tag => tag.size > 0 - ), ['name']) + let tagList = sortBy( + data.tagNoteMap + .map((tag, name) => ({ + name, + size: tag.size, + related: relatedTags.has(name) + })) + .filter(tag => tag.size > 0), + ['name'] + ) + if (showSearch && searchText.length > 0) { + tagList = tagList.filter( + tag => tag.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1 + ) + } if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) { const notesTags = data.noteMap.map(note => note.tags) tagList = tagList.map(tag => { - tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length + tag.size = notesTags.filter( + tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags) + ).length return tag }) } if (config.sortTagsBy === 'COUNTER') { - tagList = _.sortBy(tagList, item => (0 - item.size)) + tagList = sortBy(tagList, item => 0 - item.size) } - if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) { - tagList = tagList.filter( - tag => tag.related + if (config.ui.showOnlyRelatedTags && relatedTags.size > 0) { + tagList = tagList.filter(tag => tag.related) + } + return tagList.map(tag => { + return ( + ) - } - return ( - tagList.map(tag => { - return ( - - ) - }) - ) + }) } - getRelatedTags (activeTags, noteMap) { + getRelatedTags(activeTags, noteMap) { if (activeTags.length === 0) { return new Set() } - const relatedNotes = noteMap.map( - note => ({key: note.key, tags: note.tags}) - ).filter( - note => activeTags.every(tag => note.tags.includes(tag)) - ) + const relatedNotes = noteMap + .map(note => ({ key: note.key, tags: note.tags })) + .filter(note => activeTags.every(tag => note.tags.includes(tag))) const relatedTags = new Set() relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag))) return relatedTags } - getTagActive (path, tag) { + getTagActive(path, tag) { return this.getActiveTags(path).includes(tag) } - getActiveTags (path) { + getActiveTags(path) { const pathSegments = path.split('/') const tags = pathSegments[pathSegments.length - 1] - return (tags === 'alltags') - ? [] - : decodeURIComponent(tags).split(' ') + return tags === 'alltags' ? [] : decodeURIComponent(tags).split(' ') } - handleClickTagListItem (name) { - const { router } = this.context - router.push(`/tags/${encodeURIComponent(name)}`) + handleClickTagListItem(name) { + const { dispatch } = this.props + dispatch(push(`/tags/${encodeURIComponent(name)}`)) } - handleSortTagsByChange (e) { + handleSortTagsByChange(e) { const { dispatch } = this.props const config = { @@ -292,9 +493,8 @@ class SideNav extends React.Component { }) } - handleClickNarrowToTag (tag) { - const { router } = this.context - const { location } = this.props + handleClickNarrowToTag(tag) { + const { dispatch, location } = this.props const listOfTags = this.getActiveTags(location.pathname) const indexOfTag = listOfTags.indexOf(tag) if (indexOfTag > -1) { @@ -302,72 +502,115 @@ class SideNav extends React.Component { } else { listOfTags.push(tag) } - router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`) + dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)) } - emptyTrash (entries) { + emptyTrash(entries) { const { dispatch } = this.props - const deletionPromises = entries.map((note) => { + const deletionPromises = entries.map(note => { return dataApi.deleteNote(note.storage, note.key) }) const { confirmDeletion } = this.props.config.ui if (!confirmDeleteNote(confirmDeletion, true)) return Promise.all(deletionPromises) - .then((arrayOfStorageAndNoteKeys) => { - arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => { - dispatch({ type: 'DELETE_NOTE', storageKey, noteKey }) + .then(arrayOfStorageAndNoteKeys => { + arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => { + dispatch({ type: 'DELETE_NOTE', storageKey, noteKey }) + }) + }) + .catch(err => { + console.error('Cannot Delete note: ' + err) }) - }) - .catch((err) => { - console.error('Cannot Delete note: ' + err) - }) } - handleFilterButtonContextMenu (event) { + handleFilterButtonContextMenu(event) { const { data } = this.props - const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey)) + const trashedNotes = data.trashedSet + .toJS() + .map(uniqueKey => data.noteMap.get(uniqueKey)) context.popup([ - { label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) } + { + label: i18n.__('Empty Trash'), + click: () => this.emptyTrash(trashedNotes) + } ]) } - render () { - const { data, location, config, dispatch } = this.props + render() { + const { location, config } = this.props + const { showSearch, searchText, colorPicker: colorPickerState } = this.state + + let colorPicker + if (colorPickerState.show) { + colorPicker = ( + + ) + } const isFolded = config.isSideNavFolded - - const storageList = data.storageMap.map((storage, key) => { - const SortableStorageItem = SortableContainer(StorageItem) - return - }) const style = {} if (!isFolded) style.width = this.props.width - const isTagActive = location.pathname.match(/tag/) + const isTagActive = /tag/.test(location.pathname) + + const navSearch = ( +
+ + + {isFolded && ( + + )} +
+ ) + return ( -
- - + +
-
+
+
- {this.SideNavComponent(isFolded, storageList)} + {navSearch} + {this.SideNavComponent(isFolded)} + {colorPicker}
) } diff --git a/browser/main/StatusBar/StatusBar.styl b/browser/main/StatusBar/StatusBar.styl index 23dec208..0ff3e7e5 100644 --- a/browser/main/StatusBar/StatusBar.styl +++ b/browser/main/StatusBar/StatusBar.styl @@ -78,24 +78,19 @@ body[data-theme="dark"] border-color $ui-dark-borderColor border-left 1px solid $ui-dark-borderColor -body[data-theme="monokai"] - navButtonColor() - .zoom - border-color $ui-dark-borderColor - color $ui-monokai-text-color - &:hover - transition 0.15s - color $ui-monokai-active-color - &:active - color $ui-monokai-active-color +apply-theme(theme) + body[data-theme={theme}] + .zoom + border-color $ui-dark-borderColor + color get-theme-var(theme, 'text-color') + &:hover + transition 0.15s + color get-theme-var(theme, 'active-color') + &:active + color get-theme-var(theme, 'active-color') -body[data-theme="dracula"] - navButtonColor() - .zoom - border-color $ui-dark-borderColor - color $ui-dracula-text-color - &:hover - transition 0.15s - color $ui-dracula-active-color - &:active - color $ui-dracula-active-color \ No newline at end of file +for theme in 'dracula' 'solarized-dark' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/StatusBar/index.js b/browser/main/StatusBar/index.js index c99bf036..6b53f2d2 100644 --- a/browser/main/StatusBar/index.js +++ b/browser/main/StatusBar/index.js @@ -11,30 +11,43 @@ const electron = require('electron') const { remote, ipcRenderer } = electron const { dialog } = remote -const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] +const zoomOptions = [ + 0.8, + 0.9, + 1, + 1.1, + 1.2, + 1.3, + 1.4, + 1.5, + 1.6, + 1.7, + 1.8, + 1.9, + 2.0 +] class StatusBar extends React.Component { - - constructor (props) { + constructor(props) { super(props) this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this) this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this) this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this) } - componentDidMount () { + componentDidMount() { EventEmitter.on('status:zoomin', this.handleZoomInMenuItem) EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem) EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem) } - componentWillUnmount () { + componentWillUnmount() { EventEmitter.off('status:zoomin', this.handleZoomInMenuItem) EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem) EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem) } - updateApp () { + updateApp() { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Update Boostnote'), @@ -47,10 +60,10 @@ class StatusBar extends React.Component { } } - handleZoomButtonClick (e) { + handleZoomButtonClick(e) { const templates = [] - zoomOptions.forEach((zoom) => { + zoomOptions.forEach(zoom => { templates.push({ label: Math.floor(zoom * 100) + '%', click: () => this.handleZoomMenuItemClick(zoom) @@ -60,7 +73,7 @@ class StatusBar extends React.Component { context.popup(templates) } - handleZoomMenuItemClick (zoomFactor) { + handleZoomMenuItemClick(zoomFactor) { const { dispatch } = this.props ZoomManager.setZoom(zoomFactor) dispatch({ @@ -69,40 +82,36 @@ class StatusBar extends React.Component { }) } - handleZoomInMenuItem () { + handleZoomInMenuItem() { const zoomFactor = ZoomManager.getZoom() + 0.1 this.handleZoomMenuItemClick(zoomFactor) } - handleZoomOutMenuItem () { + handleZoomOutMenuItem() { const zoomFactor = ZoomManager.getZoom() - 0.1 this.handleZoomMenuItemClick(zoomFactor) } - handleZoomResetMenuItem () { + handleZoomResetMenuItem() { this.handleZoomMenuItemClick(1.0) } - render () { + render() { const { config, status } = this.context return ( -
- - {status.updateReady - ? - : null - } + ) : null}
) } diff --git a/browser/main/TopBar/TopBar.styl b/browser/main/TopBar/TopBar.styl index 61b21fc5..a0eeadf6 100644 --- a/browser/main/TopBar/TopBar.styl +++ b/browser/main/TopBar/TopBar.styl @@ -212,69 +212,31 @@ body[data-theme="dark"] .control-newPostButton-tooltip darkTooltip() +apply-theme(theme) + body[data-theme={theme}] + .root, .root--expanded + background-color get-theme-var(theme, 'noteList-backgroundColor') -body[data-theme="solarized-dark"] - .root, .root--expanded - background-color $ui-solarized-dark-noteList-backgroundColor + .control + border-color get-theme-var(theme, 'borderColor') + .control-search + background-color get-theme-var(theme, 'noteList-backgroundColor') - .control - border-color $ui-solarized-dark-borderColor - .control-search - background-color $ui-solarized-dark-noteList-backgroundColor + .control-search-icon + absolute top bottom left + line-height 32px + width 35px + color get-theme-var(theme, 'inactive-text-color') + background-color get-theme-var(theme, 'noteList-backgroundColor') - .control-search-icon - absolute top bottom left - line-height 32px - width 35px - color $ui-solarized-dark-inactive-text-color - background-color $ui-solarized-dark-noteList-backgroundColor + .control-search-input + background-color get-theme-var(theme, 'noteList-backgroundColor') + input + background-color get-theme-var(theme, 'noteList-backgroundColor') + color get-theme-var(theme, 'text-color') - .control-search-input - background-color $ui-solarized-dark-noteList-backgroundColor - input - background-color $ui-solarized-dark-noteList-backgroundColor - color $ui-solarized-dark-text-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="monokai"] - .root, .root--expanded - background-color $ui-monokai-noteList-backgroundColor - - .control - border-color $ui-monokai-borderColor - .control-search - background-color $ui-monokai-noteList-backgroundColor - - .control-search-icon - absolute top bottom left - line-height 32px - width 35px - color $ui-monokai-inactive-text-color - background-color $ui-monokai-noteList-backgroundColor - - .control-search-input - background-color $ui-monokai-noteList-backgroundColor - input - background-color $ui-monokai-noteList-backgroundColor - color $ui-monokai-text-color - -body[data-theme="dracula"] - .root, .root--expanded - background-color $ui-dracula-noteList-backgroundColor - - .control - border-color $ui-dracula-borderColor - .control-search - background-color $ui-dracula-noteList-backgroundColor - - .control-search-icon - absolute top bottom left - line-height 32px - width 35px - color $ui-dracula-inactive-text-color - background-color $ui-dracula-noteList-backgroundColor - - .control-search-input - background-color $ui-dracula-noteList-backgroundColor - input - background-color $ui-dracula-noteList-backgroundColor - color $ui-dracula-text-color \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index 91256daf..e9554a67 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -7,34 +7,52 @@ import ee from 'browser/main/lib/eventEmitter' import NewNoteButton from 'browser/main/NewNoteButton' import i18n from 'browser/lib/i18n' import debounce from 'lodash/debounce' +import CInput from 'react-composition-input' +import { push } from 'connected-react-router' class TopBar extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { search: '', searchOptions: [], - isSearching: false, - isAlphabet: false, - isIME: false, - isConfirmTranslation: false + isSearching: false } + const { dispatch } = this.props + this.focusSearchHandler = () => { this.handleOnSearchFocus() } this.codeInitHandler = this.handleCodeInit.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleSearchFocus = this.handleSearchFocus.bind(this) + this.handleSearchBlur = this.handleSearchBlur.bind(this) + this.handleSearchChange = this.handleSearchChange.bind(this) + this.handleSearchClearButton = this.handleSearchClearButton.bind(this) - this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, { - maxWait: 1000 / 8 - }) + this.debouncedUpdateKeyword = debounce( + keyword => { + dispatch(push(`/searched/${encodeURIComponent(keyword)}`)) + this.setState({ + search: keyword + }) + ee.emit('top:search', keyword) + }, + 1000 / 60, + { + maxWait: 1000 / 8 + } + ) } - componentDidMount () { - const { params } = this.props - const searchWord = params.searchword + componentDidMount() { + const { + match: { params } + } = this.props + const searchWord = params && params.searchword if (searchWord !== undefined) { this.setState({ search: searchWord, @@ -45,28 +63,28 @@ class TopBar extends React.Component { ee.on('code:init', this.codeInitHandler) } - componentWillUnmount () { + componentWillUnmount() { ee.off('top:focus-search', this.focusSearchHandler) ee.off('code:init', this.codeInitHandler) } - handleSearchClearButton (e) { - const { router } = this.context + handleSearchClearButton(e) { + const { dispatch } = this.props this.setState({ search: '', isSearching: false }) this.refs.search.childNodes[0].blur - router.push('/searched') + dispatch(push('/searched')) e.preventDefault() + this.debouncedUpdateKeyword('') } - handleKeyDown (e) { - // reset states - this.setState({ - isAlphabet: false, - isIME: false - }) + handleKeyDown(e) { + // Re-apply search field on ENTER key + if (e.keyCode === 13) { + this.debouncedUpdateKeyword(e.target.value) + } // Clear search on ESC if (e.keyCode === 27) { @@ -84,59 +102,20 @@ class TopBar extends React.Component { ee.emit('list:prior') e.preventDefault() } - - // When the key is an alphabet, del, enter or ctr - if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) { - this.setState({ - isAlphabet: true - }) - // When the key is an IME input (Japanese, Chinese) - } else if (e.keyCode === 229) { - this.setState({ - isIME: true - }) - } } - handleKeyUp (e) { - // reset states - this.setState({ - isConfirmTranslation: false - }) - - // When the key is translation confirmation (Enter, Space) - if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) { - this.setState({ - isConfirmTranslation: true - }) - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } + handleSearchChange(e) { + const keyword = e.target.value + this.debouncedUpdateKeyword(keyword) } - handleSearchChange (e) { - if (this.state.isAlphabet || this.state.isConfirmTranslation) { - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } else { - e.preventDefault() - } - } - - updateKeyword (keyword) { - this.context.router.push(`/searched/${encodeURIComponent(keyword)}`) - this.setState({ - search: keyword - }) - ee.emit('top:search', keyword) - } - - handleSearchFocus (e) { + handleSearchFocus(e) { this.setState({ isSearching: true }) } - handleSearchBlur (e) { + + handleSearchBlur(e) { e.stopPropagation() let el = e.relatedTarget @@ -155,7 +134,7 @@ class TopBar extends React.Component { } } - handleOnSearchFocus () { + handleOnSearchFocus() { const el = this.refs.search.childNodes[0] if (this.state.isSearching) { el.blur() @@ -164,56 +143,63 @@ class TopBar extends React.Component { } } - handleCodeInit () { - ee.emit('top:search', this.refs.searchInput.value) + handleCodeInit() { + ee.emit('top:search', this.refs.searchInput.value || '') } - render () { + render() { const { config, style, location } = this.props return ( -
-
this.handleSearchFocus(e)} - onBlur={(e) => this.handleSearchBlur(e)} +
- this.handleSearchChange(e)} - onKeyDown={(e) => this.handleKeyDown(e)} - onKeyUp={(e) => this.handleKeyUp(e)} + onInputChange={this.handleSearchChange} + onKeyDown={this.handleKeyDown} placeholder={i18n.__('Search')} type='text' className='searchInput' /> - {this.state.search !== '' && - - } + )}
- {location.pathname === '/trashed' ? '' - : } + {location.pathname === '/trashed' ? ( + '' + ) : ( + + )}
) } diff --git a/browser/main/global.styl b/browser/main/global.styl index e04060c2..3f513c78 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -96,15 +96,6 @@ modalBackColor = white z-index modalZIndex + 1 -body[data-theme="dark"] - ::-webkit-scrollbar-thumb - background-color rgba(0, 0, 0, 0.3) - .ModalBase - .modalBack - background-color $ui-dark-backgroundColor - .sortableItemHelper - color: $ui-dark-text-color - .CodeMirror font-family inherit !important line-height 1.4em @@ -147,35 +138,25 @@ body[data-theme="dark"] .sortableItemHelper z-index modalZIndex + 5 -body[data-theme="solarized-dark"] - ::-webkit-scrollbar-thumb - background-color rgba(0, 0, 0, 0.3) - .ModalBase - .modalBack - background-color $ui-solarized-dark-backgroundColor - .sortableItemHelper - color: $ui-solarized-dark-text-color +apply-theme(theme) + body[data-theme={theme}] + background-color get-theme-var(theme, 'backgroundColor') + ::-webkit-scrollbar-thumb + background-color rgba(0, 0, 0, 0.3) + .ModalBase + .modalBack + background-color get-theme-var(theme, 'backgroundColor') + .sortableItemHelper + color get-theme-var(theme, 'text-color') -body[data-theme="monokai"] - ::-webkit-scrollbar-thumb - background-color rgba(0, 0, 0, 0.3) - .ModalBase - .modalBack - background-color $ui-monokai-backgroundColor - .sortableItemHelper - color: $ui-monokai-text-color +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="dracula"] - ::-webkit-scrollbar-thumb - background-color rgba(0, 0, 0, 0.3) - .ModalBase - .modalBack - background-color $ui-dracula-backgroundColor - .sortableItemHelper - color: $ui-dracula-text-color +for theme in $themes + apply-theme(theme) body[data-theme="default"] .SideNav ::-webkit-scrollbar-thumb background-color rgba(255, 255, 255, 0.3) -@import '../styles/Detail/TagSelect.styl' \ No newline at end of file +@import '../styles/Detail/TagSelect.styl' diff --git a/browser/main/index.js b/browser/main/index.js index 6e8bdcc5..e77c62dd 100644 --- a/browser/main/index.js +++ b/browser/main/index.js @@ -1,11 +1,14 @@ import { Provider } from 'react-redux' import Main from './Main' -import store from './store' -import React from 'react' +import { store, history } from './store' +import React, { Fragment } from 'react' import ReactDOM from 'react-dom' require('!!style!css!stylus?sourceMap!./global.styl') -import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router' -import { syncHistoryWithStore } from 'react-router-redux' +import config from 'browser/main/lib/ConfigManager' +import { Route, Switch, Redirect } from 'react-router-dom' +import { ConnectedRouter } from 'connected-react-router' +import DevTools from './DevTools' + require('./lib/ipcClient') require('../lib/customMeta') import i18n from 'browser/lib/i18n' @@ -15,11 +18,11 @@ const electron = require('electron') const { remote, ipcRenderer } = electron const { dialog } = remote -document.addEventListener('drop', function (e) { +document.addEventListener('drop', function(e) { e.preventDefault() e.stopPropagation() }) -document.addEventListener('dragover', function (e) { +document.addEventListener('dragover', function(e) { e.preventDefault() e.stopPropagation() }) @@ -31,7 +34,7 @@ let isAltWithMouse = false let isAltWithOtherKey = false let isOtherKey = false -document.addEventListener('keydown', function (e) { +document.addEventListener('keydown', function(e) { if (e.key === 'Alt') { isAltPressing = true if (isOtherKey) { @@ -45,13 +48,13 @@ document.addEventListener('keydown', function (e) { } }) -document.addEventListener('mousedown', function (e) { +document.addEventListener('mousedown', function(e) { if (isAltPressing) { isAltWithMouse = true } }) -document.addEventListener('keyup', function (e) { +document.addEventListener('keyup', function(e) { if (e.key === 'Alt') { if (isAltWithMouse || isAltWithOtherKey) { e.preventDefault() @@ -63,27 +66,35 @@ document.addEventListener('keyup', function (e) { } }) -document.addEventListener('click', function (e) { +document.addEventListener('click', function(e) { const className = e.target.className - if (!className && typeof (className) !== 'string') return + if (!className && typeof className !== 'string') return const isInfoButton = className.includes('infoButton') const offsetParent = e.target.offsetParent - const isInfoPanel = offsetParent !== null - ? offsetParent.className.includes('infoPanel') - : false + const isInfoPanel = + offsetParent !== null ? offsetParent.className.includes('infoPanel') : false if (isInfoButton || isInfoPanel) return const infoPanel = document.querySelector('.infoPanel') if (infoPanel) infoPanel.style.display = 'none' }) -const el = document.getElementById('content') -const history = syncHistoryWithStore(hashHistory, store) +if (!config.get().ui.showScrollBar) { + document.styleSheets[54].insertRule('::-webkit-scrollbar {display: none}') + document.styleSheets[54].insertRule( + '::-webkit-scrollbar-corner {display: none}' + ) + document.styleSheets[54].insertRule( + '::-webkit-scrollbar-thumb {display: none}' + ) +} -function notify (...args) { +const el = document.getElementById('content') + +function notify(...args) { return new window.Notification(...args) } -function updateApp () { +function updateApp() { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Update Boostnote'), @@ -96,56 +107,56 @@ function updateApp () { } } -ReactDOM.render(( +ReactDOM.render( - - - - - - - - - - - - - - - - - - - - - - - - -), el, function () { - const loadingCover = document.getElementById('loadingCover') - loadingCover.parentNode.removeChild(loadingCover) + + + + + + + + + - ipcRenderer.on('update-ready', function () { - store.dispatch({ - type: 'UPDATE_AVAILABLE' - }) - notify('Update ready!', { - body: 'New Boostnote is ready to be installed.' - }) - updateApp() - }) + {/* storages */} + + + + + + + + , + el, + function() { + const loadingCover = document.getElementById('loadingCover') + loadingCover.parentNode.removeChild(loadingCover) - ipcRenderer.on('update-found', function () { - notify('Update found!', { - body: 'Preparing to update...' + ipcRenderer.on('update-ready', function() { + store.dispatch({ + type: 'UPDATE_AVAILABLE' + }) + notify('Update ready!', { + body: 'New Boostnote is ready to be installed.' + }) + updateApp() }) - }) - ipcRenderer.send('update-check', 'check-update') - window.addEventListener('online', function () { - if (!store.getState().status.updateReady) { - ipcRenderer.send('update-check', 'check-update') - } - }) -}) + ipcRenderer.on('update-found', function() { + notify('Update found!', { + body: 'Preparing to update...' + }) + }) + + ipcRenderer.send('update-check', 'check-update') + window.addEventListener('online', function() { + if (!store.getState().status.updateReady) { + ipcRenderer.send('update-check', 'check-update') + } + }) + } +) diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js index e4a21a92..ce7b03ef 100644 --- a/browser/main/lib/AwsMobileAnalyticsConfig.js +++ b/browser/main/lib/AwsMobileAnalyticsConfig.js @@ -22,7 +22,7 @@ if (!getSendEventCond()) { }) } -function convertPlatformName (platformName) { +function convertPlatformName(platformName) { if (platformName === 'darwin') { return 'MacOS' } else if (platformName === 'win32') { @@ -34,16 +34,16 @@ function convertPlatformName (platformName) { } } -function getSendEventCond () { +function getSendEventCond() { const isDev = process.env.NODE_ENV !== 'production' const isDisable = !ConfigManager.default.get().amaEnabled const isOffline = !window.navigator.onLine return isDev || isDisable || isOffline } -function initAwsMobileAnalytics () { +function initAwsMobileAnalytics() { if (getSendEventCond()) return - AWS.config.credentials.get((err) => { + AWS.config.credentials.get(err => { if (!err) { recordDynamicCustomEvent('APP_STARTED') recordStaticCustomEvent() @@ -51,7 +51,7 @@ function initAwsMobileAnalytics () { }) } -function recordDynamicCustomEvent (type, options = {}) { +function recordDynamicCustomEvent(type, options = {}) { if (getSendEventCond()) return try { mobileAnalyticsClient.recordEvent(type, options) @@ -62,7 +62,7 @@ function recordDynamicCustomEvent (type, options = {}) { } } -function recordStaticCustomEvent () { +function recordStaticCustomEvent() { if (getSendEventCond()) return try { mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', { diff --git a/browser/main/lib/Commander.js b/browser/main/lib/Commander.js index 6eef62ee..de6aa27c 100644 --- a/browser/main/lib/Commander.js +++ b/browser/main/lib/Commander.js @@ -1,25 +1,24 @@ let callees = [] -function bind (name, el) { +function bind(name, el) { callees.push({ name: name, element: el }) } -function release (el) { - callees = callees.filter((callee) => callee.element !== el) +function release(el) { + callees = callees.filter(callee => callee.element !== el) } -function fire (command) { +function fire(command) { console.info('COMMAND >>', command) const splitted = command.split(':') const target = splitted[0] const targetCommand = splitted[1] - const targetCallees = callees - .filter((callee) => callee.name === target) + const targetCallees = callees.filter(callee => callee.name === target) - targetCallees.forEach((callee) => { + targetCallees.forEach(callee => { callee.element.fire(targetCommand) }) } diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 29b12a37..76ef620c 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -8,9 +8,30 @@ const win = global.process.platform === 'win32' const electron = require('electron') const { ipcRenderer } = electron const consts = require('browser/lib/consts') +const electronConfig = new (require('electron-config'))() let isInitialized = false +const DEFAULT_MARKDOWN_LINT_CONFIG = `{ + "default": true +}` + +const DEFAULT_CSS_CONFIG = ` +/* Drop Your Custom CSS Code Here */ +[data-theme="default"] p code, +[data-theme="default"] li code, +[data-theme="default"] td code +{ + padding: 2px; + border-width: 1px; + border-style: solid; + border-radius: 5px; + background-color: #F4F4F4; + border-color: #d9d9d9; + color: #03C588; +} +` + export const DEFAULT_CONFIG = { zoom: 1, isSideNavFolded: false, @@ -21,34 +42,55 @@ export const DEFAULT_CONFIG = { }, sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' + listDirection: 'ASCENDING', // 'ASCENDING', 'DESCENDING' amaEnabled: true, + autoUpdateEnabled: true, hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', - deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', - pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V' + toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Alt + Right', + deleteNote: OSX + ? 'Command + Shift + Backspace' + : 'Ctrl + Shift + Backspace', + pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V', + prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F', + sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S', + insertDate: OSX ? 'Command + /' : 'Ctrl + /', + insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /', + toggleMenuBar: 'Alt' }, ui: { language: 'en', theme: 'default', + defaultTheme: 'default', + enableScheduleTheme: false, + scheduledTheme: 'monokai', + scheduleStart: 1200, + scheduleEnd: 360, showCopyNotification: true, disableDirectWrite: false, - defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' + showScrollBar: true, + defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' + showMenuBar: false, + isStacking: false }, editor: { theme: 'base16-light', keyMap: 'sublime', fontSize: '14', - fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas', + fontFamily: win ? 'Consolas' : 'Monaco', indentType: 'space', indentSize: '2', + lineWrapping: true, enableRulers: false, rulers: [80, 120], displayLineNumbers: true, - matchingPairs: '()[]{}\'\'""$$**``', + matchingPairs: '()[]{}\'\'""$$**``~~__', + matchingCloseBefore: ')]}\'":;>', matchingTriples: '```"""\'\'\'', explodingPairs: '[]{}``$$', codeBlockMatchingPairs: '()[]{}\'\'""``', + codeBlockMatchingCloseBefore: ')]}\'":;>', codeBlockMatchingTriples: '', codeBlockExplodingPairs: '[]{}``', switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' @@ -60,7 +102,17 @@ export const DEFAULT_CONFIG = { enableFrontMatterTitle: true, frontMatterTitleField: 'title', spellcheck: false, - enableSmartPaste: false + enableSmartPaste: false, + enableMarkdownLint: false, + customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, + prettierConfig: `{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true + }`, + deleteUnusedAttachments: true, + rtlEnabled: false }, preview: { fontSize: '14', @@ -78,8 +130,9 @@ export const DEFAULT_CONFIG = { breaks: true, smartArrows: false, allowCustomCSS: false, - customCSS: '', + customCSS: DEFAULT_CSS_CONFIG, sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' + mermaidHTMLLabel: false, lineThroughCheckbox: true }, blog: { @@ -89,10 +142,11 @@ export const DEFAULT_CONFIG = { token: '', username: '', password: '' - } + }, + coloredTags: {} } -function validate (config) { +function validate(config) { if (!_.isObject(config)) return false if (!_.isNumber(config.zoom) || config.zoom < 0) return false if (!_.isBoolean(config.isSideNavFolded)) return false @@ -101,14 +155,17 @@ function validate (config) { return true } -function _save (config) { - console.log(config) +function _save(config) { window.localStorage.setItem('config', JSON.stringify(config)) } -function get () { +function get() { const rawStoredConfig = window.localStorage.getItem('config') - const storedConfig = Object.assign({}, DEFAULT_CONFIG, JSON.parse(rawStoredConfig)) + const storedConfig = Object.assign( + {}, + DEFAULT_CONFIG, + JSON.parse(rawStoredConfig) + ) let config = storedConfig try { @@ -122,6 +179,11 @@ function get () { _save(config) } + config.autoUpdateEnabled = electronConfig.get( + 'autoUpdateEnabled', + config.autoUpdateEnabled + ) + if (!isInitialized) { isInitialized = true let editorTheme = document.getElementById('editorTheme') @@ -132,42 +194,37 @@ function get () { document.head.appendChild(editorTheme) } - config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme) - ? config.editor.theme - : 'default' + const theme = consts.THEMES.find( + theme => theme.name === config.editor.theme + ) - if (config.editor.theme !== 'default') { - if (config.editor.theme.startsWith('solarized')) { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css') - } else { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css') - } + if (theme) { + editorTheme.setAttribute('href', theme.path) + } else { + config.editor.theme = 'default' } } return config } -function set (updates) { +function set(updates) { const currentConfig = get() - const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates) + + const arrangedUpdates = updates + if (updates.preview !== undefined && updates.preview.customCSS === '') { + arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS + } + + const newConfig = Object.assign( + {}, + DEFAULT_CONFIG, + currentConfig, + arrangedUpdates + ) if (!validate(newConfig)) throw new Error('INVALID CONFIG') _save(newConfig) - if (newConfig.ui.theme === 'dark') { - document.body.setAttribute('data-theme', 'dark') - } else if (newConfig.ui.theme === 'white') { - document.body.setAttribute('data-theme', 'white') - } else if (newConfig.ui.theme === 'solarized-dark') { - document.body.setAttribute('data-theme', 'solarized-dark') - } else if (newConfig.ui.theme === 'monokai') { - document.body.setAttribute('data-theme', 'monokai') - } else if (newConfig.ui.theme === 'dracula') { - document.body.setAttribute('data-theme', 'dracula') - } else { - document.body.setAttribute('data-theme', 'default') - } - i18n.setLocale(newConfig.ui.language) let editorTheme = document.getElementById('editorTheme') @@ -177,41 +234,65 @@ function set (updates) { editorTheme.setAttribute('rel', 'stylesheet') document.head.appendChild(editorTheme) } - const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme) - ? newConfig.editor.theme - : 'default' - if (newTheme !== 'default') { - if (newTheme.startsWith('solarized')) { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css') - } else { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css') - } + const newTheme = consts.THEMES.find( + theme => theme.name === newConfig.editor.theme + ) + + if (newTheme) { + editorTheme.setAttribute('href', newTheme.path) } + electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled) + ipcRenderer.send('config-renew', { config: get() }) ee.emit('config-renew') } -function assignConfigValues (originalConfig, rcConfig) { +function assignConfigValues(originalConfig, rcConfig) { const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig) - config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey) - config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog) - 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) + config.hotkey = Object.assign( + {}, + DEFAULT_CONFIG.hotkey, + originalConfig.hotkey, + rcConfig.hotkey + ) + config.blog = Object.assign( + {}, + DEFAULT_CONFIG.blog, + originalConfig.blog, + rcConfig.blog + ) + 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 + ) rewriteHotkey(config) return config } -function rewriteHotkey (config) { +function rewriteHotkey(config) { const keys = [...Object.keys(config.hotkey)] keys.forEach(key => { - config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command') + config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ') config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ') }) return config diff --git a/browser/main/lib/ThemeManager.js b/browser/main/lib/ThemeManager.js new file mode 100644 index 00000000..a1b090e9 --- /dev/null +++ b/browser/main/lib/ThemeManager.js @@ -0,0 +1,65 @@ +import ConfigManager from 'browser/main/lib/ConfigManager' + +const saveChanges = newConfig => { + ConfigManager.set(newConfig) +} + +const chooseTheme = config => { + const { ui } = config + if (!ui.enableScheduleTheme) { + return + } + + const start = parseInt(ui.scheduleStart) + const end = parseInt(ui.scheduleEnd) + + const now = new Date() + const minutes = now.getHours() * 60 + now.getMinutes() + + const isEndAfterStart = end > start + const isBetweenStartAndEnd = minutes >= start && minutes < end + const isBetweenEndAndStart = minutes >= start || minutes < end + + if ( + (isEndAfterStart && isBetweenStartAndEnd) || + (!isEndAfterStart && isBetweenEndAndStart) + ) { + if (ui.theme !== ui.scheduledTheme) { + ui.defaultTheme = ui.theme + ui.theme = ui.scheduledTheme + applyTheme(ui.theme) + saveChanges(config) + } + } else { + if (ui.theme !== ui.defaultTheme) { + ui.theme = ui.defaultTheme + applyTheme(ui.theme) + saveChanges(config) + } + } +} + +const applyTheme = theme => { + const supportedThemes = [ + 'dark', + 'white', + 'solarized-dark', + 'monokai', + 'dracula' + ] + if (supportedThemes.indexOf(theme) !== -1) { + document.body.setAttribute('data-theme', theme) + if (document.body.querySelector('.MarkdownPreview')) { + document.body + .querySelector('.MarkdownPreview') + .contentDocument.body.setAttribute('data-theme', theme) + } + } else { + document.body.setAttribute('data-theme', 'default') + } +} + +module.exports = { + chooseTheme, + applyTheme +} diff --git a/browser/main/lib/ZoomManager.js b/browser/main/lib/ZoomManager.js index a8903ca3..56bc9236 100644 --- a/browser/main/lib/ZoomManager.js +++ b/browser/main/lib/ZoomManager.js @@ -5,20 +5,20 @@ const { remote } = electron _init() -function _init () { +function _init() { setZoom(getZoom(), true) } -function _saveZoom (zoomFactor) { - ConfigManager.set({zoom: zoomFactor}) +function _saveZoom(zoomFactor) { + ConfigManager.set({ zoom: zoomFactor }) } -function setZoom (zoomFactor, noSave = false) { +function setZoom(zoomFactor, noSave = false) { if (!noSave) _saveZoom(zoomFactor) remote.getCurrentWebContents().setZoomFactor(zoomFactor) } -function getZoom () { +function getZoom() { const config = ConfigManager.get() return config.zoom diff --git a/browser/main/lib/dataApi/addStorage.js b/browser/main/lib/dataApi/addStorage.js index bfd6698a..370a07e0 100644 --- a/browser/main/lib/dataApi/addStorage.js +++ b/browser/main/lib/dataApi/addStorage.js @@ -16,7 +16,7 @@ const CSON = require('@rokt33r/season') * 3. fetch notes & folders * 4. return `{storage: {...} folders: [folder]}` */ -function addStorage (input) { +function addStorage(input) { if (!_.isString(input.path)) { return Promise.reject(new Error('Path must be a string.')) } @@ -29,7 +29,7 @@ function addStorage (input) { rawStorages = [] } let key = keygen() - while (rawStorages.some((storage) => storage.key === key)) { + while (rawStorages.some(storage => storage.key === key)) { key = keygen() } @@ -43,7 +43,7 @@ function addStorage (input) { return Promise.resolve(newStorage) .then(resolveStorageData) - .then(function saveMetadataToLocalStorage (resolvedStorage) { + .then(function saveMetadataToLocalStorage(resolvedStorage) { newStorage = resolvedStorage rawStorages.push({ key: newStorage.key, @@ -56,27 +56,29 @@ function addStorage (input) { localStorage.setItem('storages', JSON.stringify(rawStorages)) return newStorage }) - .then(function (storage) { - return resolveStorageNotes(storage) - .then((notes) => { - let unknownCount = 0 - notes.forEach((note) => { - if (!storage.folders.some((folder) => note.folder === folder.key)) { - unknownCount++ - storage.folders.push({ - key: note.folder, - color: consts.FOLDER_COLORS[(unknownCount - 1) % 7], - name: 'Unknown ' + unknownCount - }) - } - }) - if (unknownCount > 0) { - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + .then(function(storage) { + return resolveStorageNotes(storage).then(notes => { + let unknownCount = 0 + notes.forEach(note => { + if (!storage.folders.some(folder => note.folder === folder.key)) { + unknownCount++ + storage.folders.push({ + key: note.folder, + color: consts.FOLDER_COLORS[(unknownCount - 1) % 7], + name: 'Unknown ' + unknownCount + }) } - return notes }) + if (unknownCount > 0) { + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) + } + return notes + }) }) - .then(function returnValue (notes) { + .then(function returnValue(notes) { return { storage: newStorage, notes diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 6a0315f7..48500f4a 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -6,27 +6,38 @@ const mdurl = require('mdurl') const fse = require('fs-extra') const escapeStringRegexp = require('escape-string-regexp') const sander = require('sander') +const url = require('url') import i18n from 'browser/lib/i18n' +import { isString } from 'lodash' const STORAGE_FOLDER_PLACEHOLDER = ':storage' const DESTINATION_FOLDER = 'attachments' -const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep) +const PATH_SEPARATORS = + escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep) /** * @description * Create a Image element to get the real size of image. * @param {File} file the File object dropped. * @returns {Promise} Image element created */ -function getImage (file) { - return new Promise((resolve) => { - const reader = new FileReader() - const img = new Image() - img.onload = () => resolve(img) - reader.onload = e => { - img.src = e.target.result - } - reader.readAsDataURL(file) - }) +function getImage(file) { + if (isString(file)) { + return new Promise(resolve => { + const img = new Image() + img.onload = () => resolve(img) + img.src = file + }) + } else { + return new Promise(resolve => { + const reader = new FileReader() + const img = new Image() + img.onload = () => resolve(img) + reader.onload = e => { + img.src = e.target.result + } + reader.readAsDataURL(file) + }) + } } /** @@ -45,38 +56,39 @@ function getImage (file) { * @param {File} file the File object dropped. * @returns {Promise} Orientation info */ -function getOrientation (file) { +function getOrientation(file) { const getData = arrayBuffer => { const view = new DataView(arrayBuffer) // Not start with SOI(Start of image) Marker return fail value - if (view.getUint16(0, false) !== 0xFFD8) return -2 + if (view.getUint16(0, false) !== 0xffd8) return -2 const length = view.byteLength let offset = 2 while (offset < length) { const marker = view.getUint16(offset, false) offset += 2 // Loop and seed for APP1 Marker - if (marker === 0xFFE1) { + if (marker === 0xffe1) { // return fail value if it isn't EXIF data - if (view.getUint32(offset += 2, false) !== 0x45786966) { + if (view.getUint32((offset += 2), false) !== 0x45786966) { return -1 } // Read TIFF header, // First 2bytes defines byte align of TIFF data. // If it is 0x4949="II", it means "Intel" type byte align. // If it is 0x4d4d="MM", it means "Motorola" type byte align - const little = view.getUint16(offset += 6, false) === 0x4949 + const little = view.getUint16((offset += 6), false) === 0x4949 offset += view.getUint32(offset + 4, little) const tags = view.getUint16(offset, little) // Get TAG number offset += 2 for (let i = 0; i < tags; i++) { // Loop to find Orientation TAG and return the value - if (view.getUint16(offset + (i * 12), little) === 0x0112) { - return view.getUint16(offset + (i * 12) + 8, little) + if (view.getUint16(offset + i * 12, little) === 0x0112) { + return view.getUint16(offset + i * 12 + 8, little) } } - } else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker + } else if ((marker & 0xff00) !== 0xff00) { + // If not start with 0xFF, not a Marker. break } else { offset += view.getUint16(offset, false) @@ -84,7 +96,7 @@ function getOrientation (file) { } return -1 } - return new Promise((resolve) => { + return new Promise(resolve => { const reader = new FileReader() reader.onload = event => resolve(getData(event.target.result)) reader.readAsArrayBuffer(file.slice(0, 64 * 1024)) @@ -97,31 +109,47 @@ function getOrientation (file) { * @param {*} file the File object dropped. * @return {String} Base64 encoded image. */ -function fixRotate (file) { - return Promise.all([getImage(file), getOrientation(file)]) - .then(([img, orientation]) => { - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - if (orientation > 4 && orientation < 9) { - canvas.width = img.height - canvas.height = img.width - } else { - canvas.width = img.width - canvas.height = img.height +function fixRotate(file) { + return Promise.all([getImage(file), getOrientation(file)]).then( + ([img, orientation]) => { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + if (orientation > 4 && orientation < 9) { + canvas.width = img.height + canvas.height = img.width + } else { + canvas.width = img.width + canvas.height = img.height + } + switch (orientation) { + case 2: + ctx.transform(-1, 0, 0, 1, img.width, 0) + break + case 3: + ctx.transform(-1, 0, 0, -1, img.width, img.height) + break + case 4: + ctx.transform(1, 0, 0, -1, 0, img.height) + break + case 5: + ctx.transform(0, 1, 1, 0, 0, 0) + break + case 6: + ctx.transform(0, 1, -1, 0, img.height, 0) + break + case 7: + ctx.transform(0, -1, -1, 0, img.height, img.width) + break + case 8: + ctx.transform(0, -1, 1, 0, 0, img.width) + break + default: + break + } + ctx.drawImage(img, 0, 0) + return canvas.toDataURL() } - switch (orientation) { - case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break - case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break - case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break - case 5: ctx.transform(0, 1, 1, 0, 0, 0); break - case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break - case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break - case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break - default: break - } - ctx.drawImage(img, 0, 0) - return canvas.toDataURL() - }) + ) } /** @@ -135,7 +163,12 @@ function fixRotate (file) { * @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used * @return {Promise} name (inclusive extension) of the generated file */ -function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) { +function copyAttachment( + sourceFilePath, + storageKey, + noteKey, + useRandomName = true +) { return new Promise((resolve, reject) => { if (!sourceFilePath) { reject('sourceFilePath has to be given') @@ -150,24 +183,42 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr } try { - const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64' - if (!fs.existsSync(sourceFilePath) && !isBase64) { + const isBase64 = + typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64' + if (!isBase64 && !fs.existsSync(sourceFilePath)) { return reject('source file does not exist') } - const targetStorage = findStorage.findStorage(storageKey) + + const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath + const sourceURL = url.parse( + /^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath + ) + let destinationName if (useRandomName) { - destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}` + destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || + '.png'}` } else { - destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath) + destinationName = path.basename(sourceURL.pathname) } - const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + + const targetStorage = findStorage.findStorage(storageKey) + const destinationDir = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey + ) createAttachmentDestinationFolder(targetStorage.path, noteKey) - const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName)) + const outputFile = fs.createWriteStream( + path.join(destinationDir, destinationName) + ) if (isBase64) { - const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '') - const dataBuffer = new Buffer(base64Data, 'base64') + const base64Data = sourceFilePath.data.replace( + /^data:image\/\w+;base64,/, + '' + ) + const dataBuffer = Buffer.from(base64Data, 'base64') outputFile.write(dataBuffer, () => { resolve(destinationName) }) @@ -184,12 +235,16 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr }) } -function createAttachmentDestinationFolder (destinationStoragePath, noteKey) { +function createAttachmentDestinationFolder(destinationStoragePath, noteKey) { let destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER) if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir) } - destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey) + destinationDir = path.join( + destinationStoragePath, + DESTINATION_FOLDER, + noteKey + ) if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir) } @@ -201,17 +256,28 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) { * @param storagePath Storage path of the current note * @param noteKey Key of the current note */ -function migrateAttachments (markdownContent, storagePath, noteKey) { - if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) { +function migrateAttachments(markdownContent, storagePath, noteKey) { + if ( + noteKey !== undefined && + sander.existsSync(path.join(storagePath, 'images')) + ) { const attachments = getAttachmentsInMarkdownContent(markdownContent) || [] if (attachments.length) { createAttachmentDestinationFolder(storagePath, noteKey) } for (const attachment of attachments) { const attachmentBaseName = path.basename(attachment) - const possibleLegacyPath = path.join(storagePath, 'images', attachmentBaseName) + const possibleLegacyPath = path.join( + storagePath, + 'images', + attachmentBaseName + ) if (sander.existsSync(possibleLegacyPath)) { - const destinationPath = path.join(storagePath, DESTINATION_FOLDER, attachmentBaseName) + const destinationPath = path.join( + storagePath, + DESTINATION_FOLDER, + attachmentBaseName + ) if (!sander.existsSync(destinationPath)) { sander.copyFileSync(possibleLegacyPath).to(destinationPath) } @@ -226,19 +292,31 @@ function migrateAttachments (markdownContent, storagePath, noteKey) { * @param {String} storagePath Path of the current storage * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. */ -function fixLocalURLS (renderedHTML, storagePath) { +function fixLocalURLS(renderedHTML, storagePath) { + const encodedWin32SeparatorRegex = /%5C/g + const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g') + const storageUrl = + 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/') + /* A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`. - `STORAGE_FOLDER_PLACEHOLDER` will match `:storage` - - `(?:(?:\\\/|%5C)[\\w.]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg` - - `(?:\\\/|%5C)[\\w.]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg` + - `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg` + - `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg` - `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows. */ - return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[\\w.]+)+', 'g'), function (match) { - var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') - return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) - }) + return renderedHTML.replace( + new RegExp( + '/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\/|%5C)[-.\\w]+)+', + 'g' + ), + function(match) { + return match + .replace(encodedWin32SeparatorRegex, '/') + .replace(storageRegex, storageUrl) + } + ) } /** @@ -248,7 +326,7 @@ function fixLocalURLS (renderedHTML, storagePath) { * @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported * @returns {String} Generated markdown code */ -function generateAttachmentMarkdown (fileName, path, showPreview) { +function generateAttachmentMarkdown(fileName, path, showPreview) { return `${showPreview ? '!' : ''}[${fileName}](${path})` } @@ -260,23 +338,114 @@ function generateAttachmentMarkdown (fileName, path, showPreview) { * @param {String} noteKey Key of the current note * @param {Event} dropEvent DropEvent */ -function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { - const file = dropEvent.dataTransfer.files[0] - const filePath = file.path - const originalFileName = path.basename(filePath) - const fileType = file['type'] - const isImage = fileType.startsWith('image') +function handleAttachmentDrop(codeEditor, storageKey, noteKey, dropEvent) { let promise - if (isImage) { - promise = fixRotate(file).then(base64data => { - return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey) - }) + if (dropEvent.dataTransfer.files.length > 0) { + promise = Promise.all( + Array.from(dropEvent.dataTransfer.files).map(file => { + const filePath = file.path + const fileType = file.type // EX) 'image/gif' or 'text/html' + if (fileType.startsWith('image')) { + if (fileType === 'image/gif' || fileType === 'image/svg+xml') { + return copyAttachment(filePath, storageKey, noteKey).then( + fileName => ({ + fileName, + title: path.basename(filePath), + isImage: true + }) + ) + } else { + return getOrientation(file) + .then(orientation => { + if (orientation === -1) { + // The image rotation is correct and does not need adjustment + return copyAttachment(filePath, storageKey, noteKey) + } else { + return fixRotate(file).then(data => + copyAttachment( + { + type: 'base64', + data: data, + sourceFilePath: filePath + }, + storageKey, + noteKey + ) + ) + } + }) + .then(fileName => ({ + fileName, + title: path.basename(filePath), + isImage: true + })) + } + } else { + return copyAttachment(filePath, storageKey, noteKey).then( + fileName => ({ + fileName, + title: path.basename(filePath), + isImage: false + }) + ) + } + }) + ) } else { - promise = copyAttachment(filePath, storageKey, noteKey) + let imageURL = dropEvent.dataTransfer.getData('text/plain') + + if (!imageURL) { + const match = /]*[\s"']src="([^"]+)"/.exec( + dropEvent.dataTransfer.getData('text/html') + ) + if (match) { + imageURL = match[1] + } + } + + if (!imageURL) { + return + } + + promise = Promise.all([ + getImage(imageURL) + .then(image => { + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + canvas.width = image.width + canvas.height = image.height + context.drawImage(image, 0, 0) + + return copyAttachment( + { + type: 'base64', + data: canvas.toDataURL(), + sourceFilePath: imageURL + }, + storageKey, + noteKey + ) + }) + .then(fileName => ({ + fileName, + title: imageURL, + isImage: true + })) + ]) } - promise.then((fileName) => { - const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage) - codeEditor.insertAttachmentMd(imageMd) + + promise.then(files => { + const attachments = files + .filter(file => !!file) + .map(file => + generateAttachmentMarkdown( + file.title, + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), + file.isImage + ) + ) + + codeEditor.insertAttachmentMd(attachments.join('\n')) }) } @@ -287,7 +456,12 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { * @param {String} noteKey Key of the current note * @param {DataTransferItem} dataTransferItem Part of the past-event */ -function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) { +function handlePasteImageEvent( + codeEditor, + storageKey, + noteKey, + dataTransferItem +) { if (!codeEditor) { throw new Error('codeEditor has to be given') } @@ -306,19 +480,31 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem const reader = new FileReader() let base64data const targetStorage = findStorage.findStorage(storageKey) - const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const destinationDir = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey + ) createAttachmentDestinationFolder(targetStorage.path, noteKey) const imageName = `${uniqueSlug()}.png` const imagePath = path.join(destinationDir, imageName) - reader.onloadend = function () { + reader.onloadend = function() { base64data = reader.result.replace(/^data:image\/png;base64,/, '') base64data += base64data.replace('+', ' ') const binaryData = new Buffer(base64data, 'base64').toString('binary') fs.writeFileSync(imagePath, binaryData, 'binary') - const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName) - const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true) + const imageReferencePath = path.join( + STORAGE_FOLDER_PLACEHOLDER, + noteKey, + imageName + ) + const imageMd = generateAttachmentMarkdown( + imageName, + imageReferencePath, + true + ) codeEditor.insertAttachmentMd(imageMd) } reader.readAsDataURL(blob) @@ -331,7 +517,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem * @param {String} noteKey Key of the current note * @param {NativeImage} image The native image */ -function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { +function handlePasteNativeImage(codeEditor, storageKey, noteKey, image) { if (!codeEditor) { throw new Error('codeEditor has to be given') } @@ -347,7 +533,11 @@ function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { } const targetStorage = findStorage.findStorage(storageKey) - const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const destinationDir = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey + ) createAttachmentDestinationFolder(targetStorage.path, noteKey) @@ -357,19 +547,42 @@ function handlePastNativeImage (codeEditor, storageKey, noteKey, image) { const binaryData = image.toPNG() fs.writeFileSync(imagePath, binaryData, 'binary') - const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName) - const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true) + const imageReferencePath = path.join( + STORAGE_FOLDER_PLACEHOLDER, + noteKey, + imageName + ) + const imageMd = generateAttachmentMarkdown( + imageName, + imageReferencePath, + true + ) codeEditor.insertAttachmentMd(imageMd) } /** -* @description Returns all attachment paths of the given markdown -* @param {String} markdownContent content in which the attachment paths should be found -* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown -*/ -function getAttachmentsInMarkdownContent (markdownContent) { - const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep) - const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g') + * @description Returns all attachment paths of the given markdown + * @param {String} markdownContent content in which the attachment paths should be found + * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown + */ +function getAttachmentsInMarkdownContent(markdownContent) { + const preparedInput = markdownContent.replace( + new RegExp('[' + PATH_SEPARATORS + ']', 'g'), + path.sep + ) + const regexp = new RegExp( + '/?' + + STORAGE_FOLDER_PLACEHOLDER + + '(' + + escapeStringRegexp(path.sep) + + ')' + + '?([a-zA-Z0-9]|-)*' + + '(' + + escapeStringRegexp(path.sep) + + ')' + + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', + 'g' + ) return preparedInput.match(regexp) } @@ -379,15 +592,76 @@ function getAttachmentsInMarkdownContent (markdownContent) { * @param {String} storagePath path of the current storage * @returns {String[]} Absolute paths of the referenced attachments */ -function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { +function getAbsolutePathsOfAttachmentsInContent(markdownContent, storagePath) { const temp = getAttachmentsInMarkdownContent(markdownContent) || [] const result = [] for (const relativePath of temp) { - result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))) + result.push( + relativePath.replace( + new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), + path.join(storagePath, DESTINATION_FOLDER) + ) + ) } return result } +/** + * @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with + * @param {String} markDownContent content in which the attachment paths should be found + * @param {String} filepath The path of the file with attachments to import + * @param {String} storageKey Storage key of the destination storage + * @param {String} noteKey Key of the current note. Will be used as subfolder in :storage + */ +function importAttachments(markDownContent, filepath, storageKey, noteKey) { + return new Promise((resolve, reject) => { + const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g + let attachPath = nameRegex.exec(markDownContent) + const promiseArray = [] + const attachmentPaths = [] + const groupIndex = 2 + + while (attachPath) { + let attachmentPath = attachPath[groupIndex] + attachmentPaths.push(attachmentPath) + attachmentPath = path.isAbsolute(attachmentPath) + ? attachmentPath + : path.join(path.dirname(filepath), attachmentPath) + promiseArray.push( + this.copyAttachment(attachmentPath, storageKey, noteKey) + ) + attachPath = nameRegex.exec(markDownContent) + } + + let numResolvedPromises = 0 + + if (promiseArray.length === 0) { + resolve(markDownContent) + } + + for (let j = 0; j < promiseArray.length; j++) { + promiseArray[j] + .then(fileName => { + const newPath = path.join( + STORAGE_FOLDER_PLACEHOLDER, + noteKey, + fileName + ) + markDownContent = markDownContent.replace(attachmentPaths[j], newPath) + }) + .catch(e => { + console.error('File does not exist in path: ' + attachmentPaths[j]) + }) + .finally(() => { + numResolvedPromises++ + if (numResolvedPromises === promiseArray.length) { + resolve(markDownContent) + } + }) + } + }) +} + /** * @description Moves the attachments of the current note to the new location. * Returns a modified version of the given content so that the links to the attachments point to the new note key. @@ -398,7 +672,7 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { * @param {String} noteContent Content of the note to be moved * @returns {String} Modified version of noteContent in which the paths of the attachments are fixed */ -function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) { +function moveAttachments(oldPath, newPath, noteKey, newNoteKey, noteContent) { const src = path.join(oldPath, DESTINATION_FOLDER, noteKey) const dest = path.join(newPath, DESTINATION_FOLDER, newNoteKey) if (fse.existsSync(src)) { @@ -414,10 +688,19 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) { * @param newNoteKey note key serving as a replacement * @returns {String} modified note content */ -function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) { +function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) { if (noteContent) { - const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep) - return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)) + const preparedInput = noteContent.replace( + new RegExp('[' + PATH_SEPARATORS + ']', 'g'), + path.sep + ) + return preparedInput.replace( + new RegExp( + STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, + 'g' + ), + path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey) + ) } return noteContent } @@ -428,8 +711,34 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) { * @param noteKey Key of the current note * @returns {String} Input without the references */ -function removeStorageAndNoteReferences (input, noteKey) { - return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER) +function removeStorageAndNoteReferences(input, noteKey) { + return input.replace( + new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|\\))', 'g'), + function(match) { + return match + .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep) + .replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.posix.sep) + .replace( + new RegExp(escapeStringRegexp(path.win32.sep), 'g'), + path.posix.sep + ) + .replace( + new RegExp(escapeStringRegexp(path.posix.sep), 'g'), + path.posix.sep + ) + .replace( + new RegExp( + STORAGE_FOLDER_PLACEHOLDER + + '(' + + escapeStringRegexp(path.sep) + + noteKey + + ')?', + 'g' + ), + DESTINATION_FOLDER + ) + } + ) } /** @@ -437,9 +746,13 @@ function removeStorageAndNoteReferences (input, noteKey) { * @param storageKey Key of the storage of the note to be deleted * @param noteKey Key of the note to be deleted */ -function deleteAttachmentFolder (storageKey, noteKey) { +function deleteAttachmentFolder(storageKey, noteKey) { const storagePath = findStorage.findStorage(storageKey) - const noteAttachmentPath = path.join(storagePath.path, DESTINATION_FOLDER, noteKey) + const noteAttachmentPath = path.join( + storagePath.path, + DESTINATION_FOLDER, + noteKey + ) sander.rimrafSync(noteAttachmentPath) } @@ -449,74 +762,227 @@ function deleteAttachmentFolder (storageKey, noteKey) { * @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder. * @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder. */ -function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) { +function deleteAttachmentsNotPresentInNote( + markdownContent, + storageKey, + noteKey +) { if (storageKey == null || noteKey == null || markdownContent == null) { return } const targetStorage = findStorage.findStorage(storageKey) - const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentFolder = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey + ) const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent) const attachmentsInNoteOnlyFileNames = [] if (attachmentsInNote) { for (let i = 0; i < attachmentsInNote.length; i++) { - attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) + attachmentsInNoteOnlyFileNames.push( + attachmentsInNote[i].replace( + new RegExp( + STORAGE_FOLDER_PLACEHOLDER + + escapeStringRegexp(path.sep) + + noteKey + + escapeStringRegexp(path.sep), + 'g' + ), + '' + ) + ) } } if (fs.existsSync(attachmentFolder)) { fs.readdir(attachmentFolder, (err, files) => { if (err) { - console.error('Error reading directory "' + attachmentFolder + '". Error:') + console.error( + 'Error reading directory "' + attachmentFolder + '". Error:' + ) console.error(err) return } files.forEach(file => { if (!attachmentsInNoteOnlyFileNames.includes(file)) { - const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) - fs.unlink(absolutePathOfFile, (err) => { + const absolutePathOfFile = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey, + file + ) + fs.unlink(absolutePathOfFile, err => { if (err) { console.error('Could not delete "%s"', absolutePathOfFile) console.error(err) return } - console.info('File "' + absolutePathOfFile + '" deleted because it was not included in the content of the note') + console.info( + 'File "' + + absolutePathOfFile + + '" deleted because it was not included in the content of the note' + ) }) } }) }) - } else { - console.info('Attachment folder ("' + attachmentFolder + '") did not exist..') } } +/** + * @description Get all existing attachments related to a specific note + including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid + * @param markdownContent markdownContent of the current note + * @param storageKey StorageKey of the current note + * @param noteKey NoteKey of the currentNote + * @return {Promise>} Promise returning the + list of attachments with their properties */ +function getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey) { + if (storageKey == null || noteKey == null || markdownContent == null) { + return null + } + let targetStorage = null + try { + targetStorage = findStorage.findStorage(storageKey) + } catch (error) { + console.warn(`No stroage found for: ${storageKey}`) + } + if (!targetStorage) { + return null + } + const attachmentFolder = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey + ) + const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent) + const attachmentsInNoteOnlyFileNames = [] + if (attachmentsInNote) { + for (let i = 0; i < attachmentsInNote.length; i++) { + attachmentsInNoteOnlyFileNames.push( + attachmentsInNote[i].replace( + new RegExp( + STORAGE_FOLDER_PLACEHOLDER + + escapeStringRegexp(path.sep) + + noteKey + + escapeStringRegexp(path.sep), + 'g' + ), + '' + ) + ) + } + } + if (fs.existsSync(attachmentFolder)) { + return new Promise((resolve, reject) => { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error( + 'Error reading directory "' + attachmentFolder + '". Error:' + ) + console.error(err) + reject(err) + return + } + const attachments = [] + for (const file of files) { + const absolutePathOfFile = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey, + file + ) + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + attachments.push({ path: absolutePathOfFile, isInUse: false }) + } else { + attachments.push({ path: absolutePathOfFile, isInUse: true }) + } + } + resolve(attachments) + }) + }) + } else { + return null + } +} + +/** + * @description Remove all specified attachment paths + * @param attachments attachment paths + * @return {Promise} Promise after all attachments are removed */ +function removeAttachmentsByPaths(attachments) { + const promises = [] + for (const attachment of attachments) { + const promise = new Promise((resolve, reject) => { + fs.unlink(attachment, err => { + if (err) { + console.error('Could not delete "%s"', attachment) + console.error(err) + reject(err) + return + } + resolve() + }) + }) + promises.push(promise) + } + return Promise.all(promises) +} + /** * Clones the attachments of a given note. * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. * @param oldNote Note that is being cloned * @param newNote Clone of the note */ -function cloneAttachments (oldNote, newNote) { +function cloneAttachments(oldNote, newNote) { if (newNote.type === 'MARKDOWN_NOTE') { const oldStorage = findStorage.findStorage(oldNote.storage) const newStorage = findStorage.findStorage(newNote.storage) - const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || [] + const attachmentsPaths = + getAbsolutePathsOfAttachmentsInContent( + oldNote.content, + oldStorage.path + ) || [] - const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key) + const destinationFolder = path.join( + newStorage.path, + DESTINATION_FOLDER, + newNote.key + ) if (!sander.existsSync(destinationFolder)) { sander.mkdirSync(destinationFolder) } for (const attachment of attachmentsPaths) { - const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment)) + const destination = path.join( + newStorage.path, + DESTINATION_FOLDER, + newNote.key, + path.basename(attachment) + ) sander.copyFileSync(attachment).to(destination) } - newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key) + newNote.content = replaceNoteKeyWithNewNoteKey( + newNote.content, + oldNote.key, + newNote.key + ) } else { - console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs') + console.debug( + 'Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs' + ) } } -function generateFileNotFoundMarkdown () { - return '**' + i18n.__('⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠') + '**' +function generateFileNotFoundMarkdown() { + return ( + '**' + + i18n.__( + '⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠' + ) + + '**' + ) } /** @@ -524,9 +990,21 @@ function generateFileNotFoundMarkdown () { * @param text Text that might contain a attachment link * @return {Boolean} Result of the test */ -function isAttachmentLink (text) { +function isAttachmentLink(text) { if (text) { - return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null + return ( + text.match( + new RegExp( + '.*\\[.*\\]\\( *' + + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + + '[' + + PATH_SEPARATORS + + ']' + + '.*\\).*', + 'gi' + ) + ) != null + ) } return false } @@ -539,38 +1017,72 @@ function isAttachmentLink (text) { * @param linkText Text that was pasted * @return {Promise} Promise returning the modified text */ -function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { +function handleAttachmentLinkPaste(storageKey, noteKey, linkText) { if (storageKey != null && noteKey != null && linkText != null) { const storagePath = findStorage.findStorage(storageKey).path const attachments = getAttachmentsInMarkdownContent(linkText) || [] const replaceInstructions = [] const copies = [] for (const attachment of attachments) { - const absPathOfAttachment = attachment.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)) + const absPathOfAttachment = attachment.replace( + new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), + path.join(storagePath, DESTINATION_FOLDER) + ) copies.push( - sander.exists(absPathOfAttachment) - .then((fileExists) => { - if (!fileExists) { - const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')')) - replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()}) - return Promise.resolve() - } - return this.copyAttachment(absPathOfAttachment, storageKey, noteKey) - .then((fileName) => { - const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')')) - replaceInstructions.push({ - regexp: replaceLinkRegExp, - replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')' - }) - return Promise.resolve() - }) + sander.exists(absPathOfAttachment).then(fileExists => { + if (!fileExists) { + const fileNotFoundRegexp = new RegExp( + '!?' + + escapeStringRegexp('[') + + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + + STORAGE_FOLDER_PLACEHOLDER + + '[\\w|\\d|\\-|' + + PATH_SEPARATORS + + ']*' + + escapeStringRegexp(path.basename(absPathOfAttachment)) + + escapeStringRegexp(')') + ) + replaceInstructions.push({ + regexp: fileNotFoundRegexp, + replacement: this.generateFileNotFoundMarkdown() + }) + return Promise.resolve() + } + return this.copyAttachment( + absPathOfAttachment, + storageKey, + noteKey + ).then(fileName => { + const replaceLinkRegExp = new RegExp( + escapeStringRegexp('(') + + ' *' + + STORAGE_FOLDER_PLACEHOLDER + + '[\\w|\\d|\\-|' + + PATH_SEPARATORS + + ']*' + + escapeStringRegexp(path.basename(absPathOfAttachment)) + + ' *' + + escapeStringRegexp(')') + ) + replaceInstructions.push({ + regexp: replaceLinkRegExp, + replacement: + '(' + + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + + ')' + }) + return Promise.resolve() }) + }) ) } return Promise.all(copies).then(() => { let modifiedLinkText = linkText for (const replaceInstruction of replaceInstructions) { - modifiedLinkText = modifiedLinkText.replace(replaceInstruction.regexp, replaceInstruction.replacement) + modifiedLinkText = modifiedLinkText.replace( + replaceInstruction.regexp, + replaceInstruction.replacement + ) } return modifiedLinkText }) @@ -584,13 +1096,16 @@ module.exports = { fixLocalURLS, generateAttachmentMarkdown, handleAttachmentDrop, - handlePastImageEvent, - handlePastNativeImage, + handlePasteImageEvent, + handlePasteNativeImage, getAttachmentsInMarkdownContent, getAbsolutePathsOfAttachmentsInContent, + importAttachments, removeStorageAndNoteReferences, + removeAttachmentsByPaths, deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, + getAttachmentsPathAndStatus, moveAttachments, cloneAttachments, isAttachmentLink, diff --git a/browser/main/lib/dataApi/copyFile.js b/browser/main/lib/dataApi/copyFile.js index 6f23aae2..0b390b2a 100755 --- a/browser/main/lib/dataApi/copyFile.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -7,7 +7,7 @@ const path = require('path') * @param {String} dstPath * @return {Promise} an image path */ -function copyFile (srcPath, dstPath) { +function copyFile(srcPath, dstPath) { if (!path.extname(dstPath)) { dstPath = path.join(dstPath, path.basename(srcPath)) } diff --git a/browser/main/lib/dataApi/createFolder.js b/browser/main/lib/dataApi/createFolder.js index 05fcea37..786d905b 100644 --- a/browser/main/lib/dataApi/createFolder.js +++ b/browser/main/lib/dataApi/createFolder.js @@ -22,7 +22,7 @@ const { findStorage } = require('browser/lib/findStorage') * } * ``` */ -function createFolder (storageKey, input) { +function createFolder(storageKey, input) { let targetStorage try { if (input == null) throw new Error('No input found.') @@ -34,26 +34,28 @@ function createFolder (storageKey, input) { return Promise.reject(e) } - return resolveStorageData(targetStorage) - .then(function createFolder (storage) { - let key = keygen() - while (storage.folders.some((folder) => folder.key === key)) { - key = keygen() - } - const newFolder = { - key, - color: input.color, - name: input.name - } + return resolveStorageData(targetStorage).then(function createFolder(storage) { + let key = keygen() + while (storage.folders.some(folder => folder.key === key)) { + key = keygen() + } + const newFolder = { + key, + color: input.color, + name: input.name + } - storage.folders.push(newFolder) + storage.folders.push(newFolder) - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) - return { - storage - } - }) + return { + storage + } + }) } module.exports = createFolder diff --git a/browser/main/lib/dataApi/createNote.js b/browser/main/lib/dataApi/createNote.js index 5bfa2457..079a988b 100644 --- a/browser/main/lib/dataApi/createNote.js +++ b/browser/main/lib/dataApi/createNote.js @@ -6,9 +6,11 @@ const path = require('path') const CSON = require('@rokt33r/season') const { findStorage } = require('browser/lib/findStorage') -function validateInput (input) { +function validateInput(input) { if (!_.isArray(input.tags)) input.tags = [] - input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0) + 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 @@ -21,20 +23,24 @@ function validateInput (input) { case 'SNIPPET_NOTE': if (!_.isString(input.description)) input.description = '' if (!_.isArray(input.snippets)) { - input.snippets = [{ - name: '', - mode: 'text', - content: '', - linesHighlighted: [] - }] + input.snippets = [ + { + name: '', + mode: 'text', + content: '', + linesHighlighted: [] + } + ] } break default: - throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.') + throw new Error( + 'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.' + ) } } -function createNote (storageKey, input) { +function createNote(storageKey, input) { let targetStorage try { if (input == null) throw new Error('No input found.') @@ -47,13 +53,13 @@ function createNote (storageKey, input) { } return resolveStorageData(targetStorage) - .then(function checkFolderExists (storage) { - if (_.find(storage.folders, {key: input.folder}) == null) { - throw new Error('Target folder doesn\'t exist.') + .then(function checkFolderExists(storage) { + if (_.find(storage.folders, { key: input.folder }) == null) { + throw new Error("Target folder doesn't exist.") } return storage }) - .then(function saveNote (storage) { + .then(function saveNote(storage) { let key = keygen(true) let isUnique = false while (!isUnique) { @@ -68,7 +74,8 @@ function createNote (storageKey, input) { } } } - const noteData = Object.assign({}, + const noteData = Object.assign( + {}, { createdAt: new Date(), updatedAt: new Date() @@ -77,9 +84,13 @@ function createNote (storageKey, input) { { key, storage: storageKey - }) + } + ) - CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage'])) + CSON.writeFileSync( + path.join(storage.path, 'notes', key + '.cson'), + _.omit(noteData, ['key', 'storage']) + ) return noteData }) diff --git a/browser/main/lib/dataApi/createNoteFromUrl.js b/browser/main/lib/dataApi/createNoteFromUrl.js new file mode 100644 index 00000000..2fd3bd9d --- /dev/null +++ b/browser/main/lib/dataApi/createNoteFromUrl.js @@ -0,0 +1,102 @@ +const http = require('http') +const https = require('https') +const { createTurndownService } = require('../../../lib/turndown') +const createNote = require('./createNote') + +import { push } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' + +function validateUrl(str) { + if ( + /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( + str + ) + ) { + return true + } else { + return false + } +} + +const ERROR_MESSAGES = { + ENOTFOUND: + 'URL not found. Please check the URL, or your internet connection and try again.', + VALIDATION_ERROR: + 'Please check if the URL follows this format: https://www.google.com', + UNEXPECTED: 'Unexpected error! Please check console for details!' +} + +function createNoteFromUrl( + url, + storage, + folder, + dispatch = null, + location = null +) { + return new Promise((resolve, reject) => { + const td = createTurndownService() + + if (!validateUrl(url)) { + reject({ result: false, error: ERROR_MESSAGES.VALIDATION_ERROR }) + } + + const request = url.startsWith('https') ? https : http + + const req = request.request(url, res => { + let data = '' + + res.on('data', chunk => { + data += chunk + }) + + res.on('end', () => { + const markdownHTML = td.turndown(data) + + if (dispatch !== null) { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }).then(note => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + dispatch( + push({ + pathname: location.pathname, + query: { key: noteHash } + }) + ) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + resolve({ result: true, error: null }) + }) + } else { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }).then(note => { + resolve({ result: true, note, error: null }) + }) + } + }) + }) + + req.on('error', e => { + console.error('error in parsing URL', e) + reject({ + result: false, + error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED + }) + }) + + req.end() + }) +} + +module.exports = createNoteFromUrl diff --git a/browser/main/lib/dataApi/createSnippet.js b/browser/main/lib/dataApi/createSnippet.js index 2e585c9f..48d6705d 100644 --- a/browser/main/lib/dataApi/createSnippet.js +++ b/browser/main/lib/dataApi/createSnippet.js @@ -3,7 +3,7 @@ import crypto from 'crypto' import consts from 'browser/lib/consts' import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet' -function createSnippet (snippetFile) { +function createSnippet(snippetFile) { return new Promise((resolve, reject) => { const newSnippet = { id: crypto.randomBytes(16).toString('hex'), @@ -12,15 +12,21 @@ function createSnippet (snippetFile) { content: '', linesHighlighted: [] } - fetchSnippet(null, snippetFile).then((snippets) => { - snippets.push(newSnippet) - fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { - if (err) reject(err) - resolve(newSnippet) + fetchSnippet(null, snippetFile) + .then(snippets => { + snippets.push(newSnippet) + fs.writeFile( + snippetFile || consts.SNIPPET_FILE, + JSON.stringify(snippets, null, 4), + err => { + if (err) reject(err) + resolve(newSnippet) + } + ) + }) + .catch(err => { + reject(err) }) - }).catch((err) => { - reject(err) - }) }) } diff --git a/browser/main/lib/dataApi/deleteFolder.js b/browser/main/lib/dataApi/deleteFolder.js index 0c7486f5..f20078c8 100644 --- a/browser/main/lib/dataApi/deleteFolder.js +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -3,7 +3,6 @@ const path = require('path') const resolveStorageData = require('./resolveStorageData') const resolveStorageNotes = require('./resolveStorageNotes') const CSON = require('@rokt33r/season') -const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') const deleteSingleNote = require('./deleteNote') @@ -19,7 +18,7 @@ const deleteSingleNote = require('./deleteNote') * } * ``` */ -function deleteFolder (storageKey, folderKey) { +function deleteFolder(storageKey, folderKey) { let targetStorage try { targetStorage = findStorage(storageKey) @@ -28,35 +27,36 @@ function deleteFolder (storageKey, folderKey) { } return resolveStorageData(targetStorage) - .then(function assignNotes (storage) { - return resolveStorageNotes(storage) - .then((notes) => { - return { - storage, - notes - } - }) + .then(function assignNotes(storage) { + return resolveStorageNotes(storage).then(notes => { + return { + storage, + notes + } + }) }) - .then(function deleteFolderAndNotes (data) { + .then(function deleteFolderAndNotes(data) { const { storage, notes } = data - storage.folders = storage.folders - .filter(function excludeTargetFolder (folder) { - return folder.key !== folderKey - }) + storage.folders = storage.folders.filter(function excludeTargetFolder( + folder + ) { + return folder.key !== folderKey + }) - const targetNotes = notes.filter(function filterTargetNotes (note) { + const targetNotes = notes.filter(function filterTargetNotes(note) { return note.folder === folderKey }) - const deleteAllNotes = targetNotes - .map(function deleteNote (note) { - return deleteSingleNote(storageKey, note.key) - }) - return Promise.all(deleteAllNotes) - .then(() => storage) + const deleteAllNotes = targetNotes.map(function deleteNote(note) { + return deleteSingleNote(storageKey, note.key) + }) + return Promise.all(deleteAllNotes).then(() => storage) }) - .then(function (storage) { - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + .then(function(storage) { + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) return { storage, diff --git a/browser/main/lib/dataApi/deleteNote.js b/browser/main/lib/dataApi/deleteNote.js index 46ec2b55..b40fdfe5 100644 --- a/browser/main/lib/dataApi/deleteNote.js +++ b/browser/main/lib/dataApi/deleteNote.js @@ -4,7 +4,7 @@ const sander = require('sander') const attachmentManagement = require('./attachmentManagement') const { findStorage } = require('browser/lib/findStorage') -function deleteNote (storageKey, noteKey) { +function deleteNote(storageKey, noteKey) { let targetStorage try { targetStorage = findStorage(storageKey) @@ -13,7 +13,7 @@ function deleteNote (storageKey, noteKey) { } return resolveStorageData(targetStorage) - .then(function deleteNoteFile (storage) { + .then(function deleteNoteFile(storage) { const notePath = path.join(storage.path, 'notes', noteKey + '.cson') try { @@ -26,8 +26,11 @@ function deleteNote (storageKey, noteKey) { storageKey } }) - .then(function deleteAttachments (storageInfo) { - attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey) + .then(function deleteAttachments(storageInfo) { + attachmentManagement.deleteAttachmentFolder( + storageInfo.storageKey, + storageInfo.noteKey + ) return storageInfo }) } diff --git a/browser/main/lib/dataApi/deleteSnippet.js b/browser/main/lib/dataApi/deleteSnippet.js index 0e446886..bd7b1223 100644 --- a/browser/main/lib/dataApi/deleteSnippet.js +++ b/browser/main/lib/dataApi/deleteSnippet.js @@ -2,14 +2,20 @@ import fs from 'fs' import consts from 'browser/lib/consts' import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet' -function deleteSnippet (snippet, snippetFile) { +function deleteSnippet(snippet, snippetFile) { return new Promise((resolve, reject) => { - fetchSnippet(null, snippetFile).then((snippets) => { - snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id) - fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { - if (err) reject(err) - resolve(snippet) - }) + fetchSnippet(null, snippetFile).then(snippets => { + snippets = snippets.filter( + currentSnippet => currentSnippet.id !== snippet.id + ) + fs.writeFile( + snippetFile || consts.SNIPPET_FILE, + JSON.stringify(snippets, null, 4), + err => { + if (err) reject(err) + resolve(snippet) + } + ) }) }) } diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js index 771f77dc..a77ba29b 100644 --- a/browser/main/lib/dataApi/exportFolder.js +++ b/browser/main/lib/dataApi/exportFolder.js @@ -22,7 +22,7 @@ import * as path from 'path' * ``` */ -function exportFolder (storageKey, folderKey, fileType, exportDir) { +function exportFolder(storageKey, folderKey, fileType, exportDir) { let targetStorage try { targetStorage = findStorage(storageKey) @@ -31,31 +31,44 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) { } return resolveStorageData(targetStorage) - .then(function assignNotes (storage) { - return resolveStorageNotes(storage) - .then((notes) => { - return { - storage, - notes - } - }) + .then(function assignNotes(storage) { + return resolveStorageNotes(storage).then(notes => { + return { + storage, + notes + } + }) }) - .then(function exportNotes (data) { + .then(function exportNotes(data) { const { storage, notes } = data - notes - .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') - .forEach(note => { - const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`) - exportNote(note.key, storage.path, note.content, notePath, null) - }) - - return { + return Promise.all( + notes + .filter( + note => + note.folder === folderKey && + note.isTrashed === false && + note.type === 'MARKDOWN_NOTE' + ) + .map(note => { + const notePath = path.join( + exportDir, + `${filenamify(note.title, { replacement: '_' })}.${fileType}` + ) + return exportNote( + note.key, + storage.path, + note.content, + notePath, + null + ) + }) + ).then(() => ({ storage, folderKey, fileType, exportDir - } + })) }) } diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index b358e548..ffd45a1c 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -19,8 +19,16 @@ const attachmentManagement = require('./attachmentManagement') * @param {function} outputFormatter * @return {Promise.<*[]>} */ -function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) { - const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path +function exportNote( + nodeKey, + storageKey, + noteContent, + targetPath, + outputFormatter +) { + const storagePath = path.isAbsolute(storageKey) + ? storageKey + : findStorage(storageKey).path const exportTasks = [] if (!storagePath) { @@ -43,22 +51,26 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt ) if (outputFormatter) { - exportedData = outputFormatter(exportedData, exportTasks) + exportedData = outputFormatter(exportedData, exportTasks, targetPath) + } else { + exportedData = Promise.resolve(exportedData) } const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) - return Promise.all(tasks.map((task) => copyFile(task.src, task.dst))) - .then(() => { - return saveToFile(exportedData, targetPath) - }).catch((err) => { - rollbackExport(tasks) - throw err - }) + return Promise.all(tasks.map(task => copyFile(task.src, task.dst))) + .then(() => exportedData) + .then(data => { + return saveToFile(data, targetPath) + }) + .catch(err => { + rollbackExport(tasks) + throw err + }) } -function prepareTasks (tasks, storagePath, targetPath) { - return tasks.map((task) => { +function prepareTasks(tasks, storagePath, targetPath) { + return tasks.map(task => { if (!path.isAbsolute(task.src)) { task.src = path.join(storagePath, task.src) } @@ -71,9 +83,9 @@ function prepareTasks (tasks, storagePath, targetPath) { }) } -function saveToFile (data, filename) { +function saveToFile(data, filename) { return new Promise((resolve, reject) => { - fs.writeFile(filename, data, (err) => { + fs.writeFile(filename, data, err => { if (err) return reject(err) resolve(filename) @@ -85,9 +97,9 @@ function saveToFile (data, filename) { * Remove exported files * @param tasks Array of copy task objects. Object consists of two mandatory fields – `src` and `dst` */ -function rollbackExport (tasks) { +function rollbackExport(tasks) { const folders = new Set() - tasks.forEach((task) => { + tasks.forEach(task => { let fullpath = task.dst if (!path.extname(task.dst)) { @@ -100,7 +112,7 @@ function rollbackExport (tasks) { } }) - folders.forEach((folder) => { + folders.forEach(folder => { if (fs.readdirSync(folder).length === 0) { fs.rmdir(folder) } diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js index ce2c4573..2a7c725c 100644 --- a/browser/main/lib/dataApi/exportStorage.js +++ b/browser/main/lib/dataApi/exportStorage.js @@ -20,7 +20,7 @@ import * as fs from 'fs' * ``` */ -function exportStorage (storageKey, fileType, exportDir) { +function exportStorage(storageKey, fileType, exportDir) { let targetStorage try { targetStorage = findStorage(storageKey) @@ -29,14 +29,17 @@ function exportStorage (storageKey, fileType, exportDir) { } return resolveStorageData(targetStorage) - .then(storage => ( - resolveStorageNotes(storage).then(notes => ({storage, notes})) - )) - .then(function exportNotes (data) { + .then(storage => + resolveStorageNotes(storage).then(notes => ({ storage, notes })) + ) + .then(function exportNotes(data) { const { storage, notes } = data const folderNamesMapping = {} storage.folders.forEach(folder => { - const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'})) + const folderExportedDir = path.join( + exportDir, + filenamify(folder.name, { replacement: '_' }) + ) folderNamesMapping[folder.key] = folderExportedDir // make sure directory exists try { @@ -47,7 +50,9 @@ function exportStorage (storageKey, fileType, exportDir) { .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE') .forEach(markdownNote => { const folderExportedDir = folderNamesMapping[markdownNote.folder] - const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}` + const snippetName = `${filenamify(markdownNote.title, { + replacement: '_' + })}.${fileType}` const notePath = path.join(folderExportedDir, snippetName) fs.writeFileSync(notePath, markdownNote.content) }) diff --git a/browser/main/lib/dataApi/fetchSnippet.js b/browser/main/lib/dataApi/fetchSnippet.js index 456a5090..8344eb5d 100644 --- a/browser/main/lib/dataApi/fetchSnippet.js +++ b/browser/main/lib/dataApi/fetchSnippet.js @@ -1,7 +1,7 @@ import fs from 'fs' import consts from 'browser/lib/consts' -function fetchSnippet (id, snippetFile) { +function fetchSnippet(id, snippetFile) { return new Promise((resolve, reject) => { fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => { if (err) { @@ -9,7 +9,9 @@ function fetchSnippet (id, snippetFile) { } const snippets = JSON.parse(data) if (id) { - const snippet = snippets.find(snippet => { return snippet.id === id }) + const snippet = snippets.find(snippet => { + return snippet.id === id + }) resolve(snippet) } resolve(snippets) diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 92be6b93..6e88bbf9 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -11,6 +11,7 @@ const dataApi = { exportFolder: require('./exportFolder'), exportStorage: require('./exportStorage'), createNote: require('./createNote'), + createNoteFromUrl: require('./createNoteFromUrl'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), moveNote: require('./moveNote'), diff --git a/browser/main/lib/dataApi/init.js b/browser/main/lib/dataApi/init.js index 7f81e90b..0ad35625 100644 --- a/browser/main/lib/dataApi/init.js +++ b/browser/main/lib/dataApi/init.js @@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData') const resolveStorageNotes = require('./resolveStorageNotes') const consts = require('browser/lib/consts') const path = require('path') +const fs = require('fs') const CSON = require('@rokt33r/season') /** * @return {Object} all storages and notes @@ -19,50 +20,64 @@ const CSON = require('@rokt33r/season') * 2. legacy * 3. empty directory */ -function init () { - const fetchStorages = function () { + +function init() { + const fetchStorages = function() { let rawStorages try { rawStorages = JSON.parse(window.localStorage.getItem('storages')) + // Remove storages who's location is inaccesible. + rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path)) if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.') } catch (e) { console.warn('Failed to parse cached data from localStorage', e) rawStorages = [] window.localStorage.setItem('storages', JSON.stringify(rawStorages)) } - return Promise.all(rawStorages - .map(resolveStorageData)) + return Promise.all(rawStorages.map(resolveStorageData)) } - const fetchNotes = function (storages) { + const fetchNotes = function(storages) { const findNotesFromEachStorage = storages - .map((storage) => { - return resolveStorageNotes(storage) - .then((notes) => { - let unknownCount = 0 - notes.forEach((note) => { - if (note && !storage.folders.some((folder) => note.folder === folder.key)) { - unknownCount++ - storage.folders.push({ - key: note.folder, - color: consts.FOLDER_COLORS[(unknownCount - 1) % 7], - name: 'Unknown ' + unknownCount - }) - } - }) - if (unknownCount > 0) { - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + .filter(storage => fs.existsSync(storage.path)) + .map(storage => { + return resolveStorageNotes(storage).then(notes => { + let unknownCount = 0 + notes.forEach(note => { + if ( + note && + !storage.folders.some(folder => note.folder === folder.key) + ) { + unknownCount++ + storage.folders.push({ + key: note.folder, + color: consts.FOLDER_COLORS[(unknownCount - 1) % 7], + name: 'Unknown ' + unknownCount + }) } - return notes }) + if (unknownCount > 0) { + try { + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) + } catch (e) { + console.log( + 'Error writting boostnote.json: ' + e + ' from init.js' + ) + } + } + return notes + }) }) return Promise.all(findNotesFromEachStorage) - .then(function concatNoteGroup (noteGroups) { - return noteGroups.reduce(function (sum, group) { + .then(function concatNoteGroup(noteGroups) { + return noteGroups.reduce(function(sum, group) { return sum.concat(group) }, []) }) - .then(function returnData (notes) { + .then(function returnData(notes) { return { storages, notes @@ -71,12 +86,11 @@ function init () { } return Promise.resolve(fetchStorages()) - .then((storages) => { - return storages - .filter((storage) => { - if (!_.isObject(storage)) return false - return true - }) + .then(storages => { + return storages.filter(storage => { + if (!_.isObject(storage)) return false + return true + }) }) .then(fetchNotes) } diff --git a/browser/main/lib/dataApi/migrateFromV5Storage.js b/browser/main/lib/dataApi/migrateFromV5Storage.js index 78d78746..abf57831 100644 --- a/browser/main/lib/dataApi/migrateFromV5Storage.js +++ b/browser/main/lib/dataApi/migrateFromV5Storage.js @@ -6,102 +6,111 @@ const CSON = require('@rokt33r/season') const path = require('path') const sander = require('sander') -function migrateFromV5Storage (storageKey, data) { +function migrateFromV5Storage(storageKey, data) { let targetStorage try { const cachedStorageList = JSON.parse(localStorage.getItem('storages')) - if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.') + 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 = _.find(cachedStorageList, { key: storageKey }) + if (targetStorage == null) throw new Error("Target storage doesn't exist.") } catch (e) { return Promise.reject(e) } - return resolveStorageData(targetStorage) - .then(function (storage) { - return importAll(storage, data) - }) + return resolveStorageData(targetStorage).then(function(storage) { + return importAll(storage, data) + }) } -function importAll (storage, data) { +function importAll(storage, data) { const oldArticles = data.articles const notes = [] - data.folders - .forEach(function (oldFolder) { - let folderKey = keygen() - while (storage.folders.some((folder) => folder.key === folderKey)) { - folderKey = keygen() - } - const newFolder = { - key: folderKey, - name: oldFolder.name, - color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] - } + data.folders.forEach(function(oldFolder) { + let folderKey = keygen() + while (storage.folders.some(folder => folder.key === folderKey)) { + folderKey = keygen() + } + const newFolder = { + key: folderKey, + name: oldFolder.name, + color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] + } - storage.folders.push(newFolder) + storage.folders.push(newFolder) - const articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key) - articles.forEach((article) => { - let noteKey = keygen() - let isUnique = false - while (!isUnique) { - try { - sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson')) - noteKey = keygen() - } catch (err) { - if (err.code === 'ENOENT') { - isUnique = true - } else { - console.error('Failed to read `notes` directory.') - throw err - } + const articles = oldArticles.filter( + article => article.FolderKey === oldFolder.key + ) + articles.forEach(article => { + let noteKey = keygen() + let isUnique = false + while (!isUnique) { + try { + sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson')) + noteKey = keygen() + } catch (err) { + if (err.code === 'ENOENT') { + isUnique = true + } else { + console.error('Failed to read `notes` directory.') + throw err } } + } - if (article.mode === 'markdown') { - const newNote = { - tags: article.tags, - createdAt: article.createdAt, - updatedAt: article.updatedAt, - folder: folderKey, - storage: storage.key, - type: 'MARKDOWN_NOTE', - isStarred: false, - title: article.title, - content: '# ' + article.title + '\n\n' + article.content, - key: noteKey, - linesHighlighted: article.linesHighlighted - } - notes.push(newNote) - } else { - const newNote = { - tags: article.tags, - createdAt: article.createdAt, - updatedAt: article.updatedAt, - folder: folderKey, - storage: storage.key, - type: 'SNIPPET_NOTE', - isStarred: false, - title: article.title, - description: article.title, - key: noteKey, - snippets: [{ + if (article.mode === 'markdown') { + const newNote = { + tags: article.tags, + createdAt: article.createdAt, + updatedAt: article.updatedAt, + folder: folderKey, + storage: storage.key, + type: 'MARKDOWN_NOTE', + isStarred: false, + title: article.title, + content: '# ' + article.title + '\n\n' + article.content, + key: noteKey, + linesHighlighted: article.linesHighlighted + } + notes.push(newNote) + } else { + const newNote = { + tags: article.tags, + createdAt: article.createdAt, + updatedAt: article.updatedAt, + folder: folderKey, + storage: storage.key, + type: 'SNIPPET_NOTE', + isStarred: false, + title: article.title, + description: article.title, + key: noteKey, + snippets: [ + { name: article.mode, mode: article.mode, content: article.content, linesHighlighted: article.linesHighlighted - }] - } - notes.push(newNote) + } + ] } - }) + notes.push(newNote) + } }) - - notes.forEach(function (note) { - CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key'])) }) - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders'])) + notes.forEach(function(note) { + CSON.writeFileSync( + path.join(storage.path, 'notes', note.key + '.cson'), + _.omit(note, ['storage', 'key']) + ) + }) + + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['version', 'folders']) + ) return { storage, diff --git a/browser/main/lib/dataApi/migrateFromV6Storage.js b/browser/main/lib/dataApi/migrateFromV6Storage.js index af4d902f..8255a0de 100644 --- a/browser/main/lib/dataApi/migrateFromV6Storage.js +++ b/browser/main/lib/dataApi/migrateFromV6Storage.js @@ -4,86 +4,91 @@ const keygen = require('browser/lib/keygen') const _ = require('lodash') const CSON = require('@rokt33r/season') -function migrateFromV5Storage (storagePath) { +function migrateFromV5Storage(storagePath) { var boostnoteJSONPath = path.join(storagePath, 'boostnote.json') return Promise.resolve() - .then(function readBoostnoteJSON () { + .then(function readBoostnoteJSON() { return sander.readFile(boostnoteJSONPath, { encoding: 'utf-8' }) }) - .then(function verifyVersion (rawData) { + .then(function verifyVersion(rawData) { var boostnoteJSONData = JSON.parse(rawData) - if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.') - if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.') + if (boostnoteJSONData.version === '1.0') + throw new Error('Target storage seems to be transformed already.') + if (!_.isArray(boostnoteJSONData.folders)) + throw new Error('the value of folders is not an array.') return boostnoteJSONData }) - .then(function setVersion (boostnoteJSONData) { + .then(function setVersion(boostnoteJSONData) { boostnoteJSONData.version = '1.0' - return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData)) + return sander + .writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData)) .then(() => boostnoteJSONData) }) - .then(function fetchNotes (boostnoteJSONData) { - var fetchNotesFromEachFolder = boostnoteJSONData.folders - .map(function (folder) { - const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json') - return sander - .readFile(folderDataJSONPath, { - encoding: 'utf-8' + .then(function fetchNotes(boostnoteJSONData) { + var fetchNotesFromEachFolder = boostnoteJSONData.folders.map(function( + folder + ) { + const folderDataJSONPath = path.join( + storagePath, + folder.key, + 'data.json' + ) + return sander + .readFile(folderDataJSONPath, { + encoding: 'utf-8' + }) + .then(function(rawData) { + var data = JSON.parse(rawData) + if (!_.isArray(data.notes)) + throw new Error('value of notes is not an array.') + return data.notes.map(function setFolderToNote(note) { + note.folder = folder.key + return note }) - .then(function (rawData) { - var data = JSON.parse(rawData) - if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.') - return data.notes - .map(function setFolderToNote (note) { - note.folder = folder.key - return note - }) - }) - .catch(function failedToReadDataJSON (err) { - console.warn('Failed to fetch notes from ', folderDataJSONPath, err) - return [] - }) - }) + }) + .catch(function failedToReadDataJSON(err) { + console.warn('Failed to fetch notes from ', folderDataJSONPath, err) + return [] + }) + }) return Promise.all(fetchNotesFromEachFolder) - .then(function flatten (folderNotes) { - return folderNotes - .reduce(function concatNotes (sum, notes) { - return sum.concat(notes) - }, []) + .then(function flatten(folderNotes) { + return folderNotes.reduce(function concatNotes(sum, notes) { + return sum.concat(notes) + }, []) }) - .then(function saveNotes (notes) { - notes.forEach(function renewKey (note) { + .then(function saveNotes(notes) { + notes.forEach(function renewKey(note) { var newKey = keygen() - while (notes.some((_note) => _note.key === newKey)) { + while (notes.some(_note => _note.key === newKey)) { newKey = keygen() } note.key = newKey }) const noteDirPath = path.join(storagePath, 'notes') - notes - .map(function saveNote (note) { - CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note) - }) + notes.map(function saveNote(note) { + CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note) + }) return true }) - .then(function deleteFolderDir (check) { + .then(function deleteFolderDir(check) { if (check) { - boostnoteJSONData.folders.forEach((folder) => { + boostnoteJSONData.folders.forEach(folder => { sander.rimrafSync(path.join(storagePath, folder.key)) }) } return check }) }) - .catch(function handleError (err) { + .catch(function handleError(err) { console.warn(err) return false }) } module.exports = migrateFromV5Storage - diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index 2d306cdf..b72db7aa 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -1,97 +1,110 @@ const resolveStorageData = require('./resolveStorageData') const _ = require('lodash') const path = require('path') -const fs = require('fs') const CSON = require('@rokt33r/season') const keygen = require('browser/lib/keygen') const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') const attachmentManagement = require('./attachmentManagement') -function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { +function moveNote(storageKey, noteKey, newStorageKey, newFolderKey) { let oldStorage, newStorage try { oldStorage = findStorage(storageKey) newStorage = findStorage(newStorageKey) - if (newStorage == null) throw new Error('Target storage doesn\'t exist.') + if (newStorage == null) throw new Error("Target storage doesn't exist.") } catch (e) { return Promise.reject(e) } - return resolveStorageData(oldStorage) - .then(function saveNote (_oldStorage) { - oldStorage = _oldStorage - let noteData - const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson') - try { - noteData = CSON.readFileSync(notePath) - } catch (err) { - console.warn('Failed to find note cson', err) - throw err - } - let newNoteKey - return Promise.resolve() - .then(function resolveNewStorage () { - if (storageKey === newStorageKey) { - newNoteKey = noteKey - return oldStorage - } - return resolveStorageData(newStorage) - .then(function findNewNoteKey (_newStorage) { - newStorage = _newStorage - newNoteKey = keygen(true) - let isUnique = false - while (!isUnique) { - try { - sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson')) - newNoteKey = keygen(true) - } catch (err) { - if (err.code === 'ENOENT') { - isUnique = true - } else { - throw err - } - } - } - - return newStorage - }) - }) - .then(function checkFolderExistsAndPrepareNoteData (newStorage) { - if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.') - - noteData.folder = newFolderKey - noteData.key = newNoteKey - noteData.storage = newStorageKey - noteData.updatedAt = new Date() - noteData.oldContent = noteData.content - - return noteData - }) - .then(function moveAttachments (noteData) { - if (oldStorage.path === newStorage.path) { - return noteData - } - - noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content) - return noteData - }) - .then(function writeAndReturn (noteData) { - CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent'])) - return noteData - }) - .then(function deleteOldNote (data) { - if (storageKey !== newStorageKey) { + return resolveStorageData(oldStorage).then(function saveNote(_oldStorage) { + oldStorage = _oldStorage + let noteData + const notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson') + try { + noteData = CSON.readFileSync(notePath) + } catch (err) { + console.warn('Failed to find note cson', err) + throw err + } + let newNoteKey + return Promise.resolve() + .then(function resolveNewStorage() { + if (storageKey === newStorageKey) { + newNoteKey = noteKey + return oldStorage + } + return resolveStorageData(newStorage).then(function findNewNoteKey( + _newStorage + ) { + newStorage = _newStorage + newNoteKey = keygen(true) + let isUnique = false + while (!isUnique) { try { - sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson')) + sander.statSync( + path.join(newStorage.path, 'notes', newNoteKey + '.cson') + ) + newNoteKey = keygen(true) } catch (err) { - console.warn(err) + if (err.code === 'ENOENT') { + isUnique = true + } else { + throw err + } } } - return data + return newStorage }) - }) + }) + .then(function checkFolderExistsAndPrepareNoteData(newStorage) { + if (_.find(newStorage.folders, { key: newFolderKey }) == null) + throw new Error("Target folder doesn't exist.") + + noteData.folder = newFolderKey + noteData.key = newNoteKey + noteData.storage = newStorageKey + noteData.updatedAt = new Date() + noteData.oldContent = noteData.content + + return noteData + }) + .then(function moveAttachments(noteData) { + if (oldStorage.path === newStorage.path) { + return noteData + } + + noteData.content = attachmentManagement.moveAttachments( + oldStorage.path, + newStorage.path, + noteKey, + newNoteKey, + noteData.content + ) + return noteData + }) + .then(function writeAndReturn(noteData) { + CSON.writeFileSync( + path.join(newStorage.path, 'notes', noteData.key + '.cson'), + _.omit(noteData, ['key', 'storage', 'oldContent']) + ) + return noteData + }) + .then(function deleteOldNote(data) { + if (storageKey !== newStorageKey) { + try { + sander.unlinkSync( + path.join(oldStorage.path, 'notes', noteKey + '.cson') + ) + } catch (err) { + console.warn(err) + } + } + + return data + }) + }) } module.exports = moveNote diff --git a/browser/main/lib/dataApi/removeStorage.js b/browser/main/lib/dataApi/removeStorage.js index c50bbd12..3f957ab2 100644 --- a/browser/main/lib/dataApi/removeStorage.js +++ b/browser/main/lib/dataApi/removeStorage.js @@ -4,7 +4,7 @@ const _ = require('lodash') * @param {String} key * @return {key} */ -function removeStorage (key) { +function removeStorage(key) { let rawStorages try { @@ -15,10 +15,9 @@ function removeStorage (key) { rawStorages = [] } - rawStorages = rawStorages - .filter(function excludeTargetStorage (rawStorage) { - return rawStorage.key !== key - }) + rawStorages = rawStorages.filter(function excludeTargetStorage(rawStorage) { + return rawStorage.key !== key + }) localStorage.setItem('storages', JSON.stringify(rawStorages)) diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js index 3b806d1c..165a5ab3 100644 --- a/browser/main/lib/dataApi/renameStorage.js +++ b/browser/main/lib/dataApi/renameStorage.js @@ -6,8 +6,9 @@ const resolveStorageData = require('./resolveStorageData') * @param {String} name * @return {Object} Storage meta data */ -function renameStorage (key, name) { - if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.')) +function renameStorage(key, name) { + if (!_.isString(name)) + return Promise.reject(new Error('Name must be a string.')) let cachedStorageList try { @@ -17,7 +18,7 @@ function renameStorage (key, name) { console.error(err) return Promise.reject(err) } - const targetStorage = _.find(cachedStorageList, {key: key}) + const targetStorage = _.find(cachedStorageList, { key: key }) if (targetStorage == null) return Promise.reject('Storage') targetStorage.name = name diff --git a/browser/main/lib/dataApi/reorderFolder.js b/browser/main/lib/dataApi/reorderFolder.js index 9102438e..e86ac838 100644 --- a/browser/main/lib/dataApi/reorderFolder.js +++ b/browser/main/lib/dataApi/reorderFolder.js @@ -17,7 +17,7 @@ const { findStorage } = require('browser/lib/findStorage') * } * ``` */ -function reorderFolder (storageKey, oldIndex, newIndex) { +function reorderFolder(storageKey, oldIndex, newIndex) { let targetStorage try { if (!_.isNumber(oldIndex)) throw new Error('oldIndex must be a number.') @@ -28,15 +28,19 @@ function reorderFolder (storageKey, oldIndex, newIndex) { return Promise.reject(e) } - return resolveStorageData(targetStorage) - .then(function reorderFolder (storage) { - storage.folders = _.move(storage.folders, oldIndex, newIndex) - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + return resolveStorageData(targetStorage).then(function reorderFolder( + storage + ) { + storage.folders = _.move(storage.folders, oldIndex, newIndex) + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) - return { - storage - } - }) + return { + storage + } + }) } module.exports = reorderFolder diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js index da41f3d0..3476ec7d 100644 --- a/browser/main/lib/dataApi/resolveStorageData.js +++ b/browser/main/lib/dataApi/resolveStorageData.js @@ -3,7 +3,7 @@ const path = require('path') const CSON = require('@rokt33r/season') const migrateFromV6Storage = require('./migrateFromV6Storage') -function resolveStorageData (storageCache) { +function resolveStorageData(storageCache) { const storage = { key: storageCache.key, name: storageCache.name, @@ -15,13 +15,14 @@ function resolveStorageData (storageCache) { const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json') try { const jsonData = CSON.readFileSync(boostnoteJSONPath) - if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.') + if (!_.isArray(jsonData.folders)) + throw new Error('folders should be an array.') storage.folders = jsonData.folders storage.version = jsonData.version } catch (err) { if (err.code === 'ENOENT') { - console.warn('boostnote.json file doesn\'t exist the given path') - CSON.writeFileSync(boostnoteJSONPath, {folders: [], version: '1.0'}) + console.warn("boostnote.json file doesn't exist the given path") + CSON.writeFileSync(boostnoteJSONPath, { folders: [], version: '1.0' }) } else { console.error(err) } @@ -34,8 +35,7 @@ function resolveStorageData (storageCache) { return Promise.resolve(storage) } - return migrateFromV6Storage(storage.path) - .then(() => storage) + return migrateFromV6Storage(storage.path).then(() => storage) } module.exports = resolveStorageData diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js index 9da27248..e86ecc26 100644 --- a/browser/main/lib/dataApi/resolveStorageNotes.js +++ b/browser/main/lib/dataApi/resolveStorageNotes.js @@ -2,14 +2,14 @@ const sander = require('sander') const path = require('path') const CSON = require('@rokt33r/season') -function resolveStorageNotes (storage) { +function resolveStorageNotes(storage) { const notesDirPath = path.join(storage.path, 'notes') let notePathList try { notePathList = sander.readdirSync(notesDirPath) } catch (err) { if (err.code === 'ENOENT') { - console.error(notesDirPath, ' doesn\'t exist.') + console.error(notesDirPath, " doesn't exist.") sander.mkdirSync(notesDirPath) } else { console.warn('Failed to find note dir', notesDirPath, err) @@ -17,10 +17,10 @@ function resolveStorageNotes (storage) { notePathList = [] } const notes = notePathList - .filter(function filterOnlyCSONFile (notePath) { + .filter(function filterOnlyCSONFile(notePath) { return /\.cson$/.test(notePath) }) - .map(function parseCSONFile (notePath) { + .map(function parseCSONFile(notePath) { try { const data = CSON.readFileSync(path.join(notesDirPath, notePath)) data.key = path.basename(notePath, '.cson') @@ -30,7 +30,7 @@ function resolveStorageNotes (storage) { console.error(`error on note path: ${notePath}, error: ${err}`) } }) - .filter(function filterOnlyNoteObject (noteObj) { + .filter(function filterOnlyNoteObject(noteObj) { return typeof noteObj === 'object' }) diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js index 246d85ef..013c15d4 100644 --- a/browser/main/lib/dataApi/toggleStorage.js +++ b/browser/main/lib/dataApi/toggleStorage.js @@ -6,7 +6,7 @@ const resolveStorageData = require('./resolveStorageData') * @param {Boolean} isOpen * @return {Object} Storage meta data */ -function toggleStorage (key, isOpen) { +function toggleStorage(key, isOpen) { let cachedStorageList try { cachedStorageList = JSON.parse(localStorage.getItem('storages')) @@ -15,7 +15,7 @@ function toggleStorage (key, isOpen) { console.error(err) return Promise.reject(err) } - const targetStorage = _.find(cachedStorageList, {key: key}) + const targetStorage = _.find(cachedStorageList, { key: key }) if (targetStorage == null) return Promise.reject('Storage') targetStorage.isOpen = isOpen diff --git a/browser/main/lib/dataApi/updateFolder.js b/browser/main/lib/dataApi/updateFolder.js index 2a325c60..84658ffc 100644 --- a/browser/main/lib/dataApi/updateFolder.js +++ b/browser/main/lib/dataApi/updateFolder.js @@ -22,7 +22,7 @@ const { findStorage } = require('browser/lib/findStorage') * } * ``` */ -function updateFolder (storageKey, folderKey, input) { +function updateFolder(storageKey, folderKey, input) { let targetStorage try { if (input == null) throw new Error('No input found.') @@ -34,19 +34,21 @@ function updateFolder (storageKey, folderKey, input) { return Promise.reject(e) } - return resolveStorageData(targetStorage) - .then(function updateFolder (storage) { - const targetFolder = _.find(storage.folders, {key: folderKey}) - if (targetFolder == null) throw new Error('Target folder doesn\'t exist.') - targetFolder.name = input.name - targetFolder.color = input.color + return resolveStorageData(targetStorage).then(function updateFolder(storage) { + const targetFolder = _.find(storage.folders, { key: folderKey }) + if (targetFolder == null) throw new Error("Target folder doesn't exist.") + targetFolder.name = input.name + targetFolder.color = input.color - CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version'])) + CSON.writeFileSync( + path.join(storage.path, 'boostnote.json'), + _.pick(storage, ['folders', 'version']) + ) - return { - storage - } - }) + return { + storage + } + }) } module.exports = updateFolder diff --git a/browser/main/lib/dataApi/updateNote.js b/browser/main/lib/dataApi/updateNote.js index ce9fabcf..775888a6 100644 --- a/browser/main/lib/dataApi/updateNote.js +++ b/browser/main/lib/dataApi/updateNote.js @@ -4,13 +4,14 @@ const path = require('path') const CSON = require('@rokt33r/season') const { findStorage } = require('browser/lib/findStorage') -function validateInput (input) { +function validateInput(input) { const validatedInput = {} if (input.tags != null) { if (!_.isArray(input.tags)) validatedInput.tags = [] - validatedInput.tags = input.tags - .filter((tag) => _.isString(tag) && tag.trim().length > 0) + validatedInput.tags = input.tags.filter( + tag => _.isString(tag) && tag.trim().length > 0 + ) } if (input.title != null) { @@ -40,7 +41,8 @@ function validateInput (input) { if (!_.isString(input.content)) validatedInput.content = '' else validatedInput.content = input.content - if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = [] + if (!_.isArray(input.linesHighlighted)) + validatedInput.linesHighlighted = [] else validatedInput.linesHighlighted = input.linesHighlighted } return validatedInput @@ -51,30 +53,33 @@ function validateInput (input) { } if (input.snippets != null) { if (!_.isArray(input.snippets)) { - validatedInput.snippets = [{ - name: '', - mode: 'text', - content: '', - linesHighlighted: [] - }] + validatedInput.snippets = [ + { + name: '', + mode: 'text', + content: '', + linesHighlighted: [] + } + ] } else { validatedInput.snippets = input.snippets } - validatedInput.snippets - .filter((snippet) => { - if (!_.isString(snippet.name)) return false - if (!_.isString(snippet.mode)) return false - if (!_.isString(snippet.content)) return false - return true - }) + validatedInput.snippets.filter(snippet => { + if (!_.isString(snippet.name)) return false + if (!_.isString(snippet.mode)) return false + if (!_.isString(snippet.content)) return false + return true + }) } return validatedInput default: - throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.') + throw new Error( + 'Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.' + ) } } -function updateNote (storageKey, noteKey, input) { +function updateNote(storageKey, noteKey, input) { let targetStorage try { if (input == null) throw new Error('No input found.') @@ -85,55 +90,61 @@ function updateNote (storageKey, noteKey, input) { return Promise.reject(e) } - return resolveStorageData(targetStorage) - .then(function saveNote (storage) { - let noteData - const notePath = path.join(storage.path, 'notes', noteKey + '.cson') - try { - noteData = CSON.readFileSync(notePath) - } catch (err) { - console.warn('Failed to find note cson', err) - noteData = input.type === 'SNIPPET_NOTE' + return resolveStorageData(targetStorage).then(function saveNote(storage) { + let noteData + const notePath = path.join(storage.path, 'notes', noteKey + '.cson') + try { + noteData = CSON.readFileSync(notePath) + } catch (err) { + console.warn('Failed to find note cson', err) + noteData = + input.type === 'SNIPPET_NOTE' ? { - type: 'SNIPPET_NOTE', - description: [], - snippets: [{ - name: '', - mode: 'text', + type: 'SNIPPET_NOTE', + description: [], + snippets: [ + { + name: '', + mode: 'text', + content: '', + linesHighlighted: [] + } + ] + } + : { + type: 'MARKDOWN_NOTE', content: '', linesHighlighted: [] - }] - } - : { - type: 'MARKDOWN_NOTE', - content: '', - linesHighlighted: [] - } - noteData.title = '' - if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.') - noteData.folder = storage.folders[0].key - noteData.createdAt = new Date() - noteData.updatedAt = new Date() - noteData.isStarred = false - noteData.isTrashed = false - noteData.tags = [] - noteData.isPinned = false - } + } + noteData.title = '' + if (storage.folders.length === 0) + throw new Error('Failed to restore note: No folder exists.') + noteData.folder = storage.folders[0].key + noteData.createdAt = new Date() + noteData.updatedAt = new Date() + noteData.isStarred = false + noteData.isTrashed = false + noteData.tags = [] + noteData.isPinned = false + } - if (noteData.type === 'SNIPPET_NOTE') { - noteData.title - } + if (noteData.type === 'SNIPPET_NOTE') { + noteData.title + } - Object.assign(noteData, input, { - key: noteKey, - updatedAt: new Date(), - storage: storageKey - }) - - CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage'])) - - return noteData + Object.assign(noteData, input, { + key: noteKey, + updatedAt: new Date(), + storage: storageKey }) + + CSON.writeFileSync( + path.join(storage.path, 'notes', noteKey + '.cson'), + _.omit(noteData, ['key', 'storage']) + ) + + return noteData + }) } module.exports = updateNote diff --git a/browser/main/lib/dataApi/updateSnippet.js b/browser/main/lib/dataApi/updateSnippet.js index f132d83f..95acf053 100644 --- a/browser/main/lib/dataApi/updateSnippet.js +++ b/browser/main/lib/dataApi/updateSnippet.js @@ -1,9 +1,11 @@ import fs from 'fs' import consts from 'browser/lib/consts' -function updateSnippet (snippet, snippetFile) { +function updateSnippet(snippet, snippetFile) { return new Promise((resolve, reject) => { - const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8')) + const snippets = JSON.parse( + fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8') + ) for (let i = 0; i < snippets.length; i++) { const currentSnippet = snippets[i] @@ -21,11 +23,15 @@ function updateSnippet (snippet, snippetFile) { currentSnippet.name = snippet.name currentSnippet.prefix = snippet.prefix currentSnippet.content = snippet.content - currentSnippet.linesHighlighted = (snippet.linesHighlighted) - fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { - if (err) reject(err) - resolve(snippets) - }) + currentSnippet.linesHighlighted = snippet.linesHighlighted + fs.writeFile( + snippetFile || consts.SNIPPET_FILE, + JSON.stringify(snippets, null, 4), + err => { + if (err) reject(err) + resolve(snippets) + } + ) } } } diff --git a/browser/main/lib/eventEmitter.js b/browser/main/lib/eventEmitter.js index 1276545b..370ea3a9 100644 --- a/browser/main/lib/eventEmitter.js +++ b/browser/main/lib/eventEmitter.js @@ -1,19 +1,19 @@ const electron = require('electron') const { ipcRenderer, remote } = electron -function on (name, listener) { +function on(name, listener) { ipcRenderer.on(name, listener) } -function off (name, listener) { +function off(name, listener) { ipcRenderer.removeListener(name, listener) } -function once (name, listener) { +function once(name, listener) { ipcRenderer.once(name, listener) } -function emit (name, ...args) { +function emit(name, ...args) { remote.getCurrentWindow().webContents.send(name, ...args) } diff --git a/browser/main/lib/ipcClient.js b/browser/main/lib/ipcClient.js index c06296b5..4c25d52c 100644 --- a/browser/main/lib/ipcClient.js +++ b/browser/main/lib/ipcClient.js @@ -12,14 +12,14 @@ nodeIpc.config.silent = true nodeIpc.connectTo( 'node', path.join(app.getPath('userData'), 'boostnote.service'), - function () { - nodeIpc.of.node.on('error', function (err) { + function() { + nodeIpc.of.node.on('error', function(err) { console.error(err) }) - nodeIpc.of.node.on('connect', function () { - ipcRenderer.send('config-renew', {config: ConfigManager.get()}) + nodeIpc.of.node.on('connect', function() { + ipcRenderer.send('config-renew', { config: ConfigManager.get() }) }) - nodeIpc.of.node.on('disconnect', function () { + nodeIpc.of.node.on('disconnect', function() { return }) } diff --git a/browser/main/lib/modal.js b/browser/main/lib/modal.js index 7a7a9c8c..08d8c7f1 100644 --- a/browser/main/lib/modal.js +++ b/browser/main/lib/modal.js @@ -1,10 +1,10 @@ import React from 'react' import { Provider } from 'react-redux' import ReactDOM from 'react-dom' -import store from '../store' +import { store } from '../store' class ModalBase extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { component: null, @@ -13,20 +13,30 @@ class ModalBase extends React.Component { } } - close () { - if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true}) + close() { + if (modalBase != null) + modalBase.setState({ + component: null, + componentProps: null, + isHidden: true + }) // Toggle overflow style on NoteList - const list = document.querySelector('.NoteList__list___browser-main-NoteList-') + const list = document.querySelector( + '.NoteList__list___browser-main-NoteList-' + ) list.style.overflow = 'auto' } - render () { + render() { return (
-
this.close(e)} className='modalBack' /> +
this.close(e)} className='modalBack' /> {this.state.component == null ? null : ( - + )}
@@ -38,21 +48,31 @@ const el = document.createElement('div') document.body.appendChild(el) const modalBase = ReactDOM.render(, el) -export function openModal (component, props) { - if (modalBase == null) { return } +export function openModal(component, props) { + if (modalBase == null) { + return + } // Hide scrollbar by removing overflow when modal opens - const list = document.querySelector('.NoteList__list___browser-main-NoteList-') + const list = document.querySelector( + '.NoteList__list___browser-main-NoteList-' + ) list.style.overflow = 'hidden' document.body.setAttribute('data-modal', 'open') - modalBase.setState({component: component, componentProps: props, isHidden: false}) + modalBase.setState({ + component: component, + componentProps: props, + isHidden: false + }) } -export function closeModal () { - if (modalBase == null) { return } +export function closeModal() { + if (modalBase == null) { + return + } modalBase.close() } -export function isModalOpen () { +export function isModalOpen() { return !modalBase.state.isHidden } diff --git a/browser/main/lib/notify.js b/browser/main/lib/notify.js index 458a784d..6054cb36 100644 --- a/browser/main/lib/notify.js +++ b/browser/main/lib/notify.js @@ -1,8 +1,12 @@ const path = require('path') -function notify (title, options) { +function notify(title, options) { if (process.platform === 'win32') { - options.icon = path.join('file://', global.__dirname, '../../resources/app.png') + options.icon = path.join( + 'file://', + global.__dirname, + '../../resources/app.png' + ) options.silent = false } return new window.Notification(title, options) diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js index 93e33c9b..aab644d7 100644 --- a/browser/main/lib/shortcut.js +++ b/browser/main/lib/shortcut.js @@ -1,10 +1,16 @@ import ee from 'browser/main/lib/eventEmitter' module.exports = { - 'toggleMode': () => { + toggleMode: () => { ee.emit('topbar:togglemodebutton') }, - 'deleteNote': () => { + toggleDirection: () => { + ee.emit('topbar:toggledirectionbutton') + }, + deleteNote: () => { ee.emit('hotkey:deletenote') + }, + toggleMenuBar: () => { + ee.emit('menubar:togglemenubar') } } diff --git a/browser/main/lib/shortcutManager.js b/browser/main/lib/shortcutManager.js index ac2a3a08..7575bb99 100644 --- a/browser/main/lib/shortcutManager.js +++ b/browser/main/lib/shortcutManager.js @@ -7,7 +7,7 @@ import functions from './shortcut' let shortcuts = CM.get().hotkey -ee.on('config-renew', function () { +ee.on('config-renew', function() { // only update if hotkey changed ! const newHotkey = CM.get().hotkey if (!isObjectEqual(newHotkey, shortcuts)) { @@ -15,17 +15,17 @@ ee.on('config-renew', function () { } }) -function updateShortcut (newHotkey) { +function updateShortcut(newHotkey) { Mousetrap.reset() shortcuts = newHotkey applyShortcuts(newHotkey) } -function formatShortcut (shortcut) { +function formatShortcut(shortcut) { return shortcut.toLowerCase().replace(/ /g, '') } -function applyShortcuts (shortcuts) { +function applyShortcuts(shortcuts) { for (const shortcut in shortcuts) { const toggler = formatShortcut(shortcuts[shortcut]) // only bind if the function for that shortcut exists diff --git a/browser/main/modals/CreateFolderModal.js b/browser/main/modals/CreateFolderModal.js index b061b0f3..26b5a245 100644 --- a/browser/main/modals/CreateFolderModal.js +++ b/browser/main/modals/CreateFolderModal.js @@ -3,14 +3,14 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './CreateFolderModal.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import consts from 'browser/lib/consts' import ModalEscButton from 'browser/components/ModalEscButton' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import i18n from 'browser/lib/i18n' class CreateFolderModal extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -18,39 +18,39 @@ class CreateFolderModal extends React.Component { } } - componentDidMount () { + componentDidMount() { this.refs.name.focus() this.refs.name.select() } - handleCloseButtonClick (e) { + handleCloseButtonClick(e) { this.props.close() } - handleChange (e) { + handleChange(e) { this.setState({ name: this.refs.name.value }) } - handleKeyDown (e) { + handleKeyDown(e) { if (e.keyCode === 27) { this.props.close() } } - handleInputKeyDown (e) { + handleInputKeyDown(e) { switch (e.keyCode) { case 13: this.confirm() } } - handleConfirmButtonClick (e) { + handleConfirmButtonClick(e) { this.confirm() } - confirm () { + confirm() { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_FOLDER') if (this.state.name.trim().length > 0) { const { storage } = this.props @@ -59,42 +59,48 @@ class CreateFolderModal extends React.Component { color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] } - dataApi.createFolder(storage.key, input) - .then((data) => { + dataApi + .createFolder(storage.key, input) + .then(data => { store.dispatch({ type: 'UPDATE_FOLDER', storage: data.storage }) this.props.close() }) - .catch((err) => { + .catch(err => { console.error(err) }) } } - render () { + render() { return ( -
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >
{i18n.__('Create new folder')}
- this.handleCloseButtonClick(e)} /> + this.handleCloseButtonClick(e)} + />
{i18n.__('Folder name')}
- this.handleChange(e)} - onKeyDown={(e) => this.handleInputKeyDown(e)} + onChange={e => this.handleChange(e)} + onKeyDown={e => this.handleInputKeyDown(e)} />
- diff --git a/browser/main/modals/CreateFolderModal.styl b/browser/main/modals/CreateFolderModal.styl index 93848683..95d6249a 100644 --- a/browser/main/modals/CreateFolderModal.styl +++ b/browser/main/modals/CreateFolderModal.styl @@ -51,106 +51,40 @@ font-size 14px colorPrimaryButton() -body[data-theme="dark"] - .root - modalDark() - width 500px - height 270px - overflow hidden - position relative +apply-theme(theme) + body[data-theme={theme}] + .root + width 500px + height 270px + overflow hidden + position relative + position relative + z-index $modal-z-index + width 100% + background-color get-theme-var(theme, 'backgroundColor') + overflow hidden + border-radius $modal-border-radius - .header - background-color transparent - border-color $ui-dark-borderColor - color $ui-dark-text-color + .header + background-color transparent + border-color $ui-dark-borderColor + color get-theme-var(theme, 'text-color') - .control-folder-label - color $ui-dark-text-color + .control-folder-label + color get-theme-var(theme, 'text-color') - .control-folder-input - border 1px solid $ui-input--create-folder-modal - color white + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white - .description - color $ui-inactive-text-color + .description + color $ui-inactive-text-color - .control-confirmButton - colorDarkPrimaryButton() + .control-confirmButton + colorThemedPrimaryButton(theme) -body[data-theme="solarized-dark"] - .root - modalSolarizedDark() - width 500px - height 270px - overflow hidden - position relative +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) - .header - background-color transparent - border-color $ui-dark-borderColor - color $ui-solarized-dark-text-color - - .control-folder-label - color $ui-solarized-dark-text-color - - .control-folder-input - border 1px solid $ui-input--create-folder-modal - color white - - .description - color $ui-inactive-text-color - - .control-confirmButton - colorSolarizedDarkPrimaryButton() - -body[data-theme="monokai"] - .root - modalMonokai() - width 500px - height 270px - overflow hidden - position relative - - .header - background-color transparent - border-color $ui-dark-borderColor - color $ui-monokai-text-color - - .control-folder-label - color $ui-monokai-text-color - - .control-folder-input - border 1px solid $ui-input--create-folder-modal - color white - - .description - color $ui-inactive-text-color - - .control-confirmButton - colorMonokaiPrimaryButton() - -body[data-theme="dracula"] - .root - modalDracula() - width 500px - height 270px - overflow hidden - position relative - - .header - background-color transparent - border-color $ui-dark-borderColor - color $ui-dracula-text-color - - .control-folder-label - color $ui-dracula-text-color - - .control-folder-input - border 1px solid $ui-input--create-folder-modal - color white - - .description - color $ui-inactive-text-color - - .control-confirmButton - colorDraculaPrimaryButton() \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/CreateMarkdownFromURLModal.js b/browser/main/modals/CreateMarkdownFromURLModal.js new file mode 100644 index 00000000..090fe5a4 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './CreateMarkdownFromURLModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' + +class CreateMarkdownFromURLModal extends React.Component { + constructor(props) { + super(props) + + this.state = { + name: '', + showerror: false, + errormessage: '' + } + } + + componentDidMount() { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick(e) { + this.props.close() + } + + handleChange(e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown(e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown(e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick(e) { + this.confirm() + } + + showError(message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + hideError() { + this.setState({ + showerror: false, + errormessage: '' + }) + } + + confirm() { + this.hideError() + const { storage, folder, dispatch, location } = this.props + + dataApi + .createNoteFromUrl(this.state.name, storage, folder, dispatch, location) + .then(result => { + this.props.close() + }) + .catch(result => { + this.showError(result.error) + }) + } + + render() { + return ( +
this.handleKeyDown(e)} + > +
+
{i18n.__('Import Markdown From URL')}
+
+ this.handleCloseButtonClick(e)} + /> +
+
+
+ {i18n.__('Insert URL Here')} +
+ this.handleChange(e)} + onKeyDown={e => this.handleInputKeyDown(e)} + /> +
+ +
+ {this.state.errormessage} +
+
+
+ ) + } +} + +CreateMarkdownFromURLModal.propTypes = { + storage: PropTypes.string, + folder: PropTypes.string, + dispatch: PropTypes.func, + location: PropTypes.shape({ + pathname: PropTypes.string + }) +} + +export default CSSModules(CreateMarkdownFromURLModal, styles) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl new file mode 100644 index 00000000..8aca1505 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -0,0 +1,89 @@ +.root + modal() + width 500px + height 270px + overflow hidden + position relative + +.header + height 80px + margin-bottom 10px + margin-top 20px + font-size 18px + line-height 50px + background-color $ui-backgroundColor + color $ui-text-color + +.title + font-size 36px + font-weight 600 + +.control-folder-label + text-align left + font-size 14px + color $ui-text-color + +.control-folder-input + display block + height 40px + width 490px + padding 0 5px + margin 10px 0 + border 1px solid $ui-input--create-folder-modal + border-radius 2px + background-color transparent + outline none + vertical-align middle + font-size 16px + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 35px + width 140px + border none + border-radius 2px + padding 0 25px + margin 20px auto + font-size 14px + colorPrimaryButton() + +.error + text-align center + color #F44336 + +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') + + .control-folder-label + color get-theme-var(theme, 'text-color') + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorThemedPrimaryButton(theme) + +for theme in 'dark' 'dracula' 'solarized-dark' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a190602c..c06e91e3 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -3,58 +3,95 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' +import { openModal } from 'browser/main/lib/modal' +import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' +import queryString from 'query-string' class NewNoteModal extends React.Component { - constructor (props) { + constructor(props) { super(props) - + this.lock = false this.state = {} } - componentDidMount () { + componentDidMount() { this.refs.markdownButton.focus() } - handleCloseButtonClick (e) { + handleCloseButtonClick(e) { this.props.close() } - handleMarkdownNoteButtonClick (e) { - const { storage, folder, dispatch, location, params, config } = this.props - createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { - setTimeout(this.props.close, 200) + handleCreateMarkdownFromUrlClick(e) { + this.props.close() + + const { storage, folder, dispatch, location } = this.props + openModal(CreateMarkdownFromURLModal, { + storage: storage, + folder: folder, + dispatch, + location }) } - handleMarkdownNoteButtonKeyDown (e) { + handleMarkdownNoteButtonClick(e) { + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) + if (!this.lock) { + this.lock = true + createMarkdownNote( + storage, + folder, + dispatch, + location, + params, + config + ).then(() => { + setTimeout(this.props.close, 200) + }) + } + } + + handleMarkdownNoteButtonKeyDown(e) { if (e.keyCode === 9) { e.preventDefault() this.refs.snippetButton.focus() } } - handleSnippetNoteButtonClick (e) { - const { storage, folder, dispatch, location, params, config } = this.props - createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { - setTimeout(this.props.close, 200) - }) + handleSnippetNoteButtonClick(e) { + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) + if (!this.lock) { + this.lock = true + createSnippetNote( + storage, + folder, + dispatch, + location, + params, + config + ).then(() => { + setTimeout(this.props.close, 200) + }) + } } - handleSnippetNoteButtonKeyDown (e) { + handleSnippetNoteButtonKeyDown(e) { if (e.keyCode === 9) { e.preventDefault() this.refs.markdownButton.focus() } } - handleKeyDown (e) { + handleKeyDown(e) { if (e.keyCode === 27) { this.props.close() } } - render () { + render() { return (
this.handleSnippetNoteButtonKeyDown(e)} ref='snippetButton' > -
+ +
{i18n.__('Snippet Note')} @@ -104,12 +142,17 @@ class NewNoteModal extends React.Component { )} -
- {i18n.__('Tab to switch format')} + + {i18n.__('Tab to switch format')} +
+
this.handleCreateMarkdownFromUrlClick(e)} + > + Or, create a new markdown note from a URL
-
) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index c82b9376..a9be5a22 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -19,6 +19,7 @@ .control padding 25px 0px text-align center + display: flex .control-button width 240px @@ -47,70 +48,32 @@ text-align center margin-bottom 25px -body[data-theme="dark"] - .root - modalDark() +.from-url + color $ui-inactive-text-color + text-align center + margin-bottom 25px + cursor pointer - .header - color $ui-dark-text-color +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent - .control-button - border-color $ui-dark-borderColor - color $ui-dark-text-color - background-color transparent - &:focus - colorDarkPrimaryButton() + .header + color get-theme-var(theme, 'text-color') - .description - color $ui-inactive-text-color + .control-button + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') + background-color transparent + &:focus + colorThemedPrimaryButton(theme) -body[data-theme="solarized-dark"] - .root - background-color transparent + .description + color get-theme-var(theme, 'text-color') - .header - color $ui-solarized-dark-text-color +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) - .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 - -body[data-theme="monokai"] - .root - background-color transparent - - .header - color $ui-monokai-text-color - - .control-button - border-color $ui-monokai-borderColor - color $ui-monokai-text-color - background-color transparent - &:focus - colorDarkPrimaryButton() - - .description - color $ui-monokai-text-color - -body[data-theme="dracula"] - .root - background-color transparent - - .header - color $ui-dracula-text-color - - .control-button - border-color $ui-dracula-borderColor - color $ui-dracula-text-color - background-color transparent - &:focus - colorDraculaPrimaryButton() - - .description - color $ui-dracula-text-color \ No newline at end of file +for theme in $themes + apply-theme(theme) diff --git a/browser/main/modals/PreferencesModal/Blog.js b/browser/main/modals/PreferencesModal/Blog.js index 2c93fb29..26b4839d 100644 --- a/browser/main/modals/PreferencesModal/Blog.js +++ b/browser/main/modals/PreferencesModal/Blog.js @@ -2,7 +2,7 @@ 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 { store } from 'browser/main/store' import PropTypes from 'prop-types' import _ from 'lodash' import i18n from 'browser/lib/i18n' @@ -11,7 +11,7 @@ const electron = require('electron') const { shell } = electron const ipc = electron.ipcRenderer class Blog extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -20,12 +20,12 @@ class Blog extends React.Component { } } - handleLinkClick (e) { + handleLinkClick(e) { shell.openExternal(e.currentTarget.href) e.preventDefault() } - clearMessage () { + clearMessage() { _.debounce(() => { this.setState({ BlogAlert: null @@ -33,30 +33,41 @@ class Blog extends React.Component { }, 2000)() } - componentDidMount () { + componentDidMount() { this.handleSettingDone = () => { - this.setState({BlogAlert: { - type: 'success', - message: i18n.__('Successfully applied!') - }}) + this.setState({ + BlogAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + } + }) } - this.handleSettingError = (err) => { - this.setState({BlogAlert: { - type: 'error', - message: err.message != null ? err.message : i18n.__('An error occurred!') - }}) + this.handleSettingError = err => { + this.setState({ + BlogAlert: { + type: 'error', + message: + err.message != null ? err.message : i18n.__('An error occurred!') + } + }) } this.oldBlog = this.state.config.blog ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) } - handleBlogChange (e) { + handleBlogChange(e) { const { config } = this.state config.blog = { - password: !_.isNil(this.refs.passwordInput) ? this.refs.passwordInput.value : config.blog.password, - username: !_.isNil(this.refs.usernameInput) ? this.refs.usernameInput.value : config.blog.username, - token: !_.isNil(this.refs.tokenInput) ? this.refs.tokenInput.value : config.blog.token, + password: !_.isNil(this.refs.passwordInput) + ? this.refs.passwordInput.value + : config.blog.password, + username: !_.isNil(this.refs.usernameInput) + ? this.refs.usernameInput.value + : config.blog.username, + token: !_.isNil(this.refs.tokenInput) + ? this.refs.tokenInput.value + : config.blog.token, authMethod: this.refs.authMethodDropdown.value, address: this.refs.addressInput.value, type: this.refs.typeDropdown.value @@ -75,7 +86,7 @@ class Blog extends React.Component { } } - handleSaveButtonClick (e) { + handleSaveButtonClick(e) { const newConfig = { blog: this.state.config.blog } @@ -90,36 +101,36 @@ class Blog extends React.Component { this.props.haveToSave() } - render () { - const {config, BlogAlert} = this.state - const blogAlertElement = BlogAlert != null - ?

- {BlogAlert.message} -

- : null + render() { + const { config, BlogAlert } = this.state + const blogAlertElement = + BlogAlert != null ? ( +

{BlogAlert.message}

+ ) : null return (
{i18n.__('Blog')}
-
- {i18n.__('Blog Type')} -
+
{i18n.__('Blog Type')}
{i18n.__('Blog Address')}
- this.handleBlogChange(e)} + this.handleBlogChange(e)} ref='addressInput' value={config.blog.address} type='text' @@ -127,8 +138,11 @@ class Blog extends React.Component {
- {blogAlertElement}
@@ -143,49 +157,59 @@ class Blog extends React.Component {
- { config.blog.authMethod === 'JWT' && + {config.blog.authMethod === 'JWT' && (
{i18n.__('Token')}
- this.handleBlogChange(e)} + this.handleBlogChange(e)} ref='tokenInput' value={config.blog.token} - type='text' /> + type='text' + />
- } - { config.blog.authMethod === 'USER' && + )} + {config.blog.authMethod === 'USER' && (
{i18n.__('UserName')}
- this.handleBlogChange(e)} + this.handleBlogChange(e)} ref='usernameInput' value={config.blog.username} - type='text' /> + type='text' + />
{i18n.__('Password')}
- this.handleBlogChange(e)} + this.handleBlogChange(e)} ref='passwordInput' value={config.blog.password} - type='password' /> + type='password' + />
- } + )}
) } diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index 9993e54b..a2d4901f 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -1,8 +1,111 @@ @import('./Tab') +.container + display flex + flex-direction column + align-items center + justify-content center + position relative + margin-bottom 2em + margin-left 2em + +.box-minmax + width 608px + height 45px + display flex + justify-content space-between + font-size $tab--button-font-size + color $ui-text-color + span first-child + margin-top 18px + padding-right 10px + padding-left 10px + padding-top 8px + position relative + border $ui-borderColor + border-radius 5px + background $ui-backgroundColor + +div[id^="secondRow"] + position absolute + z-index 2 + left 0 + top 0 + margin-bottom -42px + .rs-label + margin-left -20px + +div[id^="firstRow"] + position absolute + z-index 2 + left 0 + top 0 + margin-bottom -25px + .rs-range + &::-webkit-slider-thumb + margin-top 0px + transform rotate(180deg) + .rs-label + margin-bottom -85px + margin-top 85px + + +.rs-range + margin-top 29px + width 600px + -webkit-appearance none + &:focus + outline black + &::-webkit-slider-runnable-track + width 100% + height 0.1px + cursor pointer + box-shadow none + background $ui-backgroundColor + border-radius 0px + border 0px solid #010101 + cursor none + + &::-webkit-slider-thumb + box-shadow none + border 1px solid $ui-borderColor + box-shadow 0px 10px 10px rgba(0, 0, 0, 0.25) + height 32px + width 32px + border-radius 22px + background white + cursor pointer + -webkit-appearance none + margin-top -20px + border-color $ui-default-button-backgroundColor + height 32px + border-top-left-radius 10% + border-top-right-radius 10% + +.rs-label + position relative + transform-origin center center + display block + background transparent + border-radius none + line-height 30px + font-weight normal + box-sizing border-box + border none + margin-bottom -5px + margin-top -10px + clear both + float left + height 17px + margin-left -25px + left attr(value) + color $ui-text-color + font-style normal + font-weight normal + line-height normal + font-size $tab--button-font-size .root padding 15px - color $ui-text-color margin-bottom 30px .group @@ -14,10 +117,17 @@ .group-header2 font-size 20px - color $ui-text-color margin-bottom 15px margin-top 30px +.group-header--sub + @extend .group-header + margin-bottom 10px + +.group-header2--sub + @extend .group-header2 + margin-bottom 10px + .group-section margin-bottom 20px display flex @@ -135,30 +245,30 @@ colorDarkControl() background-color $ui-dark-backgroundColor color $ui-dark-text-color -colorSolarizedDarkControl() +colorThemedControl(theme) border none - background-color $ui-solarized-dark-button-backgroundColor - color $ui-solarized-dark-text-color + background-color get-theme-var(theme, 'button-backgroundColor') + color get-theme-var(theme, 'text-color') -colorMonokaiControl() - border none - background-color $ui-monokai-button-backgroundColor - color $ui-monokai-text-color +body[data-theme="default"], +body[data-theme="white"] + .root + color $ui-text-color -colorDraculaControl() - border none - background-color $ui-dracula-button-backgroundColor - color $ui-dracula-text-color + .group-header2 + color $ui-text-color body[data-theme="dark"] .root color $ui-dark-text-color .group-header + .group-header--sub color $ui-dark-text-color border-color $ui-dark-borderColor .group-header2 + .group-header2--sub color $ui-dark-text-color .group-section-control-input @@ -176,85 +286,44 @@ body[data-theme="dark"] .group-section-control select, .group-section-control-input colorDarkControl() + .rs-label + color $ui-dark-text-color -body[data-theme="solarized-dark"] - .root - color $ui-solarized-dark-text-color +apply-theme(theme) + body[data-theme={theme}] + .root + color get-theme-var(theme, 'text-color') - .group-header - color $ui-solarized-dark-text-color - border-color $ui-solarized-dark-borderColor + .group-header + .group-header--sub + color get-theme-var(theme, 'text-color') + border-color get-theme-var(theme, 'borderColor') - .group-header2 - color $ui-solarized-dark-text-color + .group-header2 + .group-header2--sub + color get-theme-var(theme, 'text-color') - .group-section-control-input - border-color $ui-solarized-dark-borderColor + .group-section-control-input + border-color get-theme-var(theme, '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() + .group-control + border-color get-theme-var(theme, 'borderColor') + .group-control-leftButton + colorDarkDefaultButton() + border-color get-theme-var(theme, 'borderColor') + .group-control-rightButton + colorThemedPrimaryButton(theme) + .group-hint + colorThemedControl(theme) + .group-section-control + select, .group-section-control-input + colorThemedControl(theme) + .rs-label + color get-theme-var(theme, 'text-color') -body[data-theme="monokai"] - .root - color $ui-monokai-text-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) - .group-header - color $ui-monokai-text-color - border-color $ui-monokai-borderColor - - .group-header2 - color $ui-monokai-text-color - - .group-section-control-input - border-color $ui-monokai-borderColor - - .group-control - border-color $ui-monokai-borderColor - .group-control-leftButton - colorDarkDefaultButton() - border-color $ui-monokai-borderColor - .group-control-rightButton - colorMonokaiPrimaryButton() - .group-hint - colorMonokaiControl() - .group-section-control - select, .group-section-control-input - colorMonokaiControl() - -body[data-theme="dracula"] - .root - color $ui-dracula-text-color - - .group-header - color $ui-dracula-text-color - border-color $ui-dracula-borderColor - - .group-header2 - color $ui-dracula-text-color - - .group-section-control-input - border-color $ui-dracula-borderColor - - .group-control - border-color $ui-dracula-borderColor - .group-control-leftButton - colorDarkDefaultButton() - border-color $ui-dracula-borderColor - .group-control-rightButton - colorDraculaPrimaryButton() - .group-hint - colorDraculaControl() - .group-section-control - select, .group-section-control-input - colorDraculaControl() \ No newline at end of file +for theme in $themes + apply-theme(theme) diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js index f6389cd8..a5d37718 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.js +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -7,52 +7,93 @@ const electron = require('electron') const { shell } = electron class Crowdfunding extends React.Component { - constructor (props) { + constructor(props) { super(props) - this.state = { - } + this.state = {} } - handleLinkClick (e) { + handleLinkClick(e) { shell.openExternal(e.currentTarget.href) e.preventDefault() } - render () { + render() { return (
-
{i18n.__('Crowdfunding')}
+
{i18n.__('Crowdfunding')}

{i18n.__('Thank you for using Boostnote!')}


-

{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}

-

{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}

-
-

{i18n.__('### Sustainable Open Source Ecosystem')}

-

{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}

-

{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}

-

{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}

-
-

{i18n.__('### We believe Meritocracy')}

-

{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}

-

{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}

+

+ {i18n.__( + 'We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.' + )} +

+

+ {i18n.__( + 'Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.' + )} +

+
+ {i18n.__('Sustainable Open Source Ecosystem')} +
+

+ {i18n.__( + 'We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.' + )} +

+

+ {i18n.__( + 'The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.' + )} +

+

+ {i18n.__( + 'We thought that it will be nice if we can pay reward for our contributors.' + )} +

+
+ {i18n.__('We believe Meritocracy')} +
+

+ {i18n.__( + 'We think developers who have skills and do great things must be rewarded properly.' + )} +

+

+ {i18n.__( + 'OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.' + )} +

{i18n.__('It sometimes looks like exploitation.')}

-

{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}

+

+ {i18n.__( + 'We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.' + )} +


-

{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}

+

+ {i18n.__( + 'As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.' + )} +


{i18n.__('Thank you,')}

{i18n.__('The Boostnote Team')}


) } } -Crowdfunding.propTypes = { -} +Crowdfunding.propTypes = {} export default CSSModules(Crowdfunding, styles) diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl index 6d72290b..4725aa23 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -1,14 +1,8 @@ -@import('./Tab') +@import('./ConfigTab') -.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 + line-height 1.4 .cf-link height 35px @@ -30,20 +24,15 @@ 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 +apply-theme(theme) + body[data-theme={theme}] + .root + color get-theme-var(theme, 'text-color') + p + color get-theme-var(theme, 'text-color') -body[data-theme="monokai"] - .root - color $ui-monokai-text-color - p - color $ui-monokai-text-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="dracula"] - .root - color $ui-dracula-text-color - p - color $ui-dracula-text-color \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/FolderItem.js b/browser/main/modals/PreferencesModal/FolderItem.js index dc9082b9..6418bb6a 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.js +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -4,13 +4,13 @@ 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 { store } from 'browser/main/store' import { SketchPicker } from 'react-color' import { SortableElement, SortableHandle } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' class FolderItem extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -24,7 +24,7 @@ class FolderItem extends React.Component { } } - handleEditChange (e) { + handleEditChange(e) { const { folder } = this.state folder.name = this.refs.nameInput.value @@ -33,18 +33,18 @@ class FolderItem extends React.Component { }) } - handleConfirmButtonClick (e) { + handleConfirmButtonClick(e) { this.confirm() } - 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) => { + .then(data => { store.dispatch({ type: 'UPDATE_FOLDER', storage: data.storage @@ -55,9 +55,12 @@ class FolderItem extends React.Component { }) } - handleColorButtonClick (e) { - const folder = Object.assign({}, this.state.folder, { showColumnPicker: true, colorPickerPos: { left: 0, top: 0 } }) - this.setState({ folder }, function () { + 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 @@ -67,30 +70,32 @@ class FolderItem extends React.Component { const folder = Object.assign({}, this.state.folder, { colorPickerPos: { left: 25, - top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics + top: offsetTop < 0 ? offsetTop - 5 : 0 // subtract 5px for aestetics } }) this.setState({ folder }) }) } - handleColorChange (color) { + 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 }) + handleColorPickerClose(event) { + const folder = Object.assign({}, this.state.folder, { + showColumnPicker: false + }) this.setState({ folder }) } - handleCancelButtonClick (e) { + handleCancelButtonClick(e) { this.setState({ status: 'IDLE' }) } - handleFolderItemBlur (e) { + handleFolderItemBlur(e) { let el = e.relatedTarget while (el != null) { if (el === this.refs.root) { @@ -101,7 +106,7 @@ class FolderItem extends React.Component { this.confirm() } - renderEdit (e) { + renderEdit(e) { const popover = { position: 'absolute', zIndex: 2 } const cover = { position: 'fixed', @@ -110,51 +115,64 @@ class FolderItem extends React.Component { bottom: 0, left: 0 } - const pickerStyle = Object.assign({}, { - position: 'absolute' - }, this.state.folder.colorPickerPos) + const pickerStyle = Object.assign( + {}, + { + position: 'absolute' + }, + this.state.folder.colorPickerPos + ) return ( -
this.handleFolderItemBlur(e)} +
this.handleFolderItemBlur(e)} tabIndex='-1' ref='root' >
- - this.handleEditChange(e)} + onChange={e => this.handleEditChange(e)} />
- - @@ -163,79 +181,85 @@ class FolderItem extends React.Component { ) } - handleDeleteConfirmButtonClick (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 - }) + dataApi.deleteFolder(storage.key, folder.key).then(data => { + store.dispatch({ + type: 'DELETE_FOLDER', + storage: data.storage, + folderKey: data.folderKey }) - } - - renderDelete () { - return ( -
-
- {i18n.__('Are you sure to ')} {i18n.__(' delete')} {i18n.__('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) { + renderDelete() { + return ( +
+
+ {i18n.__('Are you sure to ')}{' '} + {i18n.__(' delete')}{' '} + {i18n.__('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 () { + renderIdle() { const { folder } = this.props return ( -
this.handleEditButtonClick(e)} +
this.handleEditButtonClick(e)} > -
- {folder.name} +
+ {folder.name} ({folder.key})
- - @@ -244,7 +268,7 @@ class FolderItem extends React.Component { ) } - render () { + render() { switch (this.state.status) { case 'DELETE': return this.renderDelete() @@ -277,7 +301,7 @@ FolderItem.propTypes = { } class Handle extends React.Component { - render () { + render() { return (
@@ -287,11 +311,11 @@ class Handle extends React.Component { } class SortableFolderItemComponent extends React.Component { - render () { - const StyledHandle = CSSModules(Handle, this.props.styles) + render() { + const StyledHandle = CSSModules(Handle, styles) const DragHandle = SortableHandle(StyledHandle) - const StyledFolderItem = CSSModules(FolderItem, this.props.styles) + const StyledFolderItem = CSSModules(FolderItem, styles) return (
diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index 2ded3ada..32a83d63 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -62,7 +62,7 @@ .folderItem-right-button vertical-align middle height 25px - margin-top 2.5px + margin-top 2px colorDefaultButton() border-radius 2px border $ui-border @@ -107,73 +107,32 @@ body[data-theme="dark"] .folderItem-right-dangerButton colorDarkDangerButton() +apply-theme(theme) + body[data-theme={theme}] + .folderItem + &:hover + background-color get-theme-var(theme, 'button-backgroundColor') + .folderItem-left-danger + color $danger-color -body[data-theme="solarized-dark"] - .folderItem - &:hover - background-color $ui-solarized-dark-button-backgroundColor + .folderItem-left-key + color $ui-dark-inactive-text-color - .folderItem-left-danger - color $danger-color + .folderItem-left-colorButton + colorThemedPrimaryButton(theme) - .folderItem-left-key - color $ui-dark-inactive-text-color + .folderItem-right-button + colorThemedPrimaryButton(theme) - .folderItem-left-colorButton - colorSolarizedDarkPrimaryButton() + .folderItem-right-confirmButton + colorThemedPrimaryButton(theme) - .folderItem-right-button - colorSolarizedDarkPrimaryButton() + .folderItem-right-dangerButton + colorThemedPrimaryButton(theme) - .folderItem-right-confirmButton - colorSolarizedDarkPrimaryButton() +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) - .folderItem-right-dangerButton - colorSolarizedDarkPrimaryButton() - -body[data-theme="monokai"] - .folderItem - &:hover - background-color $ui-monokai-button-backgroundColor - - .folderItem-left-danger - color $danger-color - - .folderItem-left-key - color $ui-dark-inactive-text-color - - .folderItem-left-colorButton - colorMonokaiPrimaryButton() - - .folderItem-right-button - colorMonokaiPrimaryButton() - - .folderItem-right-confirmButton - colorMonokaiPrimaryButton() - - .folderItem-right-dangerButton - colorMonokaiPrimaryButton() - -body[data-theme="dracula"] - .folderItem - &:hover - background-color $ui-dracula-button-backgroundColor - - .folderItem-left-danger - color $danger-color - - .folderItem-left-key - color $ui-dark-inactive-text-color - - .folderItem-left-colorButton - colorDraculaPrimaryButton() - - .folderItem-right-button - colorDraculaPrimaryButton() - - .folderItem-right-confirmButton - colorDraculaPrimaryButton() - - .folderItem-right-dangerButton - colorDraculaPrimaryButton() \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/FolderList.js b/browser/main/modals/PreferencesModal/FolderList.js index e7cc6f94..f751d250 100644 --- a/browser/main/modals/PreferencesModal/FolderList.js +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -3,30 +3,34 @@ 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 { store } from 'browser/main/store' import FolderItem from './FolderItem' import { SortableContainer } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' class FolderList extends React.Component { - render () { + render() { const { storage, hostBoundingBox } = this.props const folderList = storage.folders.map((folder, index) => { - return + return ( + + ) }) return ( -
- {folderList.length > 0 - ? folderList - :
{i18n.__('No Folders')}
- } +
+ {folderList.length > 0 ? ( + folderList + ) : ( +
{i18n.__('No Folders')}
+ )}
) } @@ -52,23 +56,21 @@ FolderList.propTypes = { } class SortableFolderListComponent extends React.Component { - constructor (props) { + constructor(props) { super(props) - this.onSortEnd = ({oldIndex, newIndex}) => { + 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() + dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => { + store.dispatch({ + type: 'REORDER_FOLDER', + storage: data.storage }) + this.setState() + }) } } - render () { + render() { const StyledFolderList = CSSModules(FolderList, this.props.styles) const SortableFolderList = SortableContainer(StyledFolderList) diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 25098faa..1bba78e3 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -3,7 +3,7 @@ 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 { store } from 'browser/main/store' import _ from 'lodash' import i18n from 'browser/lib/i18n' @@ -11,7 +11,7 @@ const electron = require('electron') const ipc = electron.ipcRenderer class HotkeyTab extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -20,27 +20,35 @@ class HotkeyTab extends React.Component { } } - componentDidMount () { + componentDidMount() { this.handleSettingDone = () => { - this.setState({keymapAlert: { - type: 'success', - message: i18n.__('Successfully applied!') - }}) - } - this.handleSettingError = (err) => { - if ( - this.state.config.hotkey.toggleMain === '' || - this.state.config.hotkey.toggleMode === '' - ) { - this.setState({keymapAlert: { + this.setState({ + keymapAlert: { type: 'success', message: i18n.__('Successfully applied!') - }}) + } + }) + } + this.handleSettingError = err => { + if ( + this.state.config.hotkey.toggleMain === '' || + this.state.config.hotkey.toggleMode === '' || + this.state.config.hotkey.toggleDirection === '' + ) { + this.setState({ + keymapAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + } + }) } else { - this.setState({keymapAlert: { - type: 'error', - message: err.message != null ? err.message : i18n.__('An error occurred!') - }}) + this.setState({ + keymapAlert: { + type: 'error', + message: + err.message != null ? err.message : i18n.__('An error occurred!') + } + }) } } this.oldHotkey = this.state.config.hotkey @@ -48,12 +56,12 @@ class HotkeyTab extends React.Component { ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) } - componentWillUnmount () { + componentWillUnmount() { ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone) ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) } - handleSaveButtonClick (e) { + handleSaveButtonClick(e) { const newConfig = { hotkey: this.state.config.hotkey } @@ -68,20 +76,25 @@ class HotkeyTab extends React.Component { this.props.haveToSave() } - handleHintToggleButtonClick (e) { + handleHintToggleButtonClick(e) { this.setState({ isHotkeyHintOpen: !this.state.isHotkeyHintOpen }) } - handleHotkeyChange (e) { + handleHotkeyChange(e) { const { config } = this.state - config.hotkey = { + config.hotkey = Object.assign({}, config.hotkey, { toggleMain: this.refs.toggleMain.value, toggleMode: this.refs.toggleMode.value, + toggleDirection: this.refs.toggleDirection.value, deleteNote: this.refs.deleteNote.value, - pasteSmartly: this.refs.pasteSmartly.value - } + pasteSmartly: this.refs.pasteSmartly.value, + prettifyMarkdown: this.refs.prettifyMarkdown.value, + toggleMenuBar: this.refs.toggleMenuBar.value, + insertDate: this.refs.insertDate.value, + insertDateTime: this.refs.insertDateTime.value + }) this.setState({ config }) @@ -96,7 +109,7 @@ class HotkeyTab extends React.Component { } } - clearMessage () { + clearMessage() { _.debounce(() => { this.setState({ keymapAlert: null @@ -104,13 +117,12 @@ class HotkeyTab extends React.Component { }, 2000)() } - render () { + render() { const keymapAlert = this.state.keymapAlert - const keymapAlertElement = keymapAlert != null - ?

- {keymapAlert.message} -

- : null + const keymapAlertElement = + keymapAlert != null ? ( +

{keymapAlert.message}

+ ) : null const { config } = this.state return ( @@ -118,10 +130,13 @@ class HotkeyTab extends React.Component {
{i18n.__('Hotkeys')}
-
{i18n.__('Show/Hide Boostnote')}
+
+ {i18n.__('Show/Hide Boostnote')} +
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='toggleMain' value={config.hotkey.toggleMain} type='text' @@ -129,21 +144,53 @@ class HotkeyTab extends React.Component {
-
{i18n.__('Toggle Editor Mode')}
+
+ {i18n.__('Show/Hide Menu Bar')} +
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} + ref='toggleMenuBar' + value={config.hotkey.toggleMenuBar} + type='text' + /> +
+
+
+
+ {i18n.__('Toggle Editor Mode')} +
+
+ this.handleHotkeyChange(e)} ref='toggleMode' value={config.hotkey.toggleMode} type='text' />
+
+
+ {i18n.__('Toggle Direction')} +
+
+ this.handleHotkeyChange(e)} + ref='toggleDirection' + value={config.hotkey.toggleDirection} + type='text' + /> +
+
{i18n.__('Delete Note')}
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='deleteNote' value={config.hotkey.deleteNote} type='text' @@ -153,53 +200,139 @@ class HotkeyTab extends React.Component {
{i18n.__('Paste HTML')}
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='pasteSmartly' value={config.hotkey.pasteSmartly} type='text' />
+
+
+ {i18n.__('Prettify Markdown')} +
+
+ this.handleHotkeyChange(e)} + ref='prettifyMarkdown' + value={config.hotkey.prettifyMarkdown} + type='text' + /> +
+
+
+
+ {i18n.__('Insert Current Date')} +
+
+ +
+
+
+
+ {i18n.__('Insert Current Date and Time')} +
+
+ +
+
- - {keymapAlertElement}
- {this.state.isHotkeyHintOpen && + {this.state.isHotkeyHintOpen && (

{i18n.__('Available Keys')}

    -
  • 0 to 9
  • -
  • A to Z
  • -
  • F1 to F24
  • -
  • Punctuations like ~, !, @, #, $, etc.
  • -
  • Plus
  • -
  • Space
  • -
  • Backspace
  • -
  • Delete
  • -
  • Insert
  • -
  • Return (or Enter as alias)
  • -
  • Up, Down, Left and Right
  • -
  • Home and End
  • -
  • PageUp and PageDown
  • -
  • Escape (or Esc for short)
  • -
  • VolumeUp, VolumeDown and VolumeMute
  • -
  • MediaNextTrack, MediaPreviousTrack, MediaStop and MediaPlayPause
  • -
  • Control (or Ctrl for short)
  • -
  • Shift
  • +
  • + 0 to 9 +
  • +
  • + A to Z +
  • +
  • + F1 to F24 +
  • +
  • + Punctuations like ~, !,{' '} + @, #, $, etc. +
  • +
  • + Plus +
  • +
  • + Space +
  • +
  • + Backspace +
  • +
  • + Delete +
  • +
  • + Insert +
  • +
  • + Return (or Enter as alias) +
  • +
  • + Up, Down, Left and{' '} + Right +
  • +
  • + Home and End +
  • +
  • + PageUp and PageDown +
  • +
  • + Escape (or Esc for short) +
  • +
  • + VolumeUp, VolumeDown and{' '} + VolumeMute +
  • +
  • + MediaNextTrack, MediaPreviousTrack,{' '} + MediaStop and MediaPlayPause +
  • +
  • + Control (or Ctrl for short) +
  • +
  • + Shift +
- } + )}
) diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js index d618fa22..d4b2a408 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -2,7 +2,7 @@ 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 { store } from 'browser/main/store' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import _ from 'lodash' import i18n from 'browser/lib/i18n' @@ -12,7 +12,7 @@ const { shell, remote } = electron const appVersion = remote.app.getVersion() class InfoTab extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -20,18 +20,18 @@ class InfoTab extends React.Component { } } - handleLinkClick (e) { + handleLinkClick(e) { shell.openExternal(e.currentTarget.href) e.preventDefault() } - handleConfigChange (e) { + handleConfigChange(e) { const newConfig = { amaEnabled: this.refs.amaEnabled.checked } this.setState({ config: newConfig }) } - handleSaveButtonClick (e) { + handleSaveButtonClick(e) { const newConfig = { amaEnabled: this.state.config.amaEnabled } @@ -43,7 +43,7 @@ class InfoTab extends React.Component { }) } else { this.setState({ - amaMessage: i18n.__('Thank\'s for trusting us') + amaMessage: i18n.__("Thank's for trusting us") }) } @@ -61,62 +61,95 @@ class InfoTab extends React.Component { }) } - infoMessage () { + toggleAutoUpdate() { + const newConfig = { + autoUpdateEnabled: !this.state.config.autoUpdateEnabled + } + + this.setState({ config: newConfig }) + ConfigManager.set(newConfig) + } + + infoMessage() { const { amaMessage } = this.state return amaMessage ?

{amaMessage}

: null } - render () { + render() { return (
- -
{i18n.__('Community')}
+
{i18n.__('Community')}

-
{i18n.__('About')}
+
{i18n.__('About')}
- +
-
{i18n.__('Boostnote')} {appVersion}
+
Boostnote Legacy {appVersion}
- {i18n.__('An open source note-taking app made for programmers just like you.')} + {i18n.__( + 'An open source note-taking app made for programmers just like you.' + )}
@@ -124,37 +157,71 @@ class InfoTab extends React.Component { +
+ +
+
-
{i18n.__('Analytics')}
-
{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}
-
{i18n.__('You can see how it works on ')} this.handleLinkClick(e)}>GitHub.
+
{i18n.__('Analytics')}
+
+ {i18n.__( + 'Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.' + )} +
+
+ {i18n.__('You can see how it works on ')} + this.handleLinkClick(e)} + > + GitHub + + . +

{i18n.__('You can choose to enable or disable this option.')}
- this.handleConfigChange(e)} + this.handleConfigChange(e)} checked={this.state.config.amaEnabled} ref='amaEnabled' type='checkbox' /> - {i18n.__('Enable analytics to help improve Boostnote')}
- + {i18n.__('Enable analytics to help improve Boostnote')} +
+
{this.infoMessage()}
@@ -162,7 +229,6 @@ class InfoTab extends React.Component { } } -InfoTab.propTypes = { -} +InfoTab.propTypes = {} export default CSSModules(InfoTab, styles) diff --git a/browser/main/modals/PreferencesModal/InfoTab.styl b/browser/main/modals/PreferencesModal/InfoTab.styl index 44f2d9ae..4701d809 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.styl +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -1,16 +1,4 @@ -@import('./Tab') - -.root - padding 15px - white-space pre - line-height 1.4 - color alpha($ui-text-color, 90%) - width 100% - font-size 14px - -.top - text-align left - margin-bottom 20px +@import('./ConfigTab.styl') .icon-space margin 20px 0 @@ -45,13 +33,21 @@ .separate-line margin 40px 0 -.policy - width 100% - font-size 20px - margin-bottom 10px - .policy-submit margin-top 10px + height 35px + border-radius 2px + border none + background-color alpha(#1EC38B, 90%) + padding-left 20px + padding-right 20px + text-decoration none + color white + font-weight 600 + font-size 16px + &:hover + background-color #1EC38B + transition 0.2s .policy-confirm margin-top 10px @@ -60,25 +56,22 @@ body[data-theme="dark"] .root color alpha($tab--dark-text-color, 80%) + .appId + color $ui-dark-text-color -body[data-theme="solarized-dark"] - .root - color $ui-solarized-dark-text-color -.list - a - color $ui-solarized-dark-active-color +apply-theme(theme) + body[data-theme={theme}] + .root + color get-theme-var(theme, 'text-color') + .appId + color get-theme-var(theme, 'text-color') + .list + a + color get-theme-var(theme, 'active-color') -body[data-theme="monokai"] - .root - color $ui-monokai-text-color -.list - a - color $ui-monokai-active-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="dracula"] - .root - color $ui-dracula-text-color -.list - a - color $ui-dracula-active-color +for theme in $themes + apply-theme(theme) diff --git a/browser/main/modals/PreferencesModal/PreferencesModal.styl b/browser/main/modals/PreferencesModal/PreferencesModal.styl index 7004259b..2e3b4040 100644 --- a/browser/main/modals/PreferencesModal/PreferencesModal.styl +++ b/browser/main/modals/PreferencesModal/PreferencesModal.styl @@ -64,102 +64,31 @@ top-bar--height = 50px margin-top 10px overflow-y auto -body[data-theme="dark"] - .root - modalDark() +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent + .top-bar + background-color transparent + border-color get-theme-var(theme, 'borderColor') + p + color get-theme-var(theme, 'text-color') + .nav + background-color transparent + border-color get-theme-var(theme, 'borderColor') + .nav-button + background-color transparent + color get-theme-var(theme, 'text-color') + &:hover + color get-theme-var(theme, 'text-color') - .top-bar - background-color transparent - border-color #4A4D52 - p - color $tab--dark-text-color + .nav-button--active + @extend .nav-button + color get-theme-var(theme, 'button--active-color') + background-color get-theme-var(theme, 'button--active-backgroundColor') - .nav - background-color transparent - border-color $ui-dark-borderColor +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) - .nav-button - background-color transparent - color $tab--dark-text-color - &:hover - color $ui-dark-text-color - - .nav-button--active - @extend .nav-button - color white - 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 - -body[data-theme="monokai"] - .root - background-color transparent - .top-bar - background-color transparent - border-color $ui-monokai-borderColor - p - color $ui-monokai-text-color - .nav - background-color transparent - border-color $ui-monokai-borderColor - .nav-button - background-color transparent - color $ui-monokai-text-color - &:hover - color $ui-monokai-text-color - - .nav-button--active - @extend .nav-button - color $ui-monokai-button--active-color - background-color $ui-monokai-button--active-backgroundColor - &:hover - color white - -body[data-theme="dracula"] - .root - background-color transparent - .top-bar - background-color transparent - border-color $ui-dracula-borderColor - p - color $ui-dracula-text-color - .nav - background-color transparent - border-color $ui-dracula-borderColor - .nav-button - background-color transparent - color $ui-dracula-text-color - &:hover - color $ui-dracula-text-color - - .nav-button--active - @extend .nav-button - color $ui-dracula-button--active-color - background-color $ui-dracula-button--active-backgroundColor - &:hover - color #f8f8f2 \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/SnippetEditor.js b/browser/main/modals/PreferencesModal/SnippetEditor.js index 6e14465e..3a5eb837 100644 --- a/browser/main/modals/PreferencesModal/SnippetEditor.js +++ b/browser/main/modals/PreferencesModal/SnippetEditor.js @@ -4,14 +4,21 @@ import _ from 'lodash' import styles from './SnippetTab.styl' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' +import snippetManager from '../../../lib/SnippetManager' -const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] +const defaultEditorFontFamily = [ + 'Monaco', + 'Menlo', + 'Ubuntu Mono', + 'Consolas', + 'source-code-pro', + 'monospace' +] const buildCMRulers = (rulers, enableRulers) => enableRulers ? rulers.map(ruler => ({ column: ruler })) : [] class SnippetEditor extends React.Component { - - componentDidMount () { + componentDidMount() { this.props.onRef(this) const { rulers, enableRulers } = this.props this.cm = CodeMirror(this.refs.root, { @@ -30,11 +37,13 @@ class SnippetEditor extends React.Component { autoCloseBrackets: { codeBlock: { pairs: this.props.codeBlockMatchingPairs, + closeBefore: this.props.codeBlockMatchingCloseBefore, triples: this.props.codeBlockMatchingTriples, explode: this.props.codeBlockExplodingPairs }, markdown: { pairs: this.props.matchingPairs, + closeBefore: this.props.matchingCloseBefore, triples: this.props.matchingTriples, explode: this.props.explodingPairs } @@ -54,36 +63,50 @@ class SnippetEditor extends React.Component { }) } - componentWillUnmount () { + componentWillUnmount() { this.props.onRef(undefined) } - onSnippetChanged (newSnippet) { + onSnippetChanged(newSnippet) { this.snippet = newSnippet this.cm.setValue(this.snippet.content) } - onSnippetNameOrPrefixChanged (newSnippet) { + onSnippetNameOrPrefixChanged(newSnippet) { this.snippet.name = newSnippet.name - this.snippet.prefix = newSnippet.prefix.toString().replace(/\s/g, '').split(',') + this.snippet.prefix = newSnippet.prefix + .toString() + .replace(/\s/g, '') + .split(',') this.saveSnippet() } - saveSnippet () { - dataApi.updateSnippet(this.snippet).catch((err) => { throw err }) + saveSnippet() { + dataApi + .updateSnippet(this.snippet) + .then(snippets => snippetManager.assignSnippets(snippets)) + .catch(err => { + throw err + }) } - render () { + render() { const { fontSize } = this.props let fontFamily = this.props.fontFamily - fontFamily = _.isString(fontFamily) && fontFamily.length > 0 - ? [fontFamily].concat(defaultEditorFontFamily) - : defaultEditorFontFamily + fontFamily = + _.isString(fontFamily) && fontFamily.length > 0 + ? [fontFamily].concat(defaultEditorFontFamily) + : defaultEditorFontFamily return ( -
+
) } } diff --git a/browser/main/modals/PreferencesModal/SnippetList.js b/browser/main/modals/PreferencesModal/SnippetList.js index 3790eb3f..145cfb75 100644 --- a/browser/main/modals/PreferencesModal/SnippetList.js +++ b/browser/main/modals/PreferencesModal/SnippetList.js @@ -7,53 +7,65 @@ import eventEmitter from 'browser/main/lib/eventEmitter' import context from 'browser/lib/context' class SnippetList extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { snippets: [] } } - componentDidMount () { + componentDidMount() { this.reloadSnippetList() eventEmitter.on('snippetList:reload', this.reloadSnippetList.bind(this)) } - reloadSnippetList () { + reloadSnippetList() { dataApi.fetchSnippet().then(snippets => { - this.setState({snippets}) + this.setState({ snippets }) this.props.onSnippetSelect(this.props.currentSnippet) }) } - handleSnippetContextMenu (snippet) { - context.popup([{ - label: i18n.__('Delete snippet'), - click: () => this.deleteSnippet(snippet) - }]) + handleSnippetContextMenu(snippet) { + context.popup([ + { + label: i18n.__('Delete snippet'), + click: () => this.deleteSnippet(snippet) + } + ]) } - deleteSnippet (snippet) { - dataApi.deleteSnippet(snippet).then(() => { - this.reloadSnippetList() - this.props.onSnippetDeleted(snippet) - }).catch(err => { throw err }) + deleteSnippet(snippet) { + dataApi + .deleteSnippet(snippet) + .then(() => { + this.reloadSnippetList() + this.props.onSnippetDeleted(snippet) + }) + .catch(err => { + throw err + }) } - handleSnippetClick (snippet) { + handleSnippetClick(snippet) { this.props.onSnippetSelect(snippet) } - createSnippet () { - dataApi.createSnippet().then(() => { - this.reloadSnippetList() - // scroll to end of list when added new snippet - const snippetList = document.getElementById('snippets') - snippetList.scrollTop = snippetList.scrollHeight - }).catch(err => { throw err }) + createSnippet() { + dataApi + .createSnippet() + .then(() => { + this.reloadSnippetList() + // scroll to end of list when added new snippet + const snippetList = document.getElementById('snippets') + snippetList.scrollTop = snippetList.scrollHeight + }) + .catch(err => { + throw err + }) } - defineSnippetStyleName (snippet) { + defineSnippetStyleName(snippet) { const { currentSnippet } = this.props if (currentSnippet == null) { @@ -67,29 +79,31 @@ class SnippetList extends React.Component { } } - render () { + render() { const { snippets } = this.state return (
-
    - { - snippets.map((snippet) => ( -
  • this.handleSnippetContextMenu(snippet)} - onClick={() => this.handleSnippetClick(snippet)}> - {snippet.name} -
  • - )) - } + {snippets.map(snippet => ( +
  • this.handleSnippetContextMenu(snippet)} + onClick={() => this.handleSnippetClick(snippet)} + > + {snippet.name} +
  • + ))}
) diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js index c9eca3b1..156b4424 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.js +++ b/browser/main/modals/PreferencesModal/SnippetTab.js @@ -11,7 +11,7 @@ import copy from 'copy-to-clipboard' const path = require('path') class SnippetTab extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { currentSnippet: null @@ -19,7 +19,7 @@ class SnippetTab extends React.Component { this.changeDelay = null } - notify (title, options) { + notify(title, options) { if (global.process.platform === 'win32') { options.icon = path.join( 'file://', @@ -30,7 +30,7 @@ class SnippetTab extends React.Component { return new window.Notification(title, options) } - handleSnippetNameOrPrefixChange () { + handleSnippetNameOrPrefixChange() { clearTimeout(this.changeDelay) this.changeDelay = setTimeout(() => { // notify the snippet editor that the name or prefix of snippet has been changed @@ -39,20 +39,20 @@ class SnippetTab extends React.Component { }, 500) } - handleSnippetSelect (snippet) { + handleSnippetSelect(snippet) { const { currentSnippet } = this.state if (snippet !== null) { if (currentSnippet === null || currentSnippet.id !== snippet.id) { dataApi.fetchSnippet(snippet.id).then(changedSnippet => { // notify the snippet editor to load the content of the new snippet this.snippetEditor.onSnippetChanged(changedSnippet) - this.setState({currentSnippet: changedSnippet}) + this.setState({ currentSnippet: changedSnippet }) }) } } } - onSnippetNameOrPrefixChanged (e, type) { + onSnippetNameOrPrefixChanged(e, type) { const newSnippet = Object.assign({}, this.state.currentSnippet) if (type === 'name') { newSnippet.name = e.target.value @@ -63,14 +63,14 @@ class SnippetTab extends React.Component { this.handleSnippetNameOrPrefixChange() } - handleDeleteSnippet (snippet) { + handleDeleteSnippet(snippet) { // prevent old snippet still display when deleted if (snippet.id === this.state.currentSnippet.id) { - this.setState({currentSnippet: null}) + this.setState({ currentSnippet: null }) } } - handleCopySnippet (e) { + handleCopySnippet(e) { const showCopyNotification = this.props.config.ui.showCopyNotification copy(this.state.currentSnippet.content) if (showCopyNotification) { @@ -81,7 +81,7 @@ class SnippetTab extends React.Component { } } - render () { + render() { const { config, storageKey } = this.props const { currentSnippet } = this.state @@ -91,16 +91,23 @@ class SnippetTab extends React.Component { if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 return (
-
{i18n.__('Snippets')}
+
{i18n.__('Snippets')}
-
+ currentSnippet={currentSnippet} + /> +
-
@@ -110,18 +117,26 @@ class SnippetTab extends React.Component { { this.onSnippetNameOrPrefixChanged(e, 'name') }} - type='text' /> + onChange={e => { + this.onSnippetNameOrPrefixChanged(e, 'name') + }} + type='text' + />
-
{i18n.__('Snippet prefix')}
+
+ {i18n.__('Snippet prefix')} +
{ this.onSnippetNameOrPrefixChanged(e, 'prefix') }} - type='text' /> + onChange={e => { + this.onSnippetNameOrPrefixChanged(e, 'prefix') + }} + type='text' + />
@@ -137,13 +152,20 @@ class SnippetTab extends React.Component { rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} matchingPairs={config.editor.matchingPairs} + matchingCloseBefore={config.editor.matchingCloseBefore} matchingTriples={config.editor.matchingTriples} explodingPairs={config.editor.explodingPairs} codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs} + codeBlockMatchingCloseBefore={ + config.editor.codeBlockMatchingCloseBefore + } codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples} codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs} scrollPastEnd={config.editor.scrollPastEnd} - onRef={ref => { this.snippetEditor = ref }} /> + onRef={ref => { + this.snippetEditor = ref + }} + />
@@ -151,7 +173,6 @@ class SnippetTab extends React.Component { } } -SnippetTab.PropTypes = { -} +SnippetTab.PropTypes = {} export default CSSModules(SnippetTab, styles) diff --git a/browser/main/modals/PreferencesModal/SnippetTab.styl b/browser/main/modals/PreferencesModal/SnippetTab.styl index dd22b72e..83b0a296 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.styl +++ b/browser/main/modals/PreferencesModal/SnippetTab.styl @@ -1,14 +1,5 @@ -@import('./Tab') @import('./ConfigTab') -.root - padding 15px - white-space pre - line-height 1.4 - color alpha($ui-text-color, 90%) - width 100% - font-size 14px - .group margin-bottom 45px @@ -127,7 +118,7 @@ background darken(#f5f5f5, 5) .snippet-detail - width 70% + width 67% height calc(100% - 200px) position absolute left 33% @@ -149,66 +140,25 @@ body[data-theme="default"], body[data-theme="white"] .snippet-item-selected background darken($ui-backgroundColor, 5) -body[data-theme="dark"] - .snippets - background $ui-dark-backgroundColor - .snippet-item - color white - &::after - background $ui-dark-borderColor - &:hover - background darken($ui-dark-backgroundColor, 5) - .snippet-item-selected - background darken($ui-dark-backgroundColor, 5) - .snippet-detail - color white - .group-control-button - colorDarkPrimaryButton() +apply-theme(theme) + body[data-theme={theme}] + .snippets + background get-theme-var(theme, 'backgroundColor') + .snippet-item + color get-theme-var(theme, 'text-color') + &::after + background get-theme-var(theme, 'borderColor') + &:hover + background darken(get-theme-var(theme, 'backgroundColor'), 5) + .snippet-item-selected + background darken(get-theme-var(theme, 'backgroundColor'), 5) + .snippet-detail + color get-theme-var(theme, 'text-color') + .group-control-button + colorThemedPrimaryButton(theme) -body[data-theme="solarized-dark"] - .snippets - background $ui-solarized-dark-backgroundColor - .snippet-item - color white - &::after - background $ui-solarized-dark-borderColor - &:hover - background darken($ui-solarized-dark-backgroundColor, 5) - .snippet-item-selected - background darken($ui-solarized-dark-backgroundColor, 5) - .snippet-detail - color white - .group-control-button - colorSolarizedDarkPrimaryButton() +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) -body[data-theme="monokai"] - .snippets - background $ui-monokai-backgroundColor - .snippet-item - color White - &::after - background $ui-monokai-borderColor - &:hover - background darken($ui-monokai-backgroundColor, 5) - .snippet-item-selected - background darken($ui-monokai-backgroundColor, 5) - .snippet-detail - color white - .group-control-button - colorMonokaiPrimaryButton() - -body[data-theme="dracula"] - .snippets - background $ui-dracula-backgroundColor - .snippet-item - color #f8f8f2 - &::after - background $ui-dracula-borderColor - &:hover - background darken($ui-dracula-backgroundColor, 5) - .snippet-item-selected - background darken($ui-dracula-backgroundColor, 5) - .snippet-detail - color #f8f8f2 - .group-control-button - colorDraculaPrimaryButton() \ No newline at end of file +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index 3a2b075c..3cb18e30 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -4,7 +4,7 @@ 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 { store } from 'browser/main/store' import FolderList from './FolderList' import i18n from 'browser/lib/i18n' @@ -12,7 +12,7 @@ const { shell, remote } = require('electron') const { dialog } = remote class StorageItem extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -20,137 +20,156 @@ class StorageItem extends React.Component { } } - handleNewFolderButtonClick (e) { + handleNewFolderButtonClick(e) { const { storage } = this.props const input = { name: i18n.__('New Folder'), color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] } - dataApi.createFolder(storage.key, input) - .then((data) => { + dataApi + .createFolder(storage.key, input) + .then(data => { store.dispatch({ type: 'UPDATE_FOLDER', storage: data.storage }) }) - .catch((err) => { + .catch(err => { console.error(err) }) } - handleExternalButtonClick () { + handleExternalButtonClick() { const { storage } = this.props shell.showItemInFolder(storage.path) } - handleUnlinkButtonClick (e) { + handleUnlinkButtonClick(e) { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Unlink Storage'), - detail: i18n.__('Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.'), + detail: i18n.__( + 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.' + ), buttons: [i18n.__('Unlink'), i18n.__('Cancel')] }) if (index === 0) { const { storage } = this.props - dataApi.removeStorage(storage.key) + dataApi + .removeStorage(storage.key) .then(() => { store.dispatch({ type: 'REMOVE_STORAGE', storageKey: storage.key }) }) - .catch((err) => { + .catch(err => { throw err }) } } - handleLabelClick (e) { + handleLabelClick(e) { const { storage } = this.props - this.setState({ - isLabelEditing: true, - name: storage.name - }, () => { - this.refs.label.focus() - }) + this.setState( + { + isLabelEditing: true, + name: storage.name + }, + () => { + this.refs.label.focus() + } + ) } - handleLabelChange (e) { + handleLabelChange(e) { this.setState({ name: this.refs.label.value }) } - handleLabelBlur (e) { + handleLabelBlur(e) { const { storage } = this.props - dataApi - .renameStorage(storage.key, this.state.name) - .then((_storage) => { - store.dispatch({ - type: 'RENAME_STORAGE', - storage: _storage - }) - this.setState({ - isLabelEditing: false - }) + dataApi.renameStorage(storage.key, this.state.name).then(_storage => { + store.dispatch({ + type: 'RENAME_STORAGE', + storage: _storage }) + this.setState({ + isLabelEditing: false + }) + }) } - render () { + render() { const { storage, hostBoundingBox } = this.props return (
- {this.state.isLabelEditing - ?
- + this.handleLabelChange(e)} - onBlur={(e) => this.handleLabelBlur(e)} + onChange={e => this.handleLabelChange(e)} + onBlur={e => this.handleLabelBlur(e)} />
- :
this.handleLabelClick(e)} + ) : ( +
this.handleLabelClick(e)} > -   + +   {storage.name}  ({storage.path}) 
- } + )}
- - -
- +
) } diff --git a/browser/main/modals/PreferencesModal/StorageItem.styl b/browser/main/modals/PreferencesModal/StorageItem.styl index adcc483e..03fa65eb 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.styl +++ b/browser/main/modals/PreferencesModal/StorageItem.styl @@ -101,4 +101,19 @@ body[data-theme="solarized-dark"] .header-control-button border-color $ui-solarized-dark-button-backgroundColor background-color $ui-solarized-dark-button-backgroundColor - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +apply-theme(theme) + body[data-theme={theme}] + .header + border-color get-theme-var(theme, 'borderColor') + + .header-control-button + colorThemedPrimaryButton(theme) + border-color get-theme-var(theme, 'borderColor') + +for theme in 'dracula' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js index 046b24e6..9df1a153 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -3,30 +3,36 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StoragesTab.styl' import dataApi from 'browser/main/lib/dataApi' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import StorageItem from './StorageItem' import i18n from 'browser/lib/i18n' +import { humanFileSize } from 'browser/lib/utils' +import fs from 'fs' const electron = require('electron') const { shell, remote } = electron -function browseFolder () { +function browseFolder() { const dialog = remote.dialog const defaultPath = remote.app.getPath('home') return new Promise((resolve, reject) => { - dialog.showOpenDialog({ - title: i18n.__('Select Directory'), - defaultPath, - properties: ['openDirectory', 'createDirectory'] - }, function (targetPaths) { - if (targetPaths == null) return resolve('') - resolve(targetPaths[0]) - }) + dialog.showOpenDialog( + { + title: i18n.__('Select Directory'), + defaultPath, + properties: ['openDirectory', 'createDirectory'] + }, + function(targetPaths) { + if (targetPaths == null) return resolve('') + resolve(targetPaths[0]) + } + ) }) } class StoragesTab extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -35,60 +41,148 @@ class StoragesTab extends React.Component { name: 'Unnamed', type: 'FILESYSTEM', path: '' - } + }, + attachments: [] } + this.loadAttachmentStorage() } - handleAddStorageButton (e) { - this.setState({ - page: 'ADD_STORAGE', - newStorage: { - name: 'Unnamed', - type: 'FILESYSTEM', - path: '' - } - }, () => { - this.refs.addStorageName.select() + loadAttachmentStorage() { + const promises = [] + this.props.data.noteMap.map(note => { + const promise = attachmentManagement.getAttachmentsPathAndStatus( + note.content, + note.storage, + note.key + ) + if (promise) promises.push(promise) }) + + Promise.all(promises) + .then(data => { + const result = data.reduce((acc, curr) => acc.concat(curr), []) + this.setState({ attachments: result }) + }) + .catch(console.error) } - handleLinkClick (e) { + handleAddStorageButton(e) { + this.setState( + { + page: 'ADD_STORAGE', + newStorage: { + name: 'Unnamed', + type: 'FILESYSTEM', + path: '' + } + }, + () => { + this.refs.addStorageName.select() + } + ) + } + + handleLinkClick(e) { shell.openExternal(e.currentTarget.href) e.preventDefault() } - renderList () { - const { data, boundingBox } = this.props + handleRemoveUnusedAttachments(attachments) { + attachmentManagement + .removeAttachmentsByPaths(attachments) + .then(() => this.loadAttachmentStorage()) + .catch(console.error) + } - if (!boundingBox) { return null } - const storageList = data.storageMap.map((storage) => { - return + renderList() { + const { data, boundingBox } = this.props + const { attachments } = this.state + + const unusedAttachments = attachments.filter( + attachment => !attachment.isInUse + ) + const inUseAttachments = attachments.filter( + attachment => attachment.isInUse + ) + + const totalUnusedAttachments = unusedAttachments.length + const totalInuseAttachments = inUseAttachments.length + const totalAttachments = totalUnusedAttachments + totalInuseAttachments + + const totalUnusedAttachmentsSize = unusedAttachments.reduce((acc, curr) => { + const stats = fs.statSync(curr.path) + const fileSizeInBytes = stats.size + return acc + fileSizeInBytes + }, 0) + const totalInuseAttachmentsSize = inUseAttachments.reduce((acc, curr) => { + const stats = fs.statSync(curr.path) + const fileSizeInBytes = stats.size + return acc + fileSizeInBytes + }, 0) + const totalAttachmentsSize = + totalUnusedAttachmentsSize + totalInuseAttachmentsSize + + const unusedAttachmentPaths = unusedAttachments.reduce( + (acc, curr) => acc.concat(curr.path), + [] + ) + + if (!boundingBox) { + return null + } + const storageList = data.storageMap.map(storage => { + return ( + + ) }) return (
{i18n.__('Storage Locations')}
- {storageList.length > 0 - ? storageList - :
{i18n.__('No storage found.')}
- } + {storageList.length > 0 ? ( + storageList + ) : ( +
{i18n.__('No storage found.')}
+ )}
-
+
{i18n.__('Attachment storage')}
+

+ Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ( + {totalUnusedAttachments} items) +

+

+ In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ( + {totalInuseAttachments} items) +

+

+ Total attachments size: {humanFileSize(totalAttachmentsSize)} ( + {totalAttachments} items) +

+
) } - handleAddStorageBrowseButtonClick (e) { + handleAddStorageBrowseButtonClick(e) { browseFolder() - .then((targetPath) => { + .then(targetPath => { if (targetPath.length > 0) { const { newStorage } = this.state newStorage.path = targetPath @@ -97,13 +191,13 @@ class StoragesTab extends React.Component { }) } }) - .catch((err) => { + .catch(err => { console.error('BrowseFAILED') console.error(err) }) } - handleAddStorageChange (e) { + handleAddStorageChange(e) { const { newStorage } = this.state newStorage.name = this.refs.addStorageName.value newStorage.path = this.refs.addStoragePath.value @@ -112,13 +206,13 @@ class StoragesTab extends React.Component { }) } - handleAddStorageCreateButton (e) { + handleAddStorageCreateButton(e) { dataApi .addStorage({ name: this.state.newStorage.name, path: this.state.newStorage.path }) - .then((data) => { + .then(data => { const { dispatch } = this.props dispatch({ type: 'ADD_STORAGE', @@ -131,37 +225,39 @@ class StoragesTab extends React.Component { }) } - handleAddStorageCancelButton (e) { + handleAddStorageCancelButton(e) { this.setState({ page: 'LIST' }) } - renderAddStorage () { + renderAddStorage() { return (
-
{i18n.__('Add Storage')}
-
{i18n.__('Name')}
- this.handleAddStorageChange(e)} + onChange={e => this.handleAddStorageChange(e)} />
-
{i18n.__('Type')}
+
+ {i18n.__('Type')} +
-
-
{i18n.__('Location')} +
+ {i18n.__('Location')}
- this.handleAddStorageChange(e)} + onChange={e => this.handleAddStorageChange(e)} /> - @@ -195,21 +297,25 @@ class StoragesTab extends React.Component {
- - + +
-
-
) } - renderContent () { + renderContent() { switch (this.state.page) { case 'ADD_STORAGE': case 'ADD_FOLDER': @@ -220,12 +326,8 @@ class StoragesTab extends React.Component { } } - render () { - return ( -
- {this.renderContent()} -
- ) + render() { + return
{this.renderContent()}
} } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index 9a1a0ef8..285e9c60 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -1,8 +1,4 @@ -@import('./Tab') - -.root - padding 15px - color $ui-text-color +@import('./ConfigTab') .list margin-bottom 15px @@ -37,6 +33,17 @@ colorDefaultButton() font-size $tab--button-font-size border-radius 2px +.list-attachment-label + margin-bottom 10px + color $ui-text-color +.list-attachement-clear-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle + padding 0 20px .addStorage margin-bottom 15px @@ -158,119 +165,52 @@ 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 + .list-attachement-clear-button colorDarkPrimaryButton() - .addStorage-body-control-cancelButton - colorDarkDefaultButton() - border-color $ui-solarized-dark-borderColor -body[data-theme="monokai"] - .root - color $ui-monokai-text-color +apply-theme(theme) + body[data-theme={theme}] + .root + color get-theme-var(theme, 'text-color') - .folderList-item - border-bottom $ui-monokai-borderColor + .folderList-item + border-bottom get-theme-var(theme, 'borderColor') - .folderList-empty - color $ui-monokai-text-color + .folderList-empty + color get-theme-var(theme, 'text-color') - .list-empty - color $ui-monokai-text-color - .list-control-addStorageButton - border-color $ui-monokai-button-backgroundColor - background-color $ui-monokai-button-backgroundColor - color $ui-monokai-text-color + .list-empty + color get-theme-var(theme, 'text-color') + .list-control-addStorageButton + border-color get-theme-var(theme, 'button-backgroundColor') + background-color get-theme-var(theme, 'button-backgroundColor') + color get-theme-var(theme, 'text-color') - .addStorage-header - color $ui-monokai-text-color - border-color $ui-monokai-borderColor + .addStorage-header + color get-theme-var(theme, 'text-color') + border-color get-theme-var(theme, 'borderColor') - .addStorage-body-section-name-input - border-color $$ui-monokai-borderColor + .addStorage-body-section-name-input + border-color $get-theme-var(theme, 'borderColor') - .addStorage-body-section-type-description - color $ui-monokai-text-color + .addStorage-body-section-type-description + color get-theme-var(theme, 'text-color') - .addStorage-body-section-path-button - colorPrimaryButton() - .addStorage-body-control - border-color $ui-monokai-borderColor + .addStorage-body-section-path-button + colorPrimaryButton() + .addStorage-body-control + border-color get-theme-var(theme, 'borderColor') - .addStorage-body-control-createButton - colorDarkPrimaryButton() - .addStorage-body-control-cancelButton - colorDarkDefaultButton() - border-color $ui-monokai-borderColor + .addStorage-body-control-createButton + colorDarkPrimaryButton() + .addStorage-body-control-cancelButton + colorDarkDefaultButton() + border-color get-theme-var(theme, 'borderColor') + .list-attachement-clear-button + colorThemedPrimaryButton(theme) -body[data-theme="dracula"] - .root - color $ui-dracula-text-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) - .folderList-item - border-bottom $ui-dracula-borderColor - - .folderList-empty - color $ui-dracula-text-color - - .list-empty - color $ui-dracula-text-color - .list-control-addStorageButton - border-color $ui-dracula-button-backgroundColor - background-color $ui-dracula-button-backgroundColor - color $ui-dracula-text-color - - .addStorage-header - color $ui-dracula-text-color - border-color $ui-dracula-borderColor - - .addStorage-body-section-name-input - border-color $$ui-dracula-borderColor - - .addStorage-body-section-type-description - color $ui-dracula-text-color - - .addStorage-body-section-path-button - colorPrimaryButton() - .addStorage-body-control - border-color $ui-dracula-borderColor - - .addStorage-body-control-createButton - colorDarkPrimaryButton() - .addStorage-body-control-cancelButton - colorDarkDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file +for theme in $themes + apply-theme(theme) diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 9168a9f4..2ddb9e3a 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -3,7 +3,7 @@ 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 { store } from 'browser/main/store' import consts from 'browser/lib/consts' import ReactCodeMirror from 'react-codemirror' import CodeMirror from 'codemirror' @@ -12,6 +12,8 @@ import _ from 'lodash' import i18n from 'browser/lib/i18n' import { getLanguages } from 'browser/lib/Languages' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' +import uiThemes from 'browser/lib/ui-themes' +import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager' const OSX = global.process.platform === 'darwin' @@ -19,7 +21,7 @@ const electron = require('electron') const ipc = electron.ipcRenderer class UiTab extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { config: props.config, @@ -27,32 +29,49 @@ class UiTab extends React.Component { } } - componentDidMount () { - CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') + componentDidMount() { + CodeMirror.autoLoadMode( + this.codeMirrorInstance.getCodeMirror(), + 'javascript' + ) CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css') + CodeMirror.autoLoadMode( + this.customMarkdownLintConfigCM.getCodeMirror(), + 'javascript' + ) + CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript') + // Set CM editor Sizes this.customCSSCM.getCodeMirror().setSize('400px', '400px') + this.prettierConfigCM.getCodeMirror().setSize('400px', '400px') + this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px') + this.handleSettingDone = () => { - this.setState({UiAlert: { - type: 'success', - message: i18n.__('Successfully applied!') - }}) + this.setState({ + UiAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + } + }) } - this.handleSettingError = (err) => { - this.setState({UiAlert: { - type: 'error', - message: err.message != null ? err.message : i18n.__('An error occurred!') - }}) + this.handleSettingError = err => { + this.setState({ + UiAlert: { + type: 'error', + message: + err.message != null ? err.message : i18n.__('An error occurred!') + } + }) } ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) } - componentWillUnmount () { + componentWillUnmount() { ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone) ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) } - handleUIChange (e) { + handleUIChange(e) { const { codemirrorTheme } = this.state let checkHighLight = document.getElementById('checkHighLight') @@ -66,18 +85,25 @@ class UiTab extends React.Component { const newConfig = { ui: { theme: this.refs.uiTheme.value, + defaultTheme: this.refs.uiTheme.value, + enableScheduleTheme: this.refs.enableScheduleTheme.checked, + scheduledTheme: this.refs.uiScheduledTheme.value, + scheduleStart: this.refs.scheduleStart.value, + scheduleEnd: this.refs.scheduleEnd.value, language: this.refs.uiLanguage.value, defaultNote: this.refs.defaultNote.value, - tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked, + tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags + .checked, showCopyNotification: this.refs.showCopyNotification.checked, confirmDeletion: this.refs.confirmDeletion.checked, showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked, showTagsAlphabetically: this.refs.showTagsAlphabetically.checked, saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked, enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked, - disableDirectWrite: this.refs.uiD2w != null - ? this.refs.uiD2w.checked - : false + showScrollBar: this.refs.showScrollBar.checked, + showMenuBar: this.refs.showMenuBar.checked, + disableDirectWrite: + this.refs.uiD2w != null ? this.refs.uiD2w.checked : false }, editor: { theme: this.refs.editorTheme.value, @@ -88,6 +114,7 @@ class UiTab extends React.Component { enableRulers: this.refs.enableEditorRulers.value === 'true', rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','), displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, + lineWrapping: this.refs.editorLineWrapping.checked, switchPreview: this.refs.editorSwitchPreview.value, keyMap: this.refs.editorKeyMap.value, snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, @@ -97,13 +124,23 @@ class UiTab extends React.Component { enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, frontMatterTitleField: this.refs.frontMatterTitleField.value, matchingPairs: this.refs.matchingPairs.value, + matchingCloseBefore: this.refs.matchingCloseBefore.value, matchingTriples: this.refs.matchingTriples.value, explodingPairs: this.refs.explodingPairs.value, codeBlockMatchingPairs: this.refs.codeBlockMatchingPairs.value, + codeBlockMatchingCloseBefore: this.refs.codeBlockMatchingCloseBefore + .value, codeBlockMatchingTriples: this.refs.codeBlockMatchingTriples.value, codeBlockExplodingPairs: this.refs.codeBlockExplodingPairs.value, spellcheck: this.refs.spellcheck.checked, - enableSmartPaste: this.refs.enableSmartPaste.checked + enableSmartPaste: this.refs.enableSmartPaste.checked, + enableMarkdownLint: this.refs.enableMarkdownLint.checked, + customMarkdownLintConfig: this.customMarkdownLintConfigCM + .getCodeMirror() + .getValue(), + prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), + deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked, + rtlEnabled: this.refs.rtlEnabled.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -121,6 +158,7 @@ class UiTab extends React.Component { breaks: this.refs.previewBreaks.checked, smartArrows: this.refs.previewSmartArrows.checked, sanitize: this.refs.previewSanitize.value, + mermaidHTMLLabel: this.refs.previewMermaidHTMLLabel.checked, allowCustomCSS: this.refs.previewAllowCustomCSS.checked, lineThroughCheckbox: this.refs.lineThroughCheckbox.checked, customCSS: this.customCSSCM.getCodeMirror().getValue() @@ -130,30 +168,43 @@ class UiTab extends React.Component { const newCodemirrorTheme = this.refs.editorTheme.value if (newCodemirrorTheme !== codemirrorTheme) { - checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`) - } - this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }, () => { - const {ui, editor, preview} = this.props.config - this.currentConfig = {ui, editor, preview} - if (_.isEqual(this.currentConfig, this.state.config)) { - this.props.haveToSave() - } else { - this.props.haveToSave({ - tab: 'UI', - type: 'warning', - message: i18n.__('Unsaved Changes!') - }) + const theme = consts.THEMES.find( + theme => theme.name === newCodemirrorTheme + ) + + if (theme) { + checkHighLight.setAttribute('href', theme.path) } - }) + } + + this.setState( + { config: newConfig, codemirrorTheme: newCodemirrorTheme }, + () => { + const { ui, editor, preview } = this.props.config + this.currentConfig = { ui, editor, preview } + if (_.isEqual(this.currentConfig, this.state.config)) { + this.props.haveToSave() + } else { + this.props.haveToSave({ + tab: 'UI', + type: 'warning', + message: i18n.__('Unsaved Changes!') + }) + } + } + ) } - handleSaveUIClick (e) { + handleSaveUIClick(e) { const newConfig = { ui: this.state.config.ui, editor: this.state.config.editor, preview: this.state.config.preview } + chooseTheme(newConfig) + applyTheme(newConfig.ui.theme) + ConfigManager.set(newConfig) store.dispatch({ @@ -164,7 +215,7 @@ class UiTab extends React.Component { this.props.haveToSave() } - clearMessage () { + clearMessage() { _.debounce(() => { this.setState({ UiAlert: null @@ -172,17 +223,32 @@ class UiTab extends React.Component { }, 2000)() } - render () { + formatTime(time) { + let hour = Math.floor(time / 60) + let minute = time % 60 + + if (hour < 10) { + hour = '0' + hour + } + + if (minute < 10) { + minute = '0' + minute + } + + return `${hour}:${minute}` + } + + render() { const UiAlert = this.state.UiAlert - const UiAlertElement = UiAlert != null - ?

- {UiAlert.message} -

- : null + 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};' + 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};' const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none' const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily) return ( @@ -195,32 +261,147 @@ class UiTab extends React.Component { {i18n.__('Interface Theme')}
- this.handleUIChange(e)} ref='uiTheme' > - - - - - - + + {uiThemes + .filter(theme => !theme.isDark) + .sort((a, b) => a.label.localeCompare(b.label)) + .map(theme => { + return ( + + ) + })} + + + {uiThemes + .filter(theme => theme.isDark) + .sort((a, b) => a.label.localeCompare(b.label)) + .map(theme => { + return ( + + ) + })} +
+
{i18n.__('Theme Schedule')}
+
+ +
+
+
+ {i18n.__('Scheduled Theme')} +
+
+ +
+
+
+
+
+ {`End: ${this.formatTime(config.ui.scheduleEnd)}`} + this.handleUIChange(e)} + /> +
+
+ {`Start: ${this.formatTime(config.ui.scheduleStart)}`} + this.handleUIChange(e)} + /> +
+
+ 00:00 + 24:00 +
+
+
-
- {i18n.__('Language')} -
+
{i18n.__('Language')}
- this.handleUIChange(e)} ref='uiLanguage' > - { - getLanguages().map((language) => ) - } + {getLanguages().map(language => ( + + ))}
@@ -230,12 +411,15 @@ class UiTab extends React.Component { {i18n.__('Default New Note')}
- this.handleUIChange(e)} ref='defaultNote' > - +
@@ -243,93 +427,133 @@ class UiTab extends React.Component {
+
+
+
- { - global.process.platform === 'win32' - ?
+ {global.process.platform === 'win32' ? ( +
- : null - } - -
Tags
- + ) : null}
+
+
Tags
+
+
@@ -337,21 +561,22 @@ class UiTab extends React.Component {
Editor
-
- {i18n.__('Editor Theme')} -
+
{i18n.__('Editor Theme')}
- -
+
(this.codeMirrorInstance = e)} value={codemirrorSampleCode} @@ -360,7 +585,8 @@ class UiTab extends React.Component { readOnly: true, mode: 'javascript', theme: codemirrorTheme - }} /> + }} + />
@@ -369,10 +595,11 @@ class UiTab extends React.Component { {i18n.__('Editor Font Size')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -382,10 +609,11 @@ class UiTab extends React.Component { {i18n.__('Editor Font Family')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -395,18 +623,21 @@ class UiTab extends React.Component { {i18n.__('Editor Indent Style')}
-   - this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} > @@ -420,23 +651,21 @@ class UiTab extends React.Component {
-
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -447,12 +676,15 @@ class UiTab extends React.Component { {i18n.__('Switch to Preview')}
-
@@ -463,15 +695,20 @@ class UiTab extends React.Component { {i18n.__('Editor Keymap')}
- -

{i18n.__('⚠️ Please restart boostnote after you change the keymap')}

+

+ {i18n.__( + '⚠️ Please restart boostnote after you change the keymap' + )} +

@@ -480,13 +717,21 @@ class UiTab extends React.Component { {i18n.__('Snippet Default Language')}
-
@@ -496,10 +741,11 @@ class UiTab extends React.Component { {i18n.__('Front matter title field')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -510,10 +756,11 @@ class UiTab extends React.Component { {i18n.__('Matching character pairs')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -524,10 +771,41 @@ class UiTab extends React.Component { {i18n.__('in code blocks')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} + type='text' + /> +
+
+ +
+
+ {i18n.__('Close pairs before')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+
+ {i18n.__('in code blocks')} +
+
+ this.handleUIChange(e)} type='text' />
@@ -538,10 +816,11 @@ class UiTab extends React.Component { {i18n.__('Matching character triples')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -552,10 +831,11 @@ class UiTab extends React.Component { {i18n.__('in code blocks')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -566,10 +846,11 @@ class UiTab extends React.Component { {i18n.__('Exploding character pairs')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -580,10 +861,11 @@ class UiTab extends React.Component { {i18n.__('in code blocks')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -591,80 +873,176 @@ class UiTab extends React.Component {
+
+ +
+
+
+ +
+
+ +
+ +
+
+ {i18n.__('Custom MarkdownLint Rules')} +
+
+ this.handleUIChange(e)} + checked={this.state.config.editor.enableMarkdownLint} + ref='enableMarkdownLint' + type='checkbox' + /> +   + {i18n.__('Enable MarkdownLint')} +
+ this.handleUIChange(e)} + ref={e => (this.customMarkdownLintConfigCM = e)} + value={config.editor.customMarkdownLintConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + theme: codemirrorTheme, + lint: true, + gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ] + }} + /> +
+
+
{i18n.__('Preview')}
@@ -672,10 +1050,11 @@ class UiTab extends React.Component { {i18n.__('Preview Font Size')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -685,126 +1064,165 @@ class UiTab extends React.Component { {i18n.__('Preview Font Family')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
-
{i18n.__('Code Block Theme')}
+
+ {i18n.__('Code Block Theme')} +
-
-
- {i18n.__('Sanitization')} -
+
{i18n.__('Sanitization')}
-
+
+ +
{i18n.__('LaTeX Inline Open Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -814,10 +1232,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Inline Close Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -827,10 +1246,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Block Open Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -840,10 +1260,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Block Close Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -853,26 +1274,27 @@ class UiTab extends React.Component { {i18n.__('PlantUML Server')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
-
- {i18n.__('Custom CSS')} -
+
{i18n.__('Custom CSS')}
- this.handleUIChange(e)} + this.handleUIChange(e)} checked={config.preview.allowCustomCSS} ref='previewAllowCustomCSS' type='checkbox' - />  + /> +   {i18n.__('Allow custom CSS for preview')} -
+
+ }} + /> +
+
+
+
+
+ {i18n.__('Prettier Config')} +
+
+
+ this.handleUIChange(e)} + ref={e => (this.prettierConfigCM = e)} + value={config.editor.prettierConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + lint: true, + theme: codemirrorTheme + }} + />
-
- {UiAlertElement}
diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index f3fc3751..2c14e6c7 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -16,7 +16,7 @@ import _ from 'lodash' import i18n from 'browser/lib/i18n' class Preferences extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -27,44 +27,39 @@ class Preferences extends React.Component { } } - componentDidMount () { + componentDidMount() { this.refs.root.focus() const boundingBox = this.getContentBoundingBox() this.setState({ boundingBox }) } - switchTeam (teamId) { - this.setState({currentTeamId: teamId}) + switchTeam(teamId) { + this.setState({ currentTeamId: teamId }) } - handleNavButtonClick (tab) { - return (e) => { - this.setState({currentTab: tab}) + handleNavButtonClick(tab) { + return e => { + this.setState({ currentTab: tab }) } } - handleEscButtonClick () { + handleEscButtonClick() { this.props.close() } - renderContent () { + renderContent() { const { boundingBox } = this.state const { dispatch, config, data } = this.props switch (this.state.currentTab) { case 'INFO': - return ( - - ) + return case 'HOTKEY': return ( this.setState({HotkeyAlert: alert})} + haveToSave={alert => this.setState({ HotkeyAlert: alert })} /> ) case 'UI': @@ -72,29 +67,21 @@ class Preferences extends React.Component { this.setState({UIAlert: alert})} + haveToSave={alert => this.setState({ UIAlert: alert })} /> ) case 'CROWDFUNDING': - return ( - - ) + return case 'BLOG': return ( this.setState({BlogAlert: alert})} + haveToSave={alert => this.setState({ BlogAlert: alert })} /> ) case 'SNIPPET': - return ( - - ) + return case 'STORAGES': default: return ( @@ -107,67 +94,69 @@ class Preferences extends React.Component { } } - handleKeyDown (e) { + handleKeyDown(e) { if (e.keyCode === 27) { this.props.close() } } - getContentBoundingBox () { + getContentBoundingBox() { return this.refs.content.getBoundingClientRect() } - haveToSaveNotif (type, message) { - return ( -

{message}

- ) + haveToSaveNotif(type, message) { + return

{message}

} - render () { + render() { const content = this.renderContent() const tabs = [ - {target: 'STORAGES', label: i18n.__('Storage')}, - {target: 'HOTKEY', label: i18n.__('Hotkeys'), Hotkey: this.state.HotkeyAlert}, - {target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert}, - {target: 'INFO', label: i18n.__('About')}, - {target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')}, - {target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert}, - {target: 'SNIPPET', label: i18n.__('Snippets')} + { target: 'STORAGES', label: i18n.__('Storage') }, + { + target: 'HOTKEY', + label: i18n.__('Hotkeys'), + Hotkey: this.state.HotkeyAlert + }, + { target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert }, + { target: 'INFO', label: i18n.__('About') }, + { target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') }, + { target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert }, + { target: 'SNIPPET', label: i18n.__('Snippets') } ] - const navButtons = tabs.map((tab) => { + const navButtons = tabs.map(tab => { const isActive = this.state.currentTab === tab.target - const isUiHotkeyTab = _.isObject(tab[tab.label]) && tab.label === tab[tab.label].tab + const isUiHotkeyTab = + _.isObject(tab[tab.label]) && tab.label === tab[tab.label].tab return ( - ) }) return ( -
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >

{i18n.__('Your preferences for Boostnote')}

- this.handleEscButtonClick(e)} /> -
- {navButtons} -
+ this.handleEscButtonClick(e)} + /> +
{navButtons}
{content}
@@ -181,4 +170,4 @@ Preferences.propTypes = { dispatch: PropTypes.func } -export default connect((x) => x)(CSSModules(Preferences, styles)) +export default connect(x => x)(CSSModules(Preferences, styles)) diff --git a/browser/main/modals/RenameFolderModal.js b/browser/main/modals/RenameFolderModal.js index edbcee67..93823e41 100644 --- a/browser/main/modals/RenameFolderModal.js +++ b/browser/main/modals/RenameFolderModal.js @@ -1,14 +1,14 @@ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' -import styles from './RenameFolderModal.styl' +import styles from './RenameModal.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' class RenameFolderModal extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -16,39 +16,39 @@ class RenameFolderModal extends React.Component { } } - componentDidMount () { + componentDidMount() { this.refs.name.focus() this.refs.name.select() } - handleCloseButtonClick (e) { + handleCloseButtonClick(e) { this.props.close() } - handleChange (e) { + handleChange(e) { this.setState({ name: this.refs.name.value }) } - handleKeyDown (e) { + handleKeyDown(e) { if (e.keyCode === 27) { this.props.close() } } - handleInputKeyDown (e) { + handleInputKeyDown(e) { switch (e.keyCode) { case 13: this.confirm() } } - handleConfirmButtonClick (e) { + handleConfirmButtonClick(e) { this.confirm() } - confirm () { + confirm() { if (this.state.name.trim().length > 0) { const { storage, folder } = this.props dataApi @@ -56,7 +56,7 @@ class RenameFolderModal extends React.Component { name: this.state.name, color: folder.color }) - .then((data) => { + .then(data => { store.dispatch({ type: 'UPDATE_FOLDER', storage: data.storage @@ -66,27 +66,32 @@ class RenameFolderModal extends React.Component { } } - render () { + render() { return ( -
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >
{i18n.__('Rename Folder')}
- this.handleCloseButtonClick(e)} /> + this.handleCloseButtonClick(e)} + />
- this.handleChange(e)} - onKeyDown={(e) => this.handleInputKeyDown(e)} + onChange={e => this.handleChange(e)} + onKeyDown={e => this.handleInputKeyDown(e)} /> - diff --git a/browser/main/modals/RenameFolderModal.styl b/browser/main/modals/RenameModal.styl similarity index 56% rename from browser/main/modals/RenameFolderModal.styl rename to browser/main/modals/RenameModal.styl index c9909d00..f1a37b33 100644 --- a/browser/main/modals/RenameFolderModal.styl +++ b/browser/main/modals/RenameModal.styl @@ -43,23 +43,36 @@ border-radius 2px padding 0 25px margin 0 auto + font-size 14px colorPrimaryButton() -body[data-theme="dark"] - .root - modalDark() +.error + text-align center + color #F44336 + height 20px - .header - background-color $ui-dark-button--hover-backgroundColor - border-color $ui-dark-borderColor - color $ui-dark-text-color +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent - .description - color $ui-inactive-text-color + .header + background-color transparent + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') - .control-input - border-color $ui-dark-borderColor - color $ui-dark-text-color + .description + color $ui-inactive-text-color - .control-confirmButton - colorDarkPrimaryButton() + .control-input + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') + + .control-confirmButton + colorThemedPrimaryButton(theme) + +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) diff --git a/browser/main/modals/RenameTagModal.js b/browser/main/modals/RenameTagModal.js new file mode 100644 index 00000000..8d2d0249 --- /dev/null +++ b/browser/main/modals/RenameTagModal.js @@ -0,0 +1,196 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './RenameModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' +import { replace } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' +import { isEmpty } from 'lodash' +import electron from 'electron' + +const { remote } = electron +const { dialog } = remote + +class RenameTagModal extends React.Component { + constructor(props) { + super(props) + + this.nameInput = null + + this.handleChange = this.handleChange.bind(this) + + this.setTextInputRef = el => { + this.nameInput = el + } + + this.state = { + name: props.tagName, + oldName: props.tagName + } + } + + componentDidMount() { + this.nameInput.focus() + this.nameInput.select() + } + + handleChange(e) { + this.setState({ + name: this.nameInput.value, + showerror: false, + errormessage: '' + }) + } + + handleKeyDown(e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown(e) { + switch (e.keyCode) { + case 13: + this.handleConfirm() + } + } + + handleConfirm() { + if (this.state.name.trim().length > 0) { + const { name, oldName } = this.state + this.renameTag(oldName, name) + } + } + + showError(message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + renameTag(tag, updatedTag) { + const { data, dispatch } = this.props + + if (tag === updatedTag) { + // confirm with-out any change - just dismiss the modal + this.props.close() + return + } + + if ( + data.noteMap + .map(note => note) + .some(note => note.tags.indexOf(updatedTag) !== -1) + ) { + const alertConfig = { + type: 'warning', + message: i18n.__('Confirm tag merge'), + detail: i18n.__( + `Tag ${tag} will be merged with existing tag ${updatedTag}` + ), + buttons: [i18n.__('Confirm'), i18n.__('Cancel')] + } + + const dialogButtonIndex = dialog.showMessageBox( + remote.getCurrentWindow(), + alertConfig + ) + + if (dialogButtonIndex === 1) { + return // bail early on cancel click + } + } + + const notes = data.noteMap + .map(note => note) + .filter( + note => note.tags.indexOf(tag) !== -1 && note.tags.indexOf(updatedTag) + ) + .map(note => { + note = Object.assign({}, note) + note.tags = note.tags.slice() + + note.tags[note.tags.indexOf(tag)] = updatedTag + + return note + }) + + if (isEmpty(notes)) { + this.showError(i18n.__('Tag exists')) + + return + } + + Promise.all( + notes.map(note => dataApi.updateNote(note.storage, note.key, note)) + ) + .then(updatedNotes => { + updatedNotes.forEach(note => { + dispatch({ + type: 'UPDATE_NOTE', + note + }) + }) + }) + .then(() => { + if (window.location.hash.includes(tag)) { + dispatch(replace(`/tags/${updatedTag}`)) + } + ee.emit('sidebar:rename-tag', { tag, updatedTag }) + this.props.close() + }) + } + + render() { + const { close } = this.props + const { errormessage } = this.state + + return ( +
this.handleKeyDown(e)} + > +
+
{i18n.__('Rename Tag')}
+
+ + +
+ this.handleInputKeyDown(e)} + /> + +
+
+ {errormessage} +
+
+ ) + } +} + +RenameTagModal.propTypes = { + storage: PropTypes.shape({ + key: PropTypes.string + }), + folder: PropTypes.shape({ + key: PropTypes.string, + name: PropTypes.string + }) +} + +export default CSSModules(RenameTagModal, styles) diff --git a/browser/main/store.js b/browser/main/store.js index 11ff2f3f..d48946a6 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -1,10 +1,12 @@ -import { combineReducers, createStore } from 'redux' -import { routerReducer } from 'react-router-redux' +import { combineReducers, createStore, compose, applyMiddleware } from 'redux' +import { connectRouter, routerMiddleware } from 'connected-react-router' +import { createHashHistory as createHistory } from 'history' import ConfigManager from 'browser/main/lib/ConfigManager' import { Map, Set } from 'browser/lib/Mutable' import _ from 'lodash' +import DevTools from './DevTools' -function defaultDataMap () { +function defaultDataMap() { return { storageMap: new Map(), noteMap: new Map(), @@ -16,16 +18,16 @@ function defaultDataMap () { } } -function data (state = defaultDataMap(), action) { +function data(state = defaultDataMap(), action) { switch (action.type) { case 'INIT_ALL': state = defaultDataMap() - action.storages.forEach((storage) => { + action.storages.forEach(storage => { state.storageMap.set(storage.key, storage) }) - action.notes.some((note) => { + action.notes.some(note => { if (note === undefined) return true const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder @@ -38,182 +40,184 @@ function data (state = defaultDataMap(), action) { if (note.isTrashed) { state.trashedSet.add(uniqueKey) } - const storageNoteList = getOrInitItem(state.storageNoteMap, note.storage) + const storageNoteList = getOrInitItem( + state.storageNoteMap, + note.storage + ) storageNoteList.add(uniqueKey) const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey) folderNoteSet.add(uniqueKey) - assignToTags(note.tags, state, uniqueKey) + if (!note.isTrashed) { + assignToTags(note.tags, state, uniqueKey) + } }) return state - case 'UPDATE_NOTE': - { - const note = action.note - const uniqueKey = note.key - const folderKey = note.storage + '-' + note.folder - const oldNote = state.noteMap.get(uniqueKey) + case 'UPDATE_NOTE': { + const note = action.note + const uniqueKey = note.key + const folderKey = note.storage + '-' + note.folder + const oldNote = state.noteMap.get(uniqueKey) - state = Object.assign({}, state) - state.noteMap = new Map(state.noteMap) - state.noteMap.set(uniqueKey, note) + state = Object.assign({}, state) + state.noteMap = new Map(state.noteMap) + state.noteMap.set(uniqueKey, note) - updateStarredChange(oldNote, note, state, uniqueKey) + updateStarredChange(oldNote, note, state, uniqueKey) - if (oldNote == null || oldNote.isTrashed !== note.isTrashed) { - state.trashedSet = new Set(state.trashedSet) - if (note.isTrashed) { - state.trashedSet.add(uniqueKey) - state.starredSet.delete(uniqueKey) - removeFromTags(note.tags, state, uniqueKey) - } else { - state.trashedSet.delete(uniqueKey) - - assignToTags(note.tags, state, uniqueKey) - - if (note.isStarred) { - state.starredSet.add(uniqueKey) - } - } - } - - // Update storageNoteMap if oldNote doesn't exist - if (oldNote == null) { - state.storageNoteMap = new Map(state.storageNoteMap) - let storageNoteSet = state.storageNoteMap.get(note.storage) - storageNoteSet = new Set(storageNoteSet) - storageNoteSet.add(uniqueKey) - state.storageNoteMap.set(note.storage, storageNoteSet) - } - - // Update foldermap if folder changed or post created - updateFolderChange(oldNote, note, state, folderKey, uniqueKey) - - if (oldNote != null) { - updateTagChanges(oldNote, note, state, uniqueKey) + if (oldNote == null || oldNote.isTrashed !== note.isTrashed) { + state.trashedSet = new Set(state.trashedSet) + if (note.isTrashed) { + state.trashedSet.add(uniqueKey) + state.starredSet.delete(uniqueKey) + removeFromTags(note.tags, state, uniqueKey) } else { + state.trashedSet.delete(uniqueKey) + assignToTags(note.tags, state, uniqueKey) - } - return state + if (note.isStarred) { + state.starredSet.add(uniqueKey) + } + } } - case 'MOVE_NOTE': - { - const originNote = action.originNote - const originKey = originNote.key - const note = action.note - const uniqueKey = note.key - const folderKey = note.storage + '-' + note.folder - const oldNote = state.noteMap.get(uniqueKey) - state = Object.assign({}, state) - state.noteMap = new Map(state.noteMap) - state.noteMap.delete(originKey) - state.noteMap.set(uniqueKey, note) + // Update storageNoteMap if oldNote doesn't exist + if (oldNote == null) { + state.storageNoteMap = new Map(state.storageNoteMap) + let storageNoteSet = state.storageNoteMap.get(note.storage) + storageNoteSet = new Set(storageNoteSet) + storageNoteSet.add(uniqueKey) + state.storageNoteMap.set(note.storage, storageNoteSet) + } - // If storage chanced, origin key must be discarded - if (originKey !== uniqueKey) { - // From isStarred - if (originNote.isStarred) { - state.starredSet = new Set(state.starredSet) - state.starredSet.delete(originKey) - } + // Update foldermap if folder changed or post created + updateFolderChange(oldNote, note, state, folderKey, uniqueKey) - if (originNote.isTrashed) { - state.trashedSet = new Set(state.trashedSet) - state.trashedSet.delete(originKey) - } + if (oldNote != null) { + updateTagChanges(oldNote, note, state, uniqueKey) + } else { + assignToTags(note.tags, state, uniqueKey) + } - // From storageNoteMap - state.storageNoteMap = new Map(state.storageNoteMap) - let noteSet = state.storageNoteMap.get(originNote.storage) - noteSet = new Set(noteSet) - noteSet.delete(originKey) - state.storageNoteMap.set(originNote.storage, noteSet) + return state + } + case 'MOVE_NOTE': { + const originNote = action.originNote + const originKey = originNote.key + const note = action.note + const uniqueKey = note.key + const folderKey = note.storage + '-' + note.folder + const oldNote = state.noteMap.get(uniqueKey) - // From folderNoteMap - state.folderNoteMap = new Map(state.folderNoteMap) - const originFolderKey = originNote.storage + '-' + originNote.folder - let originFolderList = state.folderNoteMap.get(originFolderKey) - originFolderList = new Set(originFolderList) - originFolderList.delete(originKey) - state.folderNoteMap.set(originFolderKey, originFolderList) + state = Object.assign({}, state) + state.noteMap = new Map(state.noteMap) + state.noteMap.delete(originKey) + state.noteMap.set(uniqueKey, note) - removeFromTags(originNote.tags, state, originKey) + // If storage chanced, origin key must be discarded + if (originKey !== uniqueKey) { + // From isStarred + if (originNote.isStarred) { + state.starredSet = new Set(state.starredSet) + state.starredSet.delete(originKey) } - updateStarredChange(oldNote, note, state, uniqueKey) - - if (oldNote == null || oldNote.isTrashed !== note.isTrashed) { + if (originNote.isTrashed) { state.trashedSet = new Set(state.trashedSet) - if (note.isTrashed) { - state.trashedSet.add(uniqueKey) - } else { - state.trashedSet.delete(uniqueKey) - } + state.trashedSet.delete(originKey) } - // Update storageNoteMap if oldNote doesn't exist - if (oldNote == null) { - state.storageNoteMap = new Map(state.storageNoteMap) - let noteSet = state.storageNoteMap.get(note.storage) - noteSet = new Set(noteSet) - noteSet.add(uniqueKey) - state.storageNoteMap.set(folderKey, noteSet) - } - - // Update foldermap if folder changed or post created - updateFolderChange(oldNote, note, state, folderKey, uniqueKey) - - // Remove from old folder map - if (oldNote != null) { - updateTagChanges(oldNote, note, state, uniqueKey) - } else { - assignToTags(note.tags, state, uniqueKey) - } - - return state - } - case 'DELETE_NOTE': - { - const uniqueKey = action.noteKey - const targetNote = state.noteMap.get(uniqueKey) - - state = Object.assign({}, state) - // From storageNoteMap state.storageNoteMap = new Map(state.storageNoteMap) - let noteSet = state.storageNoteMap.get(targetNote.storage) + let noteSet = state.storageNoteMap.get(originNote.storage) noteSet = new Set(noteSet) - noteSet.delete(uniqueKey) - state.storageNoteMap.set(targetNote.storage, noteSet) + noteSet.delete(originKey) + state.storageNoteMap.set(originNote.storage, noteSet) - if (targetNote != null) { - // From isStarred - if (targetNote.isStarred) { - state.starredSet = new Set(state.starredSet) - state.starredSet.delete(uniqueKey) - } + // From folderNoteMap + state.folderNoteMap = new Map(state.folderNoteMap) + const originFolderKey = originNote.storage + '-' + originNote.folder + let originFolderList = state.folderNoteMap.get(originFolderKey) + originFolderList = new Set(originFolderList) + originFolderList.delete(originKey) + state.folderNoteMap.set(originFolderKey, originFolderList) - if (targetNote.isTrashed) { - state.trashedSet = new Set(state.trashedSet) - state.trashedSet.delete(uniqueKey) - } - - // From folderNoteMap - const folderKey = targetNote.storage + '-' + targetNote.folder - state.folderNoteMap = new Map(state.folderNoteMap) - let folderSet = state.folderNoteMap.get(folderKey) - folderSet = new Set(folderSet) - folderSet.delete(uniqueKey) - state.folderNoteMap.set(folderKey, folderSet) - - removeFromTags(targetNote.tags, state, uniqueKey) - } - state.noteMap = new Map(state.noteMap) - state.noteMap.delete(uniqueKey) - return state + removeFromTags(originNote.tags, state, originKey) } + + updateStarredChange(oldNote, note, state, uniqueKey) + + 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) + let noteSet = state.storageNoteMap.get(note.storage) + noteSet = new Set(noteSet) + noteSet.add(uniqueKey) + state.storageNoteMap.set(folderKey, noteSet) + } + + // Update foldermap if folder changed or post created + updateFolderChange(oldNote, note, state, folderKey, uniqueKey) + + // Remove from old folder map + if (oldNote != null) { + updateTagChanges(oldNote, note, state, uniqueKey) + } else { + assignToTags(note.tags, state, uniqueKey) + } + + return state + } + case 'DELETE_NOTE': { + const uniqueKey = action.noteKey + const targetNote = state.noteMap.get(uniqueKey) + + state = Object.assign({}, state) + + // From storageNoteMap + state.storageNoteMap = new Map(state.storageNoteMap) + let noteSet = state.storageNoteMap.get(targetNote.storage) + noteSet = new Set(noteSet) + noteSet.delete(uniqueKey) + state.storageNoteMap.set(targetNote.storage, noteSet) + + if (targetNote != null) { + // From isStarred + if (targetNote.isStarred) { + state.starredSet = new Set(state.starredSet) + state.starredSet.delete(uniqueKey) + } + + if (targetNote.isTrashed) { + state.trashedSet = new Set(state.trashedSet) + state.trashedSet.delete(uniqueKey) + } + + // From folderNoteMap + const folderKey = targetNote.storage + '-' + targetNote.folder + state.folderNoteMap = new Map(state.folderNoteMap) + let folderSet = state.folderNoteMap.get(folderKey) + folderSet = new Set(folderSet) + folderSet.delete(uniqueKey) + state.folderNoteMap.set(folderKey, folderSet) + + removeFromTags(targetNote.tags, state, uniqueKey) + } + state.noteMap = new Map(state.noteMap) + state.noteMap.delete(uniqueKey) + return state + } case 'UPDATE_FOLDER': case 'REORDER_FOLDER': case 'EXPORT_FOLDER': @@ -243,7 +247,7 @@ function data (state = defaultDataMap(), action) { state.storageNoteMap.set(action.storage.key, storageNoteSet) if (noteSet != null) { - noteSet.forEach(function handleNoteKey (noteKey) { + noteSet.forEach(function handleNoteKey(noteKey) { // Get note from noteMap const note = state.noteMap.get(noteKey) if (note != null) { @@ -265,7 +269,7 @@ function data (state = defaultDataMap(), action) { // Delete key from tag map state.tagNoteMap = new Map(state.tagNoteMap) - note.tags.forEach((tag) => { + note.tags.forEach(tag => { const tagNoteSet = getOrInitItem(state.tagNoteMap, tag) tagNoteSet.delete(noteKey) }) @@ -284,7 +288,7 @@ function data (state = defaultDataMap(), action) { state.storageNoteMap.set(action.storage.key, new Set()) state.folderNoteMap = new Map(state.folderNoteMap) state.tagNoteMap = new Map(state.tagNoteMap) - action.notes.forEach((note) => { + action.notes.forEach(note => { const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder state.noteMap.set(uniqueKey, note) @@ -303,7 +307,7 @@ function data (state = defaultDataMap(), action) { } folderNoteSet.add(uniqueKey) - note.tags.forEach((tag) => { + note.tags.forEach(tag => { const tagNoteSet = getOrInitItem(state.tagNoteMap, tag) tagNoteSet.add(uniqueKey) }) @@ -318,7 +322,7 @@ function data (state = defaultDataMap(), action) { // Remove folders from folderMap if (storage != null) { state.folderMap = new Map(state.folderMap) - storage.folders.forEach((folder) => { + storage.folders.forEach(folder => { const folderKey = storage.key + '-' + folder.key state.folderMap.delete(folderKey) }) @@ -330,17 +334,17 @@ function data (state = defaultDataMap(), action) { state.storageNoteMap.delete(action.storageKey) if (storageNoteSet != null) { const notes = storageNoteSet - .map((noteKey) => state.noteMap.get(noteKey)) - .filter((note) => note != null) + .map(noteKey => state.noteMap.get(noteKey)) + .filter(note => note != null) state.noteMap = new Map(state.noteMap) state.tagNoteMap = new Map(state.tagNoteMap) state.starredSet = new Set(state.starredSet) - notes.forEach((note) => { + notes.forEach(note => { const noteKey = note.key state.noteMap.delete(noteKey) state.starredSet.delete(noteKey) - note.tags.forEach((tag) => { + note.tags.forEach(tag => { let tagNoteSet = state.tagNoteMap.get(tag) tagNoteSet = new Set(tagNoteSet) tagNoteSet.delete(noteKey) @@ -360,7 +364,7 @@ function data (state = defaultDataMap(), action) { const defaultConfig = ConfigManager.get() -function config (state = defaultConfig, action) { +function config(state = defaultConfig, action) { switch (action.type) { case 'SET_IS_SIDENAV_FOLDED': state.isSideNavFolded = action.isFolded @@ -386,7 +390,7 @@ const defaultStatus = { updateReady: false } -function status (state = defaultStatus, action) { +function status(state = defaultStatus, action) { switch (action.type) { case 'UPDATE_AVAILABLE': return Object.assign({}, defaultStatus, { @@ -396,7 +400,7 @@ function status (state = defaultStatus, action) { return state } -function updateStarredChange (oldNote, note, state, uniqueKey) { +function updateStarredChange(oldNote, note, state, uniqueKey) { if (oldNote == null || oldNote.isStarred !== note.isStarred) { state.starredSet = new Set(state.starredSet) if (note.isStarred) { @@ -407,7 +411,7 @@ function updateStarredChange (oldNote, note, state, uniqueKey) { } } -function updateFolderChange (oldNote, note, state, folderKey, uniqueKey) { +function updateFolderChange(oldNote, note, state, folderKey, uniqueKey) { if (oldNote == null || oldNote.folder !== note.folder) { state.folderNoteMap = new Map(state.folderNoteMap) let folderNoteList = state.folderNoteMap.get(folderKey) @@ -425,7 +429,7 @@ function updateFolderChange (oldNote, note, state, folderKey, uniqueKey) { } } -function updateTagChanges (oldNote, note, state, uniqueKey) { +function updateTagChanges(oldNote, note, state, uniqueKey) { const discardedTags = _.difference(oldNote.tags, note.tags) const addedTags = _.difference(note.tags, oldNote.tags) if (discardedTags.length + addedTags.length > 0) { @@ -434,15 +438,15 @@ function updateTagChanges (oldNote, note, state, uniqueKey) { } } -function assignToTags (tags, state, uniqueKey) { +function assignToTags(tags, state, uniqueKey) { state.tagNoteMap = new Map(state.tagNoteMap) - tags.forEach((tag) => { + tags.forEach(tag => { const tagNoteList = getOrInitItem(state.tagNoteMap, tag) tagNoteList.add(uniqueKey) }) } -function removeFromTags (tags, state, uniqueKey) { +function removeFromTags(tags, state, uniqueKey) { state.tagNoteMap = new Map(state.tagNoteMap) tags.forEach(tag => { let tagNoteList = state.tagNoteMap.get(tag) @@ -454,7 +458,7 @@ function removeFromTags (tags, state, uniqueKey) { }) } -function getOrInitItem (target, key) { +function getOrInitItem(target, key) { let results = target.get(key) if (results == null) { results = new Set() @@ -463,13 +467,24 @@ function getOrInitItem (target, key) { return results } +const history = createHistory() + const reducer = combineReducers({ data, config, status, - routing: routerReducer + router: connectRouter(history) }) -const store = createStore(reducer) +const store = createStore( + reducer, + undefined, + process.env.NODE_ENV === 'development' + ? compose( + applyMiddleware(routerMiddleware(history)), + DevTools.instrument() + ) + : applyMiddleware(routerMiddleware(history)) +) -export default store +export { store, history } diff --git a/browser/styles/Detail/TagSelect.styl b/browser/styles/Detail/TagSelect.styl index 17e9f993..5fc23310 100644 --- a/browser/styles/Detail/TagSelect.styl +++ b/browser/styles/Detail/TagSelect.styl @@ -4,8 +4,10 @@ border none background-color transparent outline none - padding 0 4px + padding 2px 4px + margin 0px 2px 2px font-size 13px + height 23px ul position fixed @@ -49,78 +51,34 @@ background-color alpha($ui-button--active-backgroundColor, 40%) color $ui-text-color -body[data-theme="dark"] - .TagSelect - .react-autosuggest__input - color $ui-dark-text-color - - ul - border-color $ui-dark-borderColor - background-color $ui-dark-noteList-backgroundColor - color $ui-dark-text-color - - &:before - background-color $ui-dark-noteList-backgroundColor - - li[aria-selected="true"] - background-color $ui-dark-button--active-backgroundColor - color $ui-dark-text-color - -body[data-theme="monokai"] - .TagSelect - .react-autosuggest__input - color $ui-monokai-text-color - - ul - border-color $ui-monokai-borderColor - background-color $ui-monokai-noteList-backgroundColor - color $ui-monokai-text-color - - &:before - background-color $ui-dark-noteList-backgroundColor - - li[aria-selected="true"] - background-color $ui-monokai-button-backgroundColor - color $ui-monokai-text-color - -body[data-theme="dracula"] - .TagSelect - .react-autosuggest__input - color $ui-dracula-text-color - - ul - border-color $ui-dracula-borderColor - background-color $ui-dracula-noteList-backgroundColor - color $ui-dracula-text-color - - &:before - background-color $ui-dark-noteList-backgroundColor - - li[aria-selected="true"] - background-color $ui-dracula-button-backgroundColor - color $ui-dracula-text-color - -body[data-theme="solarized-dark"] - .TagSelect - .react-autosuggest__input - color $ui-solarized-dark-text-color - - ul - border-color $ui-solarized-dark-borderColor - background-color $ui-solarized-dark-noteList-backgroundColor - color $ui-solarized-dark-text-color - - &:before - background-color $ui-solarized-dark-noteList-backgroundColor - - li[aria-selected="true"] - background-color $ui-dark-button--active-backgroundColor - color $ui-solarized-dark-text-color - body[data-theme="white"] .TagSelect ul background-color $ui-white-noteList-backgroundColor li[aria-selected="true"] - background-color $ui-button--active-backgroundColor \ No newline at end of file + background-color $ui-button--active-backgroundColor + +apply-theme(theme) + body[data-theme={theme}] + .TagSelect + .react-autosuggest__input + color get-theme-var(theme, 'text-color') + + ul + border-color get-theme-var(theme, 'borderColor') + background-color get-theme-var(theme, 'noteList-backgroundColor') + color get-theme-var(theme, 'text-color') + + &:before + background-color $ui-dark-noteList-backgroundColor + + li[aria-selected="true"] + background-color get-theme-var(theme, 'button-backgroundColor') + color get-theme-var(theme, 'text-color') + +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) + +for theme in $themes + apply-theme(theme) \ No newline at end of file diff --git a/browser/styles/index.styl b/browser/styles/index.styl index b9f9c41e..2338938f 100644 --- a/browser/styles/index.styl +++ b/browser/styles/index.styl @@ -107,36 +107,18 @@ colorDarkPrimaryButton() &:active:hover background-color $dark-primary-button-background--active - -colorSolarizedDarkPrimaryButton() - color $ui-solarized-dark-text-color - background-color $ui-solarized-dark-button-backgroundColor - border none - &:hover - background-color $dark-primary-button-background--hover - &:active - &:active:hover - background-color $dark-primary-button-background--active - -colorMonokaiPrimaryButton() - color $ui-monokai-text-color - background-color $ui-monokai-button-backgroundColor - border none - &:hover - background-color $dark-primary-button-background--hover - &:active - &:active:hover - background-color $dark-primary-button-background--active - -colorDraculaPrimaryButton() - color $ui-dracula-text-color - background-color $ui-dracula-button-backgroundColor - border none - &:hover - background-color $ui-dracula-button--active-backgroundColor - &:active - &:active:hover - background-color $ui-dracula-button--active-backgroundColor +colorThemedPrimaryButton(theme) + if theme == 'dark' + colorDarkPrimaryButton() + else + color get-theme-var(theme, 'text-color') + background-color get-theme-var(theme, 'button-backgroundColor') + border none + &:hover + background-color get-theme-var(theme, 'button--hover-backgroundColor') + &:active + &:active:hover + background-color get-theme-var(theme, 'button--active-backgroundColor') // Danger button(Brand color) @@ -257,12 +239,14 @@ $ui-dark-borderColor = #444444 $ui-dark-backgroundColor = #2C3033 $ui-dark-noteList-backgroundColor = #2C3033 $ui-dark-noteDetail-backgroundColor = #2C3033 +$ui-dark-tagList-backgroundColor = #FFFFFF $ui-dark-tag-backgroundColor = #3A404C $dark-background-color = lighten($ui-dark-backgroundColor, 10%) $ui-dark-text-color = #DDDDDD $ui-dark-button--active-color = #f4f4f4 $ui-dark-button--active-backgroundColor = #3A404C +$ui-dark-button--hover-color = #c0392b $ui-dark-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%) $ui-dark-button--focus-borderColor = lighten(#369DCD, 25%) $ui-dark-topbar-button-color = #939395 @@ -332,19 +316,11 @@ darkTooltip() pointer-events none transition 0.1s -modalDark() - position relative - z-index $modal-z-index - width 100% - background-color $ui-dark-backgroundColor - overflow hidden - border-radius $modal-border-radius - - /******* Solarized Dark theme ********/ $ui-solarized-dark-backgroundColor = #073642 $ui-solarized-dark-noteList-backgroundColor = #073642 $ui-solarized-dark-noteDetail-backgroundColor = #073642 +$ui-solarized-dark-tagList-backgroundColor = #FFFFFF $ui-solarized-dark-text-color = #93a1a1 $ui-solarized-dark-active-color = #2aa198 @@ -356,21 +332,23 @@ $ui-solarized-dark-tag-backgroundColor = #002b36 $ui-solarized-dark-button-backgroundColor = #002b36 $ui-solarized-dark-button--active-color = #93a1a1 $ui-solarized-dark-button--active-backgroundColor = #073642 +$ui-solarized-dark-button--hover-color = #c0392b $ui-solarized-dark-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%) $ui-solarized-dark-button--focus-borderColor = lighten(#369DCD, 25%) -modalSolarizedDark() - position relative - z-index $modal-z-index - width 100% - background-color $ui-solarized-dark-backgroundColor - overflow hidden - border-radius $modal-border-radius +$ui-solarized-dark-kbd-backgroundColor = darken(#21252B, 10%) +$ui-solarized-dark-kbd-color = $ui-solarized-dark-text-color + +$ui-solarized-dark-table-odd-backgroundColor = $ui-solarized-dark-noteDetail-backgroundColor +$ui-solarized-dark-table-even-backgroundColor = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) +$ui-solarized-dark-table-head-backgroundColor = $ui-solarized-dark-table-even-backgroundColor +$ui-solarized-dark-table-borderColor = lighten(darken(#21252B, 10%), 20%) /******* Monokai theme ********/ $ui-monokai-backgroundColor = #272822 $ui-monokai-noteList-backgroundColor = #272822 $ui-monokai-noteDetail-backgroundColor = #272822 +$ui-monokai-tagList-backgroundColor = #FFFFFF $ui-monokai-text-color = #f8f8f2 $ui-monokai-active-color = #f92672 @@ -382,21 +360,23 @@ $ui-monokai-tag-backgroundColor = #373831 $ui-monokai-button-backgroundColor = #373831 $ui-monokai-button--active-color = white $ui-monokai-button--active-backgroundColor = #f92672 +$ui-monokai-button--hover-color = #f92672 $ui-monokai-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%) $ui-monokai-button--focus-borderColor = lighten(#369DCD, 25%) -modalMonokai() - position relative - z-index $modal-z-index - width 100% - background-color $ui-monokai-backgroundColor - overflow hidden - border-radius $modal-border-radius +$ui-monokai-kbd-backgroundColor = darken(#21252B, 10%) +$ui-monokai-kbd-color = $ui-monokai-text-color + +$ui-monokai-table-odd-backgroundColor = $ui-monokai-noteDetail-backgroundColor +$ui-monokai-table-even-backgroundColor = darken($ui-monokai-noteDetail-backgroundColor, 10%) +$ui-monokai-table-head-backgroundColor = $ui-monokai-table-even-backgroundColor +$ui-monokai-table-borderColor = lighten(darken(#21252B, 10%), 20%) /******* Dracula theme ********/ $ui-dracula-backgroundColor = #282a36 $ui-dracula-noteList-backgroundColor = #282a36 $ui-dracula-noteDetail-backgroundColor = #282a36 +$ui-dracula-tagList-backgroundColor = #f8f8f2 $ui-dracula-text-color = #f8f8f2 $ui-dracula-active-color = #bd93f9 @@ -408,13 +388,79 @@ $ui-dracula-tag-backgroundColor = #8be9fd $ui-dracula-button-backgroundColor = #44475a $ui-dracula-button--active-color = #f8f8f2 $ui-dracula-button--active-backgroundColor = #bd93f9 +$ui-dracula-button--hover-color = #ff79c6 $ui-dracula-button--hover-backgroundColor = lighten($ui-dracula-backgroundColor, 10%) $ui-dracula-button--focus-borderColor = lighten(#44475a, 25%) -modalDracula() - position relative - z-index $modal-z-index - width 100% - background-color $ui-dracula-backgroundColor - overflow hidden - border-radius $modal-border-radius \ No newline at end of file +$ui-dracula-kbd-backgroundColor = darken(#21252B, 10%) +$ui-dracula-kbd-color = $ui-monokai-text-color + +$ui-dracula-table-odd-backgroundColor = $ui-dracula-noteDetail-backgroundColor +$ui-dracula-table-even-backgroundColor = darken($ui-dracula-noteDetail-backgroundColor, 10%) +$ui-dracula-table-head-backgroundColor = $ui-dracula-table-even-backgroundColor +$ui-dracula-table-borderColor = lighten(darken(#21252B, 10%), 20%) + +/******* Nord theme ********/ +$ui-nord-backgroundColor = #2e3440 +$ui-nord-noteList-backgroundColor = #2e3440 +$ui-nord-noteDetail-backgroundColor = #2e3440 +$ui-nord-tagList-backgroundColor = #FFFFFF + +$ui-nord-text-color = #d8dee9 +$ui-nord-inactive-text-color = #8fbcbb +$ui-nord-active-color = #5e81ac + +$ui-nord-borderColor = #3b4252 + +$ui-nord-tag-backgroundColor = #3b4252 + +$ui-nord-button-backgroundColor = #434c5e +$ui-nord-button--active-color = #d8dee9 +$ui-nord-button--active-backgroundColor = #5e81ac +$ui-nord-button--hover-color = #c0392b +$ui-nord-button--hover-backgroundColor = #434c5e + +$ui-nord-kbd-backgroundColor = $ui-nord-text-color +$ui-nord-kbd-color = $ui-nord-backgroundColor + +$ui-nord-table-odd-backgroundColor = $ui-nord-noteDetail-backgroundColor +$ui-nord-table-even-backgroundColor = darken($ui-nord-noteDetail-backgroundColor, 10%) +$ui-nord-table-head-backgroundColor = $ui-nord-table-even-backgroundColor +$ui-nord-table-borderColor = lighten(darken(#21252B, 10%), 20%) + +/******* Vulcan theme ********/ +$ui-vulcan-backgroundColor = #161719 +$ui-vulcan-noteList-backgroundColor = #161719 +$ui-vulcan-noteDetail-backgroundColor = #161719 +$ui-vulcan-tagList-backgroundColor = #FFFFFF + +$ui-vulcan-text-color = #999999 +$ui-vulcan-inactive-text-color = #999999 +$ui-vulcan-active-color = #ffffff + +$ui-vulcan-borderColor = #282a2e + +$ui-vulcan-tag-backgroundColor = #282a2e + +$ui-vulcan-button-backgroundColor = #282a2e +$ui-vulcan-button--active-color = #a3a8ae +$ui-vulcan-button--active-backgroundColor = #282a2e +$ui-vulcan-button--hover-backgroundColor = #282a2e + +$ui-vulcan-kbd-backgroundColor = lighten($ui-vulcan-text-color, 50%) +$ui-vulcan-kbd-color = $ui-vulcan-backgroundColor + +$ui-vulcan-table-odd-backgroundColor = $ui-vulcan-noteDetail-backgroundColor +$ui-vulcan-table-even-backgroundColor = darken($ui-vulcan-noteDetail-backgroundColor, 10%) +$ui-vulcan-table-head-backgroundColor = $ui-vulcan-table-even-backgroundColor +$ui-vulcan-table-borderColor = lighten(darken(#21252B, 10%), 20%) + + + +debug-theme-var(theme, suffix) + '$ui-' + theme + '-' + suffix + +get-theme-var(theme, suffix) + lookup('$ui-' + theme + '-' + suffix) + +$themes = 'monokai' 'nord' 'vulcan' \ No newline at end of file diff --git a/contributing.md b/contributing.md index fa71d5a5..43d68d85 100644 --- a/contributing.md +++ b/contributing.md @@ -1,3 +1,5 @@ +> [Please consider to contribute to the new Boost Note app too!](https://github.com/BoostIO/BoostNote.next) + # Contributing to Boostnote (English) ### When you open an issue or a bug report @@ -48,14 +50,26 @@ GPL v3 is too strict to be compatible with another license, so we thought it mig # Contributing to Boostnote (Korean) -### 버그 리포트를 보고할 때 -이슈의 양식은 없습니다. 하지만 부탁이 있습니다. - -**개발자 도구를 연 상태의 Boostnote 스크린샷을 첨부해주세요** +### 이슈 또는 버그 리포트를 제출하는 절차 +이슈를 제기할 때에 사용하는 양식(issue template)이 준비되어 있으니, 해당 양식에 맞추어 최대한 구체적인 정보를 첨부하여 주시기 바랍니다. 도움을 주셔서 감사합니다. -### Pull Request의 저작권에 관하여 +### Pull Request를 제출하는 절차 +Pull Request에 사용하는 양식(pull request template)이 준비되어 있으니, 코드를 접수하기 전에 미리 해당 양식을 작성해 주시기 바랍니다. 코드가 해결하고자 하는 문제가 무엇인지 정확히 알면 저희가 훨씬 신속하게 해당 pull request를 검토할 수 있습니다. + +다음 사항을 준수하여 주십시오: +- [`code_style.md`](docs/code_style.md) 에 정리된 코드 스타일 정보를 확인할 것 +- 테스트 코드를 작성하고, 아래와 같은 테스트 커맨드를 실행할 것 +``` +npm run test +``` +- 아래와 같은 린팅 커맨드로 코드를 확인할 것 +``` +npm run lint +``` + +### 저작권에 관한 기준 당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 BoostIO에 양도한다는 것에 동의한다는 의미입니다. @@ -67,12 +81,24 @@ GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 # Contributing to Boostnote (Japanese) ### バグレポートに関してのissueを立てる時 -イシューテンプレートはありませんが、1つお願いがあります。 - -**開発者ツールを開いた状態のBoostnoteのスクリーンショットを貼ってください** +イシューテンプレートがあります。このテンプレートに従って、できるだけ多くの情報を提供してください。 よろしくお願いします。 +### Pull requestを出す時 +Pull requestのテンプレートがあります。このテンプレートを埋めてからコードをサブミットしてください。内容を正確に把握できるPull requestが作られていれば、迅速にレビューを行えます。 + +以下のことを必ず行ってください: +- [`code_style.md`](docs/code_style.md)を読み、コーディングスタイルを確認する +- 変更分のコードに対するテストコードを書き、以下のコマンドでテストを実行する +``` +npm run test +``` +- 以下のコマンドを使って、コードの静的解析を実行する +``` +npm run lint +``` + ### Pull requestの著作権について Pull requestをすることはその変化分のコードの著作権をBoostIOに譲渡することに同意することになります。 @@ -87,17 +113,17 @@ Pull requestをすることはその変化分のコードの著作権をBoostIO # Contributing to Boostnote (Simplified Chinese) ### 当您创建一个issue的时候 -我们对您的issue格式没有要求,但是我们有一个请求: +我们对您的issue格式没有要求,但是我们有一个请求: -**如果可能,请在开发者模式打开的情况下,为我们提供屏幕截图** +**如果可能,请在开发者模式打开的情况下,为我们提供屏幕截图** -(您可以通过`Ctrl+Shift+I`打开开发者模式)。 -感谢您对我们的支持。 +(您可以通过`Ctrl+Shift+I`打开开发者模式)。 +感谢您对我们的支持。 ### 关于您提供的Pull Request的著作权(版权)问题 -如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。 +如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。 -这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。 +这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。 因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。 --- diff --git a/dev-scripts/dev.js b/dev-scripts/dev.js index 9698a2fe..4cff0ca2 100644 --- a/dev-scripts/dev.js +++ b/dev-scripts/dev.js @@ -15,7 +15,7 @@ const options = { quiet: true } -function startServer () { +function startServer() { config.plugins.push(new webpack.HotModuleReplacementPlugin()) config.entry.main.unshift( `webpack-dev-server/client?http://localhost:${port}/`, @@ -25,7 +25,7 @@ function startServer () { server = new WebpackDevServer(compiler, options) return new Promise((resolve, reject) => { - server.listen(port, 'localhost', function (err) { + server.listen(port, 'localhost', function(err) { if (err) { reject(err) } @@ -48,7 +48,7 @@ function startServer () { }) } -function startElectron () { +function startElectron() { spawn(electron, ['--hot', './index.js'], { stdio: 'inherit' }) .on('close', () => { server.close() diff --git a/docs/build.md b/docs/build.md index f2b3e5ac..937a4a58 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,10 +1,11 @@ # 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), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). + +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), [Traditional Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_TW/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md), [Portuguese](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). ## Environments -* npm: 6.x -* node: 8.x +- npm: 6.x +- node: 8.x ## Development @@ -24,10 +25,40 @@ $ yarn run dev ``` > ### Notice +> > There are some cases where you have to refresh the app manually. +> > 1. When editing a constructor method of a component > 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.) +## Accessing code used in Pull Requests +Visit the page for the pull request and look at the end of the url for the PR number +
+https://github.com/BoostIO/Boostnote/pull/2794
+
+In the following, replace \ with that number (no brackets). +For URLs below, you would replace \ with 2794 + +_If you do not have a local copy of the master branch yet_ +``` +git clone https://github.com/BoostIO/Boostnote.git +cd Boostnote +git fetch origin pull//head: +git checkout +``` + +_If you already have the master branch_ +``` +git fetch origin pull//head: +git checkout +``` + +_To compile and run the code_ +``` +yarn +yarn run dev +``` + ## Deploy We use Grunt to automate deployment. @@ -51,14 +82,17 @@ Distribution packages are created by exec `grunt build` on Linux platform (e.g. After installing the supported version of `node` and `npm`, install build dependency packages. +``` +$ yarn add --dev grunt-electron-installer-debian grunt-electron-installer-redhat +``` -Ubuntu/Debian: +**Ubuntu/Debian:** ``` $ sudo apt-get install -y rpm fakeroot ``` -Fedora: +**Fedora:** ``` $ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot @@ -70,4 +104,4 @@ Then execute `grunt build`. $ grunt build ``` -You will find `.deb` and `.rpm` in the `dist` directory. +> You will find `.deb` and `.rpm` in the `dist` directory. diff --git a/docs/code_style.md b/docs/code_style.md index d8f458d7..c0416216 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -79,4 +79,11 @@ class MyComponent extends React.Component { // code goes here... } } -``` \ No newline at end of file +``` + +## React Hooks +Existing code will be kept class-based and will only be changed to functional components with hooks if it improves readability or makes things more reusable. + +For new components it's OK to use hooks with functional components but don't mix hooks & class-based components within a feature - just for code style / readability reasons. + +Read more about hooks in the [React hooks introduction](https://reactjs.org/docs/hooks-intro.html). diff --git a/docs/de/build.md b/docs/de/build.md index a3d8e274..65f2b8c3 100644 --- a/docs/de/build.md +++ b/docs/de/build.md @@ -1,10 +1,11 @@ # Build -Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). + +Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md), [Portugiesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). ## Umgebungen -* npm: 6.x -* node: 8.x +- npm: 6.x +- node: 8.x ## Entwicklung @@ -24,7 +25,9 @@ $ yarn run dev ``` > ### Notiz +> > Es gibt einige Fälle bei denen die App manuell zu refreshen ist. +> > 1. Wenn eine "constructor method" einer Komponente manuell editiert wird. > 2. Wenn eine neue CSS Klasse ergänzt wird (ähnlich wie 1: die CSS Klasse wird von jeder Komponenete neu geschrieben. Dieser Prozess passiert in der "Constructor method".) @@ -51,7 +54,6 @@ Distributions Pakete können mittels `grunt build` auf Linux Plattformen (e.g. U Nach der Installation der supporteten Version von `node` and `npm`, installiere auch build dependency packages. - Ubuntu/Debian: ``` diff --git a/docs/de/debug.md b/docs/de/debug.md index 6c3de3dc..a22ad98b 100644 --- a/docs/de/debug.md +++ b/docs/de/debug.md @@ -1,9 +1,8 @@ # How to debug Boostnote (Electron app) -Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md). +Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md), [Portugiesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/debug.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md). - -Boostnote ist eine Electron App und basiert auf Chromium. +Boostnote ist eine Electron App und basiert auf Chromium. Zum Entwicklen verwendest du am Besten die `Developer Tools` von Google Chrome verwenden. Diese kannst du ganz einfach im unter dem Menüpunkt `View` mit `Toggle Developer Tools` aktivieren: @@ -13,10 +12,9 @@ Die Anzeige der `Developer Tools` sieht in etwa so aus: ![Developer_Tools](https://cloud.githubusercontent.com/assets/11307908/24343545/eff9f3a6-127b-11e7-94cf-cb67bfda634a.png) - ## Debugging -Fehlermeldungen werden in der Regel in der `console` ausgegeben, die du über den gleichnamigen Reiter der `Developer Tools` anzeigen lassen kannst. Zum Debuggen kannst du beispielsweise über den `debugger` Haltepunkte im Code setzen. +Fehlermeldungen werden in der Regel in der `console` ausgegeben, die du über den gleichnamigen Reiter der `Developer Tools` anzeigen lassen kannst. Zum Debuggen kannst du beispielsweise über den `debugger` Haltepunkte im Code setzen. ![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png) @@ -24,8 +22,8 @@ Du kannst aber natürlich auch die Art von Debugging verwenden mit der du am bes ## Referenz -* [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools) +- [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools) --- -Special thanks: Translated by [gino909](https://github.com/gino909), [mdeuerlein](https://github.com/mdeuerlein) +Special thanks: Translated by [gino909](https://github.com/gino909), [mdeuerlein](https://github.com/mdeuerlein) diff --git a/docs/debug.md b/docs/debug.md index a84a6bba..4cd8b361 100644 --- a/docs/debug.md +++ b/docs/debug.md @@ -1,8 +1,9 @@ # How to debug Boostnote (Electron app) -This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md). +This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md), [Portuguese](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md). ## Debug with Google Chrome developer Tools + 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: @@ -14,6 +15,7 @@ The `Developer Tools` will look like this: When errors occur, the error messages are displayed at the `console`. ### Debugging + For example, you can use the `debugger` to set a breakpoint in the code like this: ![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png) @@ -21,16 +23,18 @@ For example, you can use the `debugger` to set a breakpoint in the code like thi This is just an illustrative example, you should find a way to debug which fits your style. ### References -* [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools) + +- [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools) ## Debug with Visual Studio Code -1. Install **[Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome "Install Debugger for Chrome")** plugin for Visual Studio Code. Then restart it. +1. Install **[Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Install Debugger for Chrome')** plugin for Visual Studio Code. Then restart it. 2. Pressing **Shift+Command+B** or running **Run Build Task** from the global **Terminal** menu, then pick the task named **Build Boostnote**. Or run `yarn run watch` from the terminal. 3. When above task is running, open **Debug view** in **Activity Bar** on the side of VS Code or use shortcut **Shift+Command+D**. 4. Select the configuration named **Boostnote All** from the **Debug configuration**, then click the green arrow button or press **F5** to start debugging. 5. Now you should find **Boostnote** is running. You will see two processes running, one named **Boostnote Main** and the other named **Boostnote Renderer**. Now you can set **debug breakpoints** in vscode. If you find your **breakpoints** is unverified, you need to switch to the appropriate process between **Boostnote Renderer** and **Boostnote Main**. - ### References - * [Electron application debugging](https://electronjs.org/docs/tutorial/application-debugging) - * [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) \ No newline at end of file + ### References + + - [Electron application debugging](https://electronjs.org/docs/tutorial/application-debugging) + - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) diff --git a/docs/fr/build.md b/docs/fr/build.md index e66a11aa..7628a3e1 100644 --- a/docs/fr/build.md +++ b/docs/fr/build.md @@ -1,10 +1,11 @@ # Build -Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md) + +Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Portugais](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md) ## Environnements -* npm: 6.x -* node: 8.x +- npm: 6.x +- node: 8.x ## Développement @@ -16,6 +17,7 @@ Installez les paquets requis à l'aide de `yarn`. ``` $ yarn ``` + Build et start ``` @@ -23,7 +25,9 @@ $ yarn run dev ``` > ### Notice +> > Il y a certains cas où vous voudrez relancer l'application manuellement. +> > 1. Quand vous éditez la méthode constructeur dans un composant > 2. Quand vous ajoutez une nouvelle classe css. (Comme pour 1: la classe est réécrite pour chaque composant. Le process intervient dans la méthode constructeur) @@ -37,6 +41,7 @@ Nous avons donc préparé un script séparé qui va rendre un fichier exécutabl ``` grunt pre-build ``` + Vous trouverez l'exécutable dans le dossier `dist`. Note : l'auto updater ne marchera pas car l'application n'est pas signée. @@ -50,7 +55,6 @@ Les paquets sont créés en exécutant `grunt build` sur une plateforme Linux (e Après avoir installé la version supportée de `node` et de `npm`, installer les paquets de builds. - Ubuntu/Debian: ``` diff --git a/docs/ko/build.md b/docs/ko/build.md index 17e9c712..d9a5f4a9 100644 --- a/docs/ko/build.md +++ b/docs/ko/build.md @@ -5,8 +5,6 @@ * npm: 6.x * node: 8.x -`$ grunt pre-build`를 `npm v5.x`에서 실행할 수 없기 때문에, 반드시 `npm v4.x`를 사용하셔야 합니다. - ## 개발 개발에 있어서 Webpack HRM을 사용합니다. @@ -29,6 +27,34 @@ $ yarn run dev > 1. 콤포넌트의 컨스트럭터 함수를 수정할 경우 > 2. 새로운 CSS코드를 추가할 경우(1.과 같은 이유: CSS클래스는 콤포넌트마다 다시 만들어 지는데, 이 작업은 컨스트럭터에서 일어납니다.) +## Pull Request에 사용된 코드를 적용하는 방법 +관련된 Pull request 페이지를 방문하여, url 스트링 마지막에 표기된 PR 번호를 확인합니다. +
+https://github.com/BoostIO/Boostnote/pull/2794
+
+아래의 커맨드를 실행하면서, \ 대신에 위에서 확인한 번호를 입력합니다 (부등호 신호는 빼고 입력하세요). +위에 보여진 예시의 경우, \ 자리에 2794를 입력하면 됩니다. + +_본인의 로컬 컴퓨터에 마스터 브랜치가 복사되어 있지 않은 경우_ +``` +git clone https://github.com/BoostIO/Boostnote.git +cd Boostnote +git fetch origin pull//head: +git checkout +``` + +_이미 마스터 브랜치를 로컬 컴퓨터에 저장해둔 경우_ +``` +git fetch origin pull//head: +git checkout +``` + +_To compile and run the code_ +``` +yarn +yarn run dev +``` + ## 배포 Boostnote에서는 배포 자동화를 위하여 그런트를 사용합니다. @@ -43,3 +69,31 @@ grunt pre-build 실행 파일은 `dist`에서 찾을 수 있습니다. 이 경우, 인증이 되어있지 않기 때문에 업데이터는 사용할 수 없습니다. 필요로 하다면, 이 실행파일에 Codesign나 Authenticode등의 서명을 할 수 있습니다. + +## 독자적인 배포판을 제작하는 방법 (deb, rpm) + +배포판 패키지를 제작하려면 (우분투, 페도라 등) 리눅스 플랫폼에서 `grunt build` 커맨드를 실행하면 됩니다. + +> 참조: 동일한 환경에서 `.deb` 파일과 `.rpm` 파일을 모두 만들 수 있습니다. + +지원되는 버전의 `node`와 `npm`을 설치한 다음, 빌드에 필요한 패키지를 설치합니다. + +우분투/데비안 환경 (Ubuntu/Debian): + +``` +$ sudo apt-get install -y rpm fakeroot +``` + +페도라 환경 (Fedora): + +``` +$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot +``` + +그 다음 `grunt build` 커맨드를 실행합니다. + +``` +$ grunt build +``` + +`dist` 디렉토리에 `.deb` 파일과 `.rpm` 파일이 새롭게 생성됩니다. diff --git a/docs/ko/debug.md b/docs/ko/debug.md index 290eee9c..6d68d951 100644 --- a/docs/ko/debug.md +++ b/docs/ko/debug.md @@ -1,5 +1,7 @@ # Boostnote의 디버그 방법(Electron app) +## 구글 크롬 Developer Tools를 사용한 디버깅 + Boostnote는 Electron 애플리케이션이므로 Chromium위에서 작동합니다. 그렇기 때문에 개발자분들은 Google Chrome 브라우저에서 처럼 `Developer Tools`를 사용하실 수 있습니다. 다음과 같이 `Developer Tools`를 실행할 수 있습니다: @@ -10,12 +12,26 @@ Boostnote는 Electron 애플리케이션이므로 Chromium위에서 작동합니 에러가 발생할 때에는, 에러메시지가 `console`위에 표시 됩니다. -## 디버깅 +### 디버깅 예를들면 `debugger`를 사용하여 코드 안에서 다음과 같이 일시 정지지점을 설정할 수 있습니다: ![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png) 이는 단순한 하나의 예시에 불과합니다. 자기자신에게 가장 잘 맞는 디버그 방법을 찾는 것도 좋을 것 입니다. -## 참고 + ### 참고 * [디버그에 관한 Google Chrome의 공식 문서](https://developer.chrome.com/devtools) + +## 비주얼 스튜디오 코드를 사용한 디버깅 + +1. **[크롬 디버깅 플러그인](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Install Debugger for Chrome')** 을 비주얼 스튜디오 코드에 설치한 후, 프로그램을 닫았다가 재실행합니다. +2. **Shift+Command+B** 키를 누르거나, **Terminal** 메뉴 하단에 있는 **Run Build Task** 메뉴를 선택한 후 **Build Boostnote** 를 선택합니다. 아니면 터미널에서 곧바로 `yarn run watch`를 실행해도 됩니다. +3. 위의 절차가 실행되고 있을 때, 사이드바 **Activity Bar**에서 **Debug view**를 선택합니다. 키보드 단축키로는 **Shift+Command+D**를 눌러도 됩니다.. +4. **Debug configuration**에서 **Boostnote All** 설정을 선택한 후, 초록색 화살표를 클릭하거나 **F5** 키를 누르면 디버깅이 시작됩니다. +5. 이 시점에서는 **Boostnote**가 실행되고 있을 텐데, 두 개의 프로세스가 진행중인 것을 볼 수 있을 겁니다. 바로 **Boostnote Main** 프로세스와 **Boostnote Renderer** 프로세스입니다. 이제 비주얼 스튜디오 코드에서 곧바로 **디버깅 정지지점 (debug breakpoint)** 을 설정할 수 있습니다. 만약에 지정한 **정지지점 (breakpoint)** 이 등록되지 않는다면, **Boostnote Renderer** 와 **Boostnote Main** 프로세스 사이를 번갈아 확인해 보아야 합니다. + + +### 참고 + +- [일렉트론 애플리케이션 디버깅 공식 튜토리얼](https://electronjs.org/docs/tutorial/application-debugging) +- [비쥬얼 스튜디오 코드용 크롬 디버깅 플러그인](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) diff --git a/docs/pt_BR/build.md b/docs/pt_BR/build.md new file mode 100644 index 00000000..ae456522 --- /dev/null +++ b/docs/pt_BR/build.md @@ -0,0 +1,77 @@ +# Build + +Esta página também está disponível em [Japônes](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coreano](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russo](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinês simplificado](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Francês](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) e [Alemão](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). + +## Ambiente + +- npm: 6.x +- node: 8.x + +## Desenvolvimento + +Nós usamos o Webpack HMR para desenvolver o Boostnote. +Ao executar os seguintes comandos no diretório raiz do projeto, o Boostnote será iniciado com as configurações padrão. + +Instala os pacotes necessários usando o yarn. + +``` +$ yarn +``` + +Gerar e iniciar. + +``` +$ yarn run dev +``` + +> ### Notice +> +> Existe alguns casos onde você precisa atualizar o app manualmente. +> +> 1. Quando editar um método construtor de um componente +> 2. Quando adicionar uma nova classe de css (similiar ao 1: a classe do css é reescrita por cada componente. Esse processo ocorre através do método construtor) + +## Deploy + +Nós usamos o Grunt para automatizar o desenvolvimento. +Você pode gerar o programa usando `grunt`. Contudo, nós não recomendamos isso porque a tarefa padrão inclui _codedesign_ e _authenticode_. + +Então nós preparamos um _script_ separado, o qual somente cria um executável. + +``` +grunt pre-build +``` + +Você irá encontrar o executável na pasta `dist`. + +**Nota:** o atualizador automático não funciona porque o app não está certificado. + +Se você achar isto necessário, você pode usar o _codesign_ ou o _authenticode_ com esse executável. + +## Faça seus próprios pacotes de distribuição (deb, rpm) + +Pacotes de distribuição são gerados através do comando `grunt build` em plataforma Linux (e.g. Ubuntu, Fedora). + +**Nota:** você pode criar `.deb` e `.rpm` em um mesmo ambiente. + +Depois de instalar uma versão suportada do `node` e do `npm`, deve-se instalar as dependências para gerar os pacotes. + +Ubuntu/Debian: + +``` +$ sudo apt-get install -y rpm fakeroot +``` + +Fedora: + +``` +$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot +``` + +Então execute `grunt build`. + +``` +$ grunt build +``` + +Você vai encontrar o `.deb` e o `.rpm` na pasta`dist`. diff --git a/docs/pt_BR/debug.md b/docs/pt_BR/debug.md new file mode 100644 index 00000000..3de15741 --- /dev/null +++ b/docs/pt_BR/debug.md @@ -0,0 +1,40 @@ +# Como debugar Boostnote (app Electron) + +Esta página também está disponível em [Japônes](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Coreano](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russo](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Chinês simplificado](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [Francês](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) e [Alemão](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md). + +## Debugar com o Google Chrome developer Tools + +Boostnote é um app Electron, por isso ele é baseado no Chromium; desenvolvedores podem usar o `Developer Tools` igual no Google Chrome. + +Você pode habilitar e desabilitar o `Developer Tools` assim: +![how_to_toggle_devTools](https://cloud.githubusercontent.com/assets/11307908/24343585/162187e2-127c-11e7-9c01-23578db03ecf.png) + +O `Developer Tools` deve parecer assim: +![Developer_Tools](https://cloud.githubusercontent.com/assets/11307908/24343545/eff9f3a6-127b-11e7-94cf-cb67bfda634a.png) + +Quando erros acontecem, eles são apresentados na aba `console`. + +### Debugando + +Por exemplo, você pode usar o `debugger` para adicionar um _breakpoint_ (ponto de parada) no código dessa forma: + +![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png) + +Isso é só um exemplo ilustrativo, mas você deve encontrar um jeito de debugar que encaixe no seu estilo. + +### Referências + +- [Documentação do Google Chrome sobre como debugar](https://developer.chrome.com/devtools) + +## Debugar com o Visual Studio Code (VS Code) + +1. Instale o plugin **[Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome 'Instale o pacote Debugger for Chrome')** para Visual Studio Code. Então reinicie-o. +2. Pressione **Shift+Command+B** ou execute **Run Build Task** do menu global **Terminal**, então seleciona a task **Build Boostnote**. Ou execute `yarn run watch` no terminal. +3. Quando a task acima estiver rodando, abra o **Debug view** na **Activity Bar** no lado do seu VS Code ou use o atalho **Shift+Command+D**. +4. Selecione a configuração **Boostnote All** no **Debug configuration**, então clique na seta verde ou aperte **F5** para começar a debugar. +5. Agora você deve encontrar seu **Boostnote** rodando. Você vai ver dois processos rodando, um com nome de **Boostnote Main** e outro com nome de **Boostnote Renderer**. Agora você pode adicionar os **debug breakpoints** no vscode. Se os seus **breakpoints** não forem alertados você pode precisar altenrar entre os processos **Boostnote Renderer** e **Boostnote Main**. + +### Referências + +- [Debugando uma aplicação Electron](https://electronjs.org/docs/tutorial/application-debugging) +- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) diff --git a/docs/zh_TW/build.md b/docs/zh_TW/build.md index 65b086ac..360440e5 100644 --- a/docs/zh_TW/build.md +++ b/docs/zh_TW/build.md @@ -1,15 +1,15 @@ # 編譯 -此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). + +此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md), [葡萄牙](https://github.com/BoostIO/Boostnote/blob/master/docs/pt_BR/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). ## 環境 -* npm: 6.x -* node: 8.x +- npm: 6.x +- node: 8.x ## 開發 我們使用 Webpack HMR 來開發 Boostnote。 - 在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。 **用 yarn 來安裝必要 packages** @@ -18,40 +18,74 @@ $ yarn ``` -**開始開發** +**編譯及執行** ``` $ yarn run dev ``` -> ### Notice -> There are some cases where you have to refresh the app manually. -> 1. When editing a constructor method of a component -> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.) +> ### 注意 +> +> 以下是一些可能要手動重新啟動程式的情況。 +> +> 1. 修改一個 component 的 constructor 方法時。 +> 2. 新增新的 CSS 類別時 (和 1 很類似:CSS 類別會被每個 component 重寫過。這個過程在 constructor 方法中發生)。 -## Deploy +## 使用 Pull Requests 中的程式碼 +瀏覽 pull request 的頁面,從 URL 的後面找到 PR 號碼。 -We use Grunt to automate deployment. -You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode. +
+https://github.com/BoostIO/Boostnote/pull/2794
+
+接著,於底下步驟中把 \ 換成這個號碼 (沒有括號)。 +請將下方 URL 中的 \ 換置成 2794。 -So, we've prepared a separate script which just makes an executable file. +_如果您還未取得一份 master branch 的本地備份_ +``` +git clone https://github.com/BoostIO/Boostnote.git +cd Boostnote +git fetch origin pull//head: +git checkout +``` + +_如果您已經擁有了 master branch_ +``` +git fetch origin pull//head: +git checkout +``` + +_編譯及執行程式碼_ +``` +yarn +yarn run dev +``` + +## 佈署 + +我們用 Grunt 做自動佈署。 +您能使用 `grung` 建構本程式。然而,我們並不建議這麼做,因為預設工作流程包含了程式簽名以及 Authenticode 驗證。 + +所以,我們準備了一份額外的腳本用於建構可執行檔。 ``` grunt pre-build ``` -You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed. +您可以在 `dist` 資料夾下找到可執行檔。注意,自動更新功能 (auto updater) 並不會生效,因為程式沒有被簽署過。 -If you find it necessary, you can use codesign or authenticode with this executable. +必要時您可以使用程式簽名或 authenticode 驗證執行檔。 -## Make own distribution packages (deb, rpm) +## 建立您自己的發行版套件 (deb, rpm) -Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora). +發行版套件可以透過在 Linux 平台上 (如 Ubuntu, Fedora) 執行 `grunt build` 來建立。 -> Note: You can create both `.deb` and `.rpm` in a single environment. +> 注意:您可以在同個環境中同時建立 `.deb` 及`.rpm` 。 -After installing the supported version of `node` and `npm`, install build dependency packages. +安裝支援版本的 `node` 和 `npm` 後,安裝編譯相依套件。 +``` +$ yarn add --dev grunt-electron-installer-debian grunt-electron-installer-redhat +``` Ubuntu/Debian: @@ -65,10 +99,10 @@ Fedora: $ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot ``` -Then execute `grunt build`. +接著執行 `grunt build`。 ``` $ grunt build ``` -You will find `.deb` and `.rpm` in the `dist` directory. +> 於 `dist` 資料夾下找到 `.deb` 及 `.rpm`。 diff --git a/extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js b/extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js index aa766f6e..bc03e920 100644 --- a/extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js +++ b/extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js @@ -44,9 +44,46 @@ ? match[2].replace('x', ' ') : (parseInt(match[3], 10) + 1) + match[4] replacements[i] = '\n' + indent + bullet + after + + if (bullet) incrementRemainingMarkdownListNumbers(cm, pos) } } - cm.replaceSelections(replacements) } + // Auto-updating Markdown list numbers when a new item is added to the + // middle of a list + function incrementRemainingMarkdownListNumbers(cm, pos) { + var startLine = pos.line, lookAhead = 0, skipCount = 0 + var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1] + + do { + lookAhead += 1 + var nextLineNumber = startLine + lookAhead + var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine) + + if (nextItem) { + var nextIndent = nextItem[1] + var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount) + var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber + + if (startIndent === nextIndent && !isNaN(nextNumber)) { + if (newNumber === nextNumber) itemNumber = nextNumber + 1 + if (newNumber > nextNumber) itemNumber = newNumber + 1 + cm.replaceRange( + nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]), + { + line: nextLineNumber, ch: 0 + }, { + line: nextLineNumber, ch: nextLine.length + }) + } else { + if (startIndent.length > nextIndent.length) return + // This doesn't run if the next line immediatley indents, as it is + // not clear of the users intention (new indented item or same level) + if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return + skipCount += 1 + } + } + } while (nextItem) + } }) diff --git a/extra_scripts/codemirror/addon/edit/closebrackets.js b/extra_scripts/codemirror/addon/edit/closebrackets.js index 1401159b..357185ca 100644 --- a/extra_scripts/codemirror/addon/edit/closebrackets.js +++ b/extra_scripts/codemirror/addon/edit/closebrackets.js @@ -11,6 +11,7 @@ })(function(CodeMirror) { var defaults = { pairs: "()[]{}''\"\"", + closeBefore: ")]}'\":;>", triples: "", explode: "[]{}" }; @@ -114,6 +115,9 @@ var pairs = getOption(conf, "pairs"); var pos = pairs.indexOf(ch); if (pos == -1) return CodeMirror.Pass; + + var closeBefore = getOption(conf,"closeBefore"); + var triples = getOption(conf, "triples"); var identical = pairs.charAt(pos + 1) == ch; @@ -141,7 +145,7 @@ var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; else return CodeMirror.Pass; - } else if (opening) { + } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { curType = "both"; } else { return CodeMirror.Pass; @@ -189,4 +193,4 @@ return /\bstring/.test(token.type) && token.start == pos.ch && (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) } -}); +}); \ No newline at end of file diff --git a/extra_scripts/codemirror/addon/hyperlink/hyperlink.js b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js index 4740bd21..4ccbbe01 100755 --- a/extra_scripts/codemirror/addon/hyperlink/hyperlink.js +++ b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js @@ -1,15 +1,24 @@ -(function (mod) { - if (typeof exports === 'object' && typeof module === 'object') { // Common JS +;(function(mod) { + if (typeof exports === 'object' && typeof module === 'object') { + // Common JS mod(require('../codemirror/lib/codemirror')) - } else if (typeof define === 'function' && define.amd) { // AMD + } else if (typeof define === 'function' && define.amd) { + // AMD define(['../codemirror/lib/codemirror'], mod) - } else { // Plain browser env + } else { + // Plain browser env mod(CodeMirror) } -})(function (CodeMirror) { +})(function(CodeMirror) { 'use strict' const shell = require('electron').shell + const remote = require('electron').remote + const eventEmitter = { + emit: function() { + remote.getCurrentWindow().webContents.send.apply(null, arguments) + } + } const yOffset = 2 const macOS = global.process.platform === 'darwin' @@ -28,11 +37,16 @@ this.tooltip = document.createElement('div') this.tooltipContent = document.createElement('div') this.tooltipIndicator = document.createElement('div') - this.tooltip.setAttribute('class', 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected') + this.tooltip.setAttribute( + 'class', + 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected' + ) this.tooltip.setAttribute('cm-ignore-events', 'true') this.tooltip.appendChild(this.tooltipContent) this.tooltip.appendChild(this.tooltipIndicator) - this.tooltipContent.textContent = `${macOS ? 'Cmd(⌘)' : 'Ctrl(^)'} + click to follow link` + this.tooltipContent.textContent = `${ + macOS ? 'Cmd(⌘)' : 'Ctrl(^)' + } + click to follow link` this.lineDiv.addEventListener('mousedown', this.onMouseDown) this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, { @@ -51,7 +65,16 @@ const className = el.className.split(' ') if (className.indexOf('cm-url') !== -1) { - const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(el.textContent) + // multiple cm-url because of search term + const cmUrlSpans = Array.from( + el.parentNode.getElementsByClassName('cm-url') + ) + const textContent = + cmUrlSpans.length > 1 + ? cmUrlSpans.map(span => span.textContent).join('') + : el.textContent + + const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(textContent) const url = match[1] || match[2] || match[3] // `:storage` is the value of the variable `STORAGE_FOLDER_PLACEHOLDER` defined in `browser/main/lib/dataApi/attachmentManagement` @@ -60,13 +83,90 @@ return null } + specialLinkHandler(e, rawHref, linkHash) { + const isStartWithHash = rawHref[0] === '#' + + const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html + const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`) + if (isStartWithHash || regexNoteInternalLink.test(rawHref)) { + const posOfHash = linkHash.indexOf('#') + if (posOfHash > -1) { + const extractedId = linkHash.slice(posOfHash + 1) + const targetId = mdurl.encode(extractedId) + const targetElement = document.getElementById(targetId) // this.getWindow().document.getElementById(targetId) + + if (targetElement != null) { + this.scrollTo(0, targetElement.offsetTop) + } + return + } + } + + // this will match the new uuid v4 hash and the old hash + // e.g. + // :note:1c211eb7dcb463de6490 and + // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c + const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/ + if (regexIsNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.replace(':note:', '')) + return + } + + const regexIsLine = /^:line:[0-9]/ + if (regexIsLine.test(linkHash)) { + const numberPattern = /\d+/g + + const lineNumber = parseInt(linkHash.match(numberPattern)[0]) + eventEmitter.emit('line:jump', lineNumber) + return + } + + // this will match the old link format storage.key-note.key + // e.g. + // 877f99c3268608328037-1c211eb7dcb463de6490 + const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/ + if (regexIsLegacyNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.split('-')[1]) + return + } + + const regexIsTagLink = /^:tag:([\w]+)$/ + if (regexIsTagLink.test(rawHref)) { + const tag = rawHref.match(regexIsTagLink)[1] + eventEmitter.emit('dispatch:push', `/tags/${encodeURIComponent(tag)}`) + return + } + } onMouseDown(e) { const { target } = e if (!e[modifier]) { return } + // Create URL spans array used for special case "search term is hitting a link". + const cmUrlSpans = Array.from( + e.target.parentNode.getElementsByClassName('cm-url') + ) + + const innerText = + cmUrlSpans.length > 1 + ? cmUrlSpans.map(span => span.textContent).join('') + : e.target.innerText + const rawHref = innerText.trim().slice(1, -1) // get link text from markdown text + + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() + + const parser = document.createElement('a') + parser.href = rawHref + const { href, hash } = parser + + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 + + this.specialLinkHandler(target, rawHref, linkHash) + const url = this.getUrl(target) + + // all special cases handled --> other case if (url) { e.preventDefault() @@ -79,9 +179,11 @@ const url = this.getUrl(target) if (url) { if (e[modifier]) { - target.classList.add('CodeMirror-activeline-background', 'CodeMirror-hyperlink') - } - else { + target.classList.add( + 'CodeMirror-activeline-background', + 'CodeMirror-hyperlink' + ) + } else { target.classList.add('CodeMirror-activeline-background') } @@ -90,7 +192,10 @@ } onMouseLeave(e) { if (this.tooltip.parentElement === this.lineDiv) { - e.target.classList.remove('CodeMirror-activeline-background', 'CodeMirror-hyperlink') + e.target.classList.remove( + 'CodeMirror-activeline-background', + 'CodeMirror-hyperlink' + ) this.lineDiv.removeChild(this.tooltip) } @@ -99,8 +204,7 @@ if (this.tooltip.parentElement === this.lineDiv) { if (e[modifier]) { e.target.classList.add('CodeMirror-hyperlink') - } - else { + } else { e.target.classList.remove('CodeMirror-hyperlink') } } @@ -110,21 +214,20 @@ const b2 = this.lineDiv.getBoundingClientRect() const tdiv = this.tooltip - tdiv.style.left = (b1.left - b2.left) + 'px' + tdiv.style.left = b1.left - b2.left + 'px' this.lineDiv.appendChild(tdiv) const b3 = tdiv.getBoundingClientRect() const top = b1.top - b2.top - b3.height - yOffset if (top < 0) { - tdiv.style.top = (b1.top - b2.top + b1.height + yOffset) + 'px' - } - else { + tdiv.style.top = b1.top - b2.top + b1.height + yOffset + 'px' + } else { tdiv.style.top = top + 'px' } } } - CodeMirror.defineOption('hyperlink', true, (cm) => { + CodeMirror.defineOption('hyperlink', true, cm => { const addon = new HyperLink(cm) }) -}) \ No newline at end of file +}) diff --git a/extra_scripts/codemirror/mode/bfm/bfm.js b/extra_scripts/codemirror/mode/bfm/bfm.js index 22c20b76..76d06336 100644 --- a/extra_scripts/codemirror/mode/bfm/bfm.js +++ b/extra_scripts/codemirror/mode/bfm/bfm.js @@ -1,10 +1,20 @@ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../codemirror/lib/codemirror"), require("../codemirror/mode/gfm/gfm"), require("../codemirror/mode/yaml-frontmatter/yaml-frontmatter")) - else if (typeof define == "function" && define.amd) // AMD - define(["../codemirror/lib/codemirror", "../codemirror/mode/gfm/gfm", "../codemirror/mode/yaml-frontmatter/yaml-frontmatter"], mod) - else // Plain browser env - mod(CodeMirror) +;(function(mod) { + if (typeof exports == 'object' && typeof module == 'object') + // CommonJS + mod( + require('../codemirror/lib/codemirror'), + require('../codemirror/mode/gfm/gfm'), + require('../codemirror/mode/yaml-frontmatter/yaml-frontmatter') + ) + else if (typeof define == 'function' && define.amd) + // AMD + define([ + '../codemirror/lib/codemirror', + '../codemirror/mode/gfm/gfm', + '../codemirror/mode/yaml-frontmatter/yaml-frontmatter' + ], mod) + // Plain browser env + else mod(CodeMirror) })(function(CodeMirror) { 'use strict' @@ -211,8 +221,8 @@ CodeMirror.defineMIME('text/x-bfm', 'bfm') CodeMirror.modeInfo.push({ - name: "Boost Flavored Markdown", - mime: "text/x-bfm", - mode: "bfm" + name: 'Boost Flavored Markdown', + mime: 'text/x-bfm', + mode: 'bfm' }) }) \ No newline at end of file diff --git a/extra_scripts/codemirror/mode/gfm/gfm.js b/extra_scripts/codemirror/mode/gfm/gfm.js new file mode 100644 index 00000000..9fed7591 --- /dev/null +++ b/extra_scripts/codemirror/mode/gfm/gfm.js @@ -0,0 +1,157 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +;(function(mod) { + if (typeof exports == 'object' && typeof module == 'object') + // CommonJS + mod( + require('../codemirror/lib/codemirror'), + require('../codemirror/mode/markdown/markdown'), + require('../codemirror/addon/mode/overlay') + ) + else if (typeof define == 'function' && define.amd) + // AMD + define([ + '../codemirror/lib/codemirror', + '../codemirror/mode/markdown/markdown', + '../codemirror/addon/mode/overlay' + ], mod) + // Plain browser env + else mod(CodeMirror) +})(function(CodeMirror) { + 'use strict' + + var urlRE = /^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i + + CodeMirror.defineMode( + 'gfm', + function(config, modeConfig) { + var codeDepth = 0 + function blankLine(state) { + state.code = false + return null + } + var gfmOverlay = { + startState: function() { + return { + code: false, + codeBlock: false, + ateSpace: false + } + }, + copyState: function(s) { + return { + code: s.code, + codeBlock: s.codeBlock, + ateSpace: s.ateSpace + } + }, + token: function(stream, state) { + state.combineTokens = null + + // Hack to prevent formatting override inside code blocks (block and inline) + if (state.codeBlock) { + if (stream.match(/^```+/)) { + state.codeBlock = false + return null + } + stream.skipToEnd() + return null + } + if (stream.sol()) { + state.code = false + } + if (stream.sol() && stream.match(/^```+/)) { + stream.skipToEnd() + state.codeBlock = true + return null + } + // If this block is changed, it may need to be updated in Markdown mode + if (stream.peek() === '`') { + stream.next() + var before = stream.pos + stream.eatWhile('`') + var difference = 1 + stream.pos - before + if (!state.code) { + codeDepth = difference + state.code = true + } else { + if (difference === codeDepth) { + // Must be exact + state.code = false + } + } + return null + } else if (state.code) { + stream.next() + return null + } + // Check if space. If so, links can be formatted later on + if (stream.eatSpace()) { + state.ateSpace = true + return null + } + if (stream.sol() || state.ateSpace) { + state.ateSpace = false + if (modeConfig.gitHubSpice !== false) { + if ( + stream.match( + /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/ + ) + ) { + // User/Project@SHA + // User@SHA + // SHA + state.combineTokens = true + return 'link' + } else if ( + stream.match( + /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/ + ) + ) { + // User/Project#Num + // User#Num + // #Num + state.combineTokens = true + return 'link' + } + } + } + if ( + stream.match(urlRE) && + stream.string.slice(stream.start - 2, stream.start) != '](' && + (stream.start == 0 || + /\W/.test(stream.string.charAt(stream.start - 1))) + ) { + // URLs + // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls + // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine + // And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL + state.combineTokens = true + return 'link' + } + stream.next() + return null + }, + blankLine: blankLine + } + + var markdownConfig = { + taskLists: true, + strikethrough: true, + emoji: true + } + for (var attr in modeConfig) { + markdownConfig[attr] = modeConfig[attr] + } + markdownConfig.name = 'markdown' + return CodeMirror.overlayMode( + CodeMirror.getMode(config, markdownConfig), + gfmOverlay + ) + }, + 'markdown' + ) + + CodeMirror.defineMIME('text/x-gfm', 'gfm') +}) diff --git a/extra_scripts/codemirror/theme/nord.css b/extra_scripts/codemirror/theme/nord.css new file mode 100644 index 00000000..c6b52e8a --- /dev/null +++ b/extra_scripts/codemirror/theme/nord.css @@ -0,0 +1,25 @@ +/* Theme: nord */ + +.cm-s-nord.CodeMirror { color: #d8dee9; } +.cm-s-nord.CodeMirror { background: #2e3440; } +.cm-s-nord .CodeMirror-cursor { color: #d8dee9; border-color: #d8dee9; } +.cm-s-nord .CodeMirror-activeline-background { background: #434c5e52 !important; } +.cm-s-nord .CodeMirror-selected { background: undefined; } +.cm-s-nord .cm-comment { color: #4c566a; } +.cm-s-nord .cm-string { color: #a3be8c; } +.cm-s-nord .cm-string-2 { color: #8fbcbb; } +.cm-s-nord .cm-property { color: #8fbcbb; } +.cm-s-nord .cm-qualifier { color: #8fbcbb; } +.cm-s-nord .cm-tag { color: #81a1c1; } +.cm-s-nord .cm-attribute { color: #8fbcbb; } +.cm-s-nord .cm-number { color: #b48ead; } +.cm-s-nord .cm-keyword { color: #81a1c1; } +.cm-s-nord .cm-operator { color: #81a1c1; } +.cm-s-nord .cm-error { background: #bf616a; color: #d8dee9; } +.cm-s-nord .cm-invalidchar { background: #bf616a; color: #d8dee9; } +.cm-s-nord .cm-variable { color: #d8dee9; } +.cm-s-nord .cm-variable-2 { color: #8fbcbb; } +.cm-s-nord .CodeMirror-gutters { + background: #3b4252; + color: #d8dee9; +} \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index ec3bbf79..f235ceaa 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -5,13 +5,15 @@ const packager = require('electron-packager') const WIN = process.platform === 'win32' -module.exports = function (grunt) { +module.exports = function(grunt) { var authCode try { authCode = grunt.file.readJSON('secret/auth_code.json') } catch (e) { if (e.origError.code === 'ENOENT') { - console.warn('secret/auth_code.json is not found. CodeSigning is not available.') + console.warn( + 'secret/auth_code.json is not found. CodeSigning is not available.' + ) } } const OSX_COMMON_NAME = authCode != null ? authCode.OSX_COMMON_NAME : '' @@ -39,12 +41,9 @@ module.exports = function (grunt) { name: 'boostnote', productName: 'Boostnote', genericName: 'Boostnote', - productDescription: 'The opensource note app for developer.', + productDescription: 'The opensource note app for developers.', arch: 'amd64', - categories: [ - 'Development', - 'Utility' - ], + categories: ['Development', 'Utility'], icon: path.join(__dirname, 'resources/app.png'), bin: 'Boostnote' }, @@ -58,12 +57,9 @@ module.exports = function (grunt) { name: 'boostnote', productName: 'Boostnote', genericName: 'Boostnote', - productDescription: 'The opensource note app for developer.', + productDescription: 'The opensource note app for developers.', arch: 'x86_64', - categories: [ - 'Development', - 'Utility' - ], + categories: ['Development', 'Utility'], icon: path.join(__dirname, 'resources/app.png'), bin: 'Boostnote' }, @@ -80,18 +76,21 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-electron-installer-redhat') } - grunt.registerTask('compile', function () { + grunt.registerTask('compile', function() { var done = this.async() - var execPath = path.join('node_modules', '.bin', 'webpack') + ' --config webpack-production.config.js' + var execPath = + path.join('node_modules', '.bin', 'webpack') + + ' --config webpack-production.config.js' grunt.log.writeln(execPath) - ChildProcess.exec(execPath, + ChildProcess.exec( + execPath, { env: Object.assign({}, process.env, { BABEL_ENV: 'production', NODE_ENV: 'production' }) }, - function (err, stdout, stderr) { + function(err, stdout, stderr) { grunt.log.writeln(stdout) if (err) { @@ -105,7 +104,7 @@ module.exports = function (grunt) { ) }) - grunt.registerTask('pack', function (platform) { + grunt.registerTask('pack', function(platform) { grunt.log.writeln(path.join(__dirname, 'dist')) var done = this.async() var opts = { @@ -137,7 +136,7 @@ module.exports = function (grunt) { InternalName: 'Boostnote' } }) - packager(opts, function (err, appPath) { + packager(opts, function(err, appPath) { if (err) { grunt.log.writeln(err) done(err) @@ -149,10 +148,11 @@ module.exports = function (grunt) { case 'osx': Object.assign(opts, { platform: 'darwin', + darwinDarkModeSupport: true, icon: path.join(__dirname, 'resources/app.icns'), 'app-category-type': 'public.app-category.developer-tools' }) - packager(opts, function (err, appPath) { + packager(opts, function(err, appPath) { if (err) { grunt.log.writeln(err) done(err) @@ -167,7 +167,7 @@ module.exports = function (grunt) { icon: path.join(__dirname, 'resources/app.icns'), 'app-category-type': 'public.app-category.developer-tools' }) - packager(opts, function (err, appPath) { + packager(opts, function(err, appPath) { if (err) { grunt.log.writeln(err) done(err) @@ -179,15 +179,16 @@ module.exports = function (grunt) { } }) - grunt.registerTask('codesign', function (platform) { + grunt.registerTask('codesign', function(platform) { var done = this.async() if (process.platform !== 'darwin') { done(false) return } - ChildProcess.exec(`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`, - function (err, stdout, stderr) { + ChildProcess.exec( + `codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`, + function(err, stdout, stderr) { grunt.log.writeln(stdout) if (err) { grunt.log.writeln(err) @@ -196,44 +197,43 @@ module.exports = function (grunt) { return } done() - }) + } + ) }) - grunt.registerTask('create-osx-installer', function () { + grunt.registerTask('create-osx-installer', function() { var done = this.async() var execPath = 'appdmg appdmg.json dist/Boostnote-mac.dmg' grunt.log.writeln(execPath) - ChildProcess.exec(execPath, - function (err, stdout, stderr) { - grunt.log.writeln(stdout) - if (err) { - grunt.log.writeln(err) - grunt.log.writeln(stderr) - done(false) - return - } - done() - }) + ChildProcess.exec(execPath, function(err, stdout, stderr) { + grunt.log.writeln(stdout) + if (err) { + grunt.log.writeln(err) + grunt.log.writeln(stderr) + done(false) + return + } + done() + }) }) - grunt.registerTask('zip', function (platform) { + grunt.registerTask('zip', function(platform) { var done = this.async() switch (platform) { case 'osx': - var execPath = 'cd dist/Boostnote-darwin-x64 && zip -r -y -q ../Boostnote-mac.zip Boostnote.app' + var execPath = + 'cd dist/Boostnote-darwin-x64 && zip -r -y -q ../Boostnote-mac.zip Boostnote.app' grunt.log.writeln(execPath) - ChildProcess.exec(execPath, - function (err, stdout, stderr) { - grunt.log.writeln(stdout) - if (err) { - grunt.log.writeln(err) - grunt.log.writeln(stderr) - done(false) - return - } - done() + ChildProcess.exec(execPath, function(err, stdout, stderr) { + grunt.log.writeln(stdout) + if (err) { + grunt.log.writeln(err) + grunt.log.writeln(stderr) + done(false) + return } - ) + done() + }) break default: done() @@ -241,7 +241,7 @@ module.exports = function (grunt) { } }) - function getTarget () { + function getTarget() { switch (process.platform) { case 'darwin': return 'osx' @@ -254,7 +254,7 @@ module.exports = function (grunt) { } } - grunt.registerTask('build', function (platform) { + grunt.registerTask('build', function(platform) { if (platform == null) platform = getTarget() switch (platform) { @@ -262,15 +262,26 @@ module.exports = function (grunt) { grunt.task.run(['compile', 'pack:win', 'create-windows-installer']) break case 'osx': - grunt.task.run(['compile', 'pack:osx', 'codesign', 'create-osx-installer', 'zip:osx']) + grunt.task.run([ + 'compile', + 'pack:osx', + 'codesign', + 'create-osx-installer', + 'zip:osx' + ]) break case 'linux': - grunt.task.run(['compile', 'pack:linux', 'electron-installer-debian', 'electron-installer-redhat']) + grunt.task.run([ + 'compile', + 'pack:linux', + 'electron-installer-debian', + 'electron-installer-redhat' + ]) break } }) - grunt.registerTask('pre-build', function (platform) { + grunt.registerTask('pre-build', function(platform) { if (platform == null) platform = getTarget() switch (platform) { @@ -285,11 +296,11 @@ module.exports = function (grunt) { } }) - grunt.registerTask('bfm', function () { + grunt.registerTask('bfm', function() { const Color = require('color') const parseCSS = require('css').parse - function generateRule (selector, bgColor, fgColor) { + function generateRule(selector, bgColor, fgColor) { if (bgColor.isLight()) { bgColor = bgColor.mix(fgColor, 0.05) } else { @@ -297,48 +308,79 @@ module.exports = function (grunt) { } if (selector && selector.length > 0) { - return `${selector} .cm-table-row-even { background-color: ${bgColor.rgb().string()}; }` + return `${selector} .cm-table-row-even { background-color: ${bgColor + .rgb() + .string()}; }` } else { - return `.cm-table-row-even { background-color: ${bgColor.rgb().string()}; }` + return `.cm-table-row-even { background-color: ${bgColor + .rgb() + .string()}; }` } } const root = path.join(__dirname, 'node_modules/codemirror/theme/') - const colors = fs.readdirSync(root).filter(file => file !== 'solarized.css').map(file => { - const css = parseCSS(fs.readFileSync(path.join(root, file), 'utf8')) + const colors = fs + .readdirSync(root) + .filter(file => file !== 'solarized.css') + .map(file => { + const css = parseCSS(fs.readFileSync(path.join(root, file), 'utf8')) - const rules = css.stylesheet.rules.filter(rule => rule.selectors && /\b\.CodeMirror$/.test(rule.selectors[0])) - if (rules.length === 1) { - let bgColor = Color('white') - let fgColor = Color('black') + const rules = css.stylesheet.rules.filter( + rule => rule.selectors && /\b\.CodeMirror$/.test(rule.selectors[0]) + ) + if (rules.length === 1) { + let bgColor = Color('white') + let fgColor = Color('black') - rules[0].declarations.forEach(declaration => { - if (declaration.property === 'background-color' || declaration.property === 'background') { - bgColor = Color(declaration.value.split(' ')[0]) - } else if (declaration.property === 'color') { - const value = /^(.*?)(?:\s*!important)?$/.exec(declaration.value)[1] - const match = /^rgba\((.*?),\s*1\)$/.exec(value) - if (match) { - fgColor = Color(`rgb(${match[1]})`) - } else { - fgColor = Color(value) + rules[0].declarations.forEach(declaration => { + if ( + declaration.property === 'background-color' || + declaration.property === 'background' + ) { + bgColor = Color(declaration.value.split(' ')[0]) + } else if (declaration.property === 'color') { + const value = /^(.*?)(?:\s*!important)?$/.exec( + declaration.value + )[1] + const match = /^rgba\((.*?),\s*1\)$/.exec(value) + if (match) { + fgColor = Color(`rgb(${match[1]})`) + } else { + fgColor = Color(value) + } } - } - }) + }) - return generateRule(rules[0].selectors[0], bgColor, fgColor) - } - }).filter(value => !!value) + return generateRule(rules[0].selectors[0], bgColor, fgColor) + } + }) + .filter(value => !!value) // default colors.unshift(generateRule(null, Color('white'), Color('black'))) // solarized dark - colors.push(generateRule('.cm-s-solarized.cm-s-dark', Color('#002b36'), Color('#839496'))) + colors.push( + generateRule( + '.cm-s-solarized.cm-s-dark', + Color('#002b36'), + Color('#839496') + ) + ) // solarized light - colors.push(generateRule('.cm-s-solarized.cm-s-light', Color('#fdf6e3'), Color('#657b83'))) + colors.push( + generateRule( + '.cm-s-solarized.cm-s-light', + Color('#fdf6e3'), + Color('#657b83') + ) + ) - fs.writeFileSync(path.join(__dirname, 'extra_scripts/codemirror/mode/bfm/bfm.css'), colors.join('\n'), 'utf8') + fs.writeFileSync( + path.join(__dirname, 'extra_scripts/codemirror/mode/bfm/bfm.css'), + colors.join('\n'), + 'utf8' + ) }) grunt.registerTask('default', ['build']) diff --git a/index.js b/index.js index 96f98e73..bcc4e879 100644 --- a/index.js +++ b/index.js @@ -4,30 +4,30 @@ const path = require('path') var error = null -function execMainApp () { +function execMainApp() { const appRootPath = path.join(process.execPath, '../..') const updateDotExePath = path.join(appRootPath, 'Update.exe') const exeName = path.basename(process.execPath) - function spawnUpdate (args, cb) { + function spawnUpdate(args, cb) { var stdout = '' var updateProcess = null try { updateProcess = ChildProcess.spawn(updateDotExePath, args) } catch (e) { - process.nextTick(function () { + process.nextTick(function() { cb(e) }) } - updateProcess.stdout.on('data', function (data) { + updateProcess.stdout.on('data', function(data) { stdout += data }) - updateProcess.on('error', function (_error) { + updateProcess.on('error', function(_error) { error = _error }) - updateProcess.on('close', function (code, signal) { + updateProcess.on('close', function(code, signal) { if (code !== 0) { error = new Error('Command failed: #{signal ? code}') error.code = code @@ -38,7 +38,7 @@ function execMainApp () { }) } - var handleStartupEvent = function () { + var handleStartupEvent = function() { if (process.platform !== 'win32') { return false } @@ -46,7 +46,7 @@ function execMainApp () { var squirrelCommand = process.argv[1] switch (squirrelCommand) { case '--squirrel-install': - spawnUpdate(['--createShortcut', exeName], function (err) { + spawnUpdate(['--createShortcut', exeName], function(err) { if (err) console.error(err) app.quit() }) @@ -55,7 +55,7 @@ function execMainApp () { app.quit() return true case '--squirrel-uninstall': - spawnUpdate(['--removeShortcut', exeName], function (err) { + spawnUpdate(['--removeShortcut', exeName], function(err) { if (err) console.error(err) app.quit() }) diff --git a/lib/ipcServer.js b/lib/ipcServer.js index 487a9a63..41d3ea7b 100644 --- a/lib/ipcServer.js +++ b/lib/ipcServer.js @@ -7,7 +7,7 @@ nodeIpc.config.id = 'node' nodeIpc.config.retry = 1500 nodeIpc.config.silent = true -function toggleMainWindow () { +function toggleMainWindow() { switch (global.process.platform) { case 'darwin': if (mainWindow.isFocused()) { @@ -32,6 +32,7 @@ ipcMain.on('config-renew', (e, payload) => { globalShortcut.unregisterAll() var { config } = payload + mainWindow.setMenuBarVisibility(config.ui.showMenuBar) var errors = [] try { globalShortcut.register(config.hotkey.toggleMain, toggleMainWindow) @@ -51,14 +52,14 @@ ipcMain.on('config-renew', (e, payload) => { nodeIpc.serve( path.join(app.getPath('userData'), 'boostnote.service'), - function () { - nodeIpc.server.on('connect', function (socket) { + function() { + nodeIpc.server.on('connect', function(socket) { nodeIpc.log('ipc server >> socket joinned'.rainbow) - socket.on('close', function () { + socket.on('close', function() { nodeIpc.log('ipc server >> socket closed'.rainbow) }) }) - nodeIpc.server.on('error', function (err) { + nodeIpc.server.on('error', function(err) { nodeIpc.log('Node IPC error'.rainbow, err) }) } diff --git a/lib/main-app.js b/lib/main-app.js index f25d07d2..2293fd58 100644 --- a/lib/main-app.js +++ b/lib/main-app.js @@ -3,24 +3,26 @@ const app = electron.app const Menu = electron.Menu const ipc = electron.ipcMain const GhReleases = require('electron-gh-releases') +const { isPackaged } = app +const electronConfig = new (require('electron-config'))() // electron.crashReporter.start() +const singleInstance = app.requestSingleInstanceLock() + var ipcServer = null var mainWindow = null -var shouldQuit = app.makeSingleInstance(function (commandLine, workingDirectory) { - if (mainWindow) { - if (process.platform === 'win32') { - mainWindow.minimize() - mainWindow.restore() - } - mainWindow.focus() - } - return true -}) - -if (shouldQuit) { +// Single Instance Lock +if (!singleInstance) { app.quit() +} else { + app.on('second-instance', () => { + // Someone tried to run a second instance, it should focus the existing instance. + if (mainWindow) { + if (!mainWindow.isVisible()) mainWindow.show() + mainWindow.focus() + } + }) } var isUpdateReady = false @@ -34,7 +36,13 @@ const updater = new GhReleases(ghReleasesOpts) // Check for updates // `status` returns true if there is a new update available -function checkUpdate () { +function checkUpdate() { + if (!isPackaged) { + // Prevents app from attempting to update when in dev mode. + console.log('Updates are disabled in Development mode, see main-app.js') + return true + } + if (!electronConfig.get('autoUpdateEnabled', true)) return if (process.platform === 'linux' || isUpdateReady) { return true } @@ -51,29 +59,29 @@ function checkUpdate () { }) } -updater.on('update-downloaded', (info) => { +updater.on('update-downloaded', info => { if (mainWindow != null) { mainWindow.webContents.send('update-ready', 'Update available!') isUpdateReady = true } }) -updater.autoUpdater.on('error', (err) => { +updater.autoUpdater.on('error', err => { console.error(err) }) -ipc.on('update-app-confirm', function (event, msg) { +ipc.on('update-app-confirm', function(event, msg) { if (isUpdateReady) { mainWindow.removeAllListeners() updater.install() } }) -app.on('window-all-closed', function () { +app.on('window-all-closed', function() { app.quit() }) -app.on('ready', function () { +app.on('ready', function() { mainWindow = require('./main-window') var template = require('./main-menu') @@ -93,15 +101,15 @@ app.on('ready', function () { } // Check update every day - setInterval(function () { - checkUpdate() + setInterval(function() { + if (isPackaged) checkUpdate() }, 1000 * 60 * 60 * 24) // Check update after 10 secs to prevent file locking of Windows setTimeout(() => { - checkUpdate() + if (isPackaged) checkUpdate() - ipc.on('update-check', function (event, msg) { + ipc.on('update-check', function(event, msg) { if (isUpdateReady) { mainWindow.webContents.send('update-ready', 'Update available!') } else { diff --git a/lib/main-menu.js b/lib/main-menu.js index 05921347..0c91cf1f 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -3,6 +3,7 @@ const BrowserWindow = electron.BrowserWindow const shell = electron.shell const ipc = electron.ipcMain const mainWindow = require('./main-window') +const os = require('os') const macOS = process.platform === 'darwin' // const WIN = process.platform === 'win32' @@ -10,68 +11,68 @@ const LINUX = process.platform === 'linux' const boost = macOS ? { - label: 'Boostnote', - submenu: [ - { - label: 'About Boostnote', - selector: 'orderFrontStandardAboutPanel:' - }, - { - type: 'separator' - }, - { - label: 'Preferences', - accelerator: 'Command+,', - click () { - mainWindow.webContents.send('side:preferences') + label: 'Boostnote', + submenu: [ + { + label: 'About Boostnote', + selector: 'orderFrontStandardAboutPanel:' + }, + { + type: 'separator' + }, + { + label: 'Preferences', + accelerator: 'Command+,', + click() { + mainWindow.webContents.send('side:preferences') + } + }, + { + type: 'separator' + }, + { + label: 'Hide Boostnote', + accelerator: 'Command+H', + selector: 'hide:' + }, + { + label: 'Hide Others', + accelerator: 'Command+Shift+H', + selector: 'hideOtherApplications:' + }, + { + label: 'Show All', + selector: 'unhideAllApplications:' + }, + { + type: 'separator' + }, + { + label: 'Quit Boostnote', + role: 'quit', + accelerator: 'CommandOrControl+Q' } - }, - { - type: 'separator' - }, - { - label: 'Hide Boostnote', - accelerator: 'Command+H', - selector: 'hide:' - }, - { - label: 'Hide Others', - accelerator: 'Command+Shift+H', - selector: 'hideOtherApplications:' - }, - { - label: 'Show All', - selector: 'unhideAllApplications:' - }, - { - type: 'separator' - }, - { - label: 'Quit Boostnote', - role: 'quit', - accelerator: 'CommandOrControl+Q' - } - ] - } + ] + } : { - label: 'Boostnote', - submenu: [ - { - label: 'Preferences', - accelerator: 'Control+,', - click () { - mainWindow.webContents.send('side:preferences') + label: 'Boostnote', + submenu: [ + { + label: 'Preferences', + accelerator: 'Control+,', + click() { + mainWindow.webContents.send('side:preferences') + } + }, + { + type: 'separator' + }, + { + role: 'quit', + accelerator: 'Control+Q' } - }, - { - type: 'separator' - }, - { - role: 'quit', - accelerator: 'Control+Q' - } - ] - } + ] + } const file = { label: 'File', @@ -79,28 +80,28 @@ const file = { { label: 'New Note', accelerator: 'CommandOrControl+N', - click () { + click() { mainWindow.webContents.send('top:new-note') } }, { label: 'Focus Note', - accelerator: macOS ? 'Command+E' : 'Control+E', - click () { + accelerator: 'CommandOrControl+E', + click() { mainWindow.webContents.send('detail:focus') } }, { label: 'Delete Note', - accelerator: macOS ? 'Command+Shift+Backspace' : 'Control+Shift+Backspace', - click () { + accelerator: 'CommandOrControl+Shift+Backspace', + click() { mainWindow.webContents.send('detail:delete') } }, { label: 'Clone Note', - accelerator: macOS ? 'Command+D' : 'Control+D', - click () { + accelerator: 'CommandOrControl+D', + click() { mainWindow.webContents.send('list:clone') } }, @@ -112,7 +113,7 @@ const file = { submenu: [ { label: 'Plain Text, MarkDown (.txt, .md)', - click () { + click() { mainWindow.webContents.send('import:file') } } @@ -123,24 +124,31 @@ const file = { submenu: [ { label: 'Plain Text (.txt)', - click () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'export-txt') mainWindow.webContents.send('export:save-text') } }, { label: 'MarkDown (.md)', - click () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'export-md') mainWindow.webContents.send('export:save-md') } }, { label: 'HTML (.html)', - click () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'export-html') mainWindow.webContents.send('export:save-html') } + }, + { + label: 'PDF (.pdf)', + click() { + mainWindow.webContents.send('list:isMarkdownNote', 'export-pdf') + mainWindow.webContents.send('export:save-pdf') + } } ] }, @@ -150,13 +158,13 @@ const file = { { label: 'Generate/Update Markdown TOC', accelerator: 'Shift+Ctrl+T', - click () { + click() { mainWindow.webContents.send('code:generate-toc') } }, { label: 'Format Table', - click () { + click() { mainWindow.webContents.send('code:format-table') } }, @@ -166,7 +174,7 @@ const file = { { label: 'Print', accelerator: 'CommandOrControl+P', - click () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'print') mainWindow.webContents.send('print') } @@ -175,20 +183,25 @@ const file = { } if (LINUX) { - file.submenu.push({ - type: 'separator' - }, { - label: 'Preferences', - accelerator: 'Control+,', - click () { - mainWindow.webContents.send('side:preferences') + file.submenu.push( + { + type: 'separator' + }, + { + label: 'Preferences', + accelerator: 'Control+,', + click() { + mainWindow.webContents.send('side:preferences') + } + }, + { + type: 'separator' + }, + { + role: 'quit', + accelerator: 'Control+Q' } - }, { - type: 'separator' - }, { - role: 'quit', - accelerator: 'Control+Q' - }) + ) } const edit = { @@ -233,7 +246,7 @@ const edit = { { label: 'Add Tag', accelerator: 'CommandOrControl+Shift+T', - click () { + click() { mainWindow.webContents.send('editor:add-tag') } } @@ -246,14 +259,14 @@ const view = { { label: 'Reload', accelerator: 'CommandOrControl+R', - click () { + click() { BrowserWindow.getFocusedWindow().reload() } }, { label: 'Toggle Developer Tools', - accelerator: macOS ? 'Command+Alt+I' : 'Control+Shift+I', - click () { + accelerator: 'CommandOrControl+Alt+I', + click() { BrowserWindow.getFocusedWindow().toggleDevTools() } }, @@ -263,14 +276,14 @@ const view = { { label: 'Next Note', accelerator: 'CommandOrControl+]', - click () { + click() { mainWindow.webContents.send('list:next') } }, { label: 'Previous Note', accelerator: 'CommandOrControl+[', - click () { + click() { mainWindow.webContents.send('list:prior') } }, @@ -280,7 +293,7 @@ const view = { { label: 'Focus Search', accelerator: 'CommandOrControl+Shift+L', - click () { + click() { mainWindow.webContents.send('top:focus-search') } }, @@ -290,38 +303,44 @@ const view = { { label: 'Toggle Full Screen', accelerator: macOS ? 'Command+Control+F' : 'F11', - click () { + click() { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }, { label: 'Toggle Side Bar', accelerator: 'CommandOrControl+B', - click () { + click() { mainWindow.webContents.send('editor:fullscreen') } }, + { + label: 'Toggle Editor Orientation', + click() { + mainWindow.webContents.send('editor:orientation') + } + }, { type: 'separator' }, { label: 'Actual Size', - accelerator: macOS ? 'CommandOrControl+0' : 'Control+0', - click () { + accelerator: 'CommandOrControl+0', + click() { mainWindow.webContents.send('status:zoomreset') } }, { label: 'Zoom In', - accelerator: macOS ? 'CommandOrControl+=' : 'Control+=', - click () { + accelerator: 'CommandOrControl+=', + click() { mainWindow.webContents.send('status:zoomin') } }, { label: 'Zoom Out', - accelerator: macOS ? 'CommandOrControl+-' : 'Control+-', - click () { + accelerator: 'CommandOrControl+-', + click() { mainWindow.webContents.send('status:zoomout') } } @@ -374,21 +393,88 @@ const help = { submenu: [ { label: 'Boostnote official site', - click () { shell.openExternal('https://boostnote.io/') } + click() { + shell.openExternal('https://boostnote.io/') + } + }, + { + label: 'Wiki', + click() { + shell.openExternal('https://github.com/BoostIO/Boostnote/wiki') + } }, { label: 'Issue Tracker', - click () { shell.openExternal('https://github.com/BoostIO/Boostnote/issues') } + click() { + shell.openExternal('https://github.com/BoostIO/Boostnote/issues') + } }, { label: 'Changelog', - click () { shell.openExternal('https://github.com/BoostIO/boost-releases') } + click() { + shell.openExternal('https://github.com/BoostIO/boost-releases') + } + }, + { + label: 'Cheatsheets', + submenu: [ + { + label: 'Markdown', + click() { + shell.openExternal( + 'https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet' + ) + } + }, + { + label: 'Latex', + click() { + shell.openExternal('https://katex.org/docs/supported.html') + } + }, + { + label: 'HTML', + click() { + shell.openExternal('https://htmlcheatsheet.com/') + } + }, + { + label: 'Boostnote', + click() { + shell.openExternal( + 'https://github.com/TobseF/boostnote-markdown-cheatsheet/blob/master/BOOSTNOTE_MARKDOWN_CHEAT_SHEET.md' + ) + } + } + ] + }, + { + type: 'separator' + }, + { + label: 'About', + click() { + const version = electron.app.getVersion() + const electronVersion = process.versions.electron + const chromeVersion = process.versions.chrome + const nodeVersion = process.versions.node + const v8Version = process.versions.v8 + const OSInfo = `${os.type()} ${os.arch()} ${os.release()}` + const detail = `Version: ${version}\nElectron: ${electronVersion}\nChrome: ${chromeVersion}\nNode.js: ${nodeVersion}\nV8: ${v8Version}\nOS: ${OSInfo}` + electron.dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { + title: 'BoostNote', + message: 'BoostNote', + type: 'info', + detail: `\n${detail}` + }) + } } ] } -module.exports = process.platform === 'darwin' - ? [boost, file, edit, view, window, help] - : process.platform === 'win32' - ? [boost, file, view, help] - : [file, view, help] +module.exports = + process.platform === 'darwin' + ? [boost, file, edit, view, window, help] + : process.platform === 'win32' + ? [boost, file, view, help] + : [file, view, help] diff --git a/lib/main-window.js b/lib/main-window.js index 512782de..e6b0cb29 100644 --- a/lib/main-window.js +++ b/lib/main-window.js @@ -6,7 +6,33 @@ const Config = require('electron-config') const config = new Config() const _ = require('lodash') -var showMenu = process.platform !== 'win32' +// set up some chrome extensions +if (process.env.NODE_ENV === 'development') { + const { + default: installExtension, + REACT_DEVELOPER_TOOLS, + REACT_PERF + } = require('electron-devtools-installer') + + require('electron-debug')({ showDevTools: false }) + + const ChromeLens = { + // ID of the extension (https://chrome.google.com/webstore/detail/chromelens/idikgljglpfilbhaboonnpnnincjhjkd) + id: 'idikgljglpfilbhaboonnpnnincjhjkd', + electron: '>=1.2.1' + } + + const extensions = [REACT_DEVELOPER_TOOLS, REACT_PERF, ChromeLens] + + for (const extension of extensions) { + try { + installExtension(extension) + } catch (e) { + console.error(`[ELECTRON] Extension installation failed`, e) + } + } +} + const windowSize = config.get('windowsize') || { x: null, y: null, @@ -22,19 +48,23 @@ const mainWindow = new BrowserWindow({ useContentSize: true, minWidth: 500, minHeight: 320, - autoHideMenuBar: showMenu, webPreferences: { zoomFactor: 1.0, enableBlinkFeatures: 'OverlayScrollbars' }, icon: path.resolve(__dirname, '../resources/app.png') }) - -const url = path.resolve(__dirname, './main.html') +const url = path.resolve( + __dirname, + process.env.NODE_ENV === 'development' + ? './main.development.html' + : './main.production.html' +) mainWindow.loadURL('file://' + url) +mainWindow.setMenuBarVisibility(false) -mainWindow.webContents.on('new-window', function (e) { +mainWindow.webContents.on('new-window', function(e) { e.preventDefault() }) @@ -49,10 +79,10 @@ mainWindow.webContents.sendInputEvent({ }) if (process.platform === 'darwin') { - mainWindow.on('close', function (e) { + mainWindow.on('close', function(e) { e.preventDefault() if (mainWindow.isFullScreen()) { - mainWindow.once('leave-full-screen', function () { + mainWindow.once('leave-full-screen', function() { mainWindow.hide() }) mainWindow.setFullScreen(false) @@ -61,7 +91,7 @@ if (process.platform === 'darwin') { } }) - app.on('before-quit', function (e) { + app.on('before-quit', function(e) { mainWindow.removeAllListeners() }) } @@ -69,7 +99,7 @@ if (process.platform === 'darwin') { mainWindow.on('resize', _.throttle(storeWindowSize, 500)) mainWindow.on('move', _.throttle(storeWindowSize, 500)) -function storeWindowSize () { +function storeWindowSize() { try { config.set('windowsize', mainWindow.getBounds()) } catch (e) { @@ -78,7 +108,7 @@ function storeWindowSize () { } } -app.on('activate', function () { +app.on('activate', function() { if (mainWindow == null) return null mainWindow.show() }) diff --git a/lib/main.development.html b/lib/main.development.html new file mode 100644 index 00000000..d6216b7e --- /dev/null +++ b/lib/main.development.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + Boostnote + + + + + +
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/main.html b/lib/main.production.html similarity index 89% rename from lib/main.html rename to lib/main.production.html index ff48d6e1..289fe1b3 100644 --- a/lib/main.html +++ b/lib/main.production.html @@ -10,6 +10,7 @@ + Boostnote @@ -71,6 +72,11 @@ border-left-color: rgba(142, 142, 142, 0.5); mix-blend-mode: difference; } + + .CodeMirror-scroll { + margin-bottom: 0; + padding-bottom: 0; + } @@ -98,15 +104,15 @@ - + - + @@ -124,15 +130,19 @@ + + + + - - - + + +