diff --git a/.eslintrc b/.eslintrc index be8cb903..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,7 +13,8 @@ "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, diff --git a/.gitignore b/.gitignore index ace5316c..aac64950 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ node_modules/* /secret *.log .idea -.vscode \ No newline at end of file +.vscode +package-lock.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 90548ee9..71b65671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ node_js: - 8 script: - npm run lint && npm run test - - yarn jest - '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 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 24c573b5..faa04e9a 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -6,11 +6,7 @@ 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' @@ -20,11 +16,15 @@ import styles from '../components/CodeEditor.styl' const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') -const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu +const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') + .buildEditorContextMenu import { createTurndownService } from '../lib/turndown' -import {languageMaps} from '../lib/CMLanguageList' +import { languageMaps } from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' -import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' +import { + generateInEditor, + tocExistsInEditor +} from 'browser/lib/markdown-toc-generator' import markdownlint from 'markdownlint' import Jsonlint from 'jsonlint-mod' import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' @@ -33,28 +33,38 @@ 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) + const debouncedDeletionOfAttachments = _.debounce( + attachmentManagement.deleteAttachmentsNotPresentInNote, + 30000 + ) this.blurHandler = (editor, e) => { ipcRenderer.send('editor:focused', false) if (e == null) return null @@ -66,12 +76,13 @@ export default class CodeEditor extends React.Component { el = el.parentNode } this.props.onBlur != null && this.props.onBlur(e) - const { - storageKey, - noteKey - } = this.props + const { storageKey, noteKey } = this.props if (this.props.deleteUnusedAttachments === true) { - debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) + debouncedDeletionOfAttachments( + this.editor.getValue(), + storageKey, + noteKey + ) } } this.pasteHandler = (editor, e) => { @@ -91,7 +102,7 @@ export default class CodeEditor extends React.Component { 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) @@ -104,24 +115,24 @@ export default class CodeEditor extends React.Component { this.turndownService = createTurndownService() } - handleSearch (msg) { + handleSearch(msg) { const cm = this.editor const component = this if (component.searchState) cm.removeOverlay(component.searchState) 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) { @@ -138,25 +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 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 @@ -198,17 +211,17 @@ 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) { + [translateHotkey(hotkey.insertDate)]: function(cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleDateString()) }, - [translateHotkey(hotkey.insertDateTime)]: function (cm) { + [translateHotkey(hotkey.insertDateTime)]: function(cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleString()) }, @@ -231,7 +244,10 @@ export default class CodeEditor extends React.Component { currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos) // Prettify contents of editor - const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig) + const formattedTextDetails = prettier.formatWithCursor( + cm.doc.getValue(), + currentConfig + ) const formattedText = formattedTextDetails.formatted const formattedCursorPos = formattedTextDetails.cursorOffset @@ -241,13 +257,23 @@ export default class CodeEditor extends React.Component { 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') { @@ -263,8 +289,8 @@ export default class CodeEditor extends React.Component { } } - componentDidMount () { - const { rulers, enableRulers, enableMarkdownLint } = this.props + componentDidMount() { + const { rulers, enableRulers, enableMarkdownLint, RTL } = this.props eventEmitter.on('line:jump', this.scrollToLineHandeler) snippetManager.init() @@ -285,9 +311,15 @@ export default class CodeEditor extends React.Component { scrollPastEnd: this.props.scrollPastEnd, inputStyle: 'textarea', dragDrop: false, + direction: RTL ? 'rtl' : 'ltr', + rtlMoveVisually: RTL, foldGutter: true, lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], + gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ], autoCloseBrackets: { pairs: this.props.matchingPairs, triples: this.props.matchingTriples, @@ -298,7 +330,9 @@ export default class CodeEditor extends React.Component { prettierConfig: this.props.prettierConfig }) - document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' + 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) @@ -331,7 +365,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) @@ -341,13 +375,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': () => { @@ -466,7 +500,7 @@ export default class CodeEditor extends React.Component { this.initialHighlighting() } - getWordBeforeCursor (line, lineNumber, cursorPosition) { + getWordBeforeCursor(line, lineNumber, cursorPosition) { let wordBeforeCursor = '' const originCursorPosition = cursorPosition const emptyChars = /\t|\s|\r|\n|\$/ @@ -503,11 +537,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) @@ -522,7 +556,7 @@ 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, @@ -546,13 +580,22 @@ export default class CodeEditor extends React.Component { if (prevProps.keyMap !== this.props.keyMap) { needRefresh = true } - if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) { + 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' + 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' + document.querySelector('.CodeMirror-lint-markers').style.display = + 'inline-block' } needRefresh = true } @@ -584,9 +627,11 @@ export default class CodeEditor extends React.Component { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } - if (prevProps.matchingPairs !== this.props.matchingPairs || + if ( + prevProps.matchingPairs !== this.props.matchingPairs || prevProps.matchingTriples !== this.props.matchingTriples || - prevProps.explodingPairs !== this.props.explodingPairs) { + prevProps.explodingPairs !== this.props.explodingPairs + ) { const bracketObject = { pairs: this.props.matchingPairs, triples: this.props.matchingTriples, @@ -631,11 +676,18 @@ 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 ( + prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments + ) { + this.editor.setOption( + 'deleteUnusedAttachments', + this.props.deleteUnusedAttachments + ) } if (needRefresh) { @@ -643,17 +695,19 @@ export default class CodeEditor extends React.Component { } } - getCodeEditorLintConfig () { + getCodeEditorLintConfig() { const { mode } = this.props const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown' - return checkMarkdownNoteIsOpen ? { - 'getAnnotations': this.validatorOfMarkdown, - 'async': true - } : false + return checkMarkdownNoteIsOpen + ? { + getAnnotations: this.validatorOfMarkdown, + async: true + } + : false } - validatorOfMarkdown (text, updateLinting) { + validatorOfMarkdown(text, updateLinting) { const { customMarkdownLintConfig } = this.props let lintConfigJson try { @@ -664,10 +718,10 @@ export default class CodeEditor extends React.Component { return } const lintOptions = { - 'strings': { - 'content': text + strings: { + content: text }, - 'config': lintConfigJson + config: lintConfigJson } return markdownlint(lintOptions, (err, result) => { @@ -678,7 +732,7 @@ export default class CodeEditor extends React.Component { let ruleNames = '' item.ruleNames.map((ruleName, index) => { ruleNames += ruleName - ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/' + ruleNames += index === item.ruleNames.length - 1 ? ': ' : '/' }) const lineNumber = item.lineNumber - 1 foundIssues.push({ @@ -693,7 +747,7 @@ export default class CodeEditor extends React.Component { }) } - setMode (mode) { + setMode(mode) { let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') @@ -701,7 +755,7 @@ export default class CodeEditor extends React.Component { 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. @@ -711,7 +765,11 @@ export default class CodeEditor extends React.Component { // 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))) { + if ( + this.linePossibleContainsHeadline( + editor.getLine(changeObject.from.line + line) + ) + ) { requireTocUpdate = true break } @@ -740,13 +798,13 @@ export default class CodeEditor extends React.Component { } } - linePossibleContainsHeadline (currentLine) { + 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) { + incrementLines(start, linesAdded, linesRemoved, editor) { const highlightedLines = editor.options.linesHighlighted const totalHighlightedLines = highlightedLines.length @@ -767,7 +825,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) } } @@ -781,22 +839,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 @@ -827,28 +893,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 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 @@ -859,7 +925,7 @@ 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) @@ -870,18 +936,19 @@ export default class CodeEditor extends React.Component { * @param {Number} lineNumber * @param {String} content */ - setLineContent (lineNumber, 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 }) + this.editor.replaceRange( + content, + { line: lineNumber, ch: 0 }, + { line: lineNumber, ch: prevContentLength } + ) } - handleDropImage (dropEvent) { + handleDropImage(dropEvent) { dropEvent.preventDefault() - const { - storageKey, - noteKey - } = this.props + const { storageKey, noteKey } = this.props attachmentManagement.handleAttachmentDrop( this, storageKey, @@ -890,37 +957,44 @@ export default class CodeEditor extends React.Component { ) } - insertAttachmentMd (imageMd) { + insertAttachmentMd(imageMd) { this.editor.replaceSelection(imageMd) } - autoDetectLanguage (content) { + autoDetectLanguage(content) { const res = hljs.highlightAuto(content, Object.keys(languageMaps)) this.setMode(languageMaps[res.language]) } - handlePaste (editor, forceSmartPaste) { + handlePaste(editor, forceSmartPaste) { const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props - const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.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 === ')' } @@ -932,7 +1006,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, @@ -964,7 +1038,11 @@ export default class CodeEditor extends React.Component { if (isInFencedCodeBlock(editor)) { this.handlePasteText(editor, pastedTxt) - } else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) { + } else if ( + fetchUrlTitle && + isMarkdownTitleURL(pastedTxt) && + !isInLinkTag(editor) + ) { this.handlePasteUrl(editor, pastedTxt) } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this.handlePasteUrl(editor, pastedTxt) @@ -1000,13 +1078,13 @@ export default class CodeEditor extends React.Component { } } - handleScroll (e) { + handleScroll(e) { if (this.props.onScroll) { this.props.onScroll(e) } } - handlePasteUrl (editor, pastedTxt) { + handlePasteUrl(editor, pastedTxt) { let taggedUrl = `<${pastedTxt}>` let urlToFetch = pastedTxt let titleMark = '' @@ -1056,16 +1134,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 { @@ -1073,10 +1151,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) @@ -1085,7 +1165,7 @@ export default class CodeEditor extends React.Component { }) } - initialHighlighting () { + initialHighlighting() { if (this.editor.options.linesHighlighted == null) { return } @@ -1099,16 +1179,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 @@ -1121,7 +1205,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')) @@ -1129,10 +1213,10 @@ 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' + const charset = + _charset !== undefined && iconv.encodingExists(_charset) + ? _charset + : 'utf-8' resolve(iconv.decode(Buffer.from(buff), charset).toString()) } catch (e) { reject(e) @@ -1141,54 +1225,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, - fontFamily, - width, - height - } = this.props - const normalizedFontFamily = normalizeEditorFontFamily(fontFamily) + render() { + const { className, fontSize, fontFamily, width, height } = this.props + const normalisedFontFamily = normalizeEditorFontFamily(fontFamily) - return (< - div className={ - className == null ? 'CodeEditor' : `CodeEditor ${className}` - } - ref='root' - tabIndex='-1' - style={{ - normalizedFontFamily, - fontSize: fontSize, - width: width, - height: height - }} - onDrop={ - e => this.handleDropImage(e) - } + 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') @@ -1214,7 +1294,8 @@ CodeEditor.propTypes = { spellCheck: PropTypes.bool, enableMarkdownLint: PropTypes.bool, customMarkdownLintConfig: PropTypes.string, - deleteUnusedAttachments: PropTypes.bool + deleteUnusedAttachments: PropTypes.bool, + RTL: PropTypes.bool } CodeEditor.defaultProps = { @@ -1230,5 +1311,6 @@ CodeEditor.defaultProps = { enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig, prettierConfig: DEFAULT_CONFIG.editor.prettierConfig, - deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments + deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments, + RTL: false } diff --git a/browser/components/ColorPicker.js b/browser/components/ColorPicker.js index 9e0199c2..4d4e80e4 100644 --- a/browser/components/ColorPicker.js +++ b/browser/components/ColorPicker.js @@ -7,7 +7,7 @@ import styles from './ColorPicker.styl' const componentHeight = 330 class ColorPicker extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -18,21 +18,21 @@ class ColorPicker extends React.Component { this.handleConfirm = this.handleConfirm.bind(this) } - componentWillReceiveProps (nextProps) { + componentWillReceiveProps(nextProps) { this.onColorChange(nextProps.color) } - onColorChange (color) { + onColorChange(color) { this.setState({ color }) } - handleConfirm () { + handleConfirm() { this.props.onConfirm(this.state.color) } - render () { + render() { const { onReset, onCancel, targetRect } = this.props const { color } = this.state @@ -44,13 +44,22 @@ class ColorPicker extends React.Component { } return ( -
+
- - - + + +
) diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index cd885fd9..d07ffb0e 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,142 +21,171 @@ class MarkdownEditor extends React.Component { this.supportMdSelectionBold = [16, 17, 186] this.state = { - status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE', + status: + props.config.editor.switchPreview === 'RIGHTCLICK' + ? props.config.editor.delfaultStatus + : 'CODE', renderValue: props.value, keyPressed: new Set(), 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.bind(this)) + 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.bind(this)) + eventEmitter.off('editor:focus', this.focusEditor) } - focusEditor () { - this.setState({ - status: 'CODE' - }, () => { - this.refs.code.focus() - }) + focusEditor() { + this.setState( + { + status: 'CODE' + }, + () => { + if (this.refs.code == null) { + return + } + this.refs.code.focus() + } + ) } - queueRendering (value) { + 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]+)/ @@ -164,9 +194,9 @@ class MarkdownEditor extends React.Component { 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 @@ -181,45 +211,56 @@ class MarkdownEditor extends React.Component { } } - 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) } @@ -227,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 @@ -275,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} @@ -325,10 +383,12 @@ class MarkdownEditor extends React.Component { customMarkdownLintConfig={config.editor.customMarkdownLintConfig} prettierConfig={config.editor.prettierConfig} deleteUnusedAttachments={config.editor.deleteUnusedAttachments} + RTL={RTL} /> - this.handleContextMenu(e)} - onDoubleClick={(e) => this.handleDoubleClick(e)} + 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 50f073e5..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' @@ -11,6 +12,7 @@ import mermaidRender from './render/MermaidRender' 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' @@ -20,11 +22,15 @@ import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' 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 buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder') + .buildMarkdownPreviewContextMenu const { app } = remote const path = require('path') @@ -50,22 +56,21 @@ const CSS_FILES = [ * @param {String} opts.theme * @param {Boolean} [opts.lineNumber] Should show line number * @param {Boolean} [opts.scrollPastEnd] - * @param {Boolean} [opts.optimizeOverflowScroll] Should tweak body style to optimize overflow scrollbar display * @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) { +function buildStyle(opts) { const { fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, - optimizeOverflowScroll, theme, allowCustomCSS, - customCSS + customCSS, + RTL } = opts return ` @font-face { @@ -102,8 +107,17 @@ ${markdownStyle} body { font-family: '${fontFamily.join("','")}'; font-size: ${fontSize}px; - ${scrollPastEnd ? 'padding-bottom: 90vh;' : ''} - ${optimizeOverflowScroll ? 'height: 100%;' : ''} + + ${ + scrollPastEnd + ? ` + padding-bottom: 90vh; + box-sizing: border-box; + ` + : '' + } + ${RTL ? 'direction: rtl;' : ''} + ${RTL ? 'text-align: right;' : ''} } @media print { body { @@ -113,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("','")}'; @@ -143,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 { @@ -173,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' @@ -208,7 +319,7 @@ const defaultCodeBlockFontFamily = [ // 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) { +function getSourceLineNumberByElement(element) { let isHasLineNumber = element.dataset.line !== undefined let parent = element while (!isHasLineNumber && parent.parentElement !== null) { @@ -218,8 +329,8 @@ function getSourceLineNumberByElement (element) { return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1 } -export default class MarkdownPreview extends React.Component { - constructor (props) { +class MarkdownPreview extends React.Component { + constructor(props) { super(props) this.contextMenuHandler = e => this.handleContextMenu(e) @@ -236,13 +347,14 @@ export default class MarkdownPreview extends React.Component { 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, @@ -251,17 +363,17 @@ 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) { + handleContextMenu(event) { const menu = buildMarkdownPreviewContextMenu(this, event) const switchPreview = ConfigManager.get().editor.switchPreview if (menu != null && switchPreview !== 'RIGHTCLICK') { @@ -271,17 +383,21 @@ export default class MarkdownPreview extends React.Component { } } - handleDoubleClick (e) { + handleDoubleClick(e) { if (this.props.onDoubleClick != null) this.props.onDoubleClick(e) } - handleMouseDown (e) { + handleMouseDown(e) { const config = ConfigManager.get() 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') { + if ( + config.editor.switchPreview === 'RIGHTCLICK' && + e.buttons === 2 && + config.editor.type === 'SPLIT' + ) { eventEmitter.emit('topbar:togglemodebutton', 'CODE') } if (e.ctrlKey) { @@ -297,10 +413,11 @@ export default class MarkdownPreview extends React.Component { } } - if (this.props.onMouseDown != null && targetTag === 'BODY') 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 @@ -308,15 +425,15 @@ 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') } - htmlContentFormatter (noteContent, exportTasks, targetDir) { + htmlContentFormatter(noteContent, exportTasks, targetDir) { const { fontFamily, fontSize, @@ -326,7 +443,8 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } = this.getStyleParams() const inlineStyles = buildStyle({ @@ -337,13 +455,11 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL }) - let body = this.markdown.render(noteContent) - body = attachmentManagement.fixLocalURLS( - body, - this.props.storagePath - ) + 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') { @@ -359,7 +475,7 @@ export default class MarkdownPreview extends React.Component { let styles = '' files.forEach(file => { - styles += `` + styles += `` }) return ` @@ -374,14 +490,24 @@ export default class MarkdownPreview extends React.Component { ` } - handleSaveAsHtml () { - this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir))) + handleSaveAsHtml() { + this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => + Promise.resolve( + this.htmlContentFormatter(noteContent, exportTasks, targetDir) + ) + ) } - handleSaveAsPdf () { + 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)) + 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) => { @@ -394,11 +520,11 @@ export default class MarkdownPreview extends React.Component { }) } - handlePrint () { + handlePrint() { this.refs.root.contentWindow.print() } - exportAsDocument (fileType, contentFormatter) { + exportAsDocument(fileType, contentFormatter) { const options = { filters: [{ name: 'Documents', extensions: [fileType] }], properties: ['openFile', 'createDirectory'] @@ -414,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 => { @@ -428,7 +555,7 @@ export default class MarkdownPreview extends React.Component { }) } - fixDecodedURI (node) { + fixDecodedURI(node) { if ( node && node.children.length === 1 && @@ -445,17 +572,18 @@ export default class MarkdownPreview extends React.Component { * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ``` * @returns {string} HTML in which special characters between three ``` have been converted */ - escapeHtmlCharactersInCodeTag (splitWithCodeTag) { + escapeHtmlCharactersInCodeTag(splitWithCodeTag) { for (let index = 0; index < splitWithCodeTag.length; index++) { - const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1) + const codeTagRequired = + splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1 if (codeTagRequired) { - splitWithCodeTag.splice((index + 1), 0, '\`\`\`') + splitWithCodeTag.splice(index + 1, 0, '```') } } let inCodeTag = false let result = '' for (let content of splitWithCodeTag) { - if (content === '\`\`\`') { + if (content === '```') { inCodeTag = !inCodeTag } else if (inCodeTag) { content = escapeHtmlCharacters(content) @@ -465,21 +593,15 @@ export default class MarkdownPreview extends React.Component { return result } - getScrollBarStyle () { + getScrollBarStyle() { const { theme } = this.props - switch (theme) { - case 'dark': - case 'solarized-dark': - case 'monokai': - case 'dracula': - return scrollBarDarkStyle - default: - return scrollBarStyle - } + return uiThemes.some(item => item.name === theme && item.isDark) + ? scrollBarDarkStyle + : scrollBarStyle } - componentDidMount () { + componentDidMount() { const { onDrop } = this.props this.refs.root.setAttribute('sandbox', 'allow-scripts') @@ -529,6 +651,7 @@ 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) @@ -536,7 +659,7 @@ export default class MarkdownPreview extends React.Component { eventEmitter.on('print', this.printHandler) } - componentWillUnmount () { + componentWillUnmount() { const { onDrop } = this.props this.refs.root.contentWindow.document.body.removeEventListener( @@ -567,6 +690,10 @@ 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) @@ -574,7 +701,7 @@ export default class MarkdownPreview extends React.Component { eventEmitter.off('print', this.printHandler) } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { // actual rewriteIframe function should be called only once let needsRewriteIframe = false if (prevProps.value !== this.props.value) needsRewriteIframe = true @@ -599,7 +726,8 @@ 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 @@ -611,11 +739,11 @@ export default class MarkdownPreview extends React.Component { // Should scroll to top after selecting another note if (prevProps.noteKey !== this.props.noteKey) { - this.getWindow().scrollTo(0, 0) + this.scrollTo(0, 0) } } - getStyleParams () { + getStyleParams() { const { fontSize, lineNumber, @@ -623,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, @@ -649,11 +779,12 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } } - applyStyle () { + applyStyle() { const { fontFamily, fontSize, @@ -663,7 +794,8 @@ export default class MarkdownPreview extends React.Component { scrollPastEnd, theme, allowCustomCSS, - customCSS + customCSS, + RTL } = this.getStyleParams() this.getWindow().document.getElementById( @@ -675,15 +807,14 @@ export default class MarkdownPreview extends React.Component { codeBlockFontFamily, lineNumber, scrollPastEnd, - optimizeOverflowScroll: true, theme, allowCustomCSS, - customCSS + customCSS, + RTL }) - this.getWindow().document.documentElement.style.overflowY = 'hidden' } - getCodeThemeLink (name) { + getCodeThemeLink(name) { const theme = consts.THEMES.find(theme => theme.name === name) return theme != null @@ -691,7 +822,7 @@ export default class MarkdownPreview extends React.Component { : `${appPath}/node_modules/codemirror/theme/elegant.css` } - rewriteIframe () { + rewriteIframe() { _.forEach( this.refs.root.contentWindow.document.querySelectorAll( 'input[type="checkbox"]' @@ -749,7 +880,9 @@ export default class MarkdownPreview extends React.Component { codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme) - const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default' + const codeBlockThemeClassName = codeBlockTheme + ? codeBlockTheme.className + : 'cm-s-default' _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.code code'), @@ -835,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') @@ -858,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, mermaidHTMLLabel) + mermaidRender( + el, + htmlTextHelper.decodeEntities(el.innerHTML), + theme, + mermaidHTMLLabel + ) } ) @@ -880,20 +1021,14 @@ 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) => { + const imgObserver = new MutationObserver(mutationList => { for (const mu of mutationList) { if (mu.target.className === 'carouselContent-enter-done') { this.setImgOnClickEventHelper(mu.target, rect) @@ -902,26 +1037,32 @@ export default class MarkdownPreview extends React.Component { } }) - const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img') + 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') + 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) { + 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 magnification = baseOnWidth + ? widthMagnification + : heightMagnification const zoomImgWidth = img.width * magnification const zoomImgHeight = img.height * magnification @@ -952,10 +1093,7 @@ export default class MarkdownPreview extends React.Component { width: ${zoomImgWidth}; height: ${zoomImgHeight}px; ` - zoomImg.animate([ - originalImgRect, - zoomInImgRect - ], animationSpeed) + zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed) const overlay = document.createElement('div') overlay.style = ` @@ -976,10 +1114,10 @@ export default class MarkdownPreview extends React.Component { width: ${img.width}px; height: ${img.height}px; ` - const zoomOutImgAnimation = zoomImg.animate([ - zoomInImgRect, - originalImgRect - ], animationSpeed) + const zoomOutImgAnimation = zoomImg.animate( + [zoomInImgRect, originalImgRect], + animationSpeed + ) zoomOutImgAnimation.onfinish = () => overlay.remove() } @@ -988,15 +1126,28 @@ export default class MarkdownPreview extends React.Component { } } - focus () { + handleResize() { + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'), + el => { + el.setAttribute('height', el.clientWidth / el.getAttribute('ratio')) + } + ) + } + + 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]' ) @@ -1006,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://', @@ -1028,30 +1188,35 @@ export default class MarkdownPreview extends React.Component { return new window.Notification(title, options) } - handleLinkClick (e) { + handleLinkClick(e) { e.preventDefault() e.stopPropagation() const rawHref = e.target.getAttribute('href') - const parser = document.createElement('a') - parser.href = e.target.getAttribute('href') - 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 - + const { dispatch } = this.props if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() - const extractId = /(main.html)?#/ - const regexNoteInternalLink = new RegExp(`${extractId.source}(.+)`) - if (regexNoteInternalLink.test(linkHash)) { - const targetId = mdurl.encode(linkHash.replace(extractId, '')) - 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 @@ -1082,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({ @@ -139,16 +145,16 @@ class Main extends React.Component { }) } - 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) @@ -173,38 +179,52 @@ class Main extends React.Component { delete CodeMirror.keyMap.emacs['Ctrl-V'] eventEmitter.on('editor:fullscreen', this.toggleFullScreen) - eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this)) + 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( + 'menubar:togglemenubar', + this.toggleMenuBarVisible.bind(this) + ) + eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this)) + clearInterval(this.refreshTheme) } - toggleMenuBarVisible () { + 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 newUI = Object.assign(ui, { showMenuBar: !ui.showMenuBar }) const newConfig = Object.assign(config, newUI) ConfigManager.set(newConfig) } - handleLeftSlideMouseDown (e) { + 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( @@ -244,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 @@ -270,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') @@ -284,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' @@ -292,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 @@ -312,10 +332,16 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > - {!config.isSideNavFolded && + {!config.isSideNavFolded && (
-
} +
+ )}
- + + -
-
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 187bc41a..25499463 100644 --- a/browser/main/SideNav/PreferenceButton.js +++ b/browser/main/SideNav/PreferenceButton.js @@ -4,10 +4,8 @@ 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 5cd4a491..a152fc00 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -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) { + handleHeaderInfoClick(e) { const { storage, dispatch } = this.props dispatch(push('/storages/' + storage.key)) } - handleFolderButtonClick (folderKey) { - return (e) => { + 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 3167f487..cd11e652 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -14,9 +14,10 @@ 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' @@ -24,13 +25,13 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import ColorPicker from 'browser/components/ColorPicker' import { every, sortBy } from 'lodash' -function matchActiveTags (tags, activeTags) { +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) { + constructor(props) { super(props) this.state = { @@ -38,33 +39,46 @@ class SideNav extends React.Component { show: false, color: null, tagName: null, - targetRect: 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 () { + 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, match: { params } } = this.props + const { + data, + dispatch, + location, + match: { params } + } = this.props const notes = data.noteMap .map(note => note) @@ -78,44 +92,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) - - dispatch(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) { + handleSearchButtonClick(e) { + const { showSearch } = this.state + this.setState({ + showSearch: !showSearch, + searchText: '' + }) + } + + handleSearchInputClear(e) { + this.setState({ + searchText: '' + }) + } + + handleSearchInputChange(e) { + this.setState({ + searchText: e.target.value + }) + } + + handleHomeButtonClick(e) { const { dispatch } = this.props dispatch(push('/home')) } - handleStarredButtonClick (e) { + handleStarredButtonClick(e) { const { dispatch } = this.props dispatch(push('/starred')) } - handleTagContextMenu (e, tag) { + handleTagContextMenu(e, tag) { const menu = [] menu.push({ @@ -125,13 +163,17 @@ class SideNav extends React.Component { menu.push({ label: i18n.__('Customize Color'), - click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect()) + click: this.displayColorPicker.bind( + this, + tag, + e.target.getBoundingClientRect() + ) }) context.popup(menu) } - dismissColorPicker () { + dismissColorPicker() { this.setState({ colorPicker: { show: false @@ -139,7 +181,7 @@ class SideNav extends React.Component { }) } - displayColorPicker (tagName, rect) { + displayColorPicker(tagName, rect) { const { config } = this.props this.setState({ colorPicker: { @@ -151,10 +193,17 @@ class SideNav extends React.Component { }) } - handleColorPickerConfirm (color) { - const { dispatch, config: {coloredTags} } = this.props - const { colorPicker: { tagName } } = this.state - const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex}) + 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) @@ -165,9 +214,14 @@ class SideNav extends React.Component { this.dismissColorPicker() } - handleColorPickerReset () { - const { dispatch, config: {coloredTags} } = this.props - const { colorPicker: { tagName } } = this.state + handleColorPickerReset() { + const { + dispatch, + config: { coloredTags } + } = this.props + const { + colorPicker: { tagName } + } = this.state const newColoredTags = Object.assign({}, coloredTags) delete newColoredTags[tagName] @@ -181,44 +235,50 @@ class SideNav extends React.Component { this.dismissColorPicker() } - handleToggleButtonClick (e) { + handleToggleButtonClick(e) { const { dispatch, config } = this.props + const { showSearch, searchText } = this.state - ConfigManager.set({isSideNavFolded: !config.isSideNavFolded}) + ConfigManager.set({ isSideNavFolded: !config.isSideNavFolded }) dispatch({ type: 'SET_IS_SIDENAV_FOLDED', isFolded: !config.isSideNavFolded }) + + if (showSearch && searchText.length === 0) { + this.setState({ + showSearch: false + }) + } } - handleTrashedButtonClick (e) { + handleTrashedButtonClick(e) { const { dispatch } = this.props dispatch(push('/trashed')) } - handleSwitchFoldersButtonClick () { + handleSwitchFoldersButtonClick() { const { dispatch } = this.props dispatch(push('/home')) } - handleSwitchTagsButtonClick () { + handleSwitchTagsButtonClick() { const { dispatch } = this.props dispatch(push('/alltags')) } - onSortEnd (storage) { - return ({oldIndex, newIndex}) => { + onSortEnd(storage) { + return ({ oldIndex, newIndex }) => { const { dispatch } = this.props - dataApi - .reorderFolder(storage.key, oldIndex, newIndex) - .then((data) => { - dispatch({ type: 'REORDER_FOLDER', storage: data.storage }) - }) + dataApi.reorderFolder(storage.key, oldIndex, newIndex).then(data => { + dispatch({ type: 'REORDER_FOLDER', storage: data.storage }) + }) } } - SideNavComponent (isFolded, storageList) { - const { location, data, config } = this.props + 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$/) @@ -227,25 +287,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 { @@ -257,22 +354,26 @@ class SideNav extends React.Component {
-
-
- {this.tagListComponent(data)} -
- +
{this.tagListComponent(data)}
+
) } @@ -280,82 +381,89 @@ class SideNav extends React.Component { return component } - tagListComponent () { + tagListComponent() { const { data, location, config } = this.props - const { colorPicker } = this.state + 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) { + handleClickTagListItem(name) { const { dispatch } = this.props dispatch(push(`/tags/${encodeURIComponent(name)}`)) } - handleSortTagsByChange (e) { + handleSortTagsByChange(e) { const { dispatch } = this.props const config = { @@ -369,7 +477,7 @@ class SideNav extends React.Component { }) } - handleClickNarrowToTag (tag) { + handleClickNarrowToTag(tag) { const { dispatch, location } = this.props const listOfTags = this.getActiveTags(location.pathname) const indexOfTag = listOfTags.indexOf(tag) @@ -381,51 +489,40 @@ class SideNav extends React.Component { 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 - const { colorPicker: colorPickerState } = this.state - - const isFolded = config.isSideNavFolded - - const storageList = data.storageMap.map((storage, key) => { - const SortableStorageItem = SortableContainer(StorageItem) - return - }) + render() { + const { location, config } = this.props + const { showSearch, searchText, colorPicker: colorPickerState } = this.state let colorPicker if (colorPickerState.show) { @@ -440,25 +537,63 @@ class SideNav extends React.Component { ) } + const isFolded = config.isSideNavFolded const style = {} if (!isFolded) style.width = this.props.width 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 09fd56b2..e9554a67 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -11,7 +11,7 @@ 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 = { @@ -33,19 +33,25 @@ class TopBar extends React.Component { this.handleSearchChange = this.handleSearchChange.bind(this) this.handleSearchClearButton = this.handleSearchClearButton.bind(this) - this.debouncedUpdateKeyword = debounce((keyword) => { - dispatch(push(`/searched/${encodeURIComponent(keyword)}`)) - this.setState({ - search: keyword - }) - ee.emit('top:search', keyword) - }, 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 { match: { params } } = this.props + componentDidMount() { + const { + match: { params } + } = this.props const searchWord = params && params.searchword if (searchWord !== undefined) { this.setState({ @@ -57,12 +63,12 @@ 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) { + handleSearchClearButton(e) { const { dispatch } = this.props this.setState({ search: '', @@ -74,7 +80,7 @@ class TopBar extends React.Component { this.debouncedUpdateKeyword('') } - handleKeyDown (e) { + handleKeyDown(e) { // Re-apply search field on ENTER key if (e.keyCode === 13) { this.debouncedUpdateKeyword(e.target.value) @@ -98,18 +104,18 @@ class TopBar extends React.Component { } } - handleSearchChange (e) { + handleSearchChange(e) { const keyword = e.target.value this.debouncedUpdateKeyword(keyword) } - handleSearchFocus (e) { + handleSearchFocus(e) { this.setState({ isSearching: true }) } - handleSearchBlur (e) { + handleSearchBlur(e) { e.stopPropagation() let el = e.relatedTarget @@ -128,7 +134,7 @@ class TopBar extends React.Component { } } - handleOnSearchFocus () { + handleOnSearchFocus() { const el = this.refs.search.childNodes[0] if (this.state.isSearching) { el.blur() @@ -137,20 +143,22 @@ class TopBar extends React.Component { } } - handleCodeInit () { + handleCodeInit() { ee.emit('top:search', this.refs.searchInput.value || '') } - render () { + render() { const { config, style, location } = this.props return ( -
-
- {this.state.search !== '' && - - } + )}
- {location.pathname === '/trashed' ? '' - : } + {location.pathname === '/trashed' ? ( + '' + ) : ( + + )}
) } diff --git a/browser/main/global.styl b/browser/main/global.styl index d864993d..3f513c78 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -96,16 +96,6 @@ modalBackColor = white z-index modalZIndex + 1 -body[data-theme="dark"] - background-color $ui-dark-backgroundColor - ::-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 @@ -148,38 +138,25 @@ body[data-theme="dark"] .sortableItemHelper z-index modalZIndex + 5 -body[data-theme="solarized-dark"] - background-color $ui-solarized-dark-backgroundColor - ::-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"] - background-color $ui-monokai-backgroundColor - ::-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"] - background-color $ui-dracula-backgroundColor - ::-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 b3a909e5..e77c62dd 100644 --- a/browser/main/index.js +++ b/browser/main/index.js @@ -4,6 +4,7 @@ import { store, history } from './store' import React, { Fragment } from 'react' import ReactDOM from 'react-dom' require('!!style!css!stylus?sourceMap!./global.styl') +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' @@ -17,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() }) @@ -33,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) { @@ -47,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() @@ -65,26 +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' }) +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}' + ) +} + const el = document.getElementById('content') -function notify (...args) { +function notify(...args) { return new window.Notification(...args) } -function updateApp () { +function updateApp() { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', message: i18n.__('Update Boostnote'), @@ -97,7 +107,7 @@ function updateApp () { } } -ReactDOM.render(( +ReactDOM.render( @@ -112,36 +122,41 @@ ReactDOM.render(( {/* storages */} - + - -), el, function () { - const loadingCover = document.getElementById('loadingCover') - loadingCover.parentNode.removeChild(loadingCover) + , + el, + function() { + const loadingCover = document.getElementById('loadingCover') + loadingCover.parentNode.removeChild(loadingCover) - ipcRenderer.on('update-ready', function () { - store.dispatch({ - type: 'UPDATE_AVAILABLE' + ipcRenderer.on('update-ready', function() { + store.dispatch({ + type: 'UPDATE_AVAILABLE' + }) + notify('Update ready!', { + body: 'New Boostnote is ready to be installed.' + }) + updateApp() }) - notify('Update ready!', { - body: 'New Boostnote is ready to be installed.' - }) - updateApp() - }) - ipcRenderer.on('update-found', function () { - notify('Update found!', { - body: 'Preparing to update...' + ipcRenderer.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') - } - }) -}) + 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 1d4dc69e..a0b23fc2 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -8,6 +8,7 @@ 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 @@ -15,6 +16,22 @@ 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, @@ -25,13 +42,19 @@ 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', + 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: 'Shift + F', + 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' @@ -39,8 +62,14 @@ export const DEFAULT_CONFIG = { ui: { language: 'en', theme: 'default', + defaultTheme: 'default', + enableScheduleTheme: false, + scheduledTheme: 'monokai', + scheduleStart: 1200, + scheduleEnd: 360, showCopyNotification: true, disableDirectWrite: false, + showScrollBar: true, defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' showMenuBar: false, isStacking: false @@ -71,13 +100,14 @@ export const DEFAULT_CONFIG = { enableSmartPaste: false, enableMarkdownLint: false, customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, - prettierConfig: ` { + prettierConfig: `{ "trailingComma": "es5", - "tabWidth": 4, + "tabWidth": 2, "semi": false, "singleQuote": true }`, - deleteUnusedAttachments: true + deleteUnusedAttachments: true, + rtlEnabled: false }, preview: { fontSize: '14', @@ -95,8 +125,7 @@ export const DEFAULT_CONFIG = { breaks: true, smartArrows: false, allowCustomCSS: false, - - customCSS: '/* Drop Your Custom CSS Code Here */', + customCSS: DEFAULT_CSS_CONFIG, sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' mermaidHTMLLabel: false, lineThroughCheckbox: true @@ -112,7 +141,7 @@ export const DEFAULT_CONFIG = { 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 @@ -121,13 +150,17 @@ function validate (config) { return true } -function _save (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 { @@ -141,6 +174,11 @@ function get () { _save(config) } + config.autoUpdateEnabled = electronConfig.get( + 'autoUpdateEnabled', + config.autoUpdateEnabled + ) + if (!isInitialized) { isInitialized = true let editorTheme = document.getElementById('editorTheme') @@ -151,7 +189,9 @@ function get () { document.head.appendChild(editorTheme) } - const theme = consts.THEMES.find(theme => theme.name === config.editor.theme) + const theme = consts.THEMES.find( + theme => theme.name === config.editor.theme + ) if (theme) { editorTheme.setAttribute('href', theme.path) @@ -163,7 +203,7 @@ function get () { return config } -function set (updates) { +function set(updates) { const currentConfig = get() const arrangedUpdates = updates @@ -171,24 +211,15 @@ function set (updates) { arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS } - const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates) + 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') @@ -199,32 +230,61 @@ function set (updates) { document.head.appendChild(editorTheme) } - const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme) + 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\s/g, 'Command ') 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 971ae812..48500f4a 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -12,14 +12,15 @@ 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) { +function getImage(file) { if (isString(file)) { return new Promise(resolve => { const img = new Image() @@ -55,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) @@ -94,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)) @@ -107,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() - }) + ) } /** @@ -145,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') @@ -160,28 +183,41 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr } try { - const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64' + const isBase64 = + typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64' if (!isBase64 && !fs.existsSync(sourceFilePath)) { return reject('source file does not exist') } const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath - const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath) + const sourceURL = url.parse( + /^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath + ) let destinationName if (useRandomName) { - destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}` + destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || + '.png'}` } else { destinationName = path.basename(sourceURL.pathname) } 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 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 base64Data = sourceFilePath.data.replace( + /^data:image\/\w+;base64,/, + '' + ) const dataBuffer = Buffer.from(base64Data, 'base64') outputFile.write(dataBuffer, () => { resolve(destinationName) @@ -199,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) } @@ -216,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) } @@ -241,10 +292,11 @@ 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, '/') + const storageUrl = + 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/') /* A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`. @@ -254,9 +306,17 @@ function fixLocalURLS (renderedHTML, storagePath) { - `(?:\\\/|%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) { - return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl) - }) + return renderedHTML.replace( + new RegExp( + '/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\/|%5C)[-.\\w]+)+', + 'g' + ), + function(match) { + return match + .replace(encodedWin32SeparatorRegex, '/') + .replace(storageRegex, storageUrl) + } + ) } /** @@ -266,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})` } @@ -278,53 +338,66 @@ function generateAttachmentMarkdown (fileName, path, showPreview) { * @param {String} noteKey Key of the current note * @param {Event} dropEvent DropEvent */ -function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { +function handleAttachmentDrop(codeEditor, storageKey, noteKey, dropEvent) { let promise 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 => - ({ + 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 { - return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({ - fileName, - title: path.basename(filePath), - isImage: false - })) - } - })) + }) + ) } else { let imageURL = dropEvent.dataTransfer.getData('text/plain') if (!imageURL) { - const match = /]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html')) + const match = /]*[\s"']src="([^"]+)"/.exec( + dropEvent.dataTransfer.getData('text/html') + ) if (match) { imageURL = match[1] } @@ -334,30 +407,43 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { 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) + 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 - })) + return copyAttachment( + { + type: 'base64', + data: canvas.toDataURL(), + sourceFilePath: imageURL + }, + storageKey, + noteKey + ) + }) + .then(fileName => ({ + fileName, + title: imageURL, + isImage: true + })) ]) } promise.then(files => { - const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage)) + 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')) }) @@ -370,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 handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) { +function handlePasteImageEvent( + codeEditor, + storageKey, + noteKey, + dataTransferItem +) { if (!codeEditor) { throw new Error('codeEditor has to be given') } @@ -389,19 +480,31 @@ function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferIte 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) @@ -414,7 +517,7 @@ function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferIte * @param {String} noteKey Key of the current note * @param {NativeImage} image The native image */ -function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) { +function handlePasteNativeImage(codeEditor, storageKey, noteKey, image) { if (!codeEditor) { throw new Error('codeEditor has to be given') } @@ -430,7 +533,11 @@ function handlePasteNativeImage (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) @@ -440,19 +547,42 @@ function handlePasteNativeImage (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) } @@ -462,11 +592,16 @@ 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 } @@ -478,7 +613,7 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { * @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) { +function importAttachments(markDownContent, filepath, storageKey, noteKey) { return new Promise((resolve, reject) => { const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g let attachPath = nameRegex.exec(markDownContent) @@ -489,8 +624,12 @@ function importAttachments (markDownContent, filepath, storageKey, noteKey) { 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)) + attachmentPath = path.isAbsolute(attachmentPath) + ? attachmentPath + : path.join(path.dirname(filepath), attachmentPath) + promiseArray.push( + this.copyAttachment(attachmentPath, storageKey, noteKey) + ) attachPath = nameRegex.exec(markDownContent) } @@ -502,19 +641,23 @@ function importAttachments (markDownContent, filepath, storageKey, noteKey) { 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) - } - }) + .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) + } + }) } }) } @@ -529,7 +672,7 @@ function importAttachments (markDownContent, filepath, storageKey, noteKey) { * @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)) { @@ -545,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 } @@ -559,15 +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('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) { - const temp = match - .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep) - .replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep) - .replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep) - .replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep) - return temp.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 + ) + } + ) } /** @@ -575,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) } @@ -587,36 +762,66 @@ 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' + ) }) } }) @@ -632,31 +837,61 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey * @param noteKey NoteKey of the currentNote * @return {Promise>} Promise returning the list of attachments with their properties */ -function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) { +function getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey) { if (storageKey == null || noteKey == null || markdownContent == null) { return null } - const targetStorage = findStorage.findStorage(storageKey) - const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + 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'), '')) + 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( + '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) + const absolutePathOfFile = path.join( + targetStorage.path, + DESTINATION_FOLDER, + noteKey, + file + ) if (!attachmentsInNoteOnlyFileNames.includes(file)) { attachments.push({ path: absolutePathOfFile, isInUse: false }) } else { @@ -675,11 +910,11 @@ function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) { * @description Remove all specified attachment paths * @param attachments attachment paths * @return {Promise} Promise after all attachments are removed */ -function removeAttachmentsByPaths (attachments) { +function removeAttachmentsByPaths(attachments) { const promises = [] for (const attachment of attachments) { const promise = new Promise((resolve, reject) => { - fs.unlink(attachment, (err) => { + fs.unlink(attachment, err => { if (err) { console.error('Could not delete "%s"', attachment) console.error(err) @@ -700,29 +935,54 @@ function removeAttachmentsByPaths (attachments) { * @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! ⚠' + ) + + '**' + ) } /** @@ -730,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 } @@ -745,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 }) 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 index f9878210..2fd3bd9d 100644 --- a/browser/main/lib/dataApi/createNoteFromUrl.js +++ b/browser/main/lib/dataApi/createNoteFromUrl.js @@ -6,28 +6,46 @@ 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)) { +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 } } -function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) { +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: 'Please check your URL is in correct format. (Example, https://www.google.com)'}) + reject({ result: false, error: ERROR_MESSAGES.VALIDATION_ERROR }) } const request = url.startsWith('https') ? https : http - const req = request.request(url, (res) => { + const req = request.request(url, res => { let data = '' - res.on('data', (chunk) => { + res.on('data', chunk => { data += chunk }) @@ -40,20 +58,21 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu folder: folder, title: '', content: markdownHTML - }) - .then((note) => { + }).then(note => { const noteHash = note.key dispatch({ type: 'UPDATE_NOTE', note: note }) - dispatch(push({ - pathname: location.pathname, - query: {key: noteHash} - })) + dispatch( + push({ + pathname: location.pathname, + query: { key: noteHash } + }) + ) ee.emit('list:jump', noteHash) ee.emit('detail:focus') - resolve({result: true, error: null}) + resolve({ result: true, error: null }) }) } else { createNote(storage, { @@ -61,17 +80,21 @@ function createNoteFromUrl (url, storage, folder, dispatch = null, location = nu folder: folder, title: '', content: markdownHTML - }).then((note) => { - resolve({result: true, note, error: null}) + }).then(note => { + resolve({ result: true, note, error: null }) }) } }) }) - req.on('error', (e) => { + req.on('error', e => { console.error('error in parsing URL', e) - reject({result: false, error: e}) + reject({ + result: false, + error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED + }) }) + req.end() }) } 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 5ccc1414..f20078c8 100644 --- a/browser/main/lib/dataApi/deleteFolder.js +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -18,7 +18,7 @@ const deleteSingleNote = require('./deleteNote') * } * ``` */ -function deleteFolder (storageKey, folderKey) { +function deleteFolder(storageKey, folderKey) { let targetStorage try { targetStorage = findStorage(storageKey) @@ -27,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 8f15b147..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,24 +31,38 @@ 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 - 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) - }) + 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, diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 42e1fa56..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) { @@ -50,18 +58,19 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) - 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 - }) + 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) } @@ -74,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) @@ -88,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)) { @@ -103,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/init.js b/browser/main/lib/dataApi/init.js index 0dbcc182..0ad35625 100644 --- a/browser/main/lib/dataApi/init.js +++ b/browser/main/lib/dataApi/init.js @@ -21,8 +21,8 @@ const CSON = require('@rokt33r/season') * 3. empty directory */ -function init () { - const fetchStorages = function () { +function init() { + const fetchStorages = function() { let rawStorages try { rawStorages = JSON.parse(window.localStorage.getItem('storages')) @@ -34,44 +34,50 @@ function init () { 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 - .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 - }) - } - }) - 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') - } + .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 @@ -80,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 c38968cb..b72db7aa 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -7,90 +7,104 @@ 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 955cb5c8..08d8c7f1 100644 --- a/browser/main/lib/modal.js +++ b/browser/main/lib/modal.js @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom' 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 3165606a..aab644d7 100644 --- a/browser/main/lib/shortcut.js +++ b/browser/main/lib/shortcut.js @@ -1,13 +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': () => { + 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 b48d6e42..26b5a245 100644 --- a/browser/main/modals/CreateFolderModal.js +++ b/browser/main/modals/CreateFolderModal.js @@ -10,7 +10,7 @@ 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 index 31988059..090fe5a4 100644 --- a/browser/main/modals/CreateMarkdownFromURLModal.js +++ b/browser/main/modals/CreateMarkdownFromURLModal.js @@ -7,7 +7,7 @@ import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' class CreateMarkdownFromURLModal extends React.Component { - constructor (props) { + constructor(props) { super(props) this.state = { @@ -17,89 +17,101 @@ class CreateMarkdownFromURLModal 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() } - showError (message) { + showError(message) { this.setState({ showerror: true, errormessage: message }) } - hideError () { + hideError() { this.setState({ showerror: false, errormessage: '' }) } - confirm () { + 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) - }) + dataApi + .createNoteFromUrl(this.state.name, storage, folder, dispatch, location) + .then(result => { + this.props.close() + }) + .catch(result => { + this.showError(result.error) + }) } - render () { + render() { return ( -
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >
{i18n.__('Import Markdown From URL')}
- this.handleCloseButtonClick(e)} /> + this.handleCloseButtonClick(e)} + />
-
{i18n.__('Insert URL Here')}
- + {i18n.__('Insert URL Here')} +
+ this.handleChange(e)} - onKeyDown={(e) => this.handleInputKeyDown(e)} + onChange={e => this.handleChange(e)} + onKeyDown={e => this.handleInputKeyDown(e)} />
- -
{this.state.errormessage}
+
+ {this.state.errormessage} +
) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl index 5e59e465..8aca1505 100644 --- a/browser/main/modals/CreateMarkdownFromURLModal.styl +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -51,110 +51,39 @@ font-size 14px colorPrimaryButton() -body[data-theme="dark"] - .root - modalDark() - width 500px - height 270px - overflow hidden - position relative - - .header - background-color transparent - border-color $ui-dark-borderColor - color $ui-dark-text-color - - .control-folder-label - color $ui-dark-text-color - - .control-folder-input - border 1px solid $ui-input--create-folder-modal - color white - - .description - color $ui-inactive-text-color - - .control-confirmButton - colorDarkPrimaryButton() - -body[data-theme="solarized-dark"] - .root - modalSolarizedDark() - width 500px - height 270px - overflow hidden - position relative - - .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() - .error text-align center color #F44336 -body[data-theme="monokai"] - .root - modalMonokai() - width 500px - height 270px - overflow hidden - position relative +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 $ui-dark-borderColor - color $ui-monokai-text-color + .header + background-color transparent + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') - .control-folder-label - color $ui-monokai-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 - colorMonokaiPrimaryButton() + .control-confirmButton + colorThemedPrimaryButton(theme) -body[data-theme="dracula"] - .root - modalDracula() - width 500px - height 270px - overflow hidden - position relative +for theme in 'dark' 'dracula' 'solarized-dark' + apply-theme(theme) - .header - background-color transparent - border-color $ui-dracula-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() +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 476fa252..c06e91e3 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -9,21 +9,21 @@ 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() } - handleCreateMarkdownFromUrlClick (e) { + handleCreateMarkdownFromUrlClick(e) { this.props.close() const { storage, folder, dispatch, location } = this.props @@ -35,49 +35,63 @@ class NewNoteModal extends React.Component { }) } - handleMarkdownNoteButtonClick (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(() => { + createMarkdownNote( + storage, + folder, + dispatch, + location, + params, + config + ).then(() => { setTimeout(this.props.close, 200) }) } } - handleMarkdownNoteButtonKeyDown (e) { + handleMarkdownNoteButtonKeyDown(e) { if (e.keyCode === 9) { e.preventDefault() this.refs.snippetButton.focus() } } - handleSnippetNoteButtonClick (e) { + 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(() => { + 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')} @@ -127,10 +142,17 @@ class NewNoteModal extends React.Component { )} -
-
{i18n.__('Tab to switch format')}
-
this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL
+
+ + {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 ff0052bd..a9be5a22 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -54,70 +54,26 @@ margin-bottom 25px cursor pointer -body[data-theme="dark"] - .root - modalDark() +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent - .header - color $ui-dark-text-color + .header + color get-theme-var(theme, 'text-color') - .control-button - border-color $ui-dark-borderColor - color $ui-dark-text-color - background-color transparent - &:focus - colorDarkPrimaryButton() + .control-button + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') + background-color transparent + &:focus + colorThemedPrimaryButton(theme) - .description, .from-url - color $ui-inactive-text-color + .description + color get-theme-var(theme, 'text-color') -body[data-theme="solarized-dark"] - .root - background-color transparent +for theme in 'dark' 'solarized-dark' 'dracula' + apply-theme(theme) - .header - color $ui-solarized-dark-text-color - - .control-button - border-color $ui-solarized-dark-borderColor - color $ui-solarized-dark-text-color - background-color transparent - &:focus - colorDarkPrimaryButton() - - .description, .from-url - 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, .from-url - 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 4d59bea1..26b4839d 100644 --- a/browser/main/modals/PreferencesModal/Blog.js +++ b/browser/main/modals/PreferencesModal/Blog.js @@ -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 0e22833d..c27bd3ac 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,7 +117,6 @@ .group-header2 font-size 20px - color $ui-text-color margin-bottom 15px margin-top 30px @@ -136,20 +238,18 @@ 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 @@ -179,91 +279,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 - .group-header--sub - 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 - .group-header2--sub - 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 - .group-header--sub - color $ui-monokai-text-color - border-color $ui-monokai-borderColor - - .group-header2 - .group-header2--sub - 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 - .group-header--sub - color $ui-dracula-text-color - border-color $ui-dracula-borderColor - - .group-header2 - .group-header2--sub - 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 56bb6e34..a5d37718 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.js +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -7,50 +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.__('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 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.__( + '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 d1d6fc9f..4725aa23 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -24,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 648db4e6..6418bb6a 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.js +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -10,7 +10,7 @@ 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.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,7 +311,7 @@ class Handle extends React.Component { } class SortableFolderItemComponent extends React.Component { - render () { + render() { const StyledHandle = CSSModules(Handle, styles) const DragHandle = SortableHandle(StyledHandle) diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index 618e9bc4..32a83d63 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -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 674026c5..f751d250 100644 --- a/browser/main/modals/PreferencesModal/FolderList.js +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -9,24 +9,28 @@ 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 9c4f5655..1bba78e3 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -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,17 +76,18 @@ 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 = 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, prettifyMarkdown: this.refs.prettifyMarkdown.value, @@ -100,7 +109,7 @@ class HotkeyTab extends React.Component { } } - clearMessage () { + clearMessage() { _.debounce(() => { this.setState({ keymapAlert: null @@ -108,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 ( @@ -122,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' @@ -133,10 +144,13 @@ class HotkeyTab extends React.Component {
-
{i18n.__('Show/Hide Menu Bar')}
+
+ {i18n.__('Show/Hide Menu Bar')} +
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='toggleMenuBar' value={config.hotkey.toggleMenuBar} type='text' @@ -144,21 +158,39 @@ class HotkeyTab extends React.Component {
-
{i18n.__('Toggle Editor Mode')}
+
+ {i18n.__('Toggle Editor Mode')} +
- this.handleHotkeyChange(e)} + 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' @@ -168,8 +200,9 @@ class HotkeyTab extends React.Component {
{i18n.__('Paste HTML')}
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='pasteSmartly' value={config.hotkey.pasteSmartly} type='text' @@ -177,19 +210,26 @@ class HotkeyTab extends React.Component {
-
{i18n.__('Prettify Markdown')}
+
+ {i18n.__('Prettify Markdown')} +
- this.handleHotkeyChange(e)} + this.handleHotkeyChange(e)} ref='prettifyMarkdown' value={config.hotkey.prettifyMarkdown} - type='text' /> + type='text' + />
-
{i18n.__('Insert Current Date')}
+
+ {i18n.__('Insert Current Date')} +
-
-
{i18n.__('Insert Current Date and Time')}
+
+ {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 71e99da9..fa49025f 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -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,46 +61,73 @@ 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')}
@@ -111,11 +138,20 @@ class InfoTab extends React.Component {
- +
-
{i18n.__('Boostnote')} {appVersion}
+
+ {i18n.__('Boostnote')} {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.' + )}
@@ -123,37 +159,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.__( + '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()}
@@ -161,7 +231,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 c541c91c..4701d809 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.styl +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -59,29 +59,19 @@ body[data-theme="dark"] .appId color $ui-dark-text-color -body[data-theme="solarized-dark"] - .root - color $ui-solarized-dark-text-color - .appId - color $ui-solarized-dark-text-color -.list - a - color $ui-solarized-dark-active-color -body[data-theme="monokai"] - .root - color $ui-monokai-text-color - .appId - color $ui-monokai-text-color -.list - a - color $ui-monokai-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="dracula"] - .root - color $ui-dracula-text-color - .appId - color $ui-dracula-text-color -.list - a - color $ui-dracula-active-color \ 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/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 e95afdcf..f748924c 100644 --- a/browser/main/modals/PreferencesModal/SnippetEditor.js +++ b/browser/main/modals/PreferencesModal/SnippetEditor.js @@ -6,13 +6,19 @@ 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, { @@ -49,38 +55,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) + saveSnippet() { + dataApi + .updateSnippet(this.snippet) .then(snippets => snippetManager.assignSnippets(snippets)) - .catch((err) => { throw err }) + .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 df338d7f..0476c5c2 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 @@ -95,12 +95,19 @@ class SnippetTab extends React.Component { -
+ 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' + />
@@ -140,7 +155,10 @@ class SnippetTab extends React.Component { matchingTriples={config.editor.matchingTriples} explodingPairs={config.editor.explodingPairs} scrollPastEnd={config.editor.scrollPastEnd} - onRef={ref => { this.snippetEditor = ref }} /> + onRef={ref => { + this.snippetEditor = ref + }} + />
@@ -148,7 +166,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 296f8167..83b0a296 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.styl +++ b/browser/main/modals/PreferencesModal/SnippetTab.styl @@ -140,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 9af02962..3cb18e30 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -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 3e588a17..03fa65eb 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.styl +++ b/browser/main/modals/PreferencesModal/StorageItem.styl @@ -103,10 +103,17 @@ body[data-theme="solarized-dark"] background-color $ui-solarized-dark-button-backgroundColor color $ui-solarized-dark-text-color -body[data-theme="dracula"] - .header - border-color $ui-dracula-borderColor +apply-theme(theme) + body[data-theme={theme}] + .header + border-color get-theme-var(theme, 'borderColor') - .header-control-button - colorDraculaDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file + .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 e84fa88c..9df1a153 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -12,24 +12,27 @@ 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 = { @@ -44,7 +47,7 @@ class StoragesTab extends React.Component { this.loadAttachmentStorage() } - loadAttachmentStorage () { + loadAttachmentStorage() { const promises = [] this.props.data.noteMap.map(note => { const promise = attachmentManagement.getAttachmentsPathAndStatus( @@ -58,106 +61,128 @@ class StoragesTab extends React.Component { Promise.all(promises) .then(data => { const result = data.reduce((acc, curr) => acc.concat(curr), []) - this.setState({attachments: result}) + this.setState({ attachments: result }) }) .catch(console.error) } - handleAddStorageButton (e) { - this.setState({ - page: 'ADD_STORAGE', - newStorage: { - name: 'Unnamed', - type: 'FILESYSTEM', - path: '' + handleAddStorageButton(e) { + this.setState( + { + page: 'ADD_STORAGE', + newStorage: { + name: 'Unnamed', + type: 'FILESYSTEM', + path: '' + } + }, + () => { + this.refs.addStorageName.select() } - }, () => { - this.refs.addStorageName.select() - }) + ) } - handleLinkClick (e) { + handleLinkClick(e) { shell.openExternal(e.currentTarget.href) e.preventDefault() } - handleRemoveUnusedAttachments (attachments) { - attachmentManagement.removeAttachmentsByPaths(attachments) + handleRemoveUnusedAttachments(attachments) { + attachmentManagement + .removeAttachmentsByPaths(attachments) .then(() => this.loadAttachmentStorage()) .catch(console.error) } - renderList () { + 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 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 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), []) + const unusedAttachmentPaths = unusedAttachments.reduce( + (acc, curr) => acc.concat(curr.path), + [] + ) - if (!boundingBox) { return null } - const storageList = data.storageMap.map((storage) => { - return + 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) + Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ( + {totalUnusedAttachments} items)

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

- Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} 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 @@ -166,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 @@ -181,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', @@ -200,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)} /> - @@ -264,21 +297,25 @@ class StoragesTab extends React.Component {
- - + +
-
-
) } - renderContent () { + renderContent() { switch (this.state.page) { case 'ADD_STORAGE': case 'ADD_FOLDER': @@ -289,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 fbfa89e6..285e9c60 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -168,122 +168,49 @@ body[data-theme="dark"] .list-attachement-clear-button colorDarkPrimaryButton() -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') - .folderList-item - border-bottom $ui-solarized-dark-borderColor + .folderList-item + border-bottom get-theme-var(theme, 'borderColor') - .folderList-empty - color $ui-solarized-dark-text-color + .folderList-empty + color get-theme-var(theme, '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 + .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-solarized-dark-text-color - border-color $ui-solarized-dark-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-solarized-dark-borderColor + .addStorage-body-section-name-input + border-color $get-theme-var(theme, 'borderColor') - .addStorage-body-section-type-description - color $ui-solarized-dark-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-solarized-dark-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-solarized-dark-borderColor - .list-attachement-clear-button - colorSolarizedDarkPrimaryButton() + .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="monokai"] - .root - color $ui-monokai-text-color +for theme in 'solarized-dark' 'dracula' + apply-theme(theme) - .folderList-item - border-bottom $ui-monokai-borderColor - - .folderList-empty - color $ui-monokai-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 - - .addStorage-header - color $ui-monokai-text-color - border-color $ui-monokai-borderColor - - .addStorage-body-section-name-input - border-color $$ui-monokai-borderColor - - .addStorage-body-section-type-description - color $ui-monokai-text-color - - .addStorage-body-section-path-button - colorPrimaryButton() - .addStorage-body-control - border-color $ui-monokai-borderColor - - .addStorage-body-control-createButton - colorDarkPrimaryButton() - .addStorage-body-control-cancelButton - colorDarkDefaultButton() - border-color $ui-monokai-borderColor - .list-attachement-clear-button - colorMonokaiPrimaryButton() - -body[data-theme="dracula"] - .root - color $ui-dracula-text-color - - .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 - .list-attachement-clear-button - colorDraculaPrimaryButton() \ 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 329dbfa4..17eb5558 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -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,10 +29,16 @@ 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.customMarkdownLintConfigCM.getCodeMirror(), + 'javascript' + ) CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript') // Set CM editor Sizes this.customCSSCM.getCodeMirror().setSize('400px', '400px') @@ -38,27 +46,32 @@ class UiTab extends React.Component { 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') @@ -72,19 +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, + showScrollBar: this.refs.showScrollBar.checked, showMenuBar: this.refs.showMenuBar.checked, - disableDirectWrite: this.refs.uiD2w != null - ? this.refs.uiD2w.checked - : false + disableDirectWrite: + this.refs.uiD2w != null ? this.refs.uiD2w.checked : false }, editor: { theme: this.refs.editorTheme.value, @@ -110,9 +129,12 @@ class UiTab extends React.Component { spellcheck: this.refs.spellcheck.checked, enableSmartPaste: this.refs.enableSmartPaste.checked, enableMarkdownLint: this.refs.enableMarkdownLint.checked, - customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue(), + customMarkdownLintConfig: this.customMarkdownLintConfigCM + .getCodeMirror() + .getValue(), prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), - deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked + deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked, + rtlEnabled: this.refs.rtlEnabled.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -140,35 +162,43 @@ class UiTab extends React.Component { const newCodemirrorTheme = this.refs.editorTheme.value if (newCodemirrorTheme !== codemirrorTheme) { - const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme) + 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!') - }) + 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({ @@ -179,7 +209,7 @@ class UiTab extends React.Component { this.props.haveToSave() } - clearMessage () { + clearMessage() { _.debounce(() => { this.setState({ UiAlert: null @@ -187,17 +217,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 ( @@ -210,32 +255,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 => ( + + ))}
@@ -245,12 +405,15 @@ class UiTab extends React.Component { {i18n.__('Default New Note')}
- this.handleUIChange(e)} ref='defaultNote' > - +
@@ -258,103 +421,133 @@ class UiTab extends React.Component {
- { - global.process.platform === 'win32' - ?
+ {global.process.platform === 'win32' ? ( +
- : null - } - -
Tags
- + ) : null}
+
+
Tags
+
+
@@ -362,21 +555,22 @@ class UiTab extends React.Component {
Editor
-
- {i18n.__('Editor Theme')} -
+
{i18n.__('Editor Theme')}
- -
+
(this.codeMirrorInstance = e)} value={codemirrorSampleCode} @@ -385,7 +579,8 @@ class UiTab extends React.Component { readOnly: true, mode: 'javascript', theme: codemirrorTheme - }} /> + }} + />
@@ -394,10 +589,11 @@ class UiTab extends React.Component { {i18n.__('Editor Font Size')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -407,10 +603,11 @@ class UiTab extends React.Component { {i18n.__('Editor Font Family')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -420,18 +617,21 @@ class UiTab extends React.Component { {i18n.__('Editor Indent Style')}
-   - this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} > @@ -445,23 +645,21 @@ class UiTab extends React.Component {
-
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -472,12 +670,15 @@ class UiTab extends React.Component { {i18n.__('Switch to Preview')}
-
@@ -488,15 +689,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' + )} +

@@ -505,14 +711,21 @@ class UiTab extends React.Component { {i18n.__('Snippet Default Language')}
-
@@ -522,10 +735,11 @@ class UiTab extends React.Component { {i18n.__('Front matter title field')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -533,99 +747,131 @@ class UiTab extends React.Component {
+
+
+
@@ -634,10 +880,11 @@ class UiTab extends React.Component { {i18n.__('Matching character pairs')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -648,10 +895,11 @@ class UiTab extends React.Component { {i18n.__('Matching character triples')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -662,10 +910,11 @@ class UiTab extends React.Component { {i18n.__('Exploding character pairs')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -675,13 +924,22 @@ class UiTab extends React.Component { {i18n.__('Custom MarkdownLint Rules')}
- this.handleUIChange(e)} + this.handleUIChange(e)} checked={this.state.config.editor.enableMarkdownLint} ref='enableMarkdownLint' type='checkbox' - />  + /> +   {i18n.__('Enable MarkdownLint')} -
+
+ gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ] + }} + />
@@ -705,10 +968,11 @@ class UiTab extends React.Component { {i18n.__('Preview Font Size')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -718,124 +982,152 @@ 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')}
-
@@ -844,10 +1136,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Inline Open Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -857,10 +1150,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Inline Close Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -870,10 +1164,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Block Open Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -883,10 +1178,11 @@ class UiTab extends React.Component { {i18n.__('LaTeX Block Close Delimiter')}
- this.handleUIChange(e)} + onChange={e => this.handleUIChange(e)} type='text' />
@@ -896,26 +1192,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')} -
+
+ }} + />
@@ -935,7 +1233,7 @@ class UiTab extends React.Component { {i18n.__('Prettier Config')}
-
+
+ }} + />
- {UiAlertElement}
diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index 86957083..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 9fdd70c8..a8d6f386 100644 --- a/browser/main/modals/RenameFolderModal.js +++ b/browser/main/modals/RenameFolderModal.js @@ -8,7 +8,7 @@ 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/RenameFolderModal.styl index c9909d00..435aa6a0 100644 --- a/browser/main/modals/RenameFolderModal.styl +++ b/browser/main/modals/RenameFolderModal.styl @@ -43,23 +43,31 @@ border-radius 2px padding 0 25px margin 0 auto + font-size 14px colorPrimaryButton() -body[data-theme="dark"] - .root - modalDark() +apply-theme(theme) + body[data-theme={theme}] + .root + background-color transparent - .header - background-color $ui-dark-button--hover-backgroundColor - border-color $ui-dark-borderColor - color $ui-dark-text-color + .header + background-color get-theme-var(theme, 'button--hover-backgroundColor') + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') - .description - color $ui-inactive-text-color + .description + color $ui-inactive-text-color - .control-input - border-color $ui-dark-borderColor - color $ui-dark-text-color + .control-input + border-color get-theme-var(theme, 'borderColor') + color get-theme-var(theme, 'text-color') - .control-confirmButton - colorDarkPrimaryButton() + .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/store.js b/browser/main/store.js index d48198a7..d48946a6 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -6,7 +6,7 @@ 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(), @@ -18,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 @@ -40,7 +40,10 @@ 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) @@ -51,173 +54,170 @@ function data (state = defaultDataMap(), action) { } }) 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': @@ -247,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) { @@ -269,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) }) @@ -288,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) @@ -307,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) }) @@ -322,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) }) @@ -334,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) @@ -364,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 @@ -390,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, { @@ -400,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) { @@ -411,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) @@ -429,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) { @@ -438,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) @@ -458,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() @@ -476,8 +476,15 @@ const reducer = combineReducers({ router: connectRouter(history) }) -const store = createStore(reducer, undefined, process.env.NODE_ENV === 'development' - ? compose(applyMiddleware(routerMiddleware(history)), DevTools.instrument()) - : applyMiddleware(routerMiddleware(history))) +const store = createStore( + reducer, + undefined, + process.env.NODE_ENV === 'development' + ? compose( + applyMiddleware(routerMiddleware(history)), + DevTools.instrument() + ) + : applyMiddleware(routerMiddleware(history)) +) export { store, history } diff --git a/browser/styles/Detail/TagSelect.styl b/browser/styles/Detail/TagSelect.styl index 8900422c..5fc23310 100644 --- a/browser/styles/Detail/TagSelect.styl +++ b/browser/styles/Detail/TagSelect.styl @@ -51,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 53456da9..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,22 +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%) -colorDraculaDefaultButton() - border-color $ui-dracula-borderColor - color $ui-dracula-text-color - background-color $ui-dracula-button-backgroundColor - &:hover - background-color $ui-dracula-button--hover-backgroundColor - &:active - &:active:hover - background-color $ui-dracula-button--active-backgroundColor -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 095f8628..937a4a58 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,6 +1,6 @@ # 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), [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). +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 @@ -37,7 +37,7 @@ Visit the page for the pull request and look at the end of the url for the PR nu https://github.com/BoostIO/Boostnote/pull/2794 In the following, replace \ with that number (no brackets). -For the above url, you would replace \ with 2794 +For URLs below, you would replace \ with 2794 _If you do not have a local copy of the master branch yet_ ``` @@ -82,13 +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. -Ubuntu/Debian: +``` +$ yarn add --dev grunt-electron-installer-debian grunt-electron-installer-redhat +``` + +**Ubuntu/Debian:** ``` $ sudo apt-get install -y rpm fakeroot ``` -Fedora: +**Fedora:** ``` $ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot @@ -100,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/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 index ad539996..ae456522 100644 --- a/docs/pt_BR/build.md +++ b/docs/pt_BR/build.md @@ -42,7 +42,9 @@ 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. +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. @@ -50,7 +52,7 @@ Se você achar isto necessário, você pode usar o _codesign_ ou o _authenticode 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. +**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. diff --git a/docs/zh_TW/build.md b/docs/zh_TW/build.md index 151f946d..360440e5 100644 --- a/docs/zh_TW/build.md +++ b/docs/zh_TW/build.md @@ -10,7 +10,6 @@ ## 開發 我們使用 Webpack HMR 來開發 Boostnote。 - 在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。 **用 yarn 來安裝必要 packages** @@ -19,41 +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: @@ -67,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/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 80f797b9..d08183cd 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' @@ -45,189 +55,208 @@ } } - CodeMirror.defineMode('bfm', function (config, baseConfig) { - baseConfig.name = 'yaml-frontmatter' - const baseMode = CodeMirror.getMode(config, baseConfig) + CodeMirror.defineMode( + 'bfm', + function(config, baseConfig) { + baseConfig.name = 'yaml-frontmatter' + const baseMode = CodeMirror.getMode(config, baseConfig) - return { - startState: function() { - return { - baseState: CodeMirror.startState(baseMode), + return { + startState: function() { + return { + baseState: CodeMirror.startState(baseMode), - basePos: 0, - baseCur: null, - overlayPos: 0, - overlayCur: null, - streamSeen: null, + basePos: 0, + baseCur: null, + overlayPos: 0, + overlayCur: null, + streamSeen: null, - fencedEndRE: null, + fencedEndRE: null, - inTable: false, - rowIndex: 0 - } - }, - copyState: function(s) { - return { - baseState: CodeMirror.copyState(baseMode, s.baseState), + inTable: false, + rowIndex: 0 + } + }, + copyState: function(s) { + return { + baseState: CodeMirror.copyState(baseMode, s.baseState), - basePos: s.basePos, - baseCur: null, - overlayPos: s.overlayPos, - overlayCur: null, + basePos: s.basePos, + baseCur: null, + overlayPos: s.overlayPos, + overlayCur: null, - fencedMode: s.fencedMode, - fencedState: s.fencedMode ? CodeMirror.copyState(s.fencedMode, s.fencedState) : null, + fencedMode: s.fencedMode, + fencedState: s.fencedMode + ? CodeMirror.copyState(s.fencedMode, s.fencedState) + : null, - fencedEndRE: s.fencedEndRE, + fencedEndRE: s.fencedEndRE, - inTable: s.inTable, - rowIndex: s.rowIndex - } - }, - token: function(stream, state) { - const initialPos = stream.pos + inTable: s.inTable, + rowIndex: s.rowIndex + } + }, + token: function(stream, state) { + const initialPos = stream.pos - if (state.fencedEndRE && stream.match(state.fencedEndRE)) { - state.fencedEndRE = null - state.fencedMode = null - state.fencedState = null + if (state.fencedEndRE && stream.match(state.fencedEndRE)) { + state.fencedEndRE = null + state.fencedMode = null + state.fencedState = null - stream.pos = initialPos - } - else { - if (state.fencedMode) { - return state.fencedMode.token(stream, state.fencedState) + stream.pos = initialPos + } else { + if (state.fencedMode) { + return state.fencedMode.token(stream, state.fencedState) + } + + const match = stream.match(fencedCodeRE, true) + if (match) { + state.fencedEndRE = new RegExp(match[1] + '+ *$') + + state.fencedMode = getMode( + match[2], + match[3], + config, + stream.lineOracle.doc.cm + ) + if (state.fencedMode) { + state.fencedState = CodeMirror.startState(state.fencedMode) + } + + stream.pos = initialPos + } + } + + if ( + stream != state.streamSeen || + Math.min(state.basePos, state.overlayPos) < stream.start + ) { + state.streamSeen = stream + state.basePos = state.overlayPos = stream.start + } + + if (stream.start == state.basePos) { + state.baseCur = baseMode.token(stream, state.baseState) + state.basePos = stream.pos + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start + state.overlayCur = this.overlayToken(stream, state) + state.overlayPos = stream.pos + } + stream.pos = Math.min(state.basePos, state.overlayPos) + + if (state.overlayCur == null) { + return state.baseCur + } else if (state.baseCur != null && state.combineTokens) { + return state.baseCur + ' ' + state.overlayCur + } else { + return state.overlayCur + } + }, + overlayToken: function(stream, state) { + state.combineTokens = false + + if (state.fencedEndRE && stream.match(state.fencedEndRE)) { + state.fencedEndRE = null + state.localMode = null + state.localState = null + + return null + } + + if (state.localMode) { + return state.localMode.token(stream, state.localState) || '' } const match = stream.match(fencedCodeRE, true) if (match) { state.fencedEndRE = new RegExp(match[1] + '+ *$') - state.fencedMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm) - if (state.fencedMode) { - state.fencedState = CodeMirror.startState(state.fencedMode) + state.localMode = getMode( + match[2], + match[3], + config, + stream.lineOracle.doc.cm + ) + if (state.localMode) { + state.localState = CodeMirror.startState(state.localMode) } - stream.pos = initialPos - } - } - - if (stream != state.streamSeen || Math.min(state.basePos, state.overlayPos) < stream.start) { - state.streamSeen = stream - state.basePos = state.overlayPos = stream.start - } - - if (stream.start == state.basePos) { - state.baseCur = baseMode.token(stream, state.baseState) - state.basePos = stream.pos - } - if (stream.start == state.overlayPos) { - stream.pos = stream.start - state.overlayCur = this.overlayToken(stream, state) - state.overlayPos = stream.pos - } - stream.pos = Math.min(state.basePos, state.overlayPos) - - if (state.overlayCur == null) { - return state.baseCur - } - else if (state.baseCur != null && state.combineTokens) { - return state.baseCur + ' ' + state.overlayCur - } - else { - return state.overlayCur - } - }, - overlayToken: function(stream, state) { - state.combineTokens = false - - if (state.fencedEndRE && stream.match(state.fencedEndRE)) { - state.fencedEndRE = null - state.localMode = null - state.localState = null - - return null - } - - if (state.localMode) { - return state.localMode.token(stream, state.localState) || '' - } - - const match = stream.match(fencedCodeRE, true) - if (match) { - state.fencedEndRE = new RegExp(match[1] + '+ *$') - - state.localMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm) - if (state.localMode) { - state.localState = CodeMirror.startState(state.localMode) - } - - return null - } - - state.combineTokens = true - - if (state.inTable) { - if (stream.match(/^\|/)) { - ++state.rowIndex - - stream.skipToEnd() - - if (state.rowIndex === 1) { - return 'table table-separator' - } else if (state.rowIndex % 2 === 0) { - return 'table table-row table-row-even' - } else { - return 'table table-row table-row-odd' - } - } else { - state.inTable = false - - stream.skipToEnd() return null } - } else if (stream.match(/^\|/)) { - state.inTable = true - state.rowIndex = 0 + + state.combineTokens = true + + if (state.inTable) { + if (stream.match(/^\|/)) { + ++state.rowIndex + + stream.skipToEnd() + + if (state.rowIndex === 1) { + return 'table table-separator' + } else if (state.rowIndex % 2 === 0) { + return 'table table-row table-row-even' + } else { + return 'table table-row table-row-odd' + } + } else { + state.inTable = false + + stream.skipToEnd() + return null + } + } else if (stream.match(/^\|/)) { + state.inTable = true + state.rowIndex = 0 + + stream.skipToEnd() + return 'table table-header' + } stream.skipToEnd() - return 'table table-header' - } - - stream.skipToEnd() - return null - }, - electricChars: baseMode.electricChars, - innerMode: function(state) { - if (state.fencedMode) { - return { - mode: state.fencedMode, - state: state.fencedState + return null + }, + electricChars: baseMode.electricChars, + innerMode: function(state) { + if (state.fencedMode) { + return { + mode: state.fencedMode, + state: state.fencedState + } + } else { + return { + mode: baseMode, + state: state.baseState + } } - } else { - return { - mode: baseMode, - state: state.baseState - } - } - }, - blankLine: function(state) { - state.inTable = false + }, + blankLine: function(state) { + state.inTable = false - if (state.fencedMode) { - return state.fencedMode.blankLine && state.fencedMode.blankLine(state.fencedState) - } else { - return baseMode.blankLine(state.baseState) + if (state.fencedMode) { + return ( + state.fencedMode.blankLine && + state.fencedMode.blankLine(state.fencedState) + ) + } else { + return baseMode.blankLine(state.baseState) + } } } - } - }, 'yaml-frontmatter') + }, + 'yaml-frontmatter' + ) 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/gruntfile.js b/gruntfile.js index 207f8685..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 : '' @@ -41,10 +43,7 @@ module.exports = function (grunt) { genericName: 'Boostnote', productDescription: 'The opensource note app for developers.', arch: 'amd64', - categories: [ - 'Development', - 'Utility' - ], + categories: ['Development', 'Utility'], icon: path.join(__dirname, 'resources/app.png'), bin: 'Boostnote' }, @@ -60,10 +59,7 @@ module.exports = function (grunt) { genericName: 'Boostnote', 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) @@ -153,7 +152,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) @@ -168,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) @@ -180,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) @@ -197,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() @@ -242,7 +241,7 @@ module.exports = function (grunt) { } }) - function getTarget () { + function getTarget() { switch (process.platform) { case 'darwin': return 'osx' @@ -255,7 +254,7 @@ module.exports = function (grunt) { } } - grunt.registerTask('build', function (platform) { + grunt.registerTask('build', function(platform) { if (platform == null) platform = getTarget() switch (platform) { @@ -263,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) { @@ -286,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 { @@ -298,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 42e229d3..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()) { @@ -52,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 6151c1f0..2293fd58 100644 --- a/lib/main-app.js +++ b/lib/main-app.js @@ -4,25 +4,25 @@ 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 @@ -36,11 +36,13 @@ const updater = new GhReleases(ghReleasesOpts) // Check for updates // `status` returns true if there is a new update available -function checkUpdate () { - if (!isPackaged) { // Prevents app from attempting to update when in dev mode. +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 } @@ -57,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') @@ -99,7 +101,7 @@ app.on('ready', function () { } // Check update every day - setInterval(function () { + setInterval(function() { if (isPackaged) checkUpdate() }, 1000 * 60 * 60 * 24) @@ -107,7 +109,7 @@ app.on('ready', function () { setTimeout(() => { 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 124c6675..7caef0bf 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -11,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', @@ -80,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') } }, @@ -113,7 +113,7 @@ const file = { submenu: [ { label: 'Plain Text, MarkDown (.txt, .md)', - click () { + click() { mainWindow.webContents.send('import:file') } } @@ -124,28 +124,28 @@ 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 () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'export-pdf') mainWindow.webContents.send('export:save-pdf') } @@ -158,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') } }, @@ -174,7 +174,7 @@ const file = { { label: 'Print', accelerator: 'CommandOrControl+P', - click () { + click() { mainWindow.webContents.send('list:isMarkdownNote', 'print') mainWindow.webContents.send('print') } @@ -183,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 = { @@ -241,7 +246,7 @@ const edit = { { label: 'Add Tag', accelerator: 'CommandOrControl+Shift+T', - click () { + click() { mainWindow.webContents.send('editor:add-tag') } } @@ -254,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() } }, @@ -271,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') } }, @@ -288,7 +293,7 @@ const view = { { label: 'Focus Search', accelerator: 'CommandOrControl+Shift+L', - click () { + click() { mainWindow.webContents.send('top:focus-search') } }, @@ -298,14 +303,14 @@ 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') } }, @@ -314,22 +319,22 @@ const view = { }, { 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') } } @@ -382,34 +387,58 @@ 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') } + click() { + shell.openExternal( + 'https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet' + ) + } }, { label: 'Latex', - click () { shell.openExternal('https://katex.org/docs/supported.html') } + click() { + shell.openExternal('https://katex.org/docs/supported.html') + } }, { label: 'HTML', - click () { shell.openExternal('https://htmlcheatsheet.com/') } + 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') } + click() { + shell.openExternal( + 'https://github.com/TobseF/boostnote-markdown-cheatsheet/blob/master/BOOSTNOTE_MARKDOWN_CHEAT_SHEET.md' + ) + } } ] }, @@ -418,7 +447,7 @@ const help = { }, { label: 'About', - click () { + click() { const version = electron.app.getVersion() const electronVersion = process.versions.electron const chromeVersion = process.versions.chrome @@ -426,20 +455,20 @@ const help = { 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}` - }) + 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 515dc8b4..e6b0cb29 100644 --- a/lib/main-window.js +++ b/lib/main-window.js @@ -54,12 +54,17 @@ const mainWindow = new BrowserWindow({ }, icon: path.resolve(__dirname, '../resources/app.png') }) -const url = path.resolve(__dirname, process.env.NODE_ENV === 'development' ? './main.development.html' : './main.production.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() }) @@ -74,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) @@ -86,7 +91,7 @@ if (process.platform === 'darwin') { } }) - app.on('before-quit', function (e) { + app.on('before-quit', function(e) { mainWindow.removeAllListeners() }) } @@ -94,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) { @@ -103,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 index 63e50af1..900c66c7 100644 --- a/lib/main.development.html +++ b/lib/main.development.html @@ -108,12 +108,12 @@ - + diff --git a/lib/main.production.html b/lib/main.production.html index d6828acc..05d80345 100644 --- a/lib/main.production.html +++ b/lib/main.production.html @@ -10,6 +10,7 @@ + Boostnote @@ -103,12 +104,12 @@ - + @@ -129,6 +130,10 @@ + + + +