1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

Merge branch 'master' into new-snippet-tabbar

# Conflicts:
#	browser/main/Detail/SnippetNoteDetail.js
This commit is contained in:
Nikolay Lopin
2018-03-18 21:26:12 +03:00
86 changed files with 5089 additions and 1004 deletions

View File

@@ -8,6 +8,8 @@ import copyImage from 'browser/main/lib/dataApi/copyImage'
import { findStorage } from 'browser/lib/findStorage'
import fs from 'fs'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
const { ipcRenderer } = require('electron')
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -32,8 +34,13 @@ export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.changeHandler = (e) => this.handleChange(e)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null
let el = e.relatedTarget
while (el != null) {
@@ -81,7 +88,6 @@ export default class CodeEditor extends React.Component {
}
}
})
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
}
componentDidMount () {
@@ -139,6 +145,7 @@ export default class CodeEditor extends React.Component {
this.setMode(this.props.mode)
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
@@ -162,6 +169,7 @@ export default class CodeEditor extends React.Component {
}
componentWillUnmount () {
this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler)
@@ -317,7 +325,7 @@ export default class CodeEditor extends React.Component {
fetch(pastedTxt, {
method: 'get'
}).then((response) => {
return (response.text())
return this.decodeResponse(response)
}).then((response) => {
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
const value = editor.getValue()
@@ -335,6 +343,31 @@ export default class CodeEditor extends React.Component {
})
}
decodeResponse (response) {
const headers = response.headers
const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type'))
: undefined
return response.arrayBuffer().then((buff) => {
return new Promise((resolve, reject) => {
try {
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString())
} catch (e) {
reject(e)
}
})
})
}
extractContentTypeCharset (contentType) {
return contentType.split(';').filter((str) => {
return str.trim().toLowerCase().startsWith('charset')
}).map((str) => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
}
render () {
const { className, fontSize } = this.props
let fontFamily = this.props.fontFamily

View File

@@ -279,6 +279,7 @@ class MarkdownEditor extends React.Component {
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import markdown from 'browser/lib/markdown'
import Markdown from 'browser/lib/markdown'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
@@ -130,6 +130,13 @@ export default class MarkdownPreview extends React.Component {
this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
}
initMarkdown () {
const { smartQuotes } = this.props
this.markdown = new Markdown({ typographer: smartQuotes })
}
handlePreviewAnchorClick (e) {
@@ -198,7 +205,7 @@ export default class MarkdownPreview extends React.Component {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
const body = markdown.render(noteContent)
const body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach((file) => {
@@ -216,6 +223,8 @@ export default class MarkdownPreview extends React.Component {
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
@@ -309,6 +318,10 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes) {
this.initMarkdown()
this.rewriteIframe()
}
if (prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
@@ -374,7 +387,7 @@ export default class MarkdownPreview extends React.Component {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
})
}
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
@@ -390,9 +403,9 @@ export default class MarkdownPreview extends React.Component {
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
el.src = markdown.normalizeLinkText(el.src)
el.src = this.markdown.normalizeLinkText(el.src)
if (!/\/:storage/.test(el.src)) return
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
})
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
@@ -419,9 +432,9 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = ''
if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ')
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
} else {
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
el.parentNode.className += ` cm-s-${codeBlockTheme}`
}
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
@@ -504,9 +517,20 @@ export default class MarkdownPreview extends React.Component {
handlelinkClick (e) {
const noteHash = e.target.href.split('/').pop()
const regexIsNoteLink = /^(.{20})-(.{20})$/
// this will match the new uuid v4 hash and the old hash
// e.g.
// :note:1c211eb7dcb463de6490 and
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
if (regexIsNoteLink.test(noteHash)) {
eventEmitter.emit('list:jump', noteHash)
eventEmitter.emit('list:jump', noteHash.replace(':note:', ''))
}
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
if (regexIsLegacyNoteLink.test(noteHash)) {
eventEmitter.emit('list:jump', noteHash.split('-')[1])
}
}
@@ -533,5 +557,6 @@ MarkdownPreview.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string
storagePath: PropTypes.string,
smartQuotes: PropTypes.bool
}

View File

@@ -127,6 +127,7 @@ class MarkdownSplitEditor extends React.Component {
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
ref='preview'
tabInde='0'
value={value}

View File

@@ -62,9 +62,9 @@ const NoteItem = ({
? 'item--active'
: 'item'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)}
draggable='true'
>
@@ -100,7 +100,7 @@ const NoteItem = ({
{note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
}
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
}
{note.type === 'MARKDOWN_NOTE'
@@ -123,7 +123,11 @@ NoteItem.propTypes = {
title: PropTypes.string.isrequired,
tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired
isTrashed: PropTypes.bool.isRequired,
blog: {
blogLink: PropTypes.string,
blogId: PropTypes.number
}
}),
handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired,

View File

@@ -117,7 +117,7 @@ $control-height = 30px
font-size 12px
line-height 20px
overflow ellipsis
display flex
display block
.item-bottom-tagList
flex 1
@@ -144,18 +144,18 @@ $control-height = 30px
padding-bottom 2px
.item-star
position relative
width 16px
height 16px
position absolute
right 2px
top 5px
color alpha($ui-favorite-star-button-color, 60%)
font-size 12px
padding 0
border-radius 17px
.item-pin
position relative
width 34px
height 34px
position absolute
right 25px
top 7px
color #E54D42
font-size 14px
padding 0

View File

@@ -28,9 +28,9 @@ const NoteItemSimple = ({
? 'item-simple--active'
: 'item-simple'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)}
draggable='true'
>

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNavFilter.styl'
import i18n from 'browser/lib/i18n'
/**
* @param {boolean} isFolded
@@ -31,7 +32,7 @@ const SideNavFilter = ({
}
/>
</div>
<span styleName='menu-button-label'>All Notes</span>
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
<span styleName='counters'>{counterTotalNote}</span>
</button>
@@ -45,7 +46,7 @@ const SideNavFilter = ({
}
/>
</div>
<span styleName='menu-button-label'>Starred</span>
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
<span styleName='counters'>{counterStarredNote}</span>
</button>
@@ -59,7 +60,7 @@ const SideNavFilter = ({
}
/>
</div>
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>Trash</span>
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span>
</button>

View File

@@ -6,6 +6,18 @@ import React from 'react'
import styles from './StorageItem.styl'
import CSSModules from 'browser/lib/CSSModules'
import _ from 'lodash'
import { SortableHandle } from 'react-sortable-hoc'
const DraggableIcon = SortableHandle(({ className }) => (
<i className={`fa ${className}`} />
))
const FolderIcon = ({ className, color, isActive }) => {
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
return (
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
)
}
/**
* @param {boolean} isActive
@@ -21,34 +33,54 @@ import _ from 'lodash'
* @return {React.Component}
*/
const StorageItem = ({
isActive, handleButtonClick, handleContextMenu, folderName,
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
}) => (
<button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
onClick={handleButtonClick}
onContextMenu={handleContextMenu}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
<span styleName={isFolded
? 'folderList-item-name--folded' : 'folderList-item-name'
}>
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
</span>
{(!isFolded && _.isNumber(noteCount)) &&
<span styleName='folderList-item-noteCount'>{noteCount}</span>
}
{isFolded &&
<span styleName='folderList-item-tooltip'>
{folderName}
styles,
isActive,
handleButtonClick,
handleContextMenu,
folderName,
folderColor,
isFolded,
noteCount,
handleDrop,
handleDragEnter,
handleDragLeave
}) => {
return (
<button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick}
onContextMenu={handleContextMenu}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
{!isFolded && (
<DraggableIcon className={styles['folderList-item-reorder']} />
)}
<span
styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
}
>
<FolderIcon
styleName='folderList-item-icon'
color={folderColor}
isActive={isActive}
/>
{isFolded
? _.truncate(folderName, { length: 1, omission: '' })
: folderName}
</span>
}
</button>
)
{!isFolded &&
_.isNumber(noteCount) && (
<span styleName='folderList-item-noteCount'>{noteCount}</span>
)}
{isFolded && (
<span styleName='folderList-item-tooltip'>{folderName}</span>
)}
</button>
)
}
StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired,

View File

@@ -13,6 +13,7 @@
border none
overflow ellipsis
font-size 14px
align-items: center
&:first-child
margin-top 0
&:hover
@@ -22,7 +23,7 @@
&:active
color $$ui-button-default-color
background-color alpha($ui-button-default--active-backgroundColor, 20%)
.folderList-item--active
@extend .folderList-item
color #1EC38B
@@ -34,9 +35,7 @@
.folderList-item-name
display block
flex 1
padding 0 12px
height 26px
line-height 26px
padding-right: 10px
border-width 0 0 0 2px
border-style solid
border-color transparent
@@ -69,9 +68,20 @@
.folderList-item-name--folded
@extend .folderList-item-name
padding-left 7px
text
.folderList-item-icon
font-size 9px
.folderList-item-icon
padding-right: 10px
.folderList-item-reorder
font-size: 9px
padding: 10px 8px 10px 9px;
color: rgba(147, 147, 149, 0.3)
cursor: ns-resize
&:before
content: "\f142 \f142"
body[data-theme="white"]
.folderList-item
color $ui-inactive-text-color
@@ -127,4 +137,4 @@ body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-button-backgroundColor
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
background-color $ui-solarized-dark-button-backgroundColor

8
browser/lib/i18n.js Normal file
View File

@@ -0,0 +1,8 @@
// load package for localization
const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale
locales: ['en', 'sq', 'zh-CN', 'zh-TW', 'da', 'fr', 'de', 'ja', 'ko', 'no', 'pl', 'pt', 'es'],
extension: '.json'
})
export default i18n

View File

@@ -1,7 +1,11 @@
const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4')
module.exports = function (length) {
if (!_.isFinite(length)) length = 10
module.exports = function (uuid) {
if (typeof uuid === typeof true && uuid) {
return uuidv4()
}
const length = 10
return crypto.randomBytes(length).toString('hex')
}

View File

@@ -0,0 +1,23 @@
'use strict'
import sanitizeHtml from 'sanitize-html'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => {
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
if (state.tokens[tokenIdx].type === 'html_block') {
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
}
if (state.tokens[tokenIdx].type === 'inline') {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
}
}
}
}
})
}

View File

@@ -1,14 +1,12 @@
import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import {lastFindInArray} from './utils'
// FIXME We should not depend on global variable.
const katex = window.katex
const config = ConfigManager.get()
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
@@ -19,171 +17,218 @@ function createGutter (str, firstLineNumber) {
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
}
var md = markdownit({
typographer: true,
linkify: true,
html: true,
xhtmlOut: true,
breaks: true,
highlight: function (str, lang) {
const delimiter = ':'
const langInfo = lang.split(delimiter)
const langType = langInfo[0]
const fileName = langInfo[1] || ''
const firstLineNumber = parseInt(langInfo[2], 10)
class Markdown {
constructor (options = {}) {
const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
linkify: true,
html: true,
xhtmlOut: true,
breaks: true,
highlight: function (str, lang) {
const delimiter = ':'
const langInfo = lang.split(delimiter)
const langType = langInfo[0]
const fileName = langInfo[1] || ''
const firstLineNumber = parseInt(langInfo[2], 10)
if (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
}
})
md.use(emoji, {
shortcuts: {}
})
md.use(math, {
inlineOpen: config.preview.latexInlineOpen,
inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-imsize'))
md.use(require('markdown-it-footnote'))
md.use(require('markdown-it-multimd-table'))
md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
md.use(require('markdown-it-kbd'))
const deflate = require('markdown-it-plantuml/lib/deflate')
md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
)
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
}
})
// Override task item
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token
let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
const endLine = state.lineMax
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
}
}
if (terminate) { break }
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [startLine, state.line]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
if (liToken) {
if (!liToken.attrs) {
liToken.attrs = []
if (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
liToken.attrs.push(['class', 'taskListItem'])
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code CodeMirror">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
}
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions)
// Sanitize use rinput before other plugins
this.md.use(sanitize, {
allowedTags: ['iframe', 'input', 'b',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
],
allowedAttributes: {
'*': [
'style',
'abbr', 'accept', 'accept-charset',
'accesskey', 'action', 'align', 'alt', 'axis',
'border', 'cellpadding', 'cellspacing', 'char',
'charoff', 'charset', 'checked',
'clear', 'cols', 'colspan', 'color',
'compact', 'coords', 'datetime', 'dir',
'disabled', 'enctype', 'for', 'frame',
'headers', 'height', 'hreflang',
'hspace', 'ismap', 'label', 'lang',
'maxlength', 'media', 'method',
'multiple', 'name', 'nohref', 'noshade',
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
'rows', 'rowspan', 'rules', 'scope',
'selected', 'shape', 'size', 'span',
'start', 'summary', 'tabindex', 'target',
'title', 'type', 'usemap', 'valign', 'value',
'vspace', 'width', 'itemprop'
],
'a': ['href'],
'div': ['itemscope', 'itemtype'],
'blockquote': ['cite'],
'del': ['cite'],
'ins': ['cite'],
'q': ['cite'],
'img': ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
},
allowedIframeHostnames: ['www.youtube.com']
})
this.md.use(emoji, {
shortcuts: {}
})
this.md.use(math, {
inlineOpen: config.preview.latexInlineOpen,
inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
this.md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
this.md.use(require('markdown-it-kbd'))
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
)
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
}
})
// Override task item
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token
let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
const endLine = state.lineMax
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
}
}
if (terminate) { break }
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [startLine, state.line]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
if (liToken) {
if (!liToken.attrs) {
liToken.attrs = []
}
liToken.attrs.push(['class', 'taskListItem'])
}
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [startLine, state.line]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
const result = originalRender.call(this.md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = this.md
}
token = state.push('inline', '', 0)
token.content = content
token.map = [startLine, state.line]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
const originalRender = md.renderer.render
md.renderer.render = function render (tokens, options, env) {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
const result = originalRender.call(md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = md
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
}
const markdown = {
render: function markdown (content) {
render (content) {
if (!_.isString(content)) content = ''
const renderedContent = md.render(content)
return renderedContent
},
normalizeLinkText
return this.md.render(content)
}
normalizeLinkText (linkText) {
return this.md.normalizeLinkText(linkText)
}
}
export default markdown
export default Markdown

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component {
copyNoteLink () {
@@ -19,7 +20,7 @@ class InfoPanel extends React.Component {
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div>
<hr />
@@ -29,11 +30,11 @@ class InfoPanel extends React.Component {
: <div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>Words</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
</div>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>Letters</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div>
</div>
}
@@ -45,17 +46,17 @@ class InfoPanel extends React.Component {
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div>
<div>
@@ -63,7 +64,7 @@ class InfoPanel extends React.Component {
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' />
</button>
<p styleName='infoPanel-sub'>NOTE LINK</p>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
</div>
<hr />
@@ -71,22 +72,22 @@ class InfoPanel extends React.Component {
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o' />
<p>.md</p>
<p>{i18n.__('.md')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o' />
<p>.txt</p>
<p>{i18n.__('.txt')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<i className='fa fa-html5' />
<p>.html</p>
<p>{i18n.__('.html')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print' />
<p>Print</p>
<p>{i18n.__('Print')}</p>
</button>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
@@ -9,24 +10,24 @@ const InfoPanelTrashed = ({
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div>
<hr />
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div>
<div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div>
<div id='export-wrap'>

View File

@@ -139,7 +139,7 @@ class MarkdownNoteDetail extends React.Component {
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
key: newNote.key
}
})
this.setState({
@@ -393,7 +393,7 @@ class MarkdownNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({
onClick
@@ -10,7 +11,7 @@ const PermanentDeleteButton = ({
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Permanent Delete</span>
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>
)

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n'
const RestoreButton = ({
onClick
@@ -10,7 +11,7 @@ const RestoreButton = ({
onClick={onClick}
>
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>Restore</span>
<span styleName='tooltip'>{i18n.__('Restore')}</span>
</button>
)

View File

@@ -25,7 +25,8 @@ import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import {formatDate} from 'browser/lib/date-formatter'
import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
function pass (name) {
switch (name) {
@@ -166,7 +167,7 @@ class SnippetNoteDetail extends React.Component {
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
key: newNote.key
}
})
this.setState({
@@ -328,9 +329,9 @@ class SnippetNoteDetail extends React.Component {
if (this.state.note.snippets[index].content.trim().length > 0) {
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a snippet',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Delete a snippet'),
detail: i18n.__('This work cannot be undone.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (dialogIndex === 0) {
this.deleteSnippetByIndex(index)
@@ -422,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
handleKeyDown (e) {
switch (e.keyCode) {
// tab key
case 9:
if (e.ctrlKey && !e.shiftKey) {
e.preventDefault()
@@ -434,6 +436,7 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor()
}
break
// L key
case 76:
{
const isSuper = global.process.platform === 'darwin'
@@ -445,6 +448,7 @@ class SnippetNoteDetail extends React.Component {
}
}
break
// T key
case 84:
{
const isSuper = global.process.platform === 'darwin'
@@ -634,8 +638,8 @@ class SnippetNoteDetail extends React.Component {
showWarning () {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Sorry!',
detail: 'md/text import is available only a markdown note.',
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
buttons: ['OK']
})
}
@@ -777,7 +781,7 @@ class SnippetNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
@@ -13,7 +14,7 @@ const ToggleModeButton = ({
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>Toggle Mode</span>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const TrashButton = ({
onClick
@@ -10,7 +11,7 @@ const TrashButton = ({
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>Trash</span>
<span styleName='tooltip'>{i18n.__('Trash')}</span>
</button>
)

View File

@@ -7,6 +7,7 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
@@ -40,9 +41,9 @@ class Detail extends React.Component {
const alertConfig = {
type: 'warning',
message: 'Confirm note deletion',
detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
@@ -56,11 +57,8 @@ class Detail extends React.Component {
const { location, data, config } = this.props
let note = null
if (location.query.key != null) {
const splitted = location.query.key.split('-')
const storageKey = splitted.shift()
const noteKey = splitted.shift()
note = data.noteMap.get(storageKey + '-' + noteKey)
const noteKey = location.query.key
note = data.noteMap.get(noteKey)
}
if (note == null) {
@@ -70,7 +68,7 @@ class Detail extends React.Component {
tabIndex='0'
>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

@@ -14,6 +14,7 @@ import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router'
import store from 'browser/main/store'
import i18n from 'browser/lib/i18n'
const path = require('path')
const electron = require('electron')
const { remote } = electron
@@ -148,6 +149,35 @@ class Main extends React.Component {
} else {
document.body.setAttribute('data-theme', 'default')
}
if (config.ui.language === 'sq') {
i18n.setLocale('sq')
} else if (config.ui.language === 'zh-CN') {
i18n.setLocale('zh-CN')
} else if (config.ui.language === 'zh-TW') {
i18n.setLocale('zh-TW')
} else if (config.ui.language === 'da') {
i18n.setLocale('da')
} else if (config.ui.language === 'fr') {
i18n.setLocale('fr')
} else if (config.ui.language === 'de') {
i18n.setLocale('de')
} else if (config.ui.language === 'ja') {
i18n.setLocale('ja')
} else if (config.ui.language === 'ko') {
i18n.setLocale('ko')
} else if (config.ui.language === 'no') {
i18n.setLocale('no')
} else if (config.ui.language === 'pl') {
i18n.setLocale('pl')
} else if (config.ui.language === 'pt') {
i18n.setLocale('pt')
} else if (config.ui.language === 'ru') {
i18n.setLocale('ru')
} else if (config.ui.language === 'es') {
i18n.setLocale('es')
} else {
i18n.setLocale('en')
}
// Reload all data
dataApi.init()

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { dialog } = remote
@@ -86,7 +87,7 @@ class NewNoteButton extends React.Component {
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
Make a note {OSX ? '⌘' : 'Ctrl'} + N
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span>
</button>
</div>

View File

@@ -1,3 +1,4 @@
/* global electron */
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
@@ -13,10 +14,14 @@ import searchFromNotes from 'browser/lib/search'
import fs from 'fs'
import path from 'path'
import { hashHistory } from 'react-router'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt)
@@ -31,7 +36,7 @@ function sortByUpdatedAt (a, b) {
}
function findNoteByKey (notes, noteKey) {
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
return notes.find((note) => note.key === noteKey)
}
function findNotesByKeys (notes, noteKeys) {
@@ -39,7 +44,7 @@ function findNotesByKeys (notes, noteKeys) {
}
function getNoteKey (note) {
return `${note.storage}-${note.key}`
return note.key
}
class NoteList extends React.Component {
@@ -70,6 +75,7 @@ class NoteList extends React.Component {
this.getNoteFolder = this.getNoteFolder.bind(this)
this.getViewType = this.getViewType.bind(this)
this.restoreNote = this.restoreNote.bind(this)
this.copyNoteLink = this.copyNoteLink.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
@@ -114,10 +120,10 @@ class NoteList extends React.Component {
componentDidUpdate (prevProps) {
const { location } = this.props
const { selectedNoteKeys } = this.state
const visibleNoteKeys = this.notes.map(note => `${note.storage}-${note.key}`)
const visibleNoteKeys = this.notes.map(note => note.key)
const note = this.notes[0]
const prevKey = prevProps.location.query.key
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && `${note.storage}-${note.key}`
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
if (note && location.query.key == null) {
const { router } = this.context
@@ -257,27 +263,38 @@ class NoteList extends React.Component {
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
ee.emit('top:new-note')
}
// D key
if (e.keyCode === 68) {
e.preventDefault()
this.deleteNote()
}
// E key
if (e.keyCode === 69) {
e.preventDefault()
ee.emit('detail:focus')
}
if (e.keyCode === 38) {
// F or S key
if (e.keyCode === 70 || e.keyCode === 83) {
e.preventDefault()
ee.emit('top:focus-search')
}
// UP or K key
if (e.keyCode === 38 || e.keyCode === 75) {
e.preventDefault()
this.selectPriorNote()
}
if (e.keyCode === 40) {
// DOWN or J key
if (e.keyCode === 40 || e.keyCode === 74) {
e.preventDefault()
this.selectNextNote()
}
@@ -458,6 +475,10 @@ class NoteList extends React.Component {
const deleteLabel = 'Delete Note'
const cloneNote = 'Clone Note'
const restoreNote = 'Restore Note'
const copyNoteLink = 'Copy Note Link'
const publishLabel = 'Publish Blog'
const updateLabel = 'Update Blog'
const openBlogLabel = 'Open Blog'
const menu = new Menu()
if (!location.pathname.match(/\/starred|\/trash/)) {
@@ -482,6 +503,28 @@ class NoteList extends React.Component {
label: cloneNote,
click: this.cloneNote.bind(this)
}))
menu.append(new MenuItem({
label: copyNoteLink,
click: this.copyNoteLink(note)
}))
if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) {
menu.append(new MenuItem({
label: updateLabel,
click: this.publishMarkdown.bind(this)
}))
menu.append(new MenuItem({
label: openBlogLabel,
click: () => this.openBlog.bind(this)(note)
}))
} else {
menu.append(new MenuItem({
label: publishLabel,
click: this.publishMarkdown.bind(this)
}))
}
}
menu.popup()
}
@@ -548,11 +591,9 @@ class NoteList extends React.Component {
})
if (dialogueButtonIndex === 1) return
Promise.all(
selectedNoteKeys.map((uniqueKey) => {
const storageKey = uniqueKey.split('-')[0]
const noteKey = uniqueKey.split('-')[1]
selectedNotes.map((note) => {
return dataApi
.deleteNote(storageKey, noteKey)
.deleteNote(note.storage, note.key)
})
)
.then((data) => {
@@ -613,23 +654,134 @@ class NoteList extends React.Component {
content: firstNote.content
})
.then((note) => {
const uniqueKey = note.storage + '-' + note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
this.setState({
selectedNoteKeys: [uniqueKey]
selectedNoteKeys: [note.key]
})
hashHistory.push({
pathname: location.pathname,
query: {key: uniqueKey}
query: {key: note.key}
})
})
}
copyNoteLink (note) {
const noteLink = `[${note.title}](${note.key})`
return copy(noteLink)
}
save (note) {
const { dispatch } = this.props
dataApi
.updateNote(note.storage, note.key, note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
publishMarkdown () {
if (this.pendingPublish) {
clearTimeout(this.pendingPublish)
}
this.pendingPublish = setTimeout(() => {
this.publishMarkdownNow()
}, 1000)
}
publishMarkdownNow () {
const {selectedNoteKeys} = this.state
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
const config = ConfigManager.get()
const {address, token, authMethod, username, password} = config.blog
let authToken = ''
if (authMethod === 'USER') {
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
} else {
authToken = `Bearer ${token}`
}
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
const markdown = new Markdown()
const data = {
title: firstNote.title,
content: markdown.render(contentToRender),
status: 'publish'
}
let url = ''
let method = ''
if (firstNote.blog && firstNote.blog.blogId) {
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
method = 'PUT'
} else {
url = `${address}${WP_POST_PATH}`
method = 'POST'
}
// eslint-disable-next-line no-undef
fetch(url, {
method: method,
body: JSON.stringify(data),
headers: {
'Authorization': authToken,
'Content-Type': 'application/json'
}
}).then(res => res.json())
.then(response => {
if (_.isNil(response.link) || _.isNil(response.id)) {
return Promise.reject()
}
firstNote.blog = {
blogLink: response.link,
blogId: response.id
}
this.save(firstNote)
this.confirmPublish(firstNote)
})
.catch((error) => {
console.error(error)
this.confirmPublishError()
})
}
confirmPublishError () {
const { remote } = electron
const { dialog } = remote
const alertError = {
type: 'warning',
message: 'Publish Failed',
detail: 'Check and update your blog setting and try again.',
buttons: ['Confirm']
}
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
}
confirmPublish (note) {
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Publish Succeeded',
detail: `${note.title} is published at ${note.blog.blogLink}`,
buttons: ['Confirm', 'Open Blog']
})
if (buttonIndex === 1) {
this.openBlog(note)
}
}
openBlog (note) {
const { shell } = electron
shell.openExternal(note.blog.blogLink)
}
importFromFile () {
const options = {
filters: [
@@ -844,13 +996,13 @@ class NoteList extends React.Component {
value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)}
>
<option title='Sort by update time' value='UPDATED_AT'>Updated</option>
<option title='Sort by create time' value='CREATED_AT'>Created</option>
<option title='Sort alphabetically' value='ALPHABETICAL'>Alphabetically</option>
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
</select>
</div>
<div styleName='control-button-area'>
<button title='Default View' styleName={config.listStyle === 'DEFAULT'
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
? 'control-button--active'
: 'control-button'
}
@@ -858,7 +1010,7 @@ class NoteList extends React.Component {
>
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
</button>
<button title='Compressed View' styleName={config.listStyle === 'SMALL'
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active'
: 'control-button'
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({
onClick, isTagActive
@@ -12,7 +13,7 @@ const ListButton = ({
: '../resources/icon/icon-list-active.svg'
}
/>
<span styleName='tooltip'>Notes</span>
<span styleName='tooltip'>{i18n.__('Notes')}</span>
</button>
)

View File

@@ -2,13 +2,14 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>Preferences</span>
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>
)

View File

@@ -9,6 +9,8 @@ import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem'
import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { Menu, dialog } = remote
@@ -43,9 +45,9 @@ class StorageItem extends React.Component {
handleUnlinkStorageClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Unlink Storage'),
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
@@ -159,9 +161,9 @@ class StorageItem extends React.Component {
handleFolderDeleteClick (e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete Folder',
detail: 'This will delete all notes in the folder and can not be undone.',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Delete Folder'),
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
@@ -191,33 +193,16 @@ class StorageItem extends React.Component {
dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder)
if (noteData.length === 0) return
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
Promise.all(
newNoteData.map((note) => dataApi.createNote(storage.key, note))
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
)
.then((createdNoteData) => {
createdNoteData.forEach((note) => {
createdNoteData.forEach((newNote) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
})
.catch((err) => {
console.error(`error on create notes: ${err}`)
})
.then(() => {
return Promise.all(
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
)
})
.then((deletedNoteData) => {
deletedNoteData.forEach((note) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: note.storageKey,
noteKey: note.noteKey
type: 'MOVE_NOTE',
originNote: noteData.find((note) => note.content === newNote.content),
note: newNote
})
})
})
@@ -236,7 +221,8 @@ class StorageItem extends React.Component {
render () {
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const folderList = storage.folders.map((folder) => {
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
@@ -250,8 +236,9 @@ class StorageItem extends React.Component {
noteCount = noteSet.size - trashedNoteCount
}
return (
<StorageItemChild
<SortableStorageItemChild
key={folder.key}
index={index}
isActive={isActive}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
@@ -273,9 +260,9 @@ class StorageItem extends React.Component {
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}
? 'header--active'
: 'header'
}
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
>
<button styleName='header-toggleButton'
@@ -284,7 +271,7 @@ class StorageItem extends React.Component {
<img src={this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
}
/>
</button>

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({
onClick, isTagActive
@@ -12,7 +13,7 @@ const TagButton = ({
: '../resources/icon/icon-tag.svg'
}
/>
<span styleName='tooltip'>Tags</span>
<span styleName='tooltip'>{i18n.__('Tags')}</span>
</button>
)

View File

@@ -17,6 +17,7 @@ import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton'
import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
@@ -68,6 +69,17 @@ class SideNav extends React.Component {
router.push('/alltags')
}
onSortEnd (storage) {
return ({oldIndex, newIndex}) => {
const { dispatch } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
}
}
SideNavComponent (isFolded, storageList) {
const { location, data } = this.props
@@ -117,9 +129,9 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location } = this.props
const tagList = data.tagNoteMap.map((tag, name) => {
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
return { name, size: tag.size }
})
}), ['name'])
return (
tagList.map(tag => {
return (
@@ -148,10 +160,8 @@ class SideNav extends React.Component {
emptyTrash (entries) {
const { dispatch } = this.props
const deletionPromises = entries.map((storageAndNoteKey) => {
const storageKey = storageAndNoteKey.split('-')[0]
const noteKey = storageAndNoteKey.split('-')[1]
return dataApi.deleteNote(storageKey, noteKey)
const deletionPromises = entries.map((note) => {
return dataApi.deleteNote(note.storage, note.key)
})
Promise.all(deletionPromises)
.then((arrayOfStorageAndNoteKeys) => {
@@ -167,9 +177,9 @@ class SideNav extends React.Component {
handleFilterButtonContextMenu (event) {
const { data } = this.props
const entries = data.trashedSet.toJS()
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
const menu = Menu.buildFromTemplate([
{ label: 'Empty Trash', click: () => this.emptyTrash(entries) }
{ label: 'Empty Trash', click: () => this.emptyTrash(trashedNotes) }
])
menu.popup()
}
@@ -180,13 +190,16 @@ class SideNav extends React.Component {
const isFolded = config.isSideNavFolded
const storageList = data.storageMap.map((storage, key) => {
return <StorageItem
const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/>
})
const style = {}

View File

@@ -21,20 +21,19 @@
color white
.zoom
display none
// navButtonColor()
// color rgba(0,0,0,.54)
// height 20px
// display flex
// padding 0
// align-items center
// background-color transparent
// &:hover
// color $ui-active-color
// &:active
// color $ui-active-color
// span
// margin-left 5px
navButtonColor()
color rgba(0,0,0,.54)
height 20px
display flex
padding 0
align-items center
background-color transparent
&:hover
color $ui-active-color
&:active
color $ui-active-color
span
margin-left 5px
.update
navButtonColor()

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { remote, ipcRenderer } = electron
@@ -14,9 +15,9 @@ class StatusBar extends React.Component {
updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be installed.'),
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
})
if (index === 0) {
@@ -62,7 +63,7 @@ class StatusBar extends React.Component {
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
</button>
: null
}

View File

@@ -40,6 +40,32 @@ $control-height = 34px
padding-bottom 2px
background-color $ui-noteList-backgroundColor
.control-search-input-clear
height 16px
width 16px
position absolute
right 40px
top 10px
z-index 300
border none
background-color transparent
color #999
&:hover .control-search-input-clear-tooltip
opacity 1
.control-search-input-clear-tooltip
tooltip()
position fixed
pointer-events none
top 50px
left 433px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-search-optionList
position fixed
z-index 200
@@ -207,4 +233,4 @@ body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-noteList-backgroundColor
input
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
color $ui-solarized-dark-text-color

View File

@@ -5,6 +5,7 @@ import styles from './TopBar.styl'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n'
class TopBar extends React.Component {
constructor (props) {
@@ -36,6 +37,17 @@ class TopBar extends React.Component {
ee.off('code:init', this.codeInitHandler)
}
handleSearchClearButton (e) {
const { router } = this.context
this.setState({
search: '',
isSearching: false
})
this.refs.search.childNodes[0].blur
router.push('/searched')
e.preventDefault()
}
handleKeyDown (e) {
// reset states
this.setState({
@@ -43,6 +55,23 @@ class TopBar extends React.Component {
isIME: false
})
// Clear search on ESC
if (e.keyCode === 27) {
return this.handleSearchClearButton(e)
}
// Next note on DOWN key
if (e.keyCode === 40) {
ee.emit('list:next')
e.preventDefault()
}
// Prev note on UP key
if (e.keyCode === 38) {
ee.emit('list:prior')
e.preventDefault()
}
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
@@ -114,10 +143,12 @@ class TopBar extends React.Component {
}
handleOnSearchFocus () {
const el = this.refs.search.childNodes[0]
if (this.state.isSearching) {
this.refs.search.childNodes[0].blur()
el.blur()
} else {
this.refs.search.childNodes[0].focus()
el.focus()
el.setSelectionRange(0, el.value.length)
}
}
@@ -146,19 +177,19 @@ class TopBar extends React.Component {
onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
placeholder='Search'
placeholder={i18n.__('Search')}
type='text'
className='searchInput'
/>
{this.state.search !== '' &&
<button styleName='control-search-input-clear'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
</button>
}
</div>
{this.state.search > 0 &&
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-times' />
</button>
}
</div>
</div>
{location.pathname === '/trashed' ? ''

View File

@@ -8,6 +8,7 @@ import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-rou
import { syncHistoryWithStore } from 'react-router-redux'
require('./lib/ipcClient')
require('../lib/customMeta')
import i18n from 'browser/lib/i18n'
const electron = require('electron')
@@ -46,9 +47,9 @@ function notify (...args) {
function updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be installed.'),
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
})
if (index === 0) {

View File

@@ -1,5 +1,6 @@
import _ from 'lodash'
import RcParser from 'browser/lib/RcParser'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32'
@@ -21,6 +22,7 @@ export const DEFAULT_CONFIG = {
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
},
ui: {
language: 'en',
theme: 'default',
showCopyNotification: true,
disableDirectWrite: false,
@@ -48,7 +50,16 @@ export const DEFAULT_CONFIG = {
latexInlineClose: '$',
latexBlockOpen: '$$',
latexBlockClose: '$$',
scrollPastEnd: false
scrollPastEnd: false,
smartQuotes: true
},
blog: {
type: 'wordpress', // Available value: wordpress, add more types in the future plz
address: 'http://wordpress.com/wp-json',
authMethod: 'JWT', // Available value: JWT, USER
token: '',
username: '',
password: ''
}
}
@@ -124,6 +135,36 @@ function set (updates) {
document.body.setAttribute('data-theme', 'default')
}
if (newConfig.ui.language === 'sq') {
i18n.setLocale('sq')
} else if (newConfig.ui.language === 'zh-CN') {
i18n.setLocale('zh-CN')
} else if (newConfig.ui.language === 'zh-TW') {
i18n.setLocale('zh-TW')
} else if (newConfig.ui.language === 'da') {
i18n.setLocale('da')
} else if (newConfig.ui.language === 'fr') {
i18n.setLocale('fr')
} else if (newConfig.ui.language === 'de') {
i18n.setLocale('de')
} else if (newConfig.ui.language === 'ja') {
i18n.setLocale('ja')
} else if (newConfig.ui.language === 'ko') {
i18n.setLocale('ko')
} else if (newConfig.ui.language === 'no') {
i18n.setLocale('no')
} else if (newConfig.ui.language === 'pl') {
i18n.setLocale('pl')
} else if (newConfig.ui.language === 'pt') {
i18n.setLocale('pt')
} else if (newConfig.ui.language === 'ru') {
i18n.setLocale('ru')
} else if (newConfig.ui.language === 'es') {
i18n.setLocale('es')
} else {
i18n.setLocale('en')
}
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
@@ -151,6 +192,7 @@ function set (updates) {
function assignConfigValues (originalConfig, rcConfig) {
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)

View File

@@ -3,19 +3,20 @@ const path = require('path')
const { findStorage } = require('browser/lib/findStorage')
/**
* @description To copy an image and return the path.
* @description Copy an image and return the path.
* @param {String} filePath
* @param {String} storageKey
* @return {String} an image path
* @param {Boolean} rename create new filename or leave the old one
* @return {Promise<any>} an image path
*/
function copyImage (filePath, storageKey) {
function copyImage (filePath, storageKey, rename = true) {
return new Promise((resolve, reject) => {
try {
const targetStorage = findStorage(storageKey)
const inputImage = fs.createReadStream(filePath)
const imageExt = path.extname(filePath)
const imageName = Math.random().toString(36).slice(-16)
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
const basename = `${imageName}${imageExt}`
const imageDir = path.join(targetStorage.path, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)

View File

@@ -52,12 +52,12 @@ function createNote (storageKey, input) {
return storage
})
.then(function saveNote (storage) {
let key = keygen()
let key = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
key = keygen()
key = keygen(true)
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true

View File

@@ -4,7 +4,7 @@ import {findStorage} from 'browser/lib/findStorage'
const fs = require('fs')
const path = require('path')
const LOCAL_STORED_REGEX = /!\[(.*?)\]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const IMAGES_FOLDER_NAME = 'images'
/**

View File

@@ -1,10 +1,12 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const copyImage = require('./copyImage')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage
@@ -37,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage
newNoteKey = keygen()
newNoteKey = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen()
newNoteKey = keygen(true)
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return noteData
})
.then(function moveImages (noteData) {
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
let match = searchImagesRegex.exec(noteData.content)
const moveTasks = []
while (match != null) {
const [, filename] = match
const oldPath = path.join(oldStorage.path, 'images', filename)
moveTasks.push(
copyImage(oldPath, noteData.storage, false)
.then(() => {
fs.unlinkSync(oldPath)
})
)
// find next occurence
match = searchImagesRegex.exec(noteData.content)
}
return Promise.all(moveTasks).then(() => noteData)
})
.then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData

View File

@@ -30,6 +30,9 @@ function validateInput (input) {
validatedInput.isPinned = !!input.isPinned
}
if (!_.isNil(input.blog)) {
validatedInput.blog = input.blog
}
validatedInput.type = input.type
switch (input.type) {
case 'MARKDOWN_NOTE':

View File

@@ -6,6 +6,7 @@ import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import ModalEscButton from 'browser/components/ModalEscButton'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
class NewNoteModal extends React.Component {
constructor (props) {
@@ -35,7 +36,7 @@ class NewNoteModal extends React.Component {
content: ''
})
.then((note) => {
const noteHash = `${note.storage}-${note.key}`
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
@@ -75,7 +76,7 @@ class NewNoteModal extends React.Component {
}]
})
.then((note) => {
const noteHash = `${note.storage}-${note.key}`
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
@@ -122,8 +123,8 @@ class NewNoteModal extends React.Component {
<i styleName='control-button-icon'
className='fa fa-file-text-o'
/><br />
<span styleName='control-button-label'>Markdown Note</span><br />
<span styleName='control-button-description'>This format is for creating text documents. Checklists, code blocks and Latex blocks are available.</span>
<span styleName='control-button-label'>{i18n.__('Markdown Note')}</span><br />
<span styleName='control-button-description'>{i18n.__('This format is for creating text documents. Checklists, code blocks and Latex blocks are available.')}</span>
</button>
<button styleName='control-button'
@@ -134,13 +135,13 @@ class NewNoteModal extends React.Component {
<i styleName='control-button-icon'
className='fa fa-code'
/><br />
<span styleName='control-button-label'>Snippet Note</span><br />
<span styleName='control-button-description'>This format is for creating code snippets. Multiple snippets can be grouped into a single note.
<span styleName='control-button-label'>{i18n.__('Snippet Note')}</span><br />
<span styleName='control-button-description'>{i18n.__('This format is for creating code snippets. Multiple snippets can be grouped into a single note.')}
</span>
</button>
</div>
<div styleName='description'><i className='fa fa-arrows-h' /> Tab to switch format</div>
<div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div>
</div>
)

View File

@@ -0,0 +1,199 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import PropTypes from 'prop-types'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { shell } = electron
const ipc = electron.ipcRenderer
class Blog extends React.Component {
constructor (props) {
super(props)
this.state = {
config: props.config,
BlogAlert: null
}
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
clearMessage () {
_.debounce(() => {
this.setState({
BlogAlert: null
})
}, 2000)()
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({BlogAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}})
}
this.handleSettingError = (err) => {
this.setState({BlogAlert: {
type: 'error',
message: err.message != null ? err.message : 'Error occurs!'
}})
}
this.oldBlog = this.state.config.blog
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
handleBlogChange (e) {
const { config } = this.state
config.blog = {
password: !_.isNil(this.refs.passwordInput) ? this.refs.passwordInput.value : config.blog.password,
username: !_.isNil(this.refs.usernameInput) ? this.refs.usernameInput.value : config.blog.username,
token: !_.isNil(this.refs.tokenInput) ? this.refs.tokenInput.value : config.blog.token,
authMethod: this.refs.authMethodDropdown.value,
address: this.refs.addressInput.value,
type: this.refs.typeDropdown.value
}
this.setState({
config
})
if (_.isEqual(this.oldBlog, config.blog)) {
this.props.haveToSave()
} else {
this.props.haveToSave({
tab: 'Blog',
type: 'warning',
message: 'You have to save!'
})
}
}
handleSaveButtonClick (e) {
const newConfig = {
blog: this.state.config.blog
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
this.clearMessage()
this.props.haveToSave()
}
render () {
const {config, BlogAlert} = this.state
const blogAlertElement = BlogAlert != null
? <p className={`alert ${BlogAlert.type}`}>
{BlogAlert.message}
</p>
: null
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>{i18n.__('Blog')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Blog Type')}
</div>
<div styleName='group-section-control'>
<select
value={config.blog.type}
ref='typeDropdown'
onChange={(e) => this.handleBlogChange(e)}
>
<option value='wordpress' key='wordpress'>wordpress</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Blog Address')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='addressInput'
value={config.blog.address}
type='text'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
</button>
{blogAlertElement}
</div>
</div>
<div styleName='group-header2'>{i18n.__('Auth')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Authentication Method')}
</div>
<div styleName='group-section-control'>
<select
value={config.blog.authMethod}
ref='authMethodDropdown'
onChange={(e) => this.handleBlogChange(e)}
>
<option value='JWT' key='JWT'>{i18n.__('JWT')}</option>
<option value='USER' key='USER'>{i18n.__('USER')}</option>
</select>
</div>
</div>
{ config.blog.authMethod === 'JWT' &&
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Token')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='tokenInput'
value={config.blog.token}
type='text' />
</div>
</div>
}
{ config.blog.authMethod === 'USER' &&
<div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('UserName')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='usernameInput'
value={config.blog.username}
type='text' />
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Password')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='passwordInput'
value={config.blog.password}
type='password' />
</div>
</div>
</div>
}
</div>
)
}
}
Blog.propTypes = {
dispatch: PropTypes.func,
haveToSave: PropTypes.func
}
export default CSSModules(Blog, styles)

View File

@@ -1,6 +1,7 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Crowdfunding.styl'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { shell } = electron
@@ -21,22 +22,22 @@ class Crowdfunding extends React.Component {
render () {
return (
<div styleName='root'>
<div styleName='header'>Crowdfunding</div>
<p>Dear everyone,</p>
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
<p>{i18n.__('Dear everyone,')}</p>
<br />
<p>Thank you for using Boostnote!</p>
<p>Boostnote is used in about 200 different countries and regions by an awesome community of developers.</p>
<p>{i18n.__('Thank you for using Boostnote!')}</p>
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
<br />
<p>To continue supporting this growth, and to satisfy community expectations,</p>
<p>we would like to invest more time and resources in this project.</p>
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
<br />
<p>If you like this project and see its potential, you can help by supporting us on OpenCollective!</p>
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
<br />
<p>Thanks,</p>
<p>Boostnote maintainers</p>
<p>{i18n.__('Thanks,')}</p>
<p>{i18n.__('Boostnote maintainers')}</p>
<br />
<button styleName='cf-link'>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>Support via OpenCollective</a>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
</button>
</div>
)

View File

@@ -7,6 +7,7 @@ import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { SketchPicker } from 'react-color'
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
class FolderItem extends React.Component {
constructor (props) {
@@ -179,18 +180,18 @@ class FolderItem extends React.Component {
return (
<div styleName='folderItem'>
<div styleName='folderItem-left'>
Are you sure to <span styleName='folderItem-left-danger'>delete</span> this folder?
{i18n.__('Are you sure to ')} <span styleName='folderItem-left-danger'>{i18n.__(' delete')}</span> {i18n.__('this folder?')}
</div>
<div styleName='folderItem-right'>
<button styleName='folderItem-right-dangerButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>
Confirm
{i18n.__('Confirm')}
</button>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
{i18n.__('Cancel')}
</button>
</div>
</div>
@@ -231,12 +232,12 @@ class FolderItem extends React.Component {
<button styleName='folderItem-right-button'
onClick={(e) => this.handleEditButtonClick(e)}
>
Edit
{i18n.__('Edit')}
</button>
<button styleName='folderItem-right-button'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
Delete
{i18n.__('Delete')}
</button>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const ipc = electron.ipcRenderer
@@ -23,7 +24,7 @@ class HotkeyTab extends React.Component {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully applied!'
message: i18n.__('Successfully applied!')
}})
}
this.handleSettingError = (err) => {
@@ -77,7 +78,7 @@ class HotkeyTab extends React.Component {
this.props.haveToSave({
tab: 'Hotkey',
type: 'warning',
message: 'You have to save!'
message: i18n.__('You have to save!')
})
}
}
@@ -102,9 +103,9 @@ class HotkeyTab extends React.Component {
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>Hotkeys</div>
<div styleName='group-header'>{i18n.__('Hotkeys')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Show/Hide Boostnote</div>
<div styleName='group-section-label'>{i18n.__('Show/Hide Boostnote')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
@@ -124,7 +125,7 @@ class HotkeyTab extends React.Component {
}
</button>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>Save
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
</button>
{keymapAlertElement}
</div>

View File

@@ -5,6 +5,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { shell, remote } = electron
@@ -38,11 +39,11 @@ class InfoTab extends React.Component {
if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
this.setState({
amaMessage: 'We hope we will gain your trust'
amaMessage: i18n.__('We hope we will gain your trust')
})
} else {
this.setState({
amaMessage: 'Thank\'s for trust us'
amaMessage: i18n.__('Thank\'s for trusting us')
})
}
@@ -69,48 +70,48 @@ class InfoTab extends React.Component {
return (
<div styleName='root'>
<div styleName='header--sub'>Community</div>
<div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='top'>
<ul styleName='list'>
<li>
<a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)}
>Subscribe to Newsletter</a>
>{i18n.__('Subscribe to Newsletter')}</a>
</li>
<li>
<a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)}
>GitHub</a>
>{i18n.__('GitHub')}</a>
</li>
<li>
<a href='https://boostlog.io/@junp1234'
onClick={(e) => this.handleLinkClick(e)}
>Blog</a>
>{i18n.__('Blog')}</a>
</li>
<li>
<a href='https://www.facebook.com/groups/boostnote'
onClick={(e) => this.handleLinkClick(e)}
>Facebook Group</a>
>{i18n.__('Facebook Group')}</a>
</li>
<li>
<a href='https://twitter.com/boostnoteapp'
onClick={(e) => this.handleLinkClick(e)}
>Twitter</a>
>{i18n.__('Twitter')}</a>
</li>
</ul>
</div>
<hr />
<div styleName='header--sub'>About</div>
<div styleName='header--sub'>{i18n.__('About')}</div>
<div styleName='top'>
<div styleName='icon-space'>
<img styleName='icon' src='../resources/app.png' width='92' height='92' />
<div styleName='icon-right'>
<div styleName='appId'>Boostnote {appVersion}</div>
<div styleName='appId'>{i18n.__('Boostnote')} {appVersion}</div>
<div styleName='description'>
An open source note-taking app made for programmers just like you.
{i18n.__('An open source note-taking app made for programmers just like you.')}
</div>
</div>
</div>
@@ -120,35 +121,35 @@ class InfoTab extends React.Component {
<li>
<a href='https://boostnote.io'
onClick={(e) => this.handleLinkClick(e)}
>Website</a>
>{i18n.__('Website')}</a>
</li>
<li>
<a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md'
onClick={(e) => this.handleLinkClick(e)}
>Development</a> : Development configurations for Boostnote.
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
</li>
<li styleName='cc'>
Copyright (C) 2017 - 2018 BoostIO
{i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
</li>
<li styleName='cc'>
License: GPL v3
{i18n.__('License: GPL v3')}
</li>
</ul>
<hr styleName='separate-line' />
<div styleName='policy'>Analytics</div>
<div>Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.</div>
<div>You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<div styleName='policy'>{i18n.__('Analytics')}</div>
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<br />
<div>You can choose to enable or disable this option.</div>
<div>{i18n.__('You can choose to enable or disable this option.')}</div>
<input onChange={(e) => this.handleConfigChange(e)}
checked={this.state.config.amaEnabled}
ref='amaEnabled'
type='checkbox'
/>
Enable analytics to help improve Boostnote<br />
<button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>Save</button>
{i18n.__('Enable analytics to help improve Boostnote')}<br />
<button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}</button>
<br />
{this.infoMessage()}
</div>

View File

@@ -6,6 +6,7 @@ import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import FolderList from './FolderList'
import i18n from 'browser/lib/i18n'
const { shell, remote } = require('electron')
const { dialog } = remote
@@ -46,9 +47,9 @@ class StorageItem extends React.Component {
handleUnlinkButtonClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.',
buttons: ['Unlink', 'Cancel']
message: i18n.__('Unlink Storage'),
detail: i18n.__('Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.'),
buttons: [i18n.__('Unlink'), i18n.__('Cancel')]
})
if (index === 0) {
@@ -127,7 +128,7 @@ class StorageItem extends React.Component {
<i className='fa fa-plus' />
<span styleName='header-control-button-tooltip'
style={{left: -20}}
>Add Folder</span>
>{i18n.__('Add Folder')}</span>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleExternalButtonClick(e)}
@@ -135,7 +136,7 @@ class StorageItem extends React.Component {
<i className='fa fa-external-link' />
<span styleName='header-control-button-tooltip'
style={{left: -50}}
>Open Storage folder</span>
>{i18n.__('Open Storage folder')}</span>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleUnlinkButtonClick(e)}
@@ -143,7 +144,7 @@ class StorageItem extends React.Component {
<i className='fa fa-unlink' />
<span styleName='header-control-button-tooltip'
style={{left: -10}}
>Unlink</span>
>{i18n.__('Unlink')}</span>
</button>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi'
import StorageItem from './StorageItem'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { shell, remote } = electron
@@ -69,16 +70,16 @@ class StoragesTab extends React.Component {
})
return (
<div styleName='list'>
<div styleName='header'>Storages</div>
<div styleName='header'>{i18n.__('Storages')}</div>
{storageList.length > 0
? storageList
: <div styleName='list-empty'>No storage found.</div>
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>
}
<div styleName='list-control'>
<button styleName='list-control-addStorageButton'
onClick={(e) => this.handleAddStorageButton(e)}
>
<i className='fa fa-plus' /> Add Storage Location
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
</button>
</div>
</div>
@@ -140,13 +141,13 @@ class StoragesTab extends React.Component {
return (
<div styleName='addStorage'>
<div styleName='addStorage-header'>Add Storage</div>
<div styleName='addStorage-header'>{i18n.__('Add Storage')}</div>
<div styleName='addStorage-body'>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>
Name
{i18n.__('Name')}
</div>
<div styleName='addStorage-body-section-name'>
<input styleName='addStorage-body-section-name-input'
@@ -158,25 +159,25 @@ class StoragesTab extends React.Component {
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Type</div>
<div styleName='addStorage-body-section-label'>{i18n.__('Type')}</div>
<div styleName='addStorage-body-section-type'>
<select styleName='addStorage-body-section-type-select'
value={this.state.newStorage.type}
readOnly
>
<option value='FILESYSTEM'>File System</option>
<option value='FILESYSTEM'>{i18n.__('File System')}</option>
</select>
<div styleName='addStorage-body-section-type-description'>
Setting up 3rd-party cloud storage integration:{' '}
{i18n.__('Setting up 3rd-party cloud storage integration:')}{' '}
<a href='https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup'
onClick={(e) => this.handleLinkClick(e)}
>Cloud-Syncing-and-Backup</a>
>{i18n.__('Cloud-Syncing-and-Backup')}</a>
</div>
</div>
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Location
<div styleName='addStorage-body-section-label'>{i18n.__('Location')}
</div>
<div styleName='addStorage-body-section-path'>
<input styleName='addStorage-body-section-path-input'
@@ -196,10 +197,10 @@ class StoragesTab extends React.Component {
<div styleName='addStorage-body-control'>
<button styleName='addStorage-body-control-createButton'
onClick={(e) => this.handleAddStorageCreateButton(e)}
>Add</button>
>{i18n.__('Add')}</button>
<button styleName='addStorage-body-control-cancelButton'
onClick={(e) => this.handleAddStorageCancelButton(e)}
>Cancel</button>
>{i18n.__('Cancel')}</button>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import ReactCodeMirror from 'react-codemirror'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
@@ -29,7 +30,7 @@ class UiTab extends React.Component {
this.handleSettingDone = () => {
this.setState({UiAlert: {
type: 'success',
message: 'Successfully applied!'
message: i18n.__('Successfully applied!')
}})
}
this.handleSettingError = (err) => {
@@ -61,6 +62,7 @@ class UiTab extends React.Component {
const newConfig = {
ui: {
theme: this.refs.uiTheme.value,
language: this.refs.uiLanguage.value,
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
disableDirectWrite: this.refs.uiD2w != null
@@ -88,7 +90,8 @@ class UiTab extends React.Component {
latexInlineClose: this.refs.previewLatexInlineClose.value,
latexBlockOpen: this.refs.previewLatexBlockOpen.value,
latexBlockClose: this.refs.previewLatexBlockClose.value,
scrollPastEnd: this.refs.previewScrollPastEnd.checked
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
smartQuotes: this.refs.previewSmartQuotes.checked
}
}
@@ -106,7 +109,7 @@ class UiTab extends React.Component {
this.props.haveToSave({
tab: 'UI',
type: 'warning',
message: 'You have to save!'
message: i18n.__('You have to save!')
})
}
})
@@ -151,22 +154,48 @@ class UiTab extends React.Component {
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>Interface</div>
<div styleName='group-header'>{i18n.__('Interface')}</div>
<div styleName='group-section'>
Interface Theme
{i18n.__('Interface Theme')}
<div styleName='group-section-control'>
<select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)}
ref='uiTheme'
>
<option value='default'>Default</option>
<option value='white'>White</option>
<option value='solarized-dark'>Solarized Dark</option>
<option value='dark'>Dark</option>
<option value='default'>{i18n.__('Default')}</option>
<option value='white'>{i18n.__('White')}</option>
<option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
<option value='dark'>{i18n.__('Dark')}</option>
</select>
</div>
</div>
<div styleName='group-section'>
{i18n.__('Language')}
<div styleName='group-section-control'>
<select value={config.ui.language}
onChange={(e) => this.handleUIChange(e)}
ref='uiLanguage'
>
<option value='sq'>{i18n.__('Albanian')}</option>
<option value='zh-CN'>{i18n.__('Chinese (zh-CN)')}</option>
<option value='zh-TW'>{i18n.__('Chinese (zh-TW)')}</option>
<option value='da'>{i18n.__('Danish')}</option>
<option value='en'>{i18n.__('English')}</option>
<option value='fr'>{i18n.__('French')}</option>
<option value='de'>{i18n.__('German')}</option>
<option value='ja'>{i18n.__('Japanese')}</option>
<option value='ko'>{i18n.__('Korean')}</option>
<option value='no'>{i18n.__('Norwegian')}</option>
<option value='pl'>{i18n.__('Polish')}</option>
<option value='pt'>{i18n.__('Portuguese')}</option>
<option value='ru'>{i18n.__('Russian')}</option>
<option value='es'>{i18n.__('Spanish')}</option>
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
@@ -174,7 +203,7 @@ class UiTab extends React.Component {
ref='showCopyNotification'
type='checkbox'
/>&nbsp;
Show &quot;Saved to Clipboard&quot; notification when copying
{i18n.__('Show "Saved to Clipboard" notification when copying')}
</label>
</div>
<div styleName='group-checkBoxSection'>
@@ -184,7 +213,7 @@ class UiTab extends React.Component {
ref='confirmDeletion'
type='checkbox'
/>&nbsp;
Show a confirmation dialog when deleting notes
{i18n.__('Show a confirmation dialog when deleting notes')}
</label>
</div>
{
@@ -206,7 +235,7 @@ class UiTab extends React.Component {
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Theme
{i18n.__('Editor Theme')}
</div>
<div styleName='group-section-control'>
<select value={config.editor.theme}
@@ -226,7 +255,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Size
{i18n.__('Editor Font Size')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -239,7 +268,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Family
{i18n.__('Editor Font Family')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -252,7 +281,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Indent Style
{i18n.__('Editor Indent Style')}
</div>
<div styleName='group-section-control'>
<select value={config.editor.indentSize}
@@ -268,42 +297,42 @@ class UiTab extends React.Component {
ref='editorIndentType'
onChange={(e) => this.handleUIChange(e)}
>
<option value='space'>Spaces</option>
<option value='tab'>Tabs</option>
<option value='space'>{i18n.__('Spaces')}</option>
<option value='tab'>{i18n.__('Tabs')}</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Switch to Preview
{i18n.__('Switch to Preview')}
</div>
<div styleName='group-section-control'>
<select value={config.editor.switchPreview}
ref='editorSwitchPreview'
onChange={(e) => this.handleUIChange(e)}
>
<option value='BLUR'>When Editor Blurred</option>
<option value='DBL_CLICK'>When Editor Blurred, Edit On Double Click</option>
<option value='RIGHTCLICK'>On Right Click</option>
<option value='BLUR'>{i18n.__('When Editor Blurred')}</option>
<option value='DBL_CLICK'>{i18n.__('When Editor Blurred, Edit On Double Click')}</option>
<option value='RIGHTCLICK'>{i18n.__('On Right Click')}</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Keymap
{i18n.__('Editor Keymap')}
</div>
<div styleName='group-section-control'>
<select value={config.editor.keyMap}
ref='editorKeyMap'
onChange={(e) => this.handleUIChange(e)}
>
<option value='sublime'>default</option>
<option value='vim'>vim</option>
<option value='emacs'>emacs</option>
<option value='sublime'>{i18n.__('default')}</option>
<option value='vim'>{i18n.__('vim')}</option>
<option value='emacs'>{i18n.__('emacs')}</option>
</select>
<p styleName='note-for-keymap'> Please restart boostnote after you change the keymap</p>
<p styleName='note-for-keymap'>{i18n.__('⚠️ Please restart boostnote after you change the keymap')}</p>
</div>
</div>
@@ -314,7 +343,7 @@ class UiTab extends React.Component {
ref='editorDisplayLineNumbers'
type='checkbox'
/>&nbsp;
Show line numbers in the editor
{i18n.__('Show line numbers in the editor')}
</label>
</div>
@@ -325,7 +354,7 @@ class UiTab extends React.Component {
ref='scrollPastEnd'
type='checkbox'
/>&nbsp;
Allow editor to scroll past the last line
{i18n.__('Allow editor to scroll past the last line')}
</label>
</div>
@@ -336,14 +365,14 @@ class UiTab extends React.Component {
ref='editorFetchUrlTitle'
type='checkbox'
/>&nbsp;
Bring in web page title when pasting URL on editor
{i18n.__('Bring in web page title when pasting URL on editor')}
</label>
</div>
<div styleName='group-header2'>Preview</div>
<div styleName='group-header2'>{i18n.__('Preview')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Size
{i18n.__('Preview Font Size')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -356,7 +385,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Family
{i18n.__('Preview Font Family')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -368,7 +397,7 @@ class UiTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Code block Theme</div>
<div styleName='group-section-label'>{i18n.__('Code block Theme')}</div>
<div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme'
@@ -389,7 +418,7 @@ class UiTab extends React.Component {
ref='previewScrollPastEnd'
type='checkbox'
/>&nbsp;
Allow preview to scroll past the last line
{i18n.__('Allow preview to scroll past the last line')}
</label>
</div>
<div styleName='group-checkBoxSection'>
@@ -399,12 +428,22 @@ class UiTab extends React.Component {
ref='previewLineNumber'
type='checkbox'
/>&nbsp;
Show line numbers for preview code blocks
{i18n.__('Show line numbers for preview code blocks')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.smartQuotes}
ref='previewSmartQuotes'
type='checkbox'
/>&nbsp;
Enable smart quotes
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Inline Open Delimiter
{i18n.__('LaTeX Inline Open Delimiter')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -417,7 +456,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Inline Close Delimiter
{i18n.__('LaTeX Inline Close Delimiter')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -430,7 +469,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Block Open Delimiter
{i18n.__('LaTeX Block Open Delimiter')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -443,7 +482,7 @@ class UiTab extends React.Component {
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Block Close Delimiter
{i18n.__('LaTeX Block Close Delimiter')}
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
@@ -457,7 +496,7 @@ class UiTab extends React.Component {
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}>Save
onClick={(e) => this.handleSaveUIClick(e)}>{i18n.__('Save')}
</button>
{UiAlertElement}
</div>

View File

@@ -6,11 +6,13 @@ import UiTab from './UiTab'
import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab'
import Blog from './Blog'
import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferencesModal.styl'
import RealtimeNotification from 'browser/components/RealtimeNotification'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class Preferences extends React.Component {
constructor (props) {
@@ -19,7 +21,8 @@ class Preferences extends React.Component {
this.state = {
currentTab: 'STORAGES',
UIAlert: '',
HotkeyAlert: ''
HotkeyAlert: '',
BlogAlert: ''
}
}
@@ -75,6 +78,14 @@ class Preferences extends React.Component {
return (
<Crowdfunding />
)
case 'BLOG':
return (
<Blog
dispatch={dispatch}
config={config}
haveToSave={alert => this.setState({BlogAlert: alert})}
/>
)
case 'STORAGES':
default:
return (
@@ -107,11 +118,12 @@ class Preferences extends React.Component {
const content = this.renderContent()
const tabs = [
{target: 'STORAGES', label: 'Storage'},
{target: 'HOTKEY', label: 'Hotkeys', Hotkey: this.state.HotkeyAlert},
{target: 'UI', label: 'Interface', UI: this.state.UIAlert},
{target: 'INFO', label: 'About'},
{target: 'CROWDFUNDING', label: 'Crowdfunding'}
{target: 'STORAGES', label: i18n.__('Storage')},
{target: 'HOTKEY', label: i18n.__('Hotkeys'), Hotkey: this.state.HotkeyAlert},
{target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert},
{target: 'INFO', label: i18n.__('About')},
{target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')},
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert}
]
const navButtons = tabs.map((tab) => {
@@ -140,7 +152,7 @@ class Preferences extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='top-bar'>
<p>Your preferences for Boostnote</p>
<p>{i18n.__('Your preferences for Boostnote')}</p>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleEscButtonClick(e)} />
<div styleName='nav'>

View File

@@ -5,6 +5,7 @@ import styles from './RenameFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
class RenameFolderModal extends React.Component {
constructor (props) {
@@ -72,7 +73,7 @@ class RenameFolderModal extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>Rename Folder</div>
<div styleName='title'>{i18n.__('Rename Folder')}</div>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />

View File

@@ -27,7 +27,7 @@ function data (state = defaultDataMap(), action) {
action.notes.some((note) => {
if (note === undefined) return true
const uniqueKey = note.storage + '-' + note.key
const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
@@ -66,7 +66,7 @@ function data (state = defaultDataMap(), action) {
case 'UPDATE_NOTE':
{
const note = action.note
const uniqueKey = note.storage + '-' + note.key
const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey)
@@ -162,9 +162,9 @@ function data (state = defaultDataMap(), action) {
case 'MOVE_NOTE':
{
const originNote = action.originNote
const originKey = originNote.storage + '-' + originNote.key
const originKey = originNote.key
const note = action.note
const uniqueKey = note.storage + '-' + note.key
const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey)
@@ -297,7 +297,7 @@ function data (state = defaultDataMap(), action) {
}
case 'DELETE_NOTE':
{
const uniqueKey = action.storageKey + '-' + action.noteKey
const uniqueKey = action.noteKey
const targetNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state)
@@ -423,7 +423,7 @@ function data (state = defaultDataMap(), action) {
state.folderNoteMap = new Map(state.folderNoteMap)
state.tagNoteMap = new Map(state.tagNoteMap)
action.notes.forEach((note) => {
const uniqueKey = note.storage + '-' + note.key
const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note)
@@ -483,7 +483,7 @@ function data (state = defaultDataMap(), action) {
state.tagNoteMap = new Map(state.tagNoteMap)
state.starredSet = new Set(state.starredSet)
notes.forEach((note) => {
const noteKey = storage.key + '-' + note.key
const noteKey = note.key
state.noteMap.delete(noteKey)
state.starredSet.delete(noteKey)
note.tags.forEach((tag) => {

View File

@@ -5,7 +5,7 @@ $danger-color = #c9302c
$danger-lighten-color = lighten(#c9302c, 5%)
// Layouts
$statusBar-height = 0px
$statusBar-height = 22px
$sideNav-width = 200px
$sideNav--folded-width = 44px
$topBar-height = 60px
@@ -347,4 +347,4 @@ modalSolarizedDark()
width 100%
background-color $ui-solarized-dark-backgroundColor
overflow hidden
border-radius $modal-border-radius
border-radius $modal-border-radius