1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 01:36:22 +00:00

Merge branch 'master' into crossplatform_fullscreen_shortcuts

This commit is contained in:
William Grant
2018-04-17 18:24:35 +02:00
37 changed files with 2096 additions and 541 deletions

View File

@@ -5,7 +5,7 @@
"presets": ["react-hmre"] "presets": ["react-hmre"]
}, },
"test": { "test": {
"presets": ["react", "es2015"], "presets": ["env" ,"react", "es2015"],
"plugins": [ "plugins": [
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ] [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
] ]

View File

@@ -23,6 +23,7 @@
"lineNumber": true "lineNumber": true
}, },
"sortBy": "UPDATED_AT", "sortBy": "UPDATED_AT",
"sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false, "disableDirectWrite": false,

View File

@@ -1,10 +1,25 @@
# Current behavior
<!-- <!--
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug. Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile. If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
--> -->
# Expected behavior
# Steps to reproduce
1.
2.
3.
# Environment
- Version :
- OS Version and name :
<!-- <!--
Love Boostnote? Please consider supporting us via OpenCollective: Love Boostnote? Please consider supporting us via OpenCollective:
👉 https://opencollective.com/boostnoteio 👉 https://opencollective.com/boostnoteio
--> -->

7
__mocks__/electron.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
require: jest.genMockFunction(),
match: jest.genMockFunction(),
app: jest.genMockFunction(),
remote: jest.genMockFunction(),
dialog: jest.genMockFunction()
}

View File

@@ -109,6 +109,8 @@ export default class CodeEditor extends React.Component {
scrollPastEnd: this.props.scrollPastEnd, scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea', inputStyle: 'textarea',
dragDrop: false, dragDrop: false,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: true, autoCloseBrackets: true,
extraKeys: { extraKeys: {
Tab: function (cm) { Tab: function (cm) {
@@ -275,11 +277,16 @@ export default class CodeEditor extends React.Component {
handleDropImage (e) { handleDropImage (e) {
e.preventDefault() e.preventDefault()
const imagePath = e.dataTransfer.files[0].path const ValidImageTypes = ['image/gif', 'image/jpeg', 'image/png']
const filename = path.basename(imagePath)
copyImage(imagePath, this.props.storageKey).then((imagePath) => { const file = e.dataTransfer.files[0]
const imageMd = `![${filename}](${path.join('/:storage', imagePath)})` const filePath = file.path
const filename = path.basename(filePath)
const fileType = file['type']
copyImage(filePath, this.props.storageKey).then((imagePath) => {
var showPreview = ValidImageTypes.indexOf(fileType) > 0
const imageMd = `${showPreview ? '!' : ''}[${filename}](${path.join('/:storage', imagePath)})`
this.insertImageMd(imageMd) this.insertImageMd(imageMd)
}) })
} }

View File

@@ -13,6 +13,7 @@ import htmlTextHelper from 'browser/lib/htmlTextHelper'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote' import exportNote from 'browser/main/lib/dataApi/exportNote'
import {escapeHtmlCharacters} from 'browser/lib/utils'
const { remote } = require('electron') const { remote } = require('electron')
const { app } = remote const { app } = remote
@@ -208,7 +209,7 @@ export default class MarkdownPreview extends React.Component {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
const body = this.markdown.render(noteContent) const body = this.markdown.render(escapeHtmlCharacters(noteContent))
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach((file) => { files.forEach((file) => {
@@ -394,6 +395,9 @@ export default class MarkdownPreview extends React.Component {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el) this.fixDecodedURI(el)
el.href = this.markdown.normalizeLinkText(el.href)
if (!/\/:storage/.test(el.href)) return
el.href = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.href)))}`
el.addEventListener('click', this.anchorClickHandler) el.addEventListener('click', this.anchorClickHandler)
}) })

View File

@@ -10,8 +10,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {Array} storgaeList * @param {Array} storgaeList
*/ */
const StorageList = ({storageList}) => ( const StorageList = ({storageList, isFolded}) => (
<div styleName='storageList'> <div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : ( {storageList.length > 0 ? storageList : (
<div styleName='storgaeList-empty'>No storage mount.</div> <div styleName='storgaeList-empty'>No storage mount.</div>
)} )}

View File

@@ -4,6 +4,10 @@
top 180px top 180px
overflow-y auto overflow-y auto
.storageList-folded
@extend .storageList
width 44px
.storageList-empty .storageList-empty
padding 0 10px padding 0 10px
margin-top 15px margin-top 15px

View File

@@ -0,0 +1,23 @@
import electron from 'electron'
import i18n from 'browser/lib/i18n'
const { remote } = electron
const { dialog } = remote
export function confirmDeleteNote (confirmDeletion, permanent) {
if (confirmDeletion || permanent) {
const alertConfig = {
ype: 'warning',
message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
const dialogButtonIndex = dialog.showMessageBox(
remote.getCurrentWindow(), alertConfig
)
return dialogButtonIndex === 0
}
return true
}

View File

@@ -1,8 +1,15 @@
const path = require('path')
const { remote } = require('electron')
const { app } = remote
// load package for localization // load package for localization
const i18n = new (require('i18n-2'))({ const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale // setup some locales - other locales default to the first locale
locales: ['en', 'sq', 'zh-CN', 'zh-TW', 'da', 'fr', 'de', 'hu', 'ja', 'ko', 'no', 'pl', 'pt', 'es-ES'], locales: [ 'da', 'de', 'en', 'es-ES', 'fr', 'hu', 'ja', 'ko', 'pl', 'pt-BR', 'pt-PT', 'ru', 'sq', 'zh-CN', 'zh-TW' ],
extension: '.json', extension: '.json',
directory: process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
devMode: false devMode: false
}) })

View File

@@ -4,39 +4,28 @@ export default function searchFromNotes (notes, search) {
if (search.trim().length === 0) return [] if (search.trim().length === 0) return []
const searchBlocks = search.split(' ').filter(block => { return block !== '' }) const searchBlocks = search.split(' ').filter(block => { return block !== '' })
let foundNotes = findByWord(notes, searchBlocks[0]) let foundNotes = notes
searchBlocks.forEach((block) => { searchBlocks.forEach((block) => {
foundNotes = findByWord(foundNotes, block) foundNotes = findByWordOrTag(foundNotes, block)
if (block.match(/^#.+/)) {
foundNotes = foundNotes.concat(findByTag(notes, block))
}
}) })
return foundNotes return foundNotes
} }
function findByTag (notes, block) { function findByWordOrTag (notes, block) {
const tag = block.match(/#(.+)/)[1] let tag = block
const regExp = new RegExp(_.escapeRegExp(tag), 'i') if (tag.match(/^#.+/)) {
tag = tag.match(/#(.+)/)[1]
}
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => { return notes.filter((note) => {
if (!_.isArray(note.tags)) return false if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
return note.tags.some((_tag) => {
return _tag.match(regExp)
})
})
}
function findByWord (notes, block) {
const regExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => {
return _tag.match(regExp)
})) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(regExp) return note.description.match(wordRegExp)
} else if (note.type === 'MARKDOWN_NOTE') { } else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(regExp) return note.content.match(wordRegExp)
} }
return false return false
}) })

View File

@@ -6,6 +6,55 @@ export function lastFindInArray (array, callback) {
} }
} }
export default { export function escapeHtmlCharacters (text) {
lastFindInArray const matchHtmlRegExp = /["'&<>]/
const str = '' + text
const match = matchHtmlRegExp.exec(str)
if (!match) {
return str
}
let escape
let html = ''
let index = 0
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '&quot;'
break
case 38: // &
escape = '&amp;'
break
case 39: // '
escape = '&#39;'
break
case 60: // <
escape = '&lt;'
break
case 62: // >
escape = '&gt;'
break
default:
continue
}
if (lastIndex !== index) {
html += str.substring(lastIndex, index)
}
lastIndex = index + 1
html += escape
}
return lastIndex !== index
? html + str.substring(lastIndex, index)
: html
}
export default {
lastFindInArray,
escapeHtmlCharacters
} }

View File

@@ -28,6 +28,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags' import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
class MarkdownNoteDetail extends React.Component { class MarkdownNoteDetail extends React.Component {
constructor (props) { constructor (props) {
@@ -181,10 +182,10 @@ class MarkdownNoteDetail extends React.Component {
handleTrashButtonClick (e) { handleTrashButtonClick (e) {
const { note } = this.state const { note } = this.state
const { isTrashed } = note const { isTrashed } = note
const { confirmDeletion } = this.props const { confirmDeletion } = this.props.config.ui
if (isTrashed) { if (isTrashed) {
if (confirmDeletion(true)) { if (confirmDeleteNote(confirmDeletion, true)) {
const {note, dispatch} = this.props const {note, dispatch} = this.props
dataApi dataApi
.deleteNote(note.storage, note.key) .deleteNote(note.storage, note.key)
@@ -201,7 +202,7 @@ class MarkdownNoteDetail extends React.Component {
.then(() => ee.emit('list:next')) .then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true note.isTrashed = true
this.setState({ this.setState({
@@ -437,8 +438,7 @@ MarkdownNoteDetail.propTypes = {
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),
ignorePreviewPointerEvents: PropTypes.bool, ignorePreviewPointerEvents: PropTypes.bool
confirmDeletion: PropTypes.bool.isRequired
} }
export default CSSModules(MarkdownNoteDetail, styles) export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -27,6 +27,7 @@ import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed' import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
function pass (name) { function pass (name) {
switch (name) { switch (name) {
@@ -197,10 +198,10 @@ class SnippetNoteDetail extends React.Component {
handleTrashButtonClick (e) { handleTrashButtonClick (e) {
const { note } = this.state const { note } = this.state
const { isTrashed } = note const { isTrashed } = note
const { confirmDeletion } = this.props const { confirmDeletion } = this.props.config.ui
if (isTrashed) { if (isTrashed) {
if (confirmDeletion(true)) { if (confirmDeleteNote(confirmDeletion, true)) {
const {note, dispatch} = this.props const {note, dispatch} = this.props
dataApi dataApi
.deleteNote(note.storage, note.key) .deleteNote(note.storage, note.key)
@@ -217,7 +218,7 @@ class SnippetNoteDetail extends React.Component {
.then(() => ee.emit('list:next')) .then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true note.isTrashed = true
this.setState({ this.setState({
@@ -883,8 +884,7 @@ SnippetNoteDetail.propTypes = {
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),
ignorePreviewPointerEvents: PropTypes.bool, ignorePreviewPointerEvents: PropTypes.bool
confirmDeletion: PropTypes.bool.isRequired
} }
export default CSSModules(SnippetNoteDetail, styles) export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -33,26 +33,6 @@ class Detail extends React.Component {
ee.off('detail:delete', this.deleteHandler) ee.off('detail:delete', this.deleteHandler)
} }
confirmDeletion (permanent) {
if (this.props.config.ui.confirmDeletion || permanent) {
const electron = require('electron')
const { remote } = electron
const { dialog } = remote
const alertConfig = {
type: 'warning',
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)
return dialogueButtonIndex === 0
}
return true
}
render () { render () {
const { location, data, config } = this.props const { location, data, config } = this.props
let note = null let note = null
@@ -82,7 +62,6 @@ class Detail extends React.Component {
<SnippetNoteDetail <SnippetNoteDetail
note={note} note={note}
config={config} config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',
@@ -99,7 +78,6 @@ class Detail extends React.Component {
<MarkdownNoteDetail <MarkdownNoteDetail
note={note} note={note}
config={config} config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root' ref='root'
{..._.pick(this.props, [ {..._.pick(this.props, [
'dispatch', 'dispatch',

View File

@@ -140,43 +140,37 @@ class Main extends React.Component {
componentDidMount () { componentDidMount () {
const { dispatch, config } = this.props const { dispatch, config } = this.props
if (config.ui.theme === 'dark') { const supportedThemes = [
document.body.setAttribute('data-theme', 'dark') 'dark',
} else if (config.ui.theme === 'white') { 'white',
document.body.setAttribute('data-theme', 'white') 'solarized-dark'
} else if (config.ui.theme === 'solarized-dark') { ]
document.body.setAttribute('data-theme', 'solarized-dark')
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
document.body.setAttribute('data-theme', config.ui.theme)
} else { } else {
document.body.setAttribute('data-theme', 'default') document.body.setAttribute('data-theme', 'default')
} }
if (config.ui.language === 'sq') {
i18n.setLocale('sq') const supportedLanguages = [
} else if (config.ui.language === 'zh-CN') { 'sq',
i18n.setLocale('zh-CN') 'zh-CN',
} else if (config.ui.language === 'zh-TW') { 'zh-TW',
i18n.setLocale('zh-TW') 'da',
} else if (config.ui.language === 'da') { 'fr',
i18n.setLocale('da') 'de',
} else if (config.ui.language === 'fr') { 'hu',
i18n.setLocale('fr') 'ja',
} else if (config.ui.language === 'de') { 'ko',
i18n.setLocale('de') 'no',
} else if (config.ui.language === 'hu') { 'pl',
i18n.setLocale('hu') 'pt',
} else if (config.ui.language === 'ja') { 'ru',
i18n.setLocale('ja') 'es-ES'
} else if (config.ui.language === 'ko') { ]
i18n.setLocale('ko')
} else if (config.ui.language === 'no') { if (supportedLanguages.indexOf(config.ui.language) !== -1) {
i18n.setLocale('no') i18n.setLocale(config.ui.language)
} 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-ES') {
i18n.setLocale('es-ES')
} else { } else {
i18n.setLocale('en') i18n.setLocale('en')
} }

View File

@@ -18,6 +18,7 @@ import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown' import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { Menu, MenuItem, dialog } = remote
@@ -326,8 +327,10 @@ class NoteList extends React.Component {
} }
if (location.pathname.match(/\/searched/)) { if (location.pathname.match(/\/searched/)) {
const searchInputText = document.getElementsByClassName('searchInput')[0].value const searchInputText = params.searchword
if (searchInputText === '') { const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
if (searchInputText === undefined || searchInputText === '') {
return this.sortByPin(this.contextNotes) return this.sortByPin(this.contextNotes)
} }
return searchFromNotes(this.contextNotes, searchInputText) return searchFromNotes(this.contextNotes, searchInputText)
@@ -481,50 +484,53 @@ class NoteList extends React.Component {
const openBlogLabel = i18n.__('Open Blog') const openBlogLabel = i18n.__('Open Blog')
const menu = new Menu() const menu = new Menu()
if (!location.pathname.match(/\/starred|\/trash/)) {
menu.append(new MenuItem({
label: pinLabel,
click: this.pinToTop
}))
}
if (location.pathname.match(/\/trash/)) { if (location.pathname.match(/\/trash/)) {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: restoreNote, label: restoreNote,
click: this.restoreNote click: this.restoreNote
})) }))
} menu.append(new MenuItem({
label: deleteLabel,
menu.append(new MenuItem({ click: this.deleteNote
label: deleteLabel, }))
click: this.deleteNote } else {
})) if (!location.pathname.match(/\/starred/)) {
menu.append(new MenuItem({
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({ menu.append(new MenuItem({
label: updateLabel, label: pinLabel,
click: this.publishMarkdown.bind(this) click: this.pinToTop
}))
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.append(new MenuItem({
label: deleteLabel,
click: this.deleteNote
}))
menu.append(new MenuItem({
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() menu.popup()
} }
@@ -580,16 +586,11 @@ class NoteList extends React.Component {
const notes = this.notes.map((note) => Object.assign({}, note)) const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0] const firstNote = selectedNotes[0]
const { confirmDeletion } = this.props.config.ui
if (firstNote.isTrashed) { if (firstNote.isTrashed) {
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note' if (!confirmDeleteNote(confirmDeletion, true)) return
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Confirm note deletion'),
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (dialogueButtonIndex === 1) return
Promise.all( Promise.all(
selectedNotes.map((note) => { selectedNotes.map((note) => {
return dataApi return dataApi
@@ -610,6 +611,8 @@ class NoteList extends React.Component {
}) })
console.log('Notes were all deleted') console.log('Notes were all deleted')
} else { } else {
if (!confirmDeleteNote(confirmDeletion, false)) return
Promise.all( Promise.all(
selectedNotes.map((note) => { selectedNotes.map((note) => {
note.isTrashed = true note.isTrashed = true

View File

@@ -30,11 +30,33 @@
display flex display flex
flex-direction column flex-direction column
.tag-title .tag-control
padding-left 15px display flex
padding-bottom 13px height 30px
p line-height 25px
color $ui-button-default-color overflow hidden
.tag-control-title
padding-left 15px
padding-bottom 13px
flex 1
p
color $ui-button-default-color
.tag-control-sortTagsBy
user-select none
font-size 12px
color $ui-inactive-text-color
margin-left 12px
margin-right 12px
.tag-control-sortTagsBy-select
appearance: none;
margin-left 5px
color $ui-inactive-text-color
padding 0
border none
background-color transparent
outline none
cursor pointer
font-size 12px
.tagList .tagList
overflow-y auto overflow-y auto

View File

@@ -82,7 +82,7 @@ class SideNav extends React.Component {
} }
SideNavComponent (isFolded, storageList) { SideNavComponent (isFolded, storageList) {
const { location, data } = this.props const { location, data, config } = this.props
const isHomeActive = !!location.pathname.match(/^\/home$/) const isHomeActive = !!location.pathname.match(/^\/home$/)
const isStarredActive = !!location.pathname.match(/^\/starred$/) const isStarredActive = !!location.pathname.match(/^\/starred$/)
@@ -108,15 +108,30 @@ class SideNav extends React.Component {
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)} handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
/> />
<StorageList storageList={storageList} /> <StorageList storageList={storageList} isFolded={isFolded} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} /> <NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div> </div>
) )
} else { } else {
component = ( component = (
<div styleName='tabBody'> <div styleName='tabBody'>
<div styleName='tag-title'> <div styleName='tag-control'>
<p>{i18n.__('Tags')}</p> <div styleName='tag-control-title'>
<p>{i18n.__('Tags')}</p>
</div>
<div styleName='tag-control-sortTagsBy'>
<i className='fa fa-angle-down' />
<select styleName='tag-control-sortTagsBy-select'
title={i18n.__('Select filter mode')}
value={config.sortTagsBy}
onChange={(e) => this.handleSortTagsByChange(e)}
>
<option title='Sort alphabetically'
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
<option title='Sort by update time'
value='COUNTER'>{i18n.__('Counter')}</option>
</select>
</div>
</div> </div>
<div styleName='tagList'> <div styleName='tagList'>
{this.tagListComponent(data)} {this.tagListComponent(data)}
@@ -129,17 +144,21 @@ class SideNav extends React.Component {
} }
tagListComponent () { tagListComponent () {
const { data, location } = this.props const { data, location, config } = this.props
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => { let tagList = _.sortBy(data.tagNoteMap.map(
return { name, size: tag.size } (tag, name) => ({name, size: tag.size})),
}), ['name']) ['name']
)
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
}
return ( return (
tagList.map(tag => { tagList.map(tag => {
return ( return (
<TagListItem <TagListItem
name={tag.name} name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)} handleClickTagListItem={this.handleClickTagListItem.bind(this)}
isActive={this.getTagActive(location.pathname, tag)} isActive={this.getTagActive(location.pathname, tag.name)}
key={tag.name} key={tag.name}
count={tag.size} count={tag.size}
/> />
@@ -159,6 +178,20 @@ class SideNav extends React.Component {
router.push(`/tags/${name}`) router.push(`/tags/${name}`)
} }
handleSortTagsByChange (e) {
const { dispatch } = this.props
const config = {
sortTagsBy: e.target.value
}
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
}
emptyTrash (entries) { emptyTrash (entries) {
const { dispatch } = this.props const { dispatch } = this.props
const deletionPromises = entries.map((note) => { const deletionPromises = entries.map((note) => {

View File

@@ -28,6 +28,14 @@ class TopBar extends React.Component {
} }
componentDidMount () { componentDidMount () {
const { params } = this.props
const searchWord = params.searchword
if (searchWord !== undefined) {
this.setState({
search: searchWord,
isSearching: true
})
}
ee.on('top:focus-search', this.focusSearchHandler) ee.on('top:focus-search', this.focusSearchHandler)
ee.on('code:init', this.codeInitHandler) ee.on('code:init', this.codeInitHandler)
} }
@@ -97,9 +105,10 @@ class TopBar extends React.Component {
this.setState({ this.setState({
isConfirmTranslation: true isConfirmTranslation: true
}) })
router.push('/searched') const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({ this.setState({
search: this.refs.searchInput.value search: keyword
}) })
} }
} }
@@ -108,7 +117,7 @@ class TopBar extends React.Component {
const { router } = this.context const { router } = this.context
const keyword = this.refs.searchInput.value const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) { if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push('/searched') router.push(`/searched/${encodeURIComponent(keyword)}`)
} else { } else {
e.preventDefault() e.preventDefault()
} }

View File

@@ -108,6 +108,21 @@ body[data-theme="dark"]
background #B1D7FE background #B1D7FE
::selection ::selection
background #B1D7FE background #B1D7FE
.CodeMirror-foldmarker
font-family: arial
.CodeMirror-foldgutter
width: .7em
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded
cursor: pointer
.CodeMirror-foldgutter-open:after
content: "\25BE"
.CodeMirror-foldgutter-folded:after
content: "\25B8"
.sortableItemHelper .sortableItemHelper
z-index modalZIndex + 5 z-index modalZIndex + 5

View File

@@ -24,6 +24,45 @@ document.addEventListener('dragover', function (e) {
e.stopPropagation() e.stopPropagation()
}) })
// prevent menu from popup when alt pressed
// but still able to toggle menu when only alt is pressed
let isAltPressing = false
let isAltWithMouse = false
let isAltWithOtherKey = false
let isOtherKey = false
document.addEventListener('keydown', function (e) {
if (e.key === 'Alt') {
isAltPressing = true
if (isOtherKey) {
isAltWithOtherKey = true
}
} else {
if (isAltPressing) {
isAltWithOtherKey = true
}
isOtherKey = true
}
})
document.addEventListener('mousedown', function (e) {
if (isAltPressing) {
isAltWithMouse = true
}
})
document.addEventListener('keyup', function (e) {
if (e.key === 'Alt') {
if (isAltWithMouse || isAltWithOtherKey) {
e.preventDefault()
}
isAltWithMouse = false
isAltWithOtherKey = false
isAltPressing = false
isOtherKey = false
}
})
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
const className = e.target.className const className = e.target.className
if (!className && typeof (className) !== 'string') return if (!className && typeof (className) !== 'string') return
@@ -64,7 +103,9 @@ ReactDOM.render((
<IndexRedirect to='/home' /> <IndexRedirect to='/home' />
<Route path='home' /> <Route path='home' />
<Route path='starred' /> <Route path='starred' />
<Route path='searched' /> <Route path='searched'>
<Route path=':searchword' />
</Route>
<Route path='trashed' /> <Route path='trashed' />
<Route path='alltags' /> <Route path='alltags' />
<Route path='tags'> <Route path='tags'>

View File

@@ -16,6 +16,7 @@ export const DEFAULT_CONFIG = {
listWidth: 280, listWidth: 280,
navWidth: 200, navWidth: 200,
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL' sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true, amaEnabled: true,
hotkey: { hotkey: {
@@ -138,37 +139,7 @@ function set (updates) {
document.body.setAttribute('data-theme', 'default') document.body.setAttribute('data-theme', 'default')
} }
if (newConfig.ui.language === 'sq') { i18n.setLocale(newConfig.ui.language)
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 === 'hu') {
i18n.setLocale('hu')
} 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-ES') {
i18n.setLocale('es-ES')
} else {
i18n.setLocale('en')
}
let editorTheme = document.getElementById('editorTheme') let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) { if (editorTheme == null) {

View File

@@ -27,9 +27,12 @@ function resolveStorageNotes (storage) {
data.storage = storage.key data.storage = storage.key
return data return data
} catch (err) { } catch (err) {
console.error(notePath) console.error(`error on note path: ${notePath}, error: ${err}`)
} }
}) })
.filter(function filterOnlyNoteObject (noteObj) {
return typeof noteObj === 'object'
})
return Promise.resolve(notes) return Promise.resolve(notes)
} }

View File

@@ -194,9 +194,10 @@ class UiTab extends React.Component {
<option value='ko'>{i18n.__('Korean')}</option> <option value='ko'>{i18n.__('Korean')}</option>
<option value='no'>{i18n.__('Norwegian')}</option> <option value='no'>{i18n.__('Norwegian')}</option>
<option value='pl'>{i18n.__('Polish')}</option> <option value='pl'>{i18n.__('Polish')}</option>
<option value='pt'>{i18n.__('Portuguese')}</option> <option value='pt-BR'>{i18n.__('Portuguese (Brazil)')}</option>
<option value='pt-PT'>{i18n.__('Portuguese (Portugal)')}</option>
<option value='ru'>{i18n.__('Russian')}</option> <option value='ru'>{i18n.__('Russian')}</option>
<option value='es'>{i18n.__('Spanish')}</option> <option value='es-ES'>{i18n.__('Spanish')}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -85,6 +85,7 @@
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script> <script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
<script src="../node_modules/codemirror/addon/search/search.js"></script> <script src="../node_modules/codemirror/addon/search/search.js"></script>
<script src="../node_modules/codemirror/addon/search/searchcursor.js"></script> <script src="../node_modules/codemirror/addon/search/searchcursor.js"></script>
@@ -92,6 +93,11 @@
<script src="../node_modules/codemirror/addon/scroll/scrollpastend.js"></script> <script src="../node_modules/codemirror/addon/scroll/scrollpastend.js"></script>
<script src="../node_modules/codemirror/addon/search/matchesonscrollbar.js"></script> <script src="../node_modules/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="../node_modules/codemirror/addon/search/jump-to-line.js"></script> <script src="../node_modules/codemirror/addon/search/jump-to-line.js"></script>
<script src="../node_modules/codemirror/addon/fold/brace-fold.js"></script>
<script src="../node_modules/codemirror/addon/fold/foldgutter.js"></script>
<script src="../node_modules/codemirror/addon/fold/foldcode.js"></script>
<script src="../node_modules/codemirror/addon/dialog/dialog.js"></script> <script src="../node_modules/codemirror/addon/dialog/dialog.js"></script>
<script src="../node_modules/codemirror/addon/display/rulers.js"></script> <script src="../node_modules/codemirror/addon/display/rulers.js"></script>

View File

@@ -111,6 +111,7 @@
"Updated": "Updated", "Updated": "Updated",
"Created": "Created", "Created": "Created",
"Alphabetically": "Alphabetically", "Alphabetically": "Alphabetically",
"Counter": "Counter",
"Default View": "Default View", "Default View": "Default View",
"Compressed View": "Compressed View", "Compressed View": "Compressed View",
"Search": "Search", "Search": "Search",

View File

@@ -111,6 +111,7 @@
"Updated": "Módosítás", "Updated": "Módosítás",
"Created": "Létrehozás", "Created": "Létrehozás",
"Alphabetically": "Ábécé sorrendben", "Alphabetically": "Ábécé sorrendben",
"Counter": "Számláló",
"Default View": "Alapértelmezett Nézet", "Default View": "Alapértelmezett Nézet",
"Compressed View": "Tömörített Nézet", "Compressed View": "Tömörített Nézet",
"Search": "Keresés", "Search": "Keresés",

232
locales/zh-CN.json Normal file → Executable file
View File

@@ -1,138 +1,138 @@
{ {
"Notes": "Notes", "Notes": "笔记",
"Tags": "Tags", "Tags": "标签",
"Preferences": "Preferences", "Preferences": "首选项",
"Make a note": "Make a note", "Make a note": "新建笔记",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl",
"to create a new note": "to create a new note", "to create a new note": "新建笔记",
"Toggle Mode": "Toggle Mode", "Toggle Mode": "切换模式",
"Trash": "Trash", "Trash": "废纸篓",
"MODIFICATION DATE": "MODIFICATION DATE", "MODIFICATION DATE": "更改时间",
"Words": "Words", "Words": "单词",
"Letters": "Letters", "Letters": "字数",
"STORAGE": "STORAGE", "STORAGE": "本地存储",
"FOLDER": "FOLDER", "FOLDER": "文件夹",
"CREATION DATE": "CREATION DATE", "CREATION DATE": "创建时间",
"NOTE LINK": "NOTE LINK", "NOTE LINK": "笔记链接",
".md": ".md", ".md": ".md",
".txt": ".txt", ".txt": ".txt",
".html": ".html", ".html": ".html",
"Print": "Print", "Print": "打印",
"Your preferences for Boostnote": "Your preferences for Boostnote", "Your preferences for Boostnote": "个性设置",
"Storages": "Storages", "Storages": "本地存储",
"Add Storage Location": "Add Storage Location", "Add Storage Location": "添加一个本地存储位置",
"Add Folder": "Add Folder", "Add Folder": "新建文件夹",
"Open Storage folder": "Open Storage folder", "Open Storage folder": "打开本地存储位置",
"Unlink": "Unlink", "Unlink": "取消链接",
"Edit": "Edit", "Edit": "编辑",
"Delete": "Delete", "Delete": "删除",
"Interface": "Interface", "Interface": "界面",
"Interface Theme": "Interface Theme", "Interface Theme": "主题",
"Default": "Default", "Default": "默认",
"White": "White", "White": "White",
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Dark", "Dark": "Dark",
"Show a confirmation dialog when deleting notes": "Show a confirmation dialog when deleting notes", "Show a confirmation dialog when deleting notes": "删除笔记的时候,显示确认框",
"Editor Theme": "Editor Theme", "Editor Theme": "编辑器主题",
"Editor Font Size": "Editor Font Size", "Editor Font Size": "编辑器字号",
"Editor Font Family": "Editor Font Family", "Editor Font Family": "编辑器字体",
"Editor Indent Style": "Editor Indent Style", "Editor Indent Style": "缩进风格",
"Spaces": "Spaces", "Spaces": "空格",
"Tabs": "Tabs", "Tabs": "Tabs",
"Switch to Preview": "Switch to Preview", "Switch to Preview": "快速切换到预览界面",
"When Editor Blurred": "When Editor Blurred", "When Editor Blurred": "当编辑器失去焦点的时候,切换到预览界面",
"When Editor Blurred, Edit On Double Click": "When Editor Blurred, Edit On Double Click", "When Editor Blurred, Edit On Double Click": "当编辑器失去焦点的时候预览,双击切换到编辑界面",
"On Right Click": "On Right Click", "On Right Click": "右键点击切换两个界面",
"Editor Keymap": "Editor Keymap", "Editor Keymap": "编辑器 Keymap",
"default": "default", "default": "默认",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Please restart boostnote after you change the keymap", "⚠️ Please restart boostnote after you change the keymap": "⚠️ 设置好快捷键后记得重启boostnote",
"Show line numbers in the editor": "Show line numbers in the editor", "Show line numbers in the editor": "在编辑器中显示行号",
"Allow editor to scroll past the last line": "Allow editor to scroll past the last line", "Allow editor to scroll past the last line": "允许编辑器滚动到最后一行",
"Bring in web page title when pasting URL on editor": "Bring in web page title when pasting URL on editor", "Bring in web page title when pasting URL on editor": "粘贴网页链接的时候,显示为网页标题",
"Preview": "Preview", "Preview": "预览器",
"Preview Font Size": "Preview Font Size", "Preview Font Size": "预览器字号",
"Preview Font Family": "Preview Font Family", "Preview Font Family": "预览器字体",
"Code block Theme": "Code block Theme", "Code block Theme": "代码块主题",
"Allow preview to scroll past the last line": "Allow preview to scroll past the last line", "Allow preview to scroll past the last line": "允许预览器滚动到最后一行",
"Show line numbers for preview code blocks": "Show line numbers for preview code blocks", "Show line numbers for preview code blocks": "在预览器中显示行号",
"LaTeX Inline Open Delimiter": "LaTeX Inline Open Delimiter", "LaTeX Inline Open Delimiter": "LaTeX 单行开头分隔符",
"LaTeX Inline Close Delimiter": "LaTeX Inline Close Delimiter", "LaTeX Inline Close Delimiter": "LaTeX 单行结尾分隔符",
"LaTeX Block Open Delimiter": "LaTeX Block Open Delimiter", "LaTeX Block Open Delimiter": "LaTeX 多行开头分隔符",
"LaTeX Block Close Delimiter": "LaTeX Block Close Delimiter", "LaTeX Block Close Delimiter": "LaTeX 多行结尾分隔符",
"Community": "Community", "Community": "社区",
"Subscribe to Newsletter": "Subscribe to Newsletter", "Subscribe to Newsletter": "订阅邮件",
"GitHub": "GitHub", "GitHub": "GitHub",
"Blog": "Blog", "Blog": "博客",
"Facebook Group": "Facebook Group", "Facebook Group": "Facebook Group",
"Twitter": "Twitter", "Twitter": "Twitter",
"About": "About", "About": "关于",
"Boostnote": "Boostnote", "Boostnote": "Boostnote",
"An open source note-taking app made for programmers just like you.": "An open source note-taking app made for programmers just like you.", "An open source note-taking app made for programmers just like you.": "一款专门为程序员朋友量身打造的开源笔记",
"Website": "Website", "Website": "官网",
"Development": "Development", "Development": "开发",
" : Development configurations for Boostnote.": " : Development configurations for Boostnote.", " : Development configurations for Boostnote.": " : Boostnote的开发配置",
"Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO",
"License: GPL v3": "License: GPL v3", "License: GPL v3": "License: GPL v3",
"Analytics": "Analytics", "Analytics": "分析",
"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.": "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.", "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.": "Boostnote 收集匿名数据只为了提升软件使用体验,绝对不收集任何个人信息(包括笔记内容)",
"You can see how it works on ": "You can see how it works on ", "You can see how it works on ": "你可以看看它的源码是如何运作的 ",
"You can choose to enable or disable this option.": "You can choose to enable or disable this option.", "You can choose to enable or disable this option.": "你可以选择开启或不开启这个功能",
"Enable analytics to help improve Boostnote": "Enable analytics to help improve Boostnote", "Enable analytics to help improve Boostnote": "允许对数据进行分析,帮助我们改进Boostnote",
"Crowdfunding": "Crowdfunding", "Crowdfunding": "众筹",
"Dear everyone,": "Dear everyone,", "Dear everyone,": "亲爱的用户:",
"Thank you for using Boostnote!": "Thank you for using Boostnote!", "Thank you for using Boostnote!": "谢谢你使用Boostnote",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote is used in about 200 different countries and regions by an awesome community of developers.", "Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大约有200个不同的国家和地区的优秀开发者们都在使用Boostnote",
"To continue supporting this growth, and to satisfy community expectations,": "To continue supporting this growth, and to satisfy community expectations,", "To continue supporting this growth, and to satisfy community expectations,": "为了继续支持这种发展,和满足社区的期待,",
"we would like to invest more time and resources in this project.": "we would like to invest more time and resources in this project.", "we would like to invest more time and resources in this project.": "我们非常愿意投入更多的时间和资源到这个项目中。",
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "If you like this project and see its potential, you can help by supporting us on OpenCollective!", "If you like this project and see its potential, you can help by supporting us on OpenCollective!": "如果你喜欢这款软件并且看好它的潜力, 请在OpenCollective上支持我们!",
"Thanks,": "Thanks,", "Thanks,": "十分感谢!",
"Boostnote maintainers": "Boostnote maintainers", "Boostnote maintainers": "Boostnote的维护人员",
"Support via OpenCollective": "Support via OpenCollective", "Support via OpenCollective": "OpenCollective上支持我们",
"Language": "Language", "Language": "语言",
"English": "English", "English": "English",
"German": "German", "German": "German",
"French": "French", "French": "French",
"Show \"Saved to Clipboard\" notification when copying": "Show \"Saved to Clipboard\" notification when copying", "Show \"Saved to Clipboard\" notification when copying": "复制的时候,显示 \"已复制\" 提示",
"All Notes": "All Notes", "All Notes": "所有笔记",
"Starred": "Starred", "Starred": "星标收藏",
"Are you sure to ": "Are you sure to ", "Are you sure to ": "你确定要",
" delete": " delete", " delete": " 删除",
"this folder?": "this folder?", "this folder?": "这个文件夹?",
"Confirm": "Confirm", "Confirm": "确认",
"Cancel": "Cancel", "Cancel": "取消",
"Markdown Note": "Markdown Note", "Markdown Note": "Markdown笔记",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.", "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "创建文档清单代码块甚至是Latex格式文档",
"Snippet Note": "Snippet Note", "Snippet Note": "代码笔记",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "This format is for creating code snippets. Multiple snippets can be grouped into a single note.", "This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "创建代码片段,支持多种语法代码片段",
"Tab to switch format": "Tab to switch format", "Tab to switch format": "使用Tab键切换格式",
"Updated": "Updated", "Updated": "更新时间",
"Created": "Created", "Created": "创建时间",
"Alphabetically": "Alphabetically", "Alphabetically": "A~Z排序",
"Default View": "Default View", "Default View": "默认视图",
"Compressed View": "Compressed View", "Compressed View": "列表视图",
"Search": "Search", "Search": "搜索",
"Blog Type": "Blog Type", "Blog Type": "博客类型",
"Blog Address": "Blog Address", "Blog Address": "博客地址",
"Save": "Save", "Save": "保存",
"Auth": "Auth", "Auth": "Auth",
"Authentication Method": "Authentication Method", "Authentication Method": "认证方法",
"JWT": "JWT", "JWT": "JWT",
"USER": "USER", "USER": "USER",
"Token": "Token", "Token": "Token",
"Storage": "Storage", "Storage": "本地存储",
"Hotkeys": "Hotkeys", "Hotkeys": "快捷键",
"Show/Hide Boostnote": "Show/Hide Boostnote", "Show/Hide Boostnote": "显示/隐藏 Boostnote",
"Restore": "Restore", "Restore": "恢复",
"Permanent Delete": "Permanent Delete", "Permanent Delete": "永久删除",
"Confirm note deletion": "Confirm note deletion", "Confirm note deletion": "确认删除笔记",
"This will permanently remove this note.": "This will permanently remove this note.", "This will permanently remove this note.": "永久地删除这条笔记",
"Successfully applied!": "Successfully applied!", "Successfully applied!": "设置成功",
"Albanian": "Albanian", "Albanian": "Albanian",
"Chinese (zh-CN)": "Chinese (zh-CN)", "Chinese (zh-CN)": "简体中文",
"Chinese (zh-TW)": "Chinese (zh-TW)", "Chinese (zh-TW)": "繁體中文",
"Danish": "Danish", "Danish": "Danish",
"Japanese": "Japanese", "Japanese": "Japanese",
"Korean": "Korean", "Korean": "Korean",
@@ -140,13 +140,13 @@
"Polish": "Polish", "Polish": "Polish",
"Portuguese": "Portuguese", "Portuguese": "Portuguese",
"Spanish": "Spanish", "Spanish": "Spanish",
"You have to save!": "You have to save!", "You have to save!": "你必须保存一下!",
"Russian": "Russian", "Russian": "Russian",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Editor Rulers",
"Enable": "Enable", "Enable": "开启",
"Disable": "Disable", "Disable": "关闭",
"Sanitization": "Sanitization", "Sanitization": "代码处理",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "只允许安全的html标签(推荐)",
"Allow styles": "Allow styles", "Allow styles": "允许样式",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "允许危险的html标签"
} }

232
locales/zh-TW.json Normal file → Executable file
View File

@@ -1,138 +1,138 @@
{ {
"Notes": "Notes", "Notes": "筆記",
"Tags": "Tags", "Tags": "標籤",
"Preferences": "Preferences", "Preferences": "首選項",
"Make a note": "Make a note", "Make a note": "新建筆記",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl",
"to create a new note": "to create a new note", "to create a new note": "新建筆記",
"Toggle Mode": "Toggle Mode", "Toggle Mode": "切換模式",
"Trash": "Trash", "Trash": "廢紙簍",
"MODIFICATION DATE": "MODIFICATION DATE", "MODIFICATION DATE": "更改時間",
"Words": "Words", "Words": "單詞",
"Letters": "Letters", "Letters": "字數",
"STORAGE": "STORAGE", "STORAGE": "本地儲存",
"FOLDER": "FOLDER", "FOLDER": "資料夾",
"CREATION DATE": "CREATION DATE", "CREATION DATE": "創建時間",
"NOTE LINK": "NOTE LINK", "NOTE LINK": "筆記鏈接",
".md": ".md", ".md": ".md",
".txt": ".txt", ".txt": ".txt",
".html": ".html", ".html": ".html",
"Print": "Print", "Print": "列印",
"Your preferences for Boostnote": "Your preferences for Boostnote", "Your preferences for Boostnote": "個性設置",
"Storages": "Storages", "Storages": "本地儲存",
"Add Storage Location": "Add Storage Location", "Add Storage Location": "添加一個本地儲存位置",
"Add Folder": "Add Folder", "Add Folder": "新建資料夾",
"Open Storage folder": "Open Storage folder", "Open Storage folder": "打開一個本地儲存位置",
"Unlink": "Unlink", "Unlink": "取消鏈接",
"Edit": "Edit", "Edit": "編輯",
"Delete": "Delete", "Delete": "刪除",
"Interface": "Interface", "Interface": "界面",
"Interface Theme": "Interface Theme", "Interface Theme": "主題",
"Default": "Default", "Default": "默認",
"White": "White", "White": "White",
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Dark", "Dark": "Dark",
"Show a confirmation dialog when deleting notes": "Show a confirmation dialog when deleting notes", "Show a confirmation dialog when deleting notes": "刪除筆記的時候,顯示確認框",
"Editor Theme": "Editor Theme", "Editor Theme": "編輯器主題",
"Editor Font Size": "Editor Font Size", "Editor Font Size": "編輯器字型大小",
"Editor Font Family": "Editor Font Family", "Editor Font Family": "編輯器字體",
"Editor Indent Style": "Editor Indent Style", "Editor Indent Style": "縮進風格",
"Spaces": "Spaces", "Spaces": "空格",
"Tabs": "Tabs", "Tabs": "Tabs",
"Switch to Preview": "Switch to Preview", "Switch to Preview": "快速切換到預覽界面",
"When Editor Blurred": "When Editor Blurred", "When Editor Blurred": "當編輯器失去焦點的時候,切換到預覽界面",
"When Editor Blurred, Edit On Double Click": "When Editor Blurred, Edit On Double Click", "When Editor Blurred, Edit On Double Click": "當編輯器失去焦點的時候預覽,雙擊切換到編輯界面",
"On Right Click": "On Right Click", "On Right Click": "右鍵點擊切換兩個界面",
"Editor Keymap": "Editor Keymap", "Editor Keymap": "編輯器 Keymap",
"default": "default", "default": "默認",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Please restart boostnote after you change the keymap", "⚠️ Please restart boostnote after you change the keymap": "⚠️ 設置好快捷鍵後記得重啟设置好快捷键后记得重启boostnote",
"Show line numbers in the editor": "Show line numbers in the editor", "Show line numbers in the editor": "在編輯器中顯示行號",
"Allow editor to scroll past the last line": "Allow editor to scroll past the last line", "Allow editor to scroll past the last line": "允許編輯器滾動到最後一行",
"Bring in web page title when pasting URL on editor": "Bring in web page title when pasting URL on editor", "Bring in web page title when pasting URL on editor": "粘貼網頁鏈接的時候,顯示為網頁標題",
"Preview": "Preview", "Preview": "預覽器",
"Preview Font Size": "Preview Font Size", "Preview Font Size": "預覽器字型大小",
"Preview Font Family": "Preview Font Family", "Preview Font Family": "預覽器字體",
"Code block Theme": "Code block Theme", "Code block Theme": "代碼塊主題",
"Allow preview to scroll past the last line": "Allow preview to scroll past the last line", "Allow preview to scroll past the last line": "允許預覽器滾動到最後一行",
"Show line numbers for preview code blocks": "Show line numbers for preview code blocks", "Show line numbers for preview code blocks": "在預覽器中顯示行號",
"LaTeX Inline Open Delimiter": "LaTeX Inline Open Delimiter", "LaTeX Inline Open Delimiter": "LaTeX 單行開頭分隔符",
"LaTeX Inline Close Delimiter": "LaTeX Inline Close Delimiter", "LaTeX Inline Close Delimiter": "LaTeX 單行結尾分隔符",
"LaTeX Block Open Delimiter": "LaTeX Block Open Delimiter", "LaTeX Block Open Delimiter": "LaTeX 多行開頭分隔符",
"LaTeX Block Close Delimiter": "LaTeX Block Close Delimiter", "LaTeX Block Close Delimiter": "LaTeX 多行結尾分隔符",
"Community": "Community", "Community": "社區",
"Subscribe to Newsletter": "Subscribe to Newsletter", "Subscribe to Newsletter": "訂閱郵件",
"GitHub": "GitHub", "GitHub": "GitHub",
"Blog": "Blog", "Blog": "部落格",
"Facebook Group": "Facebook Group", "Facebook Group": "Facebook Group",
"Twitter": "Twitter", "Twitter": "Twitter",
"About": "About", "About": "關於",
"Boostnote": "Boostnote", "Boostnote": "Boostnote",
"An open source note-taking app made for programmers just like you.": "An open source note-taking app made for programmers just like you.", "An open source note-taking app made for programmers just like you.": "一款專門為程式員朋友量身打造的開源筆記",
"Website": "Website", "Website": "官網",
"Development": "Development", "Development": "開發",
" : Development configurations for Boostnote.": " : Development configurations for Boostnote.", " : Development configurations for Boostnote.": " : Boostnote的開發配置",
"Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO",
"License: GPL v3": "License: GPL v3", "License: GPL v3": "License: GPL v3",
"Analytics": "Analytics", "Analytics": "分析",
"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.": "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.", "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.": "Boostnote 收集匿名數據只為了提升軟體使用體驗,絕對不收集任何個人信息(包括筆記內容)",
"You can see how it works on ": "You can see how it works on ", "You can see how it works on ": "你可以看看它的源碼是如何運作的 ",
"You can choose to enable or disable this option.": "You can choose to enable or disable this option.", "You can choose to enable or disable this option.": "你可以選擇開啟或不開啟這個功能",
"Enable analytics to help improve Boostnote": "Enable analytics to help improve Boostnote", "Enable analytics to help improve Boostnote": "允許對數據進行分析,幫助我們改進Boostnote",
"Crowdfunding": "Crowdfunding", "Crowdfunding": "眾籌",
"Dear everyone,": "Dear everyone,", "Dear everyone,": "親愛的用戶:",
"Thank you for using Boostnote!": "Thank you for using Boostnote!", "Thank you for using Boostnote!": "謝謝你使用Boostnote",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote is used in about 200 different countries and regions by an awesome community of developers.", "Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大約有200個不同的國家和地區的優秀開發者們都在使用Boostnote",
"To continue supporting this growth, and to satisfy community expectations,": "To continue supporting this growth, and to satisfy community expectations,", "To continue supporting this growth, and to satisfy community expectations,": "為了繼續支持這種發展,和滿足社區的期待,",
"we would like to invest more time and resources in this project.": "we would like to invest more time and resources in this project.", "we would like to invest more time and resources in this project.": "我們非常願意投入更多的時間和資源到這個專案中。",
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "If you like this project and see its potential, you can help by supporting us on OpenCollective!", "If you like this project and see its potential, you can help by supporting us on OpenCollective!": "如果你喜歡這款軟體並且看好它的潛力, 請在OpenCollective上支持我們!",
"Thanks,": "Thanks,", "Thanks,": "十分感謝!",
"Boostnote maintainers": "Boostnote maintainers", "Boostnote maintainers": "Boostnote的維護人員",
"Support via OpenCollective": "Support via OpenCollective", "Support via OpenCollective": "OpenCollective上支持我們",
"Language": "Language", "Language": "語言",
"English": "English", "English": "English",
"German": "German", "German": "German",
"French": "French", "French": "French",
"Show \"Saved to Clipboard\" notification when copying": "Show \"Saved to Clipboard\" notification when copying", "Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製\" 提示",
"All Notes": "All Notes", "All Notes": "所有筆記",
"Starred": "Starred", "Starred": "星標收藏",
"Are you sure to ": "Are you sure to ", "Are you sure to ": "你確定要 ",
" delete": " delete", " delete": " 刪除",
"this folder?": "this folder?", "this folder?": "這個資料夾嗎?",
"Confirm": "Confirm", "Confirm": "確認",
"Cancel": "Cancel", "Cancel": "取消",
"Markdown Note": "Markdown Note", "Markdown Note": "Markdown筆記",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.", "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "創建文檔清單代碼塊甚至是Latex格式文檔",
"Snippet Note": "Snippet Note", "Snippet Note": "代碼筆記",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "This format is for creating code snippets. Multiple snippets can be grouped into a single note.", "This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "創建代碼片段,支持多種語法代碼片段",
"Tab to switch format": "Tab to switch format", "Tab to switch format": "使用Tab鍵切換格式",
"Updated": "Updated", "Updated": "更新時間",
"Created": "Created", "Created": "創建時間",
"Alphabetically": "Alphabetically", "Alphabetically": "A~Z排序",
"Default View": "Default View", "Default View": "默認視圖",
"Compressed View": "Compressed View", "Compressed View": "列表視圖",
"Search": "Search", "Search": "搜索",
"Blog Type": "Blog Type", "Blog Type": "部落格類型",
"Blog Address": "Blog Address", "Blog Address": "部落格地址",
"Save": "Save", "Save": "保存",
"Auth": "Auth", "Auth": "Auth",
"Authentication Method": "Authentication Method", "Authentication Method": "認證方法",
"JWT": "JWT", "JWT": "JWT",
"USER": "USER", "USER": "USER",
"Token": "Token", "Token": "Token",
"Storage": "Storage", "Storage": "本地儲存",
"Hotkeys": "Hotkeys", "Hotkeys": "快捷鍵",
"Show/Hide Boostnote": "Show/Hide Boostnote", "Show/Hide Boostnote": "顯示/隱藏 Boostnote",
"Restore": "Restore", "Restore": "恢復",
"Permanent Delete": "Permanent Delete", "Permanent Delete": "永久刪除",
"Confirm note deletion": "Confirm note deletion", "Confirm note deletion": "確認刪除筆記",
"This will permanently remove this note.": "This will permanently remove this note.", "This will permanently remove this note.": "永久地刪除這條筆記",
"Successfully applied!": "Successfully applied!", "Successfully applied!": "設置成功",
"Albanian": "Albanian", "Albanian": "Albanian",
"Chinese (zh-CN)": "Chinese (zh-CN)", "Chinese (zh-CN)": "简体中文",
"Chinese (zh-TW)": "Chinese (zh-TW)", "Chinese (zh-TW)": "繁體中文",
"Danish": "Danish", "Danish": "Danish",
"Japanese": "Japanese", "Japanese": "Japanese",
"Korean": "Korean", "Korean": "Korean",
@@ -140,13 +140,13 @@
"Polish": "Polish", "Polish": "Polish",
"Portuguese": "Portuguese", "Portuguese": "Portuguese",
"Spanish": "Spanish", "Spanish": "Spanish",
"You have to save!": "You have to save!", "You have to save!": "你必須儲存一下!",
"Russian": "Russian", "Russian": "Russian",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Editor Rulers",
"Enable": "Enable", "Enable": "開啟",
"Disable": "Disable", "Disable": "關閉",
"Sanitization": "Sanitization", "Sanitization": "代碼處理",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "只允許安全的html標籤(推薦)",
"Allow styles": "Allow styles", "Allow styles": "允許樣式",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "允許危險的html標籤"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.11.3", "version": "0.11.4",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -11,6 +11,7 @@
"webpack": "webpack-dev-server --hot --inline --config webpack.config.js", "webpack": "webpack-dev-server --hot --inline --config webpack.config.js",
"compile": "grunt compile", "compile": "grunt compile",
"test": "PWD=$(pwd) NODE_ENV=test ava --serial", "test": "PWD=$(pwd) NODE_ENV=test ava --serial",
"jest": "jest",
"fix": "npm run lint --fix", "fix": "npm run lint --fix",
"lint": "eslint .", "lint": "eslint .",
"dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\"" "dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\""
@@ -96,11 +97,13 @@
"devDependencies": { "devDependencies": {
"ava": "^0.25.0", "ava": "^0.25.0",
"babel-core": "^6.14.0", "babel-core": "^6.14.0",
"babel-jest": "^22.4.3",
"babel-loader": "^6.2.0", "babel-loader": "^6.2.0",
"babel-plugin-react-transform": "^2.0.0", "babel-plugin-react-transform": "^2.0.0",
"babel-plugin-webpack-alias": "^2.1.1", "babel-plugin-webpack-alias": "^2.1.1",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.3.13", "babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13", "babel-preset-react": "^6.24.1",
"babel-preset-react-hmre": "^1.0.1", "babel-preset-react-hmre": "^1.0.1",
"babel-register": "^6.11.6", "babel-register": "^6.11.6",
"browser-env": "^3.2.5", "browser-env": "^3.2.5",
@@ -120,6 +123,9 @@
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-electron-installer": "2.1.0", "grunt-electron-installer": "2.1.0",
"history": "^1.17.0", "history": "^1.17.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^22.4.3",
"jest-localstorage-mock": "^2.2.0",
"jsdom": "^9.4.2", "jsdom": "^9.4.2",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"merge-stream": "^1.0.0", "merge-stream": "^1.0.0",
@@ -130,6 +136,7 @@
"react-input-autosize": "^1.1.0", "react-input-autosize": "^1.1.0",
"react-router": "^2.4.0", "react-router": "^2.4.0",
"react-router-redux": "^4.0.4", "react-router-redux": "^4.0.4",
"react-test-renderer": "^15.6.2",
"standard": "^8.4.0", "standard": "^8.4.0",
"style-loader": "^0.12.4", "style-loader": "^0.12.4",
"stylus": "^0.52.4", "stylus": "^0.52.4",
@@ -152,5 +159,15 @@
"./tests/helpers/setup-electron-mock.js" "./tests/helpers/setup-electron-mock.js"
], ],
"babel": "inherit" "babel": "inherit"
},
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less|styl)$": "identity-obj-proxy"
},
"setupFiles": [
"<rootDir>/tests/jest.js",
"jest-localstorage-mock"
]
} }
} }

View File

@@ -0,0 +1,9 @@
import React from 'react'
import renderer from 'react-test-renderer'
import TagListItem from 'browser/components/TagListItem'
it('TagListItem renders correctly', () => {
const tagListItem = renderer.create(<TagListItem name='Test' handleClickTagListItem={jest.fn()} />)
expect(tagListItem.toJSON()).toMatchSnapshot()
})

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TagListItem renders correctly 1`] = `
<button
className="tagList-item"
onClick={[Function]}
>
<span
className="tagList-item-name"
>
# Test
<span
className="tagList-item-count"
>
</span>
</span>
</button>
`;

12
tests/jest.js Normal file
View File

@@ -0,0 +1,12 @@
// Here you can mock the libraries connected through direct insertion <script src="" >
global.Raphael = {
setWindow: jest.fn(),
registerFont: jest.fn(),
fn: function () {
return {}
}
}
global._ = {
extend: jest.genMockFunction()
}

View File

@@ -23,6 +23,7 @@
"lineNumber": true "lineNumber": true
}, },
"sortBy": "UPDATED_AT", "sortBy": "UPDATED_AT",
"sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false, "disableDirectWrite": false,

View File

@@ -20,18 +20,25 @@ test.before(t => {
notes = [note1, note2, note3] notes = [note1, note2, note3]
}) })
test('it can find notes by tags or words', t => { test('it can find notes by tags and words', t => {
// [input, expected content (Array)] // [input, expected content (Array)]
const testCases = [ const testWithTags = [
['#tag1', [note1.content, note2.content, note3.content]], ['#tag1', [note1.content, note2.content, note3.content]],
['#tag1 #tag2', [note2.content]], ['#tag1 #tag2', [note2.content]],
['#tag2 #tag1', [note2.content]],
['#tag1 #tag2 #tag3', []], ['#tag1 #tag2 #tag3', []],
['content1', [note1.content, note2.content]], ['content1', [note1.content, note2.content]],
['content1 content2', [note2.content]], ['content1 content2', [note2.content]],
['content1 content2 content3', []], ['content1 content2 content3', []],
['#content4', [note3.content]] ['#content4', [note3.content]],
['#tag2 content1', [note2.content]],
['content1 #tag2', [note2.content]]
] ]
const testWithTagsWithoutHash = testWithTags.map(function (testCase) {
return [testCase[0].replace(/#/g, ''), testCase[1]]
})
const testCases = testWithTags.concat(testWithTagsWithoutHash)
testCases.forEach((testCase) => { testCases.forEach((testCase) => {
const [input, expectedContents] = testCase const [input, expectedContents] = testCase
const results = searchFromNotes(notes, input) const results = searchFromNotes(notes, input)

1520
yarn.lock

File diff suppressed because it is too large Load Diff