mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
Merge branch 'master' into feature/add-smartquotes-toggle
This commit is contained in:
@@ -8,6 +8,8 @@ import copyImage from 'browser/main/lib/dataApi/copyImage'
|
|||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
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'
|
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||||
|
|
||||||
@@ -32,8 +34,13 @@ export default class CodeEditor extends React.Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||||
this.changeHandler = (e) => this.handleChange(e)
|
this.changeHandler = (e) => this.handleChange(e)
|
||||||
|
this.focusHandler = () => {
|
||||||
|
ipcRenderer.send('editor:focused', true)
|
||||||
|
}
|
||||||
this.blurHandler = (editor, e) => {
|
this.blurHandler = (editor, e) => {
|
||||||
|
ipcRenderer.send('editor:focused', false)
|
||||||
if (e == null) return null
|
if (e == null) return null
|
||||||
let el = e.relatedTarget
|
let el = e.relatedTarget
|
||||||
while (el != null) {
|
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 () {
|
componentDidMount () {
|
||||||
@@ -139,6 +145,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
this.setMode(this.props.mode)
|
this.setMode(this.props.mode)
|
||||||
|
|
||||||
|
this.editor.on('focus', this.focusHandler)
|
||||||
this.editor.on('blur', this.blurHandler)
|
this.editor.on('blur', this.blurHandler)
|
||||||
this.editor.on('change', this.changeHandler)
|
this.editor.on('change', this.changeHandler)
|
||||||
this.editor.on('paste', this.pasteHandler)
|
this.editor.on('paste', this.pasteHandler)
|
||||||
@@ -162,6 +169,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
this.editor.off('focus', this.focusHandler)
|
||||||
this.editor.off('blur', this.blurHandler)
|
this.editor.off('blur', this.blurHandler)
|
||||||
this.editor.off('change', this.changeHandler)
|
this.editor.off('change', this.changeHandler)
|
||||||
this.editor.off('paste', this.pasteHandler)
|
this.editor.off('paste', this.pasteHandler)
|
||||||
@@ -317,7 +325,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
fetch(pastedTxt, {
|
fetch(pastedTxt, {
|
||||||
method: 'get'
|
method: 'get'
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
return (response.text())
|
return this.decodeResponse(response)
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
||||||
const value = editor.getValue()
|
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 () {
|
render () {
|
||||||
const { className, fontSize } = this.props
|
const { className, fontSize } = this.props
|
||||||
let fontFamily = this.props.fontFamily
|
let fontFamily = this.props.fontFamily
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
return `<html>
|
return `<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
<style id="style">${inlineStyles}</style>
|
<style id="style">${inlineStyles}</style>
|
||||||
${styles}
|
${styles}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -123,7 +123,11 @@ NoteItem.propTypes = {
|
|||||||
title: PropTypes.string.isrequired,
|
title: PropTypes.string.isrequired,
|
||||||
tags: PropTypes.array,
|
tags: PropTypes.array,
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
isTrashed: PropTypes.bool.isRequired
|
isTrashed: PropTypes.bool.isRequired,
|
||||||
|
blog: {
|
||||||
|
blogLink: PropTypes.string,
|
||||||
|
blogId: PropTypes.number
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
handleNoteClick: PropTypes.func.isRequired,
|
handleNoteClick: PropTypes.func.isRequired,
|
||||||
handleNoteContextMenu: PropTypes.func.isRequired,
|
handleNoteContextMenu: PropTypes.func.isRequired,
|
||||||
|
|||||||
23
browser/lib/markdown-it-sanitize-html.js
Normal file
23
browser/lib/markdown-it-sanitize-html.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
|
import sanitize from './markdown-it-sanitize-html'
|
||||||
import emoji from 'markdown-it-emoji'
|
import emoji from 'markdown-it-emoji'
|
||||||
import math from '@rokt33r/markdown-it-math'
|
import math from '@rokt33r/markdown-it-math'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
@@ -51,6 +52,18 @@ class Markdown {
|
|||||||
|
|
||||||
const updatedOptions = Object.assign(defaultOptions, options)
|
const updatedOptions = Object.assign(defaultOptions, options)
|
||||||
this.md = markdownit(updatedOptions)
|
this.md = markdownit(updatedOptions)
|
||||||
|
|
||||||
|
// Sanitize use rinput before other plugins
|
||||||
|
this.md.use(sanitize, {
|
||||||
|
allowedTags: ['img', 'iframe'],
|
||||||
|
allowedAttributes: {
|
||||||
|
'*': ['alt', 'style'],
|
||||||
|
'img': ['src', 'width', 'height'],
|
||||||
|
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen']
|
||||||
|
},
|
||||||
|
allowedIframeHostnames: ['www.youtube.com']
|
||||||
|
})
|
||||||
|
|
||||||
this.md.use(emoji, {
|
this.md.use(emoji, {
|
||||||
shortcuts: {}
|
shortcuts: {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
switch (e.keyCode) {
|
||||||
|
// tab key
|
||||||
case 9:
|
case 9:
|
||||||
if (e.ctrlKey && !e.shiftKey) {
|
if (e.ctrlKey && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -355,6 +356,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
this.focusEditor()
|
this.focusEditor()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// L key
|
||||||
case 76:
|
case 76:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
@@ -366,6 +368,7 @@ class SnippetNoteDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
// T key
|
||||||
case 84:
|
case 84:
|
||||||
{
|
{
|
||||||
const isSuper = global.process.platform === 'darwin'
|
const isSuper = global.process.platform === 'darwin'
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import searchFromNotes from 'browser/lib/search'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { hashHistory } from 'react-router'
|
import { hashHistory } from 'react-router'
|
||||||
|
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'
|
||||||
|
|
||||||
const { remote } = require('electron')
|
const { remote } = require('electron')
|
||||||
const { Menu, MenuItem, dialog } = remote
|
const { Menu, MenuItem, dialog } = remote
|
||||||
|
const WP_POST_PATH = '/wp/v2/posts'
|
||||||
|
|
||||||
function sortByCreatedAt (a, b) {
|
function sortByCreatedAt (a, b) {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt)
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
@@ -70,6 +73,7 @@ class NoteList extends React.Component {
|
|||||||
this.getNoteFolder = this.getNoteFolder.bind(this)
|
this.getNoteFolder = this.getNoteFolder.bind(this)
|
||||||
this.getViewType = this.getViewType.bind(this)
|
this.getViewType = this.getViewType.bind(this)
|
||||||
this.restoreNote = this.restoreNote.bind(this)
|
this.restoreNote = this.restoreNote.bind(this)
|
||||||
|
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||||
|
|
||||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -257,27 +261,38 @@ class NoteList extends React.Component {
|
|||||||
handleNoteListKeyDown (e) {
|
handleNoteListKeyDown (e) {
|
||||||
if (e.metaKey || e.ctrlKey) return true
|
if (e.metaKey || e.ctrlKey) return true
|
||||||
|
|
||||||
|
// A key
|
||||||
if (e.keyCode === 65 && !e.shiftKey) {
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('top:new-note')
|
ee.emit('top:new-note')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D key
|
||||||
if (e.keyCode === 68) {
|
if (e.keyCode === 68) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.deleteNote()
|
this.deleteNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E key
|
||||||
if (e.keyCode === 69) {
|
if (e.keyCode === 69) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ee.emit('detail:focus')
|
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()
|
e.preventDefault()
|
||||||
this.selectPriorNote()
|
this.selectPriorNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
// DOWN or J key
|
||||||
|
if (e.keyCode === 40 || e.keyCode === 74) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectNextNote()
|
this.selectNextNote()
|
||||||
}
|
}
|
||||||
@@ -458,6 +473,10 @@ class NoteList extends React.Component {
|
|||||||
const deleteLabel = 'Delete Note'
|
const deleteLabel = 'Delete Note'
|
||||||
const cloneNote = 'Clone Note'
|
const cloneNote = 'Clone Note'
|
||||||
const restoreNote = 'Restore 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()
|
const menu = new Menu()
|
||||||
if (!location.pathname.match(/\/starred|\/trash/)) {
|
if (!location.pathname.match(/\/starred|\/trash/)) {
|
||||||
@@ -482,6 +501,28 @@ class NoteList extends React.Component {
|
|||||||
label: cloneNote,
|
label: cloneNote,
|
||||||
click: this.cloneNote.bind(this)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +671,117 @@ class NoteList extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyNoteLink (note) {
|
||||||
|
const noteLink = `[${note.title}](${note.storage}-${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}`, '')
|
||||||
|
var 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 () {
|
importFromFile () {
|
||||||
const options = {
|
const options = {
|
||||||
filters: [
|
filters: [
|
||||||
|
|||||||
@@ -191,33 +191,16 @@ class StorageItem extends React.Component {
|
|||||||
dropNote (storage, folder, dispatch, location, noteData) {
|
dropNote (storage, folder, dispatch, location, noteData) {
|
||||||
noteData = noteData.filter((note) => folder.key !== note.folder)
|
noteData = noteData.filter((note) => folder.key !== note.folder)
|
||||||
if (noteData.length === 0) return
|
if (noteData.length === 0) return
|
||||||
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
|
|
||||||
|
|
||||||
Promise.all(
|
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) => {
|
.then((createdNoteData) => {
|
||||||
createdNoteData.forEach((note) => {
|
createdNoteData.forEach((newNote) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'UPDATE_NOTE',
|
type: 'MOVE_NOTE',
|
||||||
note: note
|
originNote: noteData.find((note) => note.content === newNote.content),
|
||||||
})
|
note: newNote
|
||||||
})
|
|
||||||
})
|
|
||||||
.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
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,20 +21,19 @@
|
|||||||
color white
|
color white
|
||||||
|
|
||||||
.zoom
|
.zoom
|
||||||
display none
|
navButtonColor()
|
||||||
// navButtonColor()
|
color rgba(0,0,0,.54)
|
||||||
// color rgba(0,0,0,.54)
|
height 20px
|
||||||
// height 20px
|
display flex
|
||||||
// display flex
|
padding 0
|
||||||
// padding 0
|
align-items center
|
||||||
// align-items center
|
background-color transparent
|
||||||
// background-color transparent
|
&:hover
|
||||||
// &:hover
|
color $ui-active-color
|
||||||
// color $ui-active-color
|
&:active
|
||||||
// &:active
|
color $ui-active-color
|
||||||
// color $ui-active-color
|
span
|
||||||
// span
|
margin-left 5px
|
||||||
// margin-left 5px
|
|
||||||
|
|
||||||
.update
|
.update
|
||||||
navButtonColor()
|
navButtonColor()
|
||||||
|
|||||||
@@ -40,6 +40,32 @@ $control-height = 34px
|
|||||||
padding-bottom 2px
|
padding-bottom 2px
|
||||||
background-color $ui-noteList-backgroundColor
|
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
|
.control-search-optionList
|
||||||
position fixed
|
position fixed
|
||||||
z-index 200
|
z-index 200
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ class TopBar extends React.Component {
|
|||||||
ee.off('code:init', this.codeInitHandler)
|
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) {
|
handleKeyDown (e) {
|
||||||
// reset states
|
// reset states
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -43,6 +54,23 @@ class TopBar extends React.Component {
|
|||||||
isIME: false
|
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
|
// When the key is an alphabet, del, enter or ctr
|
||||||
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -114,10 +142,12 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOnSearchFocus () {
|
handleOnSearchFocus () {
|
||||||
|
const el = this.refs.search.childNodes[0]
|
||||||
if (this.state.isSearching) {
|
if (this.state.isSearching) {
|
||||||
this.refs.search.childNodes[0].blur()
|
el.blur()
|
||||||
} else {
|
} else {
|
||||||
this.refs.search.childNodes[0].focus()
|
el.focus()
|
||||||
|
el.setSelectionRange(0, el.value.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,15 +180,15 @@ class TopBar extends React.Component {
|
|||||||
type='text'
|
type='text'
|
||||||
className='searchInput'
|
className='searchInput'
|
||||||
/>
|
/>
|
||||||
</div>
|
{this.state.search !== '' &&
|
||||||
{this.state.search > 0 &&
|
<button styleName='control-search-input-clear'
|
||||||
<button styleName='left-search-clearButton'
|
|
||||||
onClick={(e) => this.handleSearchClearButton(e)}
|
onClick={(e) => this.handleSearchClearButton(e)}
|
||||||
>
|
>
|
||||||
<i className='fa fa-times' />
|
<i className='fa fa-fw fa-times' />
|
||||||
|
<span styleName='control-search-input-clear-tooltip'>Clear Search</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{location.pathname === '/trashed' ? ''
|
{location.pathname === '/trashed' ? ''
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ export const DEFAULT_CONFIG = {
|
|||||||
latexBlockClose: '$$',
|
latexBlockClose: '$$',
|
||||||
scrollPastEnd: false,
|
scrollPastEnd: false,
|
||||||
smartQuotes: true
|
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: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ function set (updates) {
|
|||||||
function assignConfigValues (originalConfig, rcConfig) {
|
function assignConfigValues (originalConfig, rcConfig) {
|
||||||
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
|
||||||
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
|
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.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
|
||||||
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
|
||||||
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ const path = require('path')
|
|||||||
const { findStorage } = require('browser/lib/findStorage')
|
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} filePath
|
||||||
* @param {String} storageKey
|
* @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) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const targetStorage = findStorage(storageKey)
|
const targetStorage = findStorage(storageKey)
|
||||||
|
|
||||||
const inputImage = fs.createReadStream(filePath)
|
const inputImage = fs.createReadStream(filePath)
|
||||||
const imageExt = path.extname(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 basename = `${imageName}${imageExt}`
|
||||||
const imageDir = path.join(targetStorage.path, 'images')
|
const imageDir = path.join(targetStorage.path, 'images')
|
||||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {findStorage} from 'browser/lib/findStorage'
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
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'
|
const IMAGES_FOLDER_NAME = 'images'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const resolveStorageData = require('./resolveStorageData')
|
const resolveStorageData = require('./resolveStorageData')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
const CSON = require('@rokt33r/season')
|
const CSON = require('@rokt33r/season')
|
||||||
const keygen = require('browser/lib/keygen')
|
const keygen = require('browser/lib/keygen')
|
||||||
const sander = require('sander')
|
const sander = require('sander')
|
||||||
const { findStorage } = require('browser/lib/findStorage')
|
const { findStorage } = require('browser/lib/findStorage')
|
||||||
|
const copyImage = require('./copyImage')
|
||||||
|
|
||||||
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||||
let oldStorage, newStorage
|
let oldStorage, newStorage
|
||||||
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
|||||||
|
|
||||||
return noteData
|
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) {
|
.then(function writeAndReturn (noteData) {
|
||||||
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
|
||||||
return noteData
|
return noteData
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ function validateInput (input) {
|
|||||||
validatedInput.isPinned = !!input.isPinned
|
validatedInput.isPinned = !!input.isPinned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_.isNil(input.blog)) {
|
||||||
|
validatedInput.blog = input.blog
|
||||||
|
}
|
||||||
validatedInput.type = input.type
|
validatedInput.type = input.type
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case 'MARKDOWN_NOTE':
|
case 'MARKDOWN_NOTE':
|
||||||
|
|||||||
198
browser/main/modals/PreferencesModal/Blog.js
Normal file
198
browser/main/modals/PreferencesModal/Blog.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
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: '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'>Blog</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
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'>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)}>Save
|
||||||
|
</button>
|
||||||
|
{blogAlertElement}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-header2'>Auth</div>
|
||||||
|
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
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'>JWT</option>
|
||||||
|
<option value='USER' key='USER'>USER</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ config.blog.authMethod === 'JWT' &&
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>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'>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'>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)
|
||||||
@@ -6,6 +6,7 @@ import UiTab from './UiTab'
|
|||||||
import InfoTab from './InfoTab'
|
import InfoTab from './InfoTab'
|
||||||
import Crowdfunding from './Crowdfunding'
|
import Crowdfunding from './Crowdfunding'
|
||||||
import StoragesTab from './StoragesTab'
|
import StoragesTab from './StoragesTab'
|
||||||
|
import Blog from './Blog'
|
||||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './PreferencesModal.styl'
|
import styles from './PreferencesModal.styl'
|
||||||
@@ -19,7 +20,8 @@ class Preferences extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
currentTab: 'STORAGES',
|
currentTab: 'STORAGES',
|
||||||
UIAlert: '',
|
UIAlert: '',
|
||||||
HotkeyAlert: ''
|
HotkeyAlert: '',
|
||||||
|
BlogAlert: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,14 @@ class Preferences extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Crowdfunding />
|
<Crowdfunding />
|
||||||
)
|
)
|
||||||
|
case 'BLOG':
|
||||||
|
return (
|
||||||
|
<Blog
|
||||||
|
dispatch={dispatch}
|
||||||
|
config={config}
|
||||||
|
haveToSave={alert => this.setState({BlogAlert: alert})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'STORAGES':
|
case 'STORAGES':
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
@@ -111,7 +121,8 @@ class Preferences extends React.Component {
|
|||||||
{target: 'HOTKEY', label: 'Hotkeys', Hotkey: this.state.HotkeyAlert},
|
{target: 'HOTKEY', label: 'Hotkeys', Hotkey: this.state.HotkeyAlert},
|
||||||
{target: 'UI', label: 'Interface', UI: this.state.UIAlert},
|
{target: 'UI', label: 'Interface', UI: this.state.UIAlert},
|
||||||
{target: 'INFO', label: 'About'},
|
{target: 'INFO', label: 'About'},
|
||||||
{target: 'CROWDFUNDING', label: 'Crowdfunding'}
|
{target: 'CROWDFUNDING', label: 'Crowdfunding'},
|
||||||
|
{target: 'BLOG', label: 'Blog', Blog: this.state.BlogAlert}
|
||||||
]
|
]
|
||||||
|
|
||||||
const navButtons = tabs.map((tab) => {
|
const navButtons = tabs.map((tab) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ $danger-color = #c9302c
|
|||||||
$danger-lighten-color = lighten(#c9302c, 5%)
|
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
$statusBar-height = 0px
|
$statusBar-height = 22px
|
||||||
$sideNav-width = 200px
|
$sideNav-width = 200px
|
||||||
$sideNav--folded-width = 44px
|
$sideNav--folded-width = 44px
|
||||||
$topBar-height = 60px
|
$topBar-height = 60px
|
||||||
|
|||||||
86
docs/zh_TW/build.md
Normal file
86
docs/zh_TW/build.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 編譯
|
||||||
|
此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||||
|
|
||||||
|
## 環境
|
||||||
|
* npm: 4.x
|
||||||
|
* node: 7.x
|
||||||
|
|
||||||
|
`$ grunt pre-build` 在 `npm v5.x` 有問題,所以只能用 `npm v4.x` 。
|
||||||
|
|
||||||
|
## 開發
|
||||||
|
|
||||||
|
我們使用 Webpack HMR 來開發 Boostnote。
|
||||||
|
|
||||||
|
在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。
|
||||||
|
|
||||||
|
**用 yarn 來安裝必要 packages**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
**開始開發**
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn run dev-start
|
||||||
|
```
|
||||||
|
|
||||||
|
上述指令同時運行了 `yarn run webpack` 及 `yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。
|
||||||
|
|
||||||
|
`webpack` 會同時監控修改過的程式碼,並
|
||||||
|
The `webpack` will watch for code changes and then apply them automatically.
|
||||||
|
|
||||||
|
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> ### Notice
|
||||||
|
> There are some cases where you have to refresh the app manually.
|
||||||
|
> 1. When editing a constructor method of a component
|
||||||
|
> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.)
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
We use Grunt to automate deployment.
|
||||||
|
You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode.
|
||||||
|
|
||||||
|
So, we've prepared a separate script which just makes an executable file.
|
||||||
|
|
||||||
|
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||||
|
|
||||||
|
```
|
||||||
|
grunt pre-build
|
||||||
|
```
|
||||||
|
|
||||||
|
You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed.
|
||||||
|
|
||||||
|
If you find it necessary, you can use codesign or authenticode with this executable.
|
||||||
|
|
||||||
|
## Make own distribution packages (deb, rpm)
|
||||||
|
|
||||||
|
Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora).
|
||||||
|
|
||||||
|
> Note: You can create both `.deb` and `.rpm` in a single environment.
|
||||||
|
|
||||||
|
After installing the supported version of `node` and `npm`, install build dependency packages.
|
||||||
|
|
||||||
|
|
||||||
|
Ubuntu/Debian:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo apt-get install -y rpm fakeroot
|
||||||
|
```
|
||||||
|
|
||||||
|
Fedora:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then execute `grunt build`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ grunt build
|
||||||
|
```
|
||||||
|
|
||||||
|
You will find `.deb` and `.rpm` in the `dist` directory.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
(function (mod) {
|
||||||
|
if (typeof exports === 'object' && typeof module === 'object') { // Common JS
|
||||||
|
mod(require('../codemirror/lib/codemirror'))
|
||||||
|
} else if (typeof define === 'function' && define.amd) { // AMD
|
||||||
|
define(['../codemirror/lib/codemirror'], mod)
|
||||||
|
} else { // Plain browser env
|
||||||
|
mod(CodeMirror)
|
||||||
|
}
|
||||||
|
})(function (CodeMirror) {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/
|
||||||
|
var emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/
|
||||||
|
var unorderedListRE = /[*+-]\s/
|
||||||
|
|
||||||
|
CodeMirror.commands.boostNewLineAndIndentContinueMarkdownList = function (cm) {
|
||||||
|
if (cm.getOption('disableInput')) return CodeMirror.Pass
|
||||||
|
var ranges = cm.listSelections()
|
||||||
|
var replacements = []
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var pos = ranges[i].head
|
||||||
|
var eolState = cm.getStateAfter(pos.line)
|
||||||
|
var inList = eolState.list !== false
|
||||||
|
var inQuote = eolState.quote !== 0
|
||||||
|
var line = cm.getLine(pos.line)
|
||||||
|
var match = listRE.exec(line)
|
||||||
|
if (!ranges[i].empty() || (!inList && !inQuote) || !match || pos.ch < match[2].length - 1) {
|
||||||
|
cm.execCommand('newlineAndIndent')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (emptyListRE.test(line)) {
|
||||||
|
if (!/>\s*$/.test(line)) {
|
||||||
|
cm.replaceRange('', {
|
||||||
|
line: pos.line, ch: 0
|
||||||
|
}, {
|
||||||
|
line: pos.line, ch: pos.ch + 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
replacements[i] = '\n'
|
||||||
|
} else {
|
||||||
|
var indent = match[1]
|
||||||
|
var after = match[5]
|
||||||
|
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf('>') >= 0
|
||||||
|
? match[2].replace('x', ' ')
|
||||||
|
: (parseInt(match[3], 10) + 1) + match[4]
|
||||||
|
replacements[i] = '\n' + indent + bullet + after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.replaceSelections(replacements)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const BrowserWindow = electron.BrowserWindow
|
const BrowserWindow = electron.BrowserWindow
|
||||||
const shell = electron.shell
|
const shell = electron.shell
|
||||||
|
const ipc = electron.ipcMain
|
||||||
const mainWindow = require('./main-window')
|
const mainWindow = require('./main-window')
|
||||||
|
|
||||||
const macOS = process.platform === 'darwin'
|
const macOS = process.platform === 'darwin'
|
||||||
@@ -259,6 +260,23 @@ const view = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let editorFocused
|
||||||
|
|
||||||
|
// Define extra shortcut keys
|
||||||
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
// Synonyms for Search (Find)
|
||||||
|
if (input.control && input.key === 'f' && input.type === 'keyDown') {
|
||||||
|
if (!editorFocused) {
|
||||||
|
mainWindow.webContents.send('top:focus-search')
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipc.on('editor:focused', (event, isFocused) => {
|
||||||
|
editorFocused = isFocused
|
||||||
|
})
|
||||||
|
|
||||||
const window = {
|
const window = {
|
||||||
label: 'Window',
|
label: 'Window',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -267,6 +285,11 @@ const window = {
|
|||||||
accelerator: 'Command+M',
|
accelerator: 'Command+M',
|
||||||
selector: 'performMiniaturize:'
|
selector: 'performMiniaturize:'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Full Screen',
|
||||||
|
accelerator: 'Command+Control+F',
|
||||||
|
selector: 'toggleFullScreen:'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Close',
|
label: 'Close',
|
||||||
accelerator: 'Command+W',
|
accelerator: 'Command+W',
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<script src="../node_modules/codemirror/keymap/emacs.js"></script>
|
<script src="../node_modules/codemirror/keymap/emacs.js"></script>
|
||||||
<script src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
<script src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
||||||
|
|
||||||
<script src="../node_modules/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>
|
||||||
|
|
||||||
|
|||||||
46
node_modules/boost/boostNewLineIndentContinueMarkdownList.js
generated
vendored
46
node_modules/boost/boostNewLineIndentContinueMarkdownList.js
generated
vendored
@@ -1,46 +0,0 @@
|
|||||||
(function(mod) {
|
|
||||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
|
||||||
mod(require("../codemirror/lib/codemirror"));
|
|
||||||
else if (typeof define == "function" && define.amd) // AMD
|
|
||||||
define(["../codemirror/lib/codemirror"], mod);
|
|
||||||
else // Plain browser env
|
|
||||||
mod(CodeMirror);
|
|
||||||
})(function(CodeMirror) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
|
|
||||||
emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
|
|
||||||
unorderedListRE = /[*+-]\s/;
|
|
||||||
|
|
||||||
CodeMirror.commands.boostNewLineAndIndentContinueMarkdownList = function(cm) {
|
|
||||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
|
||||||
var ranges = cm.listSelections(), replacements = [];
|
|
||||||
for (var i = 0; i < ranges.length; i++) {
|
|
||||||
var pos = ranges[i].head;
|
|
||||||
var eolState = cm.getStateAfter(pos.line);
|
|
||||||
var inList = eolState.list !== false;
|
|
||||||
var inQuote = eolState.quote !== 0;
|
|
||||||
var line = cm.getLine(pos.line), match = listRE.exec(line);
|
|
||||||
if (!ranges[i].empty() || (!inList && !inQuote) || !match || pos.ch < match[2].length - 1) {
|
|
||||||
cm.execCommand("newlineAndIndent");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (emptyListRE.test(line)) {
|
|
||||||
if (!/>\s*$/.test(line)) cm.replaceRange("", {
|
|
||||||
line: pos.line, ch: 0
|
|
||||||
}, {
|
|
||||||
line: pos.line, ch: pos.ch + 1
|
|
||||||
});
|
|
||||||
replacements[i] = "\n";
|
|
||||||
} else {
|
|
||||||
var indent = match[1], after = match[5];
|
|
||||||
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
|
|
||||||
? match[2].replace("x", " ")
|
|
||||||
: (parseInt(match[3], 10) + 1) + match[4];
|
|
||||||
replacements[i] = "\n" + indent + bullet + after;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cm.replaceSelections(replacements);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
"electron-gh-releases": "^2.0.2",
|
"electron-gh-releases": "^2.0.2",
|
||||||
"flowchart.js": "^1.6.5",
|
"flowchart.js": "^1.6.5",
|
||||||
"font-awesome": "^4.3.0",
|
"font-awesome": "^4.3.0",
|
||||||
|
"iconv-lite": "^0.4.19",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"js-sequence-diagrams": "^1000000.0.6",
|
"js-sequence-diagrams": "^1000000.0.6",
|
||||||
"katex": "^0.8.3",
|
"katex": "^0.8.3",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"react-sortable-hoc": "^0.6.7",
|
"react-sortable-hoc": "^0.6.7",
|
||||||
"redux": "^3.5.2",
|
"redux": "^3.5.2",
|
||||||
"sander": "^0.5.1",
|
"sander": "^0.5.1",
|
||||||
|
"sanitize-html": "^1.18.2",
|
||||||
"striptags": "^2.2.1",
|
"striptags": "^2.2.1",
|
||||||
"superagent": "^1.2.0",
|
"superagent": "^1.2.0",
|
||||||
"superagent-promise": "^1.0.3"
|
"superagent-promise": "^1.0.3"
|
||||||
|
|||||||
@@ -3414,6 +3414,10 @@ iconv-lite@0.4.13, iconv-lite@~0.4.13:
|
|||||||
version "0.4.13"
|
version "0.4.13"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
|
||||||
|
|
||||||
|
iconv-lite@^0.4.19:
|
||||||
|
version "0.4.19"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||||
|
|
||||||
iconv-lite@~0.2.11:
|
iconv-lite@~0.2.11:
|
||||||
version "0.2.11"
|
version "0.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8"
|
||||||
|
|||||||
Reference in New Issue
Block a user