diff --git a/.eslintrc b/.eslintrc index 1709c9d8..be8cb903 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,9 @@ "globals": { "FileReader": true, "localStorage": true, - "fetch": true + "fetch": true, + "Image": true, + "MutationObserver": true }, "env": { "jest": 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/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 2a4ae71b..9214363e 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -21,13 +21,14 @@ 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 -import TurndownService from 'turndown' +import { createTurndownService } from '../lib/turndown' import {languageMaps} from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' 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' +import prettier from 'prettier' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -69,7 +70,9 @@ export default class CodeEditor extends React.Component { storageKey, noteKey } = this.props - debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) + if (this.props.deleteUnusedAttachments === true) { + debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) + } } this.pasteHandler = (editor, e) => { e.preventDefault() @@ -98,7 +101,7 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() - this.turndownService = new TurndownService() + this.turndownService = createTurndownService() } handleSearch (msg) { @@ -106,7 +109,7 @@ export default class CodeEditor extends React.Component { const component = this if (component.searchState) cm.removeOverlay(component.searchState) - if (msg.length < 3) return + if (msg.length < 1) return cm.operation(function () { component.searchState = makeOverlay(msg, 'searching') @@ -216,6 +219,37 @@ export default class CodeEditor extends React.Component { } return CodeMirror.Pass }, + [translateHotkey(hotkey.prettifyMarkdown)]: cm => { + // Default / User configured prettier options + const currentConfig = JSON.parse(self.props.prettierConfig) + + // Parser type will always need to be markdown so we override the option before use + currentConfig.parser = 'markdown' + + // Get current cursor position + const cursorPos = cm.getCursor() + currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos) + + // Prettify contents of editor + const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig) + + const formattedText = formattedTextDetails.formatted + const formattedCursorPos = formattedTextDetails.cursorOffset + cm.doc.setValue(formattedText) + + // Reset Cursor position to be at the same markdown as was before prettifying + const newCursorPos = cm.doc.posFromIndex(formattedCursorPos) + cm.doc.setCursor(newCursorPos) + }, + [translateHotkey(hotkey.sortLines)]: cm => { + const selection = cm.doc.getSelection() + const appendLineBreak = /\n$/.test(selection) + + const sorted = _.split(selection.trim(), '\n').sort() + const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '') + + cm.doc.replaceSelection(sortedString) + }, [translateHotkey(hotkey.pasteSmartly)]: cm => { this.handlePaste(cm, true) } @@ -269,7 +303,8 @@ export default class CodeEditor extends React.Component { explode: this.props.explodingPairs, override: true }, - extraKeys: this.defaultKeyMap + extraKeys: this.defaultKeyMap, + prettierConfig: this.props.prettierConfig }) document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' @@ -608,6 +643,9 @@ export default class CodeEditor extends React.Component { this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) } } + if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) { + this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments) + } if (needRefresh) { this.editor.refresh() @@ -836,6 +874,17 @@ export default class CodeEditor extends React.Component { this.editor.setCursor(cursor) } + /** + * Update content of one line + * @param {Number} lineNumber + * @param {String} content + */ + setLineContent (lineNumber, content) { + const prevContent = this.editor.getLine(lineNumber) + const prevContentLength = prevContent ? prevContent.length : 0 + this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength }) + } + handleDropImage (dropEvent) { dropEvent.preventDefault() const { @@ -1169,7 +1218,8 @@ CodeEditor.propTypes = { autoDetect: PropTypes.bool, spellCheck: PropTypes.bool, enableMarkdownLint: PropTypes.bool, - customMarkdownLintConfig: PropTypes.string + customMarkdownLintConfig: PropTypes.string, + deleteUnusedAttachments: PropTypes.bool } CodeEditor.defaultProps = { @@ -1183,5 +1233,7 @@ CodeEditor.defaultProps = { autoDetect: false, spellCheck: false, enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, - customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig + customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig, + prettierConfig: DEFAULT_CONFIG.editor.prettierConfig, + deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 3dd57f70..cd885fd9 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -169,14 +169,15 @@ class MarkdownEditor extends React.Component { .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -322,6 +323,8 @@ class MarkdownEditor extends React.Component { switchPreview={config.editor.switchPreview} enableMarkdownLint={config.editor.enableMarkdownLint} customMarkdownLintConfig={config.editor.customMarkdownLintConfig} + prettierConfig={config.editor.prettierConfig} + deleteUnusedAttachments={config.editor.deleteUnusedAttachments} /> { - const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) + 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', () => { @@ -556,7 +575,9 @@ export default class MarkdownPreview extends React.Component { } componentDidUpdate (prevProps) { - if (prevProps.value !== this.props.value) this.rewriteIframe() + // actual rewriteIframe function should be called only once + let needsRewriteIframe = false + if (prevProps.value !== this.props.value) needsRewriteIframe = true if ( prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || @@ -566,7 +587,7 @@ export default class MarkdownPreview extends React.Component { prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() - this.rewriteIframe() + needsRewriteIframe = true } if ( prevProps.fontFamily !== this.props.fontFamily || @@ -581,8 +602,17 @@ export default class MarkdownPreview extends React.Component { prevProps.customCSS !== this.props.customCSS ) { this.applyStyle() + needsRewriteIframe = true + } + + if (needsRewriteIframe) { this.rewriteIframe() } + + // Should scroll to top after selecting another note + if (prevProps.noteKey !== this.props.noteKey) { + this.getWindow().scrollTo(0, 0) + } } getStyleParams () { @@ -639,16 +669,18 @@ export default class MarkdownPreview extends React.Component { this.getWindow().document.getElementById( 'codeTheme' ).href = this.getCodeThemeLink(codeBlockTheme) - this.getWindow().document.getElementById('style').innerHTML = buildStyle( + this.getWindow().document.getElementById('style').innerHTML = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, + optimizeOverflowScroll: true, theme, allowCustomCSS, customCSS - ) + }) + this.getWindow().document.documentElement.style.overflowY = 'hidden' } getCodeThemeLink (name) { @@ -815,6 +847,7 @@ export default class MarkdownPreview extends React.Component { canvas.height = height.value + 'vh' } + // eslint-disable-next-line no-unused-vars const chart = new Chart(canvas, chartConfig) } catch (e) { el.className = 'chart-error' @@ -953,8 +986,6 @@ export default class MarkdownPreview extends React.Component { overlay.appendChild(zoomImg) document.body.appendChild(overlay) } - - this.getWindow().scrollTo(0, 0) } focus () { @@ -1002,25 +1033,31 @@ export default class MarkdownPreview extends React.Component { 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 - 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.refs.root.contentWindow.document.getElementById( + targetId + ) + + if (targetElement != null) { + this.getWindow().scrollTo(0, targetElement.offsetTop) + } + return } - return } // this will match the new uuid v4 hash and the old hash diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index b283228c..f5996c59 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -88,14 +88,15 @@ class MarkdownSplitEditor extends React.Component { .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -181,6 +182,7 @@ class MarkdownSplitEditor extends React.Component { switchPreview={config.editor.switchPreview} enableMarkdownLint={config.editor.enableMarkdownLint} customMarkdownLintConfig={config.editor.customMarkdownLintConfig} + deleteUnusedAttachments={config.editor.deleteUnusedAttachments} />
this.handleMouseDown(e)} >
diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 9ef691da..168af1ff 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -3,7 +3,7 @@ */ import PropTypes from 'prop-types' import React from 'react' -import { isArray } from 'lodash' +import { isArray, sortBy } from 'lodash' import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import { getTodoStatus } from 'browser/lib/getTodoStatus' @@ -43,7 +43,7 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => { } if (showTagsAlphabetically) { - return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) + return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } else { return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } diff --git a/browser/lib/keygen.js b/browser/lib/keygen.js index 814efedd..557a8a40 100644 --- a/browser/lib/keygen.js +++ b/browser/lib/keygen.js @@ -1,5 +1,4 @@ const crypto = require('crypto') -const _ = require('lodash') const uuidv4 = require('uuid/v4') module.exports = function (uuid) { diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 3325604a..641216e3 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -96,6 +96,10 @@ function sanitizeInline (html, options) { function naughtyHRef (href, options) { // href = href.replace(/[\x00-\x20]+/g, '') + if (!href) { + // No href + return false + } href = href.replace(/<\!\-\-.*?\-\-\>/g, '') const matches = href.match(/^([a-zA-Z]+)\:/) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 49183442..12955585 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -183,32 +183,47 @@ class Markdown { }) const deflate = require('markdown-it-plantuml/lib/deflate') - this.md.use(require('markdown-it-plantuml'), { - generateSource: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + const plantuml = require('markdown-it-plantuml') + const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url + const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress) + const parsePlantUml = function (umlCode, openMarker, closeMarker, type) { + const s = unescape(encodeURIComponent(umlCode)) + const zippedCode = deflate.encode64( + deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9) + ) + return `${plantUmlServerAddress}/${type}/${zippedCode}` + } + + this.md.use(plantuml, { + generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg') }) - // Ditaa support - this.md.use(require('markdown-it-plantuml'), { + // Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. + this.md.use(plantuml, { openMarker: '@startditaa', closeMarker: '@endditaa', - generateSource: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - // Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png') + }) + + // Mindmap support + this.md.use(plantuml, { + openMarker: '@startmindmap', + closeMarker: '@endmindmap', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg') + }) + + // WBS support + this.md.use(plantuml, { + openMarker: '@startwbs', + closeMarker: '@endwbs', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg') + }) + + // Gantt support + this.md.use(plantuml, { + openMarker: '@startgantt', + closeMarker: '@endgantt', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg') }) // Override task item diff --git a/browser/lib/turndown.js b/browser/lib/turndown.js new file mode 100644 index 00000000..a1c3e128 --- /dev/null +++ b/browser/lib/turndown.js @@ -0,0 +1,9 @@ +const TurndownService = require('turndown') +const { gfm } = require('turndown-plugin-gfm') + +export const createTurndownService = function () { + const turndown = new TurndownService() + turndown.use(gfm) + turndown.remove('script') + return turndown +} diff --git a/browser/lib/utils.js b/browser/lib/utils.js index 4bcc9698..9f6f1425 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -136,9 +136,24 @@ export function isMarkdownTitleURL (str) { return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) } +export function humanFileSize (bytes) { + const threshold = 1000 + if (Math.abs(bytes) < threshold) { + return bytes + ' B' + } + var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + var u = -1 + do { + bytes /= threshold + ++u + } while (Math.abs(bytes) >= threshold && u < units.length - 1) + return bytes.toFixed(1) + ' ' + units[u] +} + export default { lastFindInArray, escapeHtmlCharacters, isObjectEqual, - isMarkdownTitleURL + isMarkdownTitleURL, + humanFileSize } diff --git a/browser/main/Detail/FromUrlButton.js b/browser/main/Detail/FromUrlButton.js new file mode 100644 index 00000000..0d1c1b4c --- /dev/null +++ b/browser/main/Detail/FromUrlButton.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './FromUrlButton.styl' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' + +class FromUrlButton extends React.Component { + constructor (props) { + super(props) + + this.state = { + isActive: false + } + } + + handleMouseDown (e) { + this.setState({ + isActive: true + }) + } + + handleMouseUp (e) { + this.setState({ + isActive: false + }) + } + + handleMouseLeave (e) { + this.setState({ + isActive: false + }) + } + + render () { + const { className } = this.props + + return ( + + ) + } +} + +FromUrlButton.propTypes = { + isActive: PropTypes.bool, + onClick: PropTypes.func, + className: PropTypes.string +} + +export default CSSModules(FromUrlButton, styles) diff --git a/browser/main/Detail/FromUrlButton.styl b/browser/main/Detail/FromUrlButton.styl new file mode 100644 index 00000000..66c2d730 --- /dev/null +++ b/browser/main/Detail/FromUrlButton.styl @@ -0,0 +1,41 @@ +.root + top 45px + topBarButtonRight() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 50px + right 125px + width 90px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +.root--active + @extend .root + transition 0.15s + color $ui-favorite-star-button-color + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + +.icon + transition transform 0.15s + height 13px + +body[data-theme="dark"] + .root + topBarButtonDark() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 45024751..207e1e2b 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -152,7 +152,6 @@ class MarkdownNoteDetail extends React.Component { } handleFolderChange (e) { - const { dispatch } = this.props const { note } = this.state const value = this.refs.folder.value const splitted = value.split('-') @@ -410,7 +409,7 @@ class MarkdownNoteDetail extends React.Component { } render () { - const { data, location, config } = this.props + const { data, dispatch, location, config } = this.props const { note, editorType } = this.state const storageKey = note.storage const folderKey = note.folder @@ -465,6 +464,7 @@ class MarkdownNoteDetail extends React.Component { saveTagsAlphabetically={config.ui.saveTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically} data={data} + dispatch={dispatch} onChange={this.handleUpdateTag.bind(this)} coloredTags={config.coloredTags} /> @@ -472,6 +472,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleSwitchMode(e)} editorType={editorType} /> + this.handleStarButtonClick(e)} isActive={note.isStarred} @@ -511,7 +512,7 @@ class MarkdownNoteDetail extends React.Component { exportAsTxt={this.exportAsTxt} exportAsHtml={this.exportAsHtml} exportAsPdf={this.exportAsPdf} - wordCount={note.content.split(' ').length} + wordCount={note.content.trim().split(/\s+/g).length} letterCount={note.content.replace(/\r?\n/g, '').length} type={note.type} print={this.print} diff --git a/browser/main/Detail/MarkdownNoteDetail.styl b/browser/main/Detail/MarkdownNoteDetail.styl index 819bef2e..a24e9881 100644 --- a/browser/main/Detail/MarkdownNoteDetail.styl +++ b/browser/main/Detail/MarkdownNoteDetail.styl @@ -81,11 +81,4 @@ body[data-theme="dracula"] .root border-left 1px solid $ui-dracula-borderColor background-color $ui-dracula-noteDetail-backgroundColor - -div - > button, div - -webkit-user-drag none - user-select none - > img, span - -webkit-user-drag none - user-select none + diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index 1ca46516..21670a1b 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -107,4 +107,12 @@ body[data-theme="monokai"] body[data-theme="dracula"] .info border-color $ui-dracula-borderColor - background-color $ui-dracula-noteDetail-backgroundColor \ No newline at end of file + background-color $ui-dracula-noteDetail-backgroundColor + +.info > div + > button + -webkit-user-drag none + user-select none + > img, span + -webkit-user-drag none + user-select none \ No newline at end of file diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 2ae01082..ec9a1d0b 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -699,7 +699,7 @@ class SnippetNoteDetail extends React.Component { } render () { - const { data, config, location } = this.props + const { data, dispatch, config, location } = this.props const { note } = this.state const storageKey = note.storage @@ -823,6 +823,7 @@ class SnippetNoteDetail extends React.Component { saveTagsAlphabetically={config.ui.saveTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically} data={data} + dispatch={dispatch} onChange={(e) => this.handleChange(e)} coloredTags={config.coloredTags} /> diff --git a/browser/main/Detail/StarButton.styl b/browser/main/Detail/StarButton.styl index e9c523e9..e06d1ac9 100644 --- a/browser/main/Detail/StarButton.styl +++ b/browser/main/Detail/StarButton.styl @@ -42,4 +42,4 @@ body[data-theme="dark"] topBarButtonDark() &:hover transition 0.2s - color alpha($ui-favorite-star-button-color, 0.6) \ No newline at end of file + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index e3d9a567..615cb5d2 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -8,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import i18n from 'browser/lib/i18n' import ee from 'browser/main/lib/eventEmitter' import Autosuggest from 'react-autosuggest' +import { push } from 'connected-react-router' class TagSelect extends React.Component { constructor (props) { @@ -96,8 +97,11 @@ class TagSelect extends React.Component { } handleTagLabelClick (tag) { - const { router } = this.context - router.push(`/tags/${tag}`) + const { dispatch } = this.props + + // Note: `tag` requires encoding later. + // E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working. + dispatch(push(`/tags/${tag}`)) } handleTagRemoveButtonClick (tag) { @@ -255,11 +259,8 @@ class TagSelect extends React.Component { } } -TagSelect.contextTypes = { - router: PropTypes.shape({}) -} - TagSelect.propTypes = { + dispatch: PropTypes.func, className: PropTypes.string, value: PropTypes.arrayOf(PropTypes.string), onChange: PropTypes.func, diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 2b47b932..39d30973 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -75,3 +75,10 @@ body[data-theme="dracula"] .active background-color #bd93f9 box-shadow 2px 0px 7px #222222 + +.control-toggleModeButton + -webkit-user-drag none + user-select none + > div img + -webkit-user-drag none + user-select none diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js index 0ed3dd54..95b9d73d 100644 --- a/browser/main/Detail/index.js +++ b/browser/main/Detail/index.js @@ -50,16 +50,14 @@ class Detail extends React.Component { const searchStr = params.searchword displayedNotes = searchStr === undefined || searchStr === '' ? allNotes : searchFromNotes(allNotes, searchStr) - } - - if (location.pathname.match(/\/tags/)) { + } else if (location.pathname.match(/^\/tags/)) { const listOfTags = params.tagname.split(' ') displayedNotes = data.noteMap.map(note => note).filter(note => listOfTags.every(tag => note.tags.includes(tag)) ) } - if (location.pathname.match(/\/trashed/)) { + if (location.pathname.match(/^\/trashed/)) { displayedNotes = trashedNotes } else { displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key) diff --git a/browser/main/Main.js b/browser/main/Main.js index 30bf8e8a..e277c421 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -102,7 +102,7 @@ class Main extends React.Component { { name: 'example.js', mode: 'javascript', - content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)", + content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)", linesHighlighted: [] } ] @@ -169,6 +169,7 @@ class Main extends React.Component { } }) + // eslint-disable-next-line no-undef delete CodeMirror.keyMap.emacs['Ctrl-V'] eventEmitter.on('editor:fullscreen', this.toggleFullScreen) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 17e25a44..51c7822f 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -88,6 +88,7 @@ class NoteList extends React.Component { this.importFromFileHandler = this.importFromFile.bind(this) this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this) this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this) + this.handleNoteListBlur = this.handleNoteListBlur.bind(this) this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this) this.cloneNote = this.cloneNote.bind(this) this.deleteNote = this.deleteNote.bind(this) @@ -348,6 +349,13 @@ class NoteList extends React.Component { } } + handleNoteListBlur () { + this.setState({ + shiftKeyDown: false, + ctrlKeyDown: false + }) + } + getNotes () { const { data, match: { params }, location } = this.props if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) { @@ -1155,6 +1163,7 @@ class NoteList extends React.Component { tabIndex='-1' onKeyDown={(e) => this.handleNoteListKeyDown(e)} onKeyUp={this.handleNoteListKeyUp} + onBlur={this.handleNoteListBlur} > {noteList}
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 9d18a72c..3167f487 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -22,9 +22,10 @@ import context from 'browser/lib/context' import { remote } from 'electron' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import ColorPicker from 'browser/components/ColorPicker' +import { every, sortBy } from 'lodash' function matchActiveTags (tags, activeTags) { - return _.every(activeTags, v => tags.indexOf(v) >= 0) + return every(activeTags, v => tags.indexOf(v) >= 0) } class SideNav extends React.Component { @@ -271,6 +272,7 @@ class SideNav extends React.Component {
{this.tagListComponent(data)}
+
) } @@ -283,7 +285,7 @@ class SideNav extends React.Component { const { colorPicker } = this.state const activeTags = this.getActiveTags(location.pathname) const relatedTags = this.getRelatedTags(activeTags, data.noteMap) - let tagList = _.sortBy(data.tagNoteMap.map( + let tagList = sortBy(data.tagNoteMap.map( (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) ).filter( tag => tag.size > 0 @@ -296,7 +298,7 @@ class SideNav extends React.Component { }) } 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( diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index b3fb65d7..20799de5 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -31,6 +31,8 @@ export const DEFAULT_CONFIG = { toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V', + prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F', + sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S', insertDate: OSX ? 'Command + /' : 'Ctrl + /', insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /', toggleMenuBar: 'Alt' @@ -68,7 +70,14 @@ export const DEFAULT_CONFIG = { spellcheck: false, enableSmartPaste: false, enableMarkdownLint: false, - customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG + customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, + prettierConfig: ` { + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true + }`, + deleteUnusedAttachments: true }, preview: { fontSize: '14', diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 725bdc11..971ae812 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -8,6 +8,7 @@ const escapeStringRegexp = require('escape-string-regexp') const sander = require('sander') const url = require('url') import i18n from 'browser/lib/i18n' +import { isString } from 'lodash' const STORAGE_FOLDER_PLACEHOLDER = ':storage' const DESTINATION_FOLDER = 'attachments' @@ -19,7 +20,7 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp( * @returns {Promise} Image element created */ function getImage (file) { - if (_.isString(file)) { + if (isString(file)) { return new Promise(resolve => { const img = new Image() img.onload = () => resolve(img) @@ -623,6 +624,76 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey } } +/** + * @description Get all existing attachments related to a specific note + including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid + * @param markdownContent markdownContent of the current note + * @param storageKey StorageKey of the current note + * @param noteKey NoteKey of the currentNote + * @return {Promise>} Promise returning the + list of attachments with their properties */ +function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) { + if (storageKey == null || noteKey == null || markdownContent == null) { + return null + } + const targetStorage = findStorage.findStorage(storageKey) + const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent) + const attachmentsInNoteOnlyFileNames = [] + if (attachmentsInNote) { + for (let i = 0; i < attachmentsInNote.length; i++) { + attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) + } + } + if (fs.existsSync(attachmentFolder)) { + return new Promise((resolve, reject) => { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error('Error reading directory "' + attachmentFolder + '". Error:') + console.error(err) + reject(err) + return + } + const attachments = [] + for (const file of files) { + const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + attachments.push({ path: absolutePathOfFile, isInUse: false }) + } else { + attachments.push({ path: absolutePathOfFile, isInUse: true }) + } + } + resolve(attachments) + }) + }) + } else { + return null + } +} + +/** + * @description Remove all specified attachment paths + * @param attachments attachment paths + * @return {Promise} Promise after all attachments are removed */ +function removeAttachmentsByPaths (attachments) { + const promises = [] + for (const attachment of attachments) { + const promise = new Promise((resolve, reject) => { + fs.unlink(attachment, (err) => { + if (err) { + console.error('Could not delete "%s"', attachment) + console.error(err) + reject(err) + return + } + resolve() + }) + }) + promises.push(promise) + } + return Promise.all(promises) +} + /** * Clones the attachments of a given note. * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. @@ -725,8 +796,10 @@ module.exports = { getAbsolutePathsOfAttachmentsInContent, importAttachments, removeStorageAndNoteReferences, + removeAttachmentsByPaths, deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, + getAttachmentsPathAndStatus, moveAttachments, cloneAttachments, isAttachmentLink, diff --git a/browser/main/lib/dataApi/createNoteFromUrl.js b/browser/main/lib/dataApi/createNoteFromUrl.js new file mode 100644 index 00000000..f9878210 --- /dev/null +++ b/browser/main/lib/dataApi/createNoteFromUrl.js @@ -0,0 +1,79 @@ +const http = require('http') +const https = require('https') +const { createTurndownService } = require('../../../lib/turndown') +const createNote = require('./createNote') + +import { push } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' + +function validateUrl (str) { + if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) { + return true + } else { + return false + } +} + +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)'}) + } + + const request = url.startsWith('https') ? https : http + + const req = request.request(url, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + const markdownHTML = td.turndown(data) + + if (dispatch !== null) { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }) + .then((note) => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + dispatch(push({ + pathname: location.pathname, + query: {key: noteHash} + })) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + resolve({result: true, error: null}) + }) + } else { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }).then((note) => { + resolve({result: true, note, error: null}) + }) + } + }) + }) + + req.on('error', (e) => { + console.error('error in parsing URL', e) + reject({result: false, error: e}) + }) + req.end() + }) +} + +module.exports = createNoteFromUrl diff --git a/browser/main/lib/dataApi/deleteFolder.js b/browser/main/lib/dataApi/deleteFolder.js index 0c7486f5..5ccc1414 100644 --- a/browser/main/lib/dataApi/deleteFolder.js +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -3,7 +3,6 @@ const path = require('path') const resolveStorageData = require('./resolveStorageData') const resolveStorageNotes = require('./resolveStorageNotes') const CSON = require('@rokt33r/season') -const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') const deleteSingleNote = require('./deleteNote') diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 75c451c1..42e1fa56 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -43,7 +43,7 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt ) if (outputFormatter) { - exportedData = outputFormatter(exportedData, exportTasks, path.dirname(targetPath)) + exportedData = outputFormatter(exportedData, exportTasks, targetPath) } else { exportedData = Promise.resolve(exportedData) } diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 92be6b93..6e88bbf9 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -11,6 +11,7 @@ const dataApi = { exportFolder: require('./exportFolder'), exportStorage: require('./exportStorage'), createNote: require('./createNote'), + createNoteFromUrl: require('./createNoteFromUrl'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), moveNote: require('./moveNote'), diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index 2d306cdf..c38968cb 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -1,7 +1,6 @@ const resolveStorageData = require('./resolveStorageData') const _ = require('lodash') const path = require('path') -const fs = require('fs') const CSON = require('@rokt33r/season') const keygen = require('browser/lib/keygen') const sander = require('sander') diff --git a/browser/main/modals/CreateMarkdownFromURLModal.js b/browser/main/modals/CreateMarkdownFromURLModal.js new file mode 100644 index 00000000..31988059 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.js @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './CreateMarkdownFromURLModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' + +class CreateMarkdownFromURLModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + name: '', + showerror: false, + errormessage: '' + } + } + + componentDidMount () { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handleChange (e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown (e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + showError (message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + hideError () { + this.setState({ + showerror: false, + errormessage: '' + }) + } + + confirm () { + this.hideError() + const { storage, folder, dispatch, location } = this.props + + dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => { + this.props.close() + }).catch((result) => { + this.showError(result.error) + }) + } + + render () { + return ( +
this.handleKeyDown(e)} + > +
+
{i18n.__('Import Markdown From URL')}
+
+ this.handleCloseButtonClick(e)} /> +
+
+
{i18n.__('Insert URL Here')}
+ this.handleChange(e)} + onKeyDown={(e) => this.handleInputKeyDown(e)} + /> +
+ +
{this.state.errormessage}
+
+
+ ) + } +} + +CreateMarkdownFromURLModal.propTypes = { + storage: PropTypes.string, + folder: PropTypes.string, + dispatch: PropTypes.func, + location: PropTypes.shape({ + pathname: PropTypes.string + }) +} + +export default CSSModules(CreateMarkdownFromURLModal, styles) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl new file mode 100644 index 00000000..5e59e465 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -0,0 +1,160 @@ +.root + modal() + width 500px + height 270px + overflow hidden + position relative + +.header + height 80px + margin-bottom 10px + margin-top 20px + font-size 18px + line-height 50px + background-color $ui-backgroundColor + color $ui-text-color + +.title + font-size 36px + font-weight 600 + +.control-folder-label + text-align left + font-size 14px + color $ui-text-color + +.control-folder-input + display block + height 40px + width 490px + padding 0 5px + margin 10px 0 + border 1px solid $ui-input--create-folder-modal + border-radius 2px + background-color transparent + outline none + vertical-align middle + font-size 16px + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 35px + width 140px + border none + border-radius 2px + padding 0 25px + margin 20px auto + font-size 14px + colorPrimaryButton() + +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 + + .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-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() diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a17a36cd..476fa252 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -3,6 +3,8 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' +import { openModal } from 'browser/main/lib/modal' +import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' import queryString from 'query-string' @@ -21,6 +23,18 @@ class NewNoteModal extends React.Component { this.props.close() } + handleCreateMarkdownFromUrlClick (e) { + this.props.close() + + const { storage, folder, dispatch, location } = this.props + openModal(CreateMarkdownFromURLModal, { + storage: storage, + folder: folder, + dispatch, + location + }) + } + handleMarkdownNoteButtonClick (e) { const { storage, folder, dispatch, location, config } = this.props const params = location.search !== '' && queryString.parse(location.search) @@ -115,10 +129,8 @@ class NewNoteModal extends React.Component { -
- {i18n.__('Tab to switch format')} -
- +
{i18n.__('Tab to switch format')}
+
this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL
) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index 662f3f69..ff0052bd 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -48,6 +48,12 @@ text-align center margin-bottom 25px +.from-url + color $ui-inactive-text-color + text-align center + margin-bottom 25px + cursor pointer + body[data-theme="dark"] .root modalDark() @@ -62,7 +68,7 @@ body[data-theme="dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-inactive-text-color body[data-theme="solarized-dark"] @@ -79,7 +85,7 @@ body[data-theme="solarized-dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-solarized-dark-text-color body[data-theme="monokai"] @@ -96,7 +102,7 @@ body[data-theme="monokai"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-monokai-text-color body[data-theme="dracula"] diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 2c528fba..9c4f5655 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -76,13 +76,16 @@ class HotkeyTab extends React.Component { handleHotkeyChange (e) { const { config } = this.state - config.hotkey = { + config.hotkey = Object.assign({}, config.hotkey, { toggleMain: this.refs.toggleMain.value, toggleMode: this.refs.toggleMode.value, deleteNote: this.refs.deleteNote.value, pasteSmartly: this.refs.pasteSmartly.value, - toggleMenuBar: this.refs.toggleMenuBar.value - } + prettifyMarkdown: this.refs.prettifyMarkdown.value, + toggleMenuBar: this.refs.toggleMenuBar.value, + insertDate: this.refs.insertDate.value, + insertDateTime: this.refs.insertDateTime.value + }) this.setState({ config }) @@ -173,10 +176,21 @@ class HotkeyTab extends React.Component { /> +
+
{i18n.__('Prettify Markdown')}
+
+ this.handleHotkeyChange(e)} + ref='prettifyMarkdown' + value={config.hotkey.prettifyMarkdown} + type='text' /> +
+
{i18n.__('Insert Current Date')}
{i18n.__('Insert Current Date and Time')}
{ + const promise = attachmentManagement.getAttachmentsPathAndStatus( + note.content, + note.storage, + note.key + ) + if (promise) promises.push(promise) + }) + + Promise.all(promises) + .then(data => { + const result = data.reduce((acc, curr) => acc.concat(curr), []) + this.setState({attachments: result}) + }) + .catch(console.error) } handleAddStorageButton (e) { @@ -57,8 +81,39 @@ class StoragesTab extends React.Component { e.preventDefault() } + handleRemoveUnusedAttachments (attachments) { + attachmentManagement.removeAttachmentsByPaths(attachments) + .then(() => this.loadAttachmentStorage()) + .catch(console.error) + } + renderList () { const { data, boundingBox } = this.props + const { attachments } = this.state + + const unusedAttachments = attachments.filter(attachment => !attachment.isInUse) + const inUseAttachments = attachments.filter(attachment => attachment.isInUse) + + const totalUnusedAttachments = unusedAttachments.length + const totalInuseAttachments = inUseAttachments.length + const totalAttachments = totalUnusedAttachments + totalInuseAttachments + + const totalUnusedAttachmentsSize = unusedAttachments + .reduce((acc, curr) => { + const stats = fs.statSync(curr.path) + const fileSizeInBytes = stats.size + return acc + fileSizeInBytes + }, 0) + const totalInuseAttachmentsSize = inUseAttachments + .reduce((acc, curr) => { + const stats = fs.statSync(curr.path) + const fileSizeInBytes = stats.size + return acc + fileSizeInBytes + }, 0) + const totalAttachmentsSize = totalUnusedAttachmentsSize + totalInuseAttachmentsSize + + const unusedAttachmentPaths = unusedAttachments + .reduce((acc, curr) => acc.concat(curr.path), []) if (!boundingBox) { return null } const storageList = data.storageMap.map((storage) => { @@ -82,6 +137,20 @@ class StoragesTab extends React.Component { {i18n.__('Add Storage Location')}
+
{i18n.__('Attachment storage')}
+

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

+

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

+

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

+
) } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index b63cc85e..fbfa89e6 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -33,6 +33,17 @@ colorDefaultButton() font-size $tab--button-font-size border-radius 2px +.list-attachment-label + margin-bottom 10px + color $ui-text-color +.list-attachement-clear-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle + padding 0 20px .addStorage margin-bottom 15px @@ -154,8 +165,8 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor - - + .list-attachement-clear-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .root @@ -194,6 +205,8 @@ body[data-theme="solarized-dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-solarized-dark-borderColor + .list-attachement-clear-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .root @@ -232,6 +245,8 @@ body[data-theme="monokai"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-monokai-borderColor + .list-attachement-clear-button + colorMonokaiPrimaryButton() body[data-theme="dracula"] .root @@ -269,4 +284,6 @@ body[data-theme="dracula"] colorDarkPrimaryButton() .addStorage-body-control-cancelButton colorDarkDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file + border-color $ui-dracula-borderColor + .list-attachement-clear-button + colorDraculaPrimaryButton() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index fc09a37f..329dbfa4 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -14,7 +14,6 @@ import { getLanguages } from 'browser/lib/Languages' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const OSX = global.process.platform === 'darwin' -const WIN = global.process.platform === 'win32' const electron = require('electron') const ipc = electron.ipcRenderer @@ -32,8 +31,12 @@ class UiTab extends React.Component { CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css') CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript') + CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript') + // Set CM editor Sizes this.customCSSCM.getCodeMirror().setSize('400px', '400px') + this.prettierConfigCM.getCodeMirror().setSize('400px', '400px') this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px') + this.handleSettingDone = () => { this.setState({UiAlert: { type: 'success', @@ -107,7 +110,9 @@ 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 }, preview: { fontSize: this.refs.previewFontSize.value, @@ -613,6 +618,16 @@ class UiTab extends React.Component { {i18n.__('Enable spellcheck - Experimental feature!! :)')} +
+ +
@@ -915,7 +930,27 @@ class UiTab extends React.Component {
- +
+
+ {i18n.__('Prettier Config')} +
+
+
+ this.handleUIChange(e)} + ref={e => (this.prettierConfigCM = e)} + value={config.editor.prettierConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + lint: true, + theme: codemirrorTheme + }} /> +
+
+