diff --git a/.gitignore b/.gitignore index 9f75dd1b..ace5316c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ node_modules/* /compiled /secret *.log -.vscode -.idea \ No newline at end of file +.idea +.vscode \ No newline at end of file diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index b5e06a66..28fc9b1f 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types' import React from 'react' import _ from 'lodash' import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' import path from 'path' import copyImage from 'browser/main/lib/dataApi/copyImage' import { findStorage } from 'browser/lib/findStorage' @@ -249,7 +250,7 @@ export default class CodeEditor extends React.Component { render () { const { className, fontSize } = this.props - let fontFamily = this.props.className + let fontFamily = this.props.fontFamily fontFamily = _.isString(fontFamily) && fontFamily.length > 0 ? [fontFamily].concat(defaultEditorFontFamily) : defaultEditorFontFamily diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index c2693312..c1be9ef1 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -3,11 +3,13 @@ import React from 'react' import markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' import consts from 'browser/lib/consts' import Raphael from 'raphael' import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' +import fs from 'fs' import htmlTextHelper from 'browser/lib/htmlTextHelper' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' @@ -115,6 +117,9 @@ export default class MarkdownPreview extends React.Component { this.mouseUpHandler = (e) => this.handleMouseUp(e) this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) + this.saveAsTextHandler = () => this.handleSaveAsText() + this.saveAsMdHandler = () => this.handleSaveAsMd() + this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) @@ -141,10 +146,12 @@ export default class MarkdownPreview extends React.Component { } handleContextMenu (e) { + if (!this.props.onContextMenu) return this.props.onContextMenu(e) } handleMouseDown (e) { + if (!this.props.onMouseDown) return if (e.target != null) { switch (e.target.tagName) { case 'A': @@ -156,16 +163,50 @@ export default class MarkdownPreview extends React.Component { } handleMouseUp (e) { + if (!this.props.onMouseUp) return if (e.target != null && e.target.tagName === 'A') { return null } if (this.props.onMouseUp != null) this.props.onMouseUp(e) } + handleSaveAsText () { + this.exportAsDocument('txt') + } + + handleSaveAsMd () { + this.exportAsDocument('md') + } + + handleSaveAsHtml () { + this.exportAsDocument('html', (value) => { + return this.refs.root.contentWindow.document.documentElement.outerHTML + }) + } + handlePrint () { this.refs.root.contentWindow.print() } + exportAsDocument (fileType, formatter) { + const options = { + filters: [ + { name: 'Documents', extensions: [fileType] } + ], + properties: ['openFile', 'createDirectory'] + } + const value = formatter ? formatter.call(this, this.props.value) : this.props.value + + dialog.showSaveDialog(remote.getCurrentWindow(), options, + (filename) => { + if (filename) { + fs.writeFile(filename, value, (err) => { + if (err) throw err + }) + } + }) + } + fixDecodedURI (node) { if (node && node.children.length === 1 && typeof node.children[0] === 'string') { const { innerText, href } = node @@ -193,6 +234,9 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) + eventEmitter.on('export:save-text', this.saveAsTextHandler) + eventEmitter.on('export:save-md', this.saveAsMdHandler) + eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('print', this.printHandler) } @@ -202,6 +246,9 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) + eventEmitter.off('export:save-text', this.saveAsTextHandler) + eventEmitter.off('export:save-md', this.saveAsMdHandler) + eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('print', this.printHandler) } diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js new file mode 100644 index 00000000..0ea41a44 --- /dev/null +++ b/browser/components/MarkdownSplitEditor.js @@ -0,0 +1,93 @@ +import React from 'react' +import CodeEditor from 'browser/components/CodeEditor' +import MarkdownPreview from 'browser/components/MarkdownPreview' +import { findStorage } from 'browser/lib/findStorage' + +import styles from './MarkdownSplitEditor.styl' +import CSSModules from 'browser/lib/CSSModules' + +class MarkdownSplitEditor extends React.Component { + constructor (props) { + super(props) + this.value = props.value + this.focus = () => this.refs.code.focus() + this.reload = () => this.refs.code.reload() + } + + handleOnChange () { + this.value = this.refs.code.value + this.props.onChange() + } + + handleCheckboxClick (e) { + e.preventDefault() + e.stopPropagation() + const idMatch = /checkbox-([0-9]+)/ + const checkedMatch = /\[x\]/i + const uncheckedMatch = /\[ \]/ + 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 targetLine = lines[lineIndex] + + if (targetLine.match(checkedMatch)) { + lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]') + } + if (targetLine.match(uncheckedMatch)) { + lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]') + } + this.refs.code.setValue(lines.join('\n')) + } + } + + render () { + const { config, value, storageKey } = this.props + const storage = findStorage(storageKey) + let editorFontSize = parseInt(config.editor.fontSize, 10) + if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 + let editorIndentSize = parseInt(config.editor.indentSize, 10) + if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 + const previewStyle = {} + if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' + return ( +
+ + this.handleCheckboxClick(e)} + showCopyNotification={config.ui.showCopyNotification} + storagePath={storage.path} + /> +
+ ) + } +} + +export default CSSModules(MarkdownSplitEditor, styles) diff --git a/browser/components/MarkdownSplitEditor.styl b/browser/components/MarkdownSplitEditor.styl new file mode 100644 index 00000000..c9afd22f --- /dev/null +++ b/browser/components/MarkdownSplitEditor.styl @@ -0,0 +1,9 @@ +.root + width 100% + height 100% + font-size 30px + display flex + .codeEditor + width 50% + .preview + width 50% diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 53e93574..5ff05941 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -120,6 +120,7 @@ hr margin 15px 0 h1, h2, h3, h4, h5, h6 font-weight bold + word-wrap break-word h1 font-size 2.55em padding-bottom 0.3em @@ -157,6 +158,7 @@ p line-height 1.6em margin 0 0 1em white-space pre-line + word-wrap break-word img max-width 100% strong, b @@ -334,8 +336,29 @@ body[data-theme="dark"] background-color themeDarkBorder color themeDarkText +themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor +themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) +themeSolarizedDarkTableHead = themeSolarizedDarkTableEven +themeSolarizedDarkTableBorder = themeDarkBorder body[data-theme="solarized-dark"] color $ui-solarized-dark-text-color border-color themeDarkBorder - background-color $ui-solarized-dark-noteDetail-backgroundColor \ No newline at end of file + background-color $ui-solarized-dark-noteDetail-backgroundColor + table + thead + tr + background-color themeSolarizedDarkTableHead + th + border-color themeSolarizedDarkTableBorder + &:last-child + border-right solid 1px themeSolarizedDarkTableBorder + tbody + tr:nth-child(2n + 1) + background-color themeSolarizedDarkTableOdd + tr:nth-child(2n) + background-color themeSolarizedDarkTableEven + td + border-color themeSolarizedDarkTableBorder + &:last-child + border-right solid 1px themeSolarizedDarkTableBorder diff --git a/browser/finder/NoteDetail.js b/browser/finder/NoteDetail.js index 4c59ea30..3b9121d7 100644 --- a/browser/finder/NoteDetail.js +++ b/browser/finder/NoteDetail.js @@ -5,6 +5,7 @@ import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownEditor from 'browser/components/MarkdownEditor' import CodeEditor from 'browser/components/CodeEditor' import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' import { findStorage } from 'browser/lib/findStorage' const electron = require('electron') diff --git a/browser/lib/customMeta.js b/browser/lib/customMeta.js index 855e00af..0d4ee1e3 100644 --- a/browser/lib/customMeta.js +++ b/browser/lib/customMeta.js @@ -1,3 +1,5 @@ import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']}) +CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']}) diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js new file mode 100644 index 00000000..a0e8c790 --- /dev/null +++ b/browser/main/Detail/FullscreenButton.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './FullscreenButton.styl' + +const FullscreenButton = ({ + onClick +}) => ( + +) + +FullscreenButton.propTypes = { + onClick: PropTypes.func.isRequired +} + +export default CSSModules(FullscreenButton, styles) diff --git a/browser/main/Detail/FullscreenButton.styl b/browser/main/Detail/FullscreenButton.styl new file mode 100644 index 00000000..7cee4faa --- /dev/null +++ b/browser/main/Detail/FullscreenButton.styl @@ -0,0 +1,22 @@ +.control-fullScreenButton + top 80px + topBarButtonRight() + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 26px + right 0 + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +body[data-theme="dark"] + .control-fullScreenButton + topBarButtonDark() \ No newline at end of file diff --git a/browser/main/Detail/InfoButton.js b/browser/main/Detail/InfoButton.js index 02d8dfe4..8a78ab4e 100644 --- a/browser/main/Detail/InfoButton.js +++ b/browser/main/Detail/InfoButton.js @@ -10,6 +10,7 @@ const InfoButton = ({ onClick={(e) => onClick(e)} > + Info ) diff --git a/browser/main/Detail/InfoButton.styl b/browser/main/Detail/InfoButton.styl index 9a2604df..a1c302f0 100644 --- a/browser/main/Detail/InfoButton.styl +++ b/browser/main/Detail/InfoButton.styl @@ -1,6 +1,21 @@ .control-infoButton top 10px topBarButtonRight() + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 26px + right 0 + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s .infoButton padding 0px diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index e718fbf3..a5202e71 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './InfoPanel.styl' const InfoPanel = ({ - storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print + storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print }) => (
@@ -57,17 +57,22 @@ const InfoPanel = ({
+ +
@@ -82,6 +87,7 @@ InfoPanel.propTypes = { createdAt: PropTypes.string.isRequired, exportAsMd: PropTypes.func.isRequired, exportAsTxt: PropTypes.func.isRequired, + exportAsHtml: PropTypes.func.isRequired, wordCount: PropTypes.number, letterCount: PropTypes.number, type: PropTypes.string.isRequired, diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl index a115d108..72b07c87 100644 --- a/browser/main/Detail/InfoPanel.styl +++ b/browser/main/Detail/InfoPanel.styl @@ -41,12 +41,12 @@ box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1) border-radius 2px -.count-wrap +.count-wrap display flex position relative width 100% -.count-number +.count-number position relative display block width 50% @@ -81,15 +81,15 @@ margin-bottom 6px .infoPanel-trash - color #EA4447 - font-weight 600 + color #EA4447 + font-weight 600 font-size 14px width 70px background-color rgba(226,33,113,0.1) - border none + border none outline none - border-radius 2px - margin-right 5px + border-radius 2px + margin-right 5px padding 2px 5px [id=export-wrap] @@ -160,4 +160,44 @@ body[data-theme="dark"] p color $ui-dark-inactive-text-color &:hover - color $ui-dark-text-color \ No newline at end of file + color $ui-dark-text-color + +body[data-theme="solarized-dark"] + .control-infoButton-panel + background-color $ui-solarized-dark-noteList-backgroundColor + + .control-infoButton-panel-trash + background-color $ui-solarized-ark-noteList-backgroundColor + + .modification-date + color $ui-solarized-ark-text-color + + .modification-date-desc + color $ui-inactive-text-color + + .infoPanel-defaul-count + color $ui-solarized-dark-text-color + + .infoPanel-sub-count + color $ui-inactive-text-color + + .infoPanel-default + color $ui-solarized-ark-text-color + + .infoPanel-sub + color $ui-inactive-text-color + + .infoPanel-noteLink + background-color alpha($ui-solarized-dark-borderColor, 20%) + color $ui-solarized-dark-text-color + + [id=export-wrap] + button + color $ui-dark-inactive-text-color + &:hover + background-color alpha($ui-solarized-dark-borderColor, 20%) + color $ui-solarized-ark-text-color + p + color $ui-dark-inactive-text-color + &:hover + color $ui-solarized-ark-text-color diff --git a/browser/main/Detail/InfoPanelTrashed.js b/browser/main/Detail/InfoPanelTrashed.js index 77ecea22..6e86b884 100644 --- a/browser/main/Detail/InfoPanelTrashed.js +++ b/browser/main/Detail/InfoPanelTrashed.js @@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './InfoPanel.styl' const InfoPanelTrashed = ({ - storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt + storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml }) => (
@@ -31,17 +31,22 @@ const InfoPanelTrashed = ({
+ +
@@ -54,7 +59,8 @@ InfoPanelTrashed.propTypes = { updatedAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired, exportAsMd: PropTypes.func.isRequired, - exportAsTxt: PropTypes.func.isRequired + exportAsTxt: PropTypes.func.isRequired, + exportAsHtml: PropTypes.func.isRequired } export default CSSModules(InfoPanelTrashed, styles) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 956efb63..15c584d4 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -3,6 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './MarkdownNoteDetail.styl' import MarkdownEditor from 'browser/components/MarkdownEditor' +import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor' import TodoListPercentage from 'browser/components/TodoListPercentage' import StarButton from './StarButton' import TagSelect from './TagSelect' @@ -15,15 +16,17 @@ import StatusBar from '../StatusBar' import _ from 'lodash' import { findNoteTitle } from 'browser/lib/findNoteTitle' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' +import ConfigManager from 'browser/main/lib/ConfigManager' import TrashButton from './TrashButton' +import FullscreenButton from './FullscreenButton' import PermanentDeleteButton from './PermanentDeleteButton' import InfoButton from './InfoButton' +import ToggleModeButton from './ToggleModeButton' import InfoPanel from './InfoPanel' import InfoPanelTrashed from './InfoPanelTrashed' import { formatDate } from 'browser/lib/date-formatter' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' -import exportNote from 'browser/main/lib/dataApi/exportNote' const electron = require('electron') const { remote } = electron @@ -40,13 +43,12 @@ class MarkdownNoteDetail extends React.Component { content: '' }, props.note), isLockButtonShown: false, - isLocked: false + isLocked: false, + editorType: props.config.editor.type } this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) - this.saveAsText = this.handleSaveAsText.bind(this) - this.saveAsMd = this.handleSaveAsMd.bind(this) } focus () { @@ -55,8 +57,6 @@ class MarkdownNoteDetail extends React.Component { componentDidMount () { ee.on('topbar:togglelockbutton', this.toggleLockButton) - ee.on('export:save-text', this.saveAsText) - ee.on('export:save-md', this.saveAsMd) } componentWillReceiveProps (nextProps) { @@ -77,21 +77,24 @@ class MarkdownNoteDetail extends React.Component { componentDidUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) - ee.off('export:save-text', this.saveAsTextHandler) - ee.off('export:save-md', this.saveAsMdHandler) } - handleChange (e) { + handleUpdateTag () { const { note } = this.state - - note.content = this.refs.content.value if (this.refs.tags) note.tags = this.refs.tags.value - note.title = markdown.strip(striptags(findNoteTitle(note.content))) - note.updatedAt = new Date() + this.updateNote(note) + } - this.setState({ - note - }, () => { + handleUpdateContent () { + const { note } = this.state + note.content = this.refs.content.value + note.title = markdown.strip(striptags(findNoteTitle(note.content))) + this.updateNote(note) + } + + updateNote (note) { + note.updatedAt = new Date() + this.setState({note}, () => { this.save() }) } @@ -177,28 +180,8 @@ class MarkdownNoteDetail extends React.Component { ee.emit('export:save-text') } - exportAsDocument (fileType) { - const options = { - filters: [ - { name: 'Documents', extensions: [fileType] } - ], - properties: ['openFile', 'createDirectory'] - } - - dialog.showSaveDialog(remote.getCurrentWindow(), options, - (filename) => { - if (filename) { - const note = this.props.note - - exportNote(note.storage, note.content, filename) - .then((res) => { - dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`}) - }).catch((err) => { - dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export') - throw err - }) - } - }) + exportAsHtml () { + ee.emit('export:save-html') } handleTrashButtonClick (e) { @@ -238,14 +221,6 @@ class MarkdownNoteDetail extends React.Component { ee.emit('list:next') } - handleSaveAsText () { - this.exportAsDocument('txt') - } - - handleSaveAsMd () { - this.exportAsDocument('md') - } - handleUndoButtonClick (e) { const { note } = this.state @@ -272,7 +247,7 @@ class MarkdownNoteDetail extends React.Component { } getToggleLockButton () { - return this.state.isLocked ? '../resources/icon/icon-edit-lock.svg' : '../resources/icon/icon-edit.svg' + return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg' } handleDeleteKeyDown (e) { @@ -301,9 +276,42 @@ class MarkdownNoteDetail extends React.Component { ee.emit('print') } - render () { - const { data, config, location } = this.props + handleSwitchMode (type) { + this.setState({ editorType: type }, () => { + const newConfig = Object.assign({}, this.props.config) + newConfig.editor.type = type + ConfigManager.set(newConfig) + }) + } + + renderEditor () { + const { config, ignorePreviewPointerEvents } = this.props const { note } = this.state + if (this.state.editorType === 'EDITOR_PREVIEW') { + return + } else { + return + } + } + + render () { + const { data, location } = this.props + const { note, editorType } = this.state const storageKey = note.storage const folderKey = note.folder @@ -335,6 +343,7 @@ class MarkdownNoteDetail extends React.Component { folderName={currentOption.folder.name} updatedAt={formatDate(note.updatedAt)} createdAt={formatDate(note.createdAt)} + exportAsHtml={this.exportAsHtml} exportAsMd={this.exportAsMd} exportAsTxt={this.exportAsTxt} /> @@ -355,21 +364,12 @@ class MarkdownNoteDetail extends React.Component { this.handleChange(e)} + onChange={this.handleUpdateTag.bind(this)} /> -
-
- -
-
- -
-
+ this.handleSwitchMode(e)} editorType={editorType} /> - +
this.handleFullScreenButton(e)} - > - - + this.handleFullScreenButton(e)} /> this.handleTrashButtonClick(e)} /> @@ -412,6 +408,7 @@ class MarkdownNoteDetail extends React.Component { createdAt={formatDate(note.createdAt)} exportAsMd={this.exportAsMd} exportAsTxt={this.exportAsTxt} + exportAsHtml={this.exportAsHtml} wordCount={note.content.split(' ').length} letterCount={note.content.replace(/\r?\n/g, '').length} type={note.type} @@ -429,15 +426,7 @@ class MarkdownNoteDetail extends React.Component { {location.pathname === '/trashed' ? trashTopBar : detailTopBar}
- this.handleChange(e)} - ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents} - /> + {this.renderEditor()}
onClick(e)} > + Permanent Delete ) diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index bec6573b..218b4f87 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -11,6 +11,7 @@ import dataApi from 'browser/main/lib/dataApi' import { hashHistory } from 'react-router' import ee from 'browser/main/lib/eventEmitter' import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' import SnippetTab from 'browser/components/SnippetTab' import StatusBar from '../StatusBar' import context from 'browser/lib/context' @@ -380,7 +381,7 @@ class SnippetNoteDetail extends React.Component { handleModeButtonClick (e, index) { const menu = new Menu() - CodeMirror.modeInfo.forEach((mode) => { + CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => { menu.append(new MenuItem({ label: mode.name, click: (e) => this.handleModeOptionClick(index, mode.name)(e) @@ -603,6 +604,7 @@ class SnippetNoteDetail extends React.Component { createdAt={formatDate(note.createdAt)} exportAsMd={this.showWarning} exportAsTxt={this.showWarning} + exportAsHtml={this.showWarning} />
@@ -634,7 +636,7 @@ class SnippetNoteDetail extends React.Component { isActive={note.isStarred} /> - diff --git a/browser/main/Detail/StarButton.js b/browser/main/Detail/StarButton.js index 0616a1e0..57ba79c8 100644 --- a/browser/main/Detail/StarButton.js +++ b/browser/main/Detail/StarButton.js @@ -46,14 +46,14 @@ class StarButton extends React.Component { onMouseDown={(e) => this.handleMouseDown(e)} onMouseUp={(e) => this.handleMouseUp(e)} onMouseLeave={(e) => this.handleMouseLeave(e)} - onClick={this.props.onClick} - > + onClick={this.props.onClick}> + Star ) } diff --git a/browser/main/Detail/StarButton.styl b/browser/main/Detail/StarButton.styl index 1e5bf239..647f3f23 100644 --- a/browser/main/Detail/StarButton.styl +++ b/browser/main/Detail/StarButton.styl @@ -4,6 +4,22 @@ &: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 26px + right 0 + width 100% + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s .root--active @extend .root diff --git a/browser/main/Detail/ToggleModeButton.js b/browser/main/Detail/ToggleModeButton.js new file mode 100644 index 00000000..5a78cc51 --- /dev/null +++ b/browser/main/Detail/ToggleModeButton.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ToggleModeButton.styl' + +const ToggleModeButton = ({ + onClick, editorType +}) => ( +
+
onClick('SPLIT')}> + +
+
onClick('EDITOR_PREVIEW')}> + +
+ Toggle Mode +
+) + +ToggleModeButton.propTypes = { + onClick: PropTypes.func.isRequired, + editorType: PropTypes.string.Required +} + +export default CSSModules(ToggleModeButton, styles) diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl new file mode 100644 index 00000000..b9d3cc6c --- /dev/null +++ b/browser/main/Detail/ToggleModeButton.styl @@ -0,0 +1,61 @@ +.control-toggleModeButton + border 1px solid #eee + height 34px + display flex + align-items center + + div + width 40px + height 100% + background-color #f9f9f9 + display flex + align-items center + justify-content center + cursor pointer + + &:first-child + border-right 1px solid #eee + .active + background-color #fff + box-shadow 2px 0px 7px #eee + z-index 1 + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 47px + right 11px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +body[data-theme="dark"] + .control-fullScreenButton + topBarButtonDark() + + .control-toggleModeButton + border 1px solid #444444 + div + background-color $ui-dark-noteDetail-backgroundColor + &:first-child + border-right 1px solid #444444 + .active + background-color #3A404C + box-shadow 2px 0px 7px #444444 + +body[data-theme="solarized-dark"] + .control-toggleModeButton + border 1px solid #586E75 + div + background-color $ui-solarized-dark-noteDetail-backgroundColor + &:first-child + border-right 1px solid #586E75 + .active + background-color #002B36 + box-shadow 2px 0px 7px #222222 \ No newline at end of file diff --git a/browser/main/Detail/TrashButton.js b/browser/main/Detail/TrashButton.js index bfaafe1e..474eb9e5 100644 --- a/browser/main/Detail/TrashButton.js +++ b/browser/main/Detail/TrashButton.js @@ -10,6 +10,7 @@ const TrashButton = ({ onClick={(e) => onClick(e)} > + Trash ) diff --git a/browser/main/Detail/TrashButton.styl b/browser/main/Detail/TrashButton.styl index 0acd60a5..455d36a6 100644 --- a/browser/main/Detail/TrashButton.styl +++ b/browser/main/Detail/TrashButton.styl @@ -1,6 +1,21 @@ .control-trashButton top 115px topBarButtonRight() + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 26px + right 0 + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s .control-trashButton--in-trash top 60px diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index b54b0661..083eb75f 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -235,18 +235,8 @@ class NoteList extends React.Component { return } - const { router } = this.context - const { location } = this.props - - let targetIndex = this.getTargetIndex() - - if (targetIndex < 0) targetIndex = 0 - - const selectedNoteKeys = [] - const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex) - selectedNoteKeys.push(nextNoteKey) - - this.focusNote(selectedNoteKeys, nextNoteKey) + const selectedNoteKeys = [noteHash] + this.focusNote(selectedNoteKeys, noteHash) ee.emit('list:moved') } diff --git a/browser/main/SideNav/ListButton.js b/browser/main/SideNav/ListButton.js new file mode 100644 index 00000000..1365c4cb --- /dev/null +++ b/browser/main/SideNav/ListButton.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './SwitchButton.styl' + +const ListButton = ({ + onClick, isTagActive +}) => ( + +) + +ListButton.propTypes = { + onClick: PropTypes.func.isRequired, + isTagActive: PropTypes.bool.isRequired +} + +export default CSSModules(ListButton, styles) diff --git a/browser/main/SideNav/PreferenceButton.js b/browser/main/SideNav/PreferenceButton.js new file mode 100644 index 00000000..9f483a28 --- /dev/null +++ b/browser/main/SideNav/PreferenceButton.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './PreferenceButton.styl' + +const PreferenceButton = ({ + onClick +}) => ( + +) + +PreferenceButton.propTypes = { + onClick: PropTypes.func.isRequired +} + +export default CSSModules(PreferenceButton, styles) diff --git a/browser/main/SideNav/PreferenceButton.styl b/browser/main/SideNav/PreferenceButton.styl new file mode 100644 index 00000000..97a48982 --- /dev/null +++ b/browser/main/SideNav/PreferenceButton.styl @@ -0,0 +1,51 @@ +.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 \ No newline at end of file diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 474b1af3..a0ffb2e7 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -11,19 +11,6 @@ .top padding-bottom 15px -.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 - &:active, &:active:hover - color $ui-button-default--active-backgroundColor - .switch-buttons background-color transparent border 0 @@ -31,21 +18,7 @@ display flex text-align center -.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; - &:hover - color alpha(#239F86, 60%) -.active-button - @extend .non-active-button - color $ui-button-default--active-backgroundColor .top-menu-label margin-left 5px @@ -109,33 +82,6 @@ body[data-theme="white"] background-color #f9f9f9 color $ui-text-color - .top-menu-preference - navWhiteButtonColor() - background-color transparent - &:hover - color #0B99F1 - background-color transparent - &:active, &:active:hover - color #0B99F1 - background-color transparent - - .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"] .root, .root--folded border-right 1px solid $ui-dark-borderColor @@ -145,25 +91,6 @@ body[data-theme="dark"] .top border-color $ui-dark-borderColor - .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 - - .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%) - body[data-theme="solarized-dark"] .root, .root--folded background-color $ui-solarized-dark-backgroundColor diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index 4379a76c..bbf87306 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -10,6 +10,7 @@ import dataApi from 'browser/main/lib/dataApi' import StorageItemChild from 'browser/components/StorageItem' import eventEmitter from 'browser/main/lib/eventEmitter' import _ from 'lodash' +import * as path from 'path' const { remote } = require('electron') const { Menu, MenuItem, dialog } = remote @@ -24,18 +25,20 @@ class StorageItem extends React.Component { } handleHeaderContextMenu (e) { - const menu = new Menu() - menu.append(new MenuItem({ - label: 'Add Folder', - click: (e) => this.handleAddFolderButtonClick(e) - })) - menu.append(new MenuItem({ - type: 'separator' - })) - menu.append(new MenuItem({ - label: 'Unlink Storage', - click: (e) => this.handleUnlinkStorageClick(e) - })) + const menu = Menu.buildFromTemplate([ + { + label: 'Add Folder', + click: (e) => this.handleAddFolderButtonClick(e) + }, + { + type: 'separator' + }, + { + label: 'Unlink Storage', + click: (e) => this.handleUnlinkStorageClick(e) + } + ]) + menu.popup() } @@ -89,18 +92,36 @@ class StorageItem extends React.Component { } handleFolderButtonContextMenu (e, folder) { - const menu = new Menu() - menu.append(new MenuItem({ - label: 'Rename Folder', - click: (e) => this.handleRenameFolderClick(e, folder) - })) - menu.append(new MenuItem({ - type: 'separator' - })) - menu.append(new MenuItem({ - label: 'Delete Folder', - click: (e) => this.handleFolderDeleteClick(e, folder) - })) + const menu = Menu.buildFromTemplate([ + { + label: 'Rename Folder', + click: (e) => this.handleRenameFolderClick(e, folder) + }, + { + type: 'separator' + }, + { + label: 'Export Folder', + submenu: [ + { + label: 'Export as txt', + click: (e) => this.handleExportFolderClick(e, folder, 'txt') + }, + { + label: 'Export as md', + click: (e) => this.handleExportFolderClick(e, folder, 'md') + } + ] + }, + { + type: 'separator' + }, + { + label: 'Delete Folder', + click: (e) => this.handleFolderDeleteClick(e, folder) + } + ]) + menu.popup() } @@ -112,6 +133,31 @@ class StorageItem extends React.Component { }) } + handleExportFolderClick (e, folder, fileType) { + const options = { + properties: ['openDirectory', 'createDirectory'], + buttonLabel: 'Select directory', + title: '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 + .exportFolder(storage.key, folder.key, fileType, paths[0]) + .then((data) => { + dispatch({ + type: 'EXPORT_FOLDER', + storage: data.storage, + folderKey: data.folderKey, + fileType: data.fileType + }) + }) + } + }) + } + handleFolderDeleteClick (e, folder) { const index = dialog.showMessageBox(remote.getCurrentWindow(), { type: 'warning', diff --git a/browser/main/SideNav/SwitchButton.styl b/browser/main/SideNav/SwitchButton.styl new file mode 100644 index 00000000..54e21b03 --- /dev/null +++ b/browser/main/SideNav/SwitchButton.styl @@ -0,0 +1,59 @@ +.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 + +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 new file mode 100644 index 00000000..87d92c49 --- /dev/null +++ b/browser/main/SideNav/TagButton.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './SwitchButton.styl' + +const TagButton = ({ + onClick, isTagActive +}) => ( + +) + +TagButton.propTypes = { + onClick: PropTypes.func.isRequired, + isTagActive: PropTypes.bool.isRequired +} + +export default CSSModules(TagButton, styles) diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 05361ea4..4c162f1e 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -11,6 +11,9 @@ import SideNavFilter from 'browser/components/SideNavFilter' import StorageList from 'browser/components/StorageList' import NavToggleButton from 'browser/components/NavToggleButton' import EventEmitter from 'browser/main/lib/eventEmitter' +import PreferenceButton from './PreferenceButton' +import ListButton from './ListButton' +import TagButton from './TagButton' class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 @@ -162,27 +165,11 @@ class SideNav extends React.Component { >
- - + +
- +
{this.SideNavComponent(isFolded, storageList)} diff --git a/browser/main/StatusBar/index.js b/browser/main/StatusBar/index.js index 2106a230..49c1b40c 100644 --- a/browser/main/StatusBar/index.js +++ b/browser/main/StatusBar/index.js @@ -63,7 +63,7 @@ class StatusBar extends React.Component { {status.updateReady ? + : null }
diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js index f10d0b66..1ef4f8da 100644 --- a/browser/main/lib/AwsMobileAnalyticsConfig.js +++ b/browser/main/lib/AwsMobileAnalyticsConfig.js @@ -7,7 +7,7 @@ const os = require('os') let mobileAnalyticsClient AWS.config.region = 'us-east-1' -if (process.env.NODE_ENV === 'production' && ConfigManager.default.get().amaEnabled) { +if (!getSendEventCond()) { AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx' }) @@ -34,8 +34,15 @@ function convertPlatformName (platformName) { } } +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 () { - if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return + if (getSendEventCond()) return AWS.config.credentials.get((err) => { if (!err) { console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId) @@ -46,7 +53,7 @@ function initAwsMobileAnalytics () { } function recordDynamicCustomEvent (type, options = {}) { - if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return + if (getSendEventCond()) return try { mobileAnalyticsClient.recordEvent(type, options) } catch (analyticsError) { @@ -57,7 +64,7 @@ function recordDynamicCustomEvent (type, options = {}) { } function recordStaticCustomEvent () { - if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return + if (getSendEventCond()) return try { mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', { uiColorTheme: ConfigManager.default.get().ui.theme diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index fc9b4ee9..3d70a7a3 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -35,7 +35,8 @@ export const DEFAULT_CONFIG = { indentType: 'space', indentSize: '2', switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR - scrollPastEnd: false + scrollPastEnd: false, + type: 'SPLIT' }, preview: { fontSize: '14', diff --git a/browser/main/lib/dataApi/exportImage.js b/browser/main/lib/dataApi/copyFile.js similarity index 66% rename from browser/main/lib/dataApi/exportImage.js rename to browser/main/lib/dataApi/copyFile.js index a1c84390..b46ffd6a 100755 --- a/browser/main/lib/dataApi/exportImage.js +++ b/browser/main/lib/dataApi/copyFile.js @@ -2,14 +2,14 @@ const fs = require('fs') const path = require('path') /** - * @description Export an image + * @description Export a file * @param {String} storagePath * @param {String} srcFilename * @param {String} dstPath * @param {String} dstFilename if not present, destination filename will be equal to srcFilename * @return {Promise} an image path */ -function exportImage (storagePath, srcFilename, dstPath, dstFilename = '') { +function exportFile (storagePath, srcFilename, dstPath, dstFilename = '') { dstFilename = dstFilename || srcFilename const src = path.join(storagePath, 'images', srcFilename) @@ -18,20 +18,19 @@ function exportImage (storagePath, srcFilename, dstPath, dstFilename = '') { dstFilename += path.extname(srcFilename) } - const dstImagesFolder = path.join(dstPath, 'images') - const dst = path.join(dstImagesFolder, dstFilename) + const dst = path.join(dstPath, dstFilename) return new Promise((resolve, reject) => { - if (!fs.existsSync(dstImagesFolder)) fs.mkdirSync(dstImagesFolder) + if (!fs.existsSync(dstPath)) fs.mkdirSync(dstPath) const input = fs.createReadStream(src) const output = fs.createWriteStream(dst) output.on('error', reject) input.on('error', reject) - input.on('end', resolve) + input.on('end', resolve, dst) input.pipe(output) }) } -module.exports = exportImage +module.exports = exportFile diff --git a/browser/main/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js new file mode 100644 index 00000000..75dba959 --- /dev/null +++ b/browser/main/lib/dataApi/exportFolder.js @@ -0,0 +1,63 @@ +import { findStorage } from 'browser/lib/findStorage' +import resolveStorageData from './resolveStorageData' +import resolveStorageNotes from './resolveStorageNotes' +import * as path from 'path' +import * as fs from 'fs' + +/** + * @param {String} storageKey + * @param {String} folderKey + * @param {String} fileType + * @param {String} exportDir + * + * @return {Object} + * ``` + * { + * storage: Object, + * folderKey: String, + * fileType: String, + * exportDir: String + * } + * ``` + */ + +function exportFolder (storageKey, folderKey, fileType, exportDir) { + let targetStorage + try { + targetStorage = findStorage(storageKey) + } catch (e) { + return Promise.reject(e) + } + + return resolveStorageData(targetStorage) + .then(function assignNotes (storage) { + return resolveStorageNotes(storage) + .then((notes) => { + return { + storage, + notes + } + }) + }) + .then(function exportNotes (data) { + const { storage, notes } = data + + notes + .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') + .forEach(snippet => { + const notePath = path.join(exportDir, `${snippet.title}.${fileType}`) + fs.writeFileSync(notePath, snippet.content, (err) => { + if (err) throw err + }) + }) + + return { + storage, + folderKey, + fileType, + exportDir + } + }) +} + +module.exports = exportFolder diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 768dfe32..311ca2f3 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -7,6 +7,7 @@ const dataApi = { updateFolder: require('./updateFolder'), deleteFolder: require('./deleteFolder'), reorderFolder: require('./reorderFolder'), + exportFolder: require('./exportFolder'), createNote: require('./createNote'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index ae54218c..ea26af08 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -76,8 +76,8 @@ color #1EC38B .error color red - - + .warning + color #FFA500 .group-control-leftButton colorDefaultButton() diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 9a15e79f..4b4a3060 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -32,6 +32,7 @@ class HotkeyTab extends React.Component { message: err.message != null ? err.message : 'Error occurs!' }}) } + this.oldHotkey = this.state.config.hotkey ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) } @@ -53,6 +54,7 @@ class HotkeyTab extends React.Component { config: newConfig }) this.clearMessage() + this.props.haveToSave() } handleHintToggleButtonClick (e) { @@ -70,6 +72,15 @@ class HotkeyTab extends React.Component { this.setState({ config }) + if (_.isEqual(this.oldHotkey, config.hotkey)) { + this.props.haveToSave() + } else { + this.props.haveToSave({ + tab: 'Hotkey', + type: 'warning', + message: 'You have to save!' + }) + } } clearMessage () { @@ -161,7 +172,8 @@ class HotkeyTab extends React.Component { } HotkeyTab.propTypes = { - dispatch: PropTypes.func + dispatch: PropTypes.func, + haveToSave: PropTypes.func } export default CSSModules(HotkeyTab, styles) diff --git a/browser/main/modals/PreferencesModal/PreferencesModal.styl b/browser/main/modals/PreferencesModal/PreferencesModal.styl index 4a280a38..57b5dbad 100644 --- a/browser/main/modals/PreferencesModal/PreferencesModal.styl +++ b/browser/main/modals/PreferencesModal/PreferencesModal.styl @@ -42,6 +42,8 @@ top-bar--height = 50px background-color transparent color $ui-text-color font-size 16px + .saving--warning + haveToSave() .nav-button--active @extend .nav-button @@ -49,6 +51,8 @@ top-bar--height = 50px background-color $ui-button--active-backgroundColor &:hover color $ui-text-color + .saving--warning + haveToSave() .nav-button-icon display block diff --git a/browser/main/modals/PreferencesModal/Tab.styl b/browser/main/modals/PreferencesModal/Tab.styl index e5fc48da..a316f3eb 100644 --- a/browser/main/modals/PreferencesModal/Tab.styl +++ b/browser/main/modals/PreferencesModal/Tab.styl @@ -20,3 +20,8 @@ $tab--dark-text-color = #E5E5E5 body[data-theme="dark"] .header color $tab--dark-text-color + +haveToSave() + color #FFA500 + font-size 10px + margin-top 3px \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 42afd8f4..c79329d0 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -7,11 +7,11 @@ import store from 'browser/main/store' import consts from 'browser/lib/consts' import ReactCodeMirror from 'react-codemirror' import CodeMirror from 'codemirror' +import 'codemirror-mode-elixir' +import _ from 'lodash' const OSX = global.process.platform === 'darwin' -import _ from 'lodash' - const electron = require('electron') const ipc = electron.ipcRenderer @@ -93,8 +93,19 @@ class UiTab extends React.Component { if (newCodemirrorTheme !== codemirrorTheme) { checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`) } - - this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }) + 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: 'You have to save!' + }) + } + }) } handleSaveUIClick (e) { @@ -111,6 +122,7 @@ class UiTab extends React.Component { config: newConfig }) this.clearMessage() + this.props.haveToSave() } clearMessage () { @@ -412,7 +424,8 @@ UiTab.propTypes = { user: PropTypes.shape({ name: PropTypes.string }), - dispatch: PropTypes.func + dispatch: PropTypes.func, + haveToSave: PropTypes.func } export default CSSModules(UiTab, styles) diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index 2fff0364..09885e1c 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -17,7 +17,9 @@ class Preferences extends React.Component { super(props) this.state = { - currentTab: 'STORAGES' + currentTab: 'STORAGES', + UIAlert: '', + HotkeyAlert: '' } } @@ -58,6 +60,7 @@ class Preferences extends React.Component { this.setState({HotkeyAlert: alert})} /> ) case 'UI': @@ -65,6 +68,7 @@ class Preferences extends React.Component { this.setState({UIAlert: alert})} /> ) case 'CROWDFUNDING': @@ -94,19 +98,26 @@ class Preferences extends React.Component { return node.getBoundingClientRect() } + haveToSaveNotif (type, message) { + return ( +

{message}

+ ) + } + render () { const content = this.renderContent() const tabs = [ {target: 'STORAGES', label: 'Storages'}, - {target: 'HOTKEY', label: 'Hotkey'}, - {target: 'UI', label: 'UI'}, + {target: 'HOTKEY', label: 'Hotkey', Hotkey: this.state.HotkeyAlert}, + {target: 'UI', label: 'UI', UI: this.state.UIAlert}, {target: 'INFO', label: 'Community / Info'}, {target: 'CROWDFUNDING', label: 'Crowdfunding'} ] const navButtons = tabs.map((tab) => { const isActive = this.state.currentTab === tab.target + const isUiHotkeyTab = _.isObject(tab[tab.label]) && tab.label === tab[tab.label].tab return ( ) }) diff --git a/browser/main/store.js b/browser/main/store.js index 647d0ac9..36e7850d 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -87,8 +87,13 @@ function data (state = defaultDataMap(), action) { state.trashedSet = new Set(state.trashedSet) if (note.isTrashed) { state.trashedSet.add(uniqueKey) + state.starredSet.delete(uniqueKey) } else { state.trashedSet.delete(uniqueKey) + + if (note.isStarred) { + state.starredSet.add(uniqueKey) + } } } @@ -349,6 +354,13 @@ function data (state = defaultDataMap(), action) { state.storageMap = new Map(state.storageMap) state.storageMap.set(action.storage.key, action.storage) return state + case 'EXPORT_FOLDER': + { + state = Object.assign({}, state) + state.storageMap = new Map(state.storageMap) + state.storageMap.set(action.storage.key, action.storage) + } + return state case 'DELETE_FOLDER': { state = Object.assign({}, state) diff --git a/lib/finder.html b/lib/finder.html index f0ac3df5..c6ad502e 100644 --- a/lib/finder.html +++ b/lib/finder.html @@ -28,6 +28,7 @@ + diff --git a/lib/main-app.js b/lib/main-app.js index 2c55ba97..02b38a04 100644 --- a/lib/main-app.js +++ b/lib/main-app.js @@ -102,12 +102,11 @@ app.on('ready', function () { Menu.setApplicationMenu(menu) break case 'win32': - /* eslint-disable */ - finderWindow = require('./finder-window') - /* eslint-disable */ + require('./finder-window') mainWindow.setMenu(menu) break case 'linux': + require('./finder-window') Menu.setApplicationMenu(menu) mainWindow.setMenu(menu) } diff --git a/lib/main-menu.js b/lib/main-menu.js index 3555c381..0d49ab86 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -108,6 +108,13 @@ const file = { mainWindow.webContents.send('list:isMarkdownNote') mainWindow.webContents.send('export:save-md') } + }, + { + label: 'HTML (.html)', + click () { + mainWindow.webContents.send('list:isMarkdownNote') + mainWindow.webContents.send('export:save-html') + } } ] }, diff --git a/lib/main.html b/lib/main.html index 69a47900..d7936628 100644 --- a/lib/main.html +++ b/lib/main.html @@ -69,6 +69,7 @@ + diff --git a/package.json b/package.json index f1f8aa0c..f38e8958 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "boost", "productName": "Boostnote", - "version": "0.8.18", + "version": "0.8.19", "main": "index.js", "description": "Boostnote", "license": "GPL-3.0", @@ -53,6 +53,7 @@ "aws-sdk": "^2.48.0", "aws-sdk-mobile-analytics": "^0.9.2", "codemirror": "^5.19.0", + "codemirror-mode-elixir": "^1.1.1", "electron-config": "^0.2.1", "electron-gh-releases": "^2.0.2", "flowchart.js": "^1.6.5", diff --git a/readme.md b/readme.md index 9d64bf5d..4b571fa0 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,4 @@ -New:zap: - -Open sourcing our [Android and iOS apps](https://github.com/BoostIO/Boostnote-mobile)! +:mega: Open sourcing our [Android and iOS apps](https://github.com/BoostIO/Boostnote-mobile)! ![Boostnote app screenshot](./resources/repository/top.png) @@ -12,7 +10,6 @@ Open sourcing our [Android and iOS apps](https://github.com/BoostIO/Boostnote-mo ## Authors & Maintainers - [Rokt33r](https://github.com/rokt33r) -- [sota1235](https://github.com/sota1235) - [Kohei TAKATA](https://github.com/kohei-takata) - [Sosuke](https://github.com/sosukesuzuki) - [Kazz](https://github.com/kazup01) @@ -29,7 +26,7 @@ Boostnote is an open source project. It's an independent project with its ongoin ## Community - [Facebook Group](https://www.facebook.com/groups/boostnote/) - [Twitter](https://twitter.com/boostnoteapp) -- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMjc2MDM0MDEyODk2LThlZDlhYmYwMjdkMmJjMGM5MGFiMGJmNzk5ZTdhNzFhMmNmMDFlY2M2YTE1MTZkOThiOGZmNTI3YzJiOTBhMTQ) +- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMjkxMzMwODYxMDI1LTgwZmRiODg0NzA5MWRmOTJjNzBjZjAwMmMyZGQ4Y2RkOGE0MDg0YjcyMjA5OGUzMmZhNmFiNTMzOTlkYWNlMTM) - [Blog](https://medium.com/boostnote) - [Reddit](https://www.reddit.com/r/Boostnote/) diff --git a/resources/icon/icon-WYSIWYG-off.svg b/resources/icon/icon-WYSIWYG-off.svg deleted file mode 100644 index ffd6088a..00000000 --- a/resources/icon/icon-WYSIWYG-off.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - icon-WYSIWYG-off - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/resources/icon/icon-WYSIWYG-on.svg b/resources/icon/icon-WYSIWYG-on.svg deleted file mode 100644 index b8ee9489..00000000 --- a/resources/icon/icon-WYSIWYG-on.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - icon-WYSIWYG-on - Created with Sketch. - - - - - - - - - - - \ No newline at end of file diff --git a/resources/icon/icon-edit.svg b/resources/icon/icon-edit.svg index 3707c6fe..cb7d92cc 100644 --- a/resources/icon/icon-edit.svg +++ b/resources/icon/icon-edit.svg @@ -3,11 +3,22 @@ icon-edit Created with Sketch. - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-full.svg b/resources/icon/icon-full.svg new file mode 100644 index 00000000..621ebacc --- /dev/null +++ b/resources/icon/icon-full.svg @@ -0,0 +1,15 @@ + + + + icon-full + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-mode-markdown-off-active.svg b/resources/icon/icon-mode-markdown-off-active.svg new file mode 100644 index 00000000..0159836b --- /dev/null +++ b/resources/icon/icon-mode-markdown-off-active.svg @@ -0,0 +1,23 @@ + + + + icon-mode-markdown-off + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-mode-markdown-off.svg b/resources/icon/icon-mode-markdown-off.svg new file mode 100644 index 00000000..7f6a0235 --- /dev/null +++ b/resources/icon/icon-mode-markdown-off.svg @@ -0,0 +1,23 @@ + + + + icon-mode-markdown-off + Created with Sketch. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-mode-split-on-active.svg b/resources/icon/icon-mode-split-on-active.svg new file mode 100644 index 00000000..338d2bd7 --- /dev/null +++ b/resources/icon/icon-mode-split-on-active.svg @@ -0,0 +1,27 @@ + + + + icon-mode-split-on + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-mode-split-on.svg b/resources/icon/icon-mode-split-on.svg new file mode 100644 index 00000000..c212d7f2 --- /dev/null +++ b/resources/icon/icon-mode-split-on.svg @@ -0,0 +1,27 @@ + + + + icon-mode-split-on + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-previewoff-off.svg b/resources/icon/icon-previewoff-off.svg new file mode 100644 index 00000000..b0e720e7 --- /dev/null +++ b/resources/icon/icon-previewoff-off.svg @@ -0,0 +1,13 @@ + + + + icon-previewoff-off + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/resources/icon/icon-previewoff-on.svg b/resources/icon/icon-previewoff-on.svg new file mode 100644 index 00000000..8a6c5d7e --- /dev/null +++ b/resources/icon/icon-previewoff-on.svg @@ -0,0 +1,13 @@ + + + + icon-previewoff-on + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/tests/dataApi/exportFolder-test.js b/tests/dataApi/exportFolder-test.js new file mode 100644 index 00000000..ee6fb898 --- /dev/null +++ b/tests/dataApi/exportFolder-test.js @@ -0,0 +1,62 @@ +const test = require('ava') +const exportFolder = require('browser/main/lib/dataApi/exportFolder') +const createNote = require('browser/main/lib/dataApi/createNote') + +global.document = require('jsdom').jsdom('') +global.window = document.defaultView +global.navigator = window.navigator + +const Storage = require('dom-storage') +const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) +const path = require('path') +const TestDummy = require('../fixtures/TestDummy') +const os = require('os') +const faker = require('faker') +const fs = require('fs') + +const storagePath = path.join(os.tmpdir(), 'test/export-note') + +test.beforeEach((t) => { + t.context.storage = TestDummy.dummyStorage(storagePath) + localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) +}) + +test.serial('Export a folder', (t) => { + const storageKey = t.context.storage.cache.key + const folderKey = t.context.storage.json.folders[0].key + + const input1 = { + type: 'MARKDOWN_NOTE', + description: '*Some* markdown text', + tags: faker.lorem.words().split(' '), + folder: folderKey + } + input1.title = 'input1' + + const input2 = { + type: 'SNIPPET_NOTE', + description: 'Some normal text', + snippets: [{ + name: faker.system.fileName(), + mode: 'text', + content: faker.lorem.lines() + }], + tags: faker.lorem.words().split(' '), + folder: folderKey + } + input2.title = 'input2' + + return createNote(storageKey, input1) + .then(function () { + return createNote(storageKey, input2) + }) + .then(function () { + return exportFolder(storageKey, folderKey, 'md', storagePath) + }) + .then(function assert () { + let filePath = path.join(storagePath, 'input1.md') + t.true(fs.existsSync(filePath)) + filePath = path.join(storagePath, 'input2.md') + t.false(fs.existsSync(filePath)) + }) +})