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

Merge branch 'master' into dracula_theme

# Conflicts:
#	browser/components/TodoListPercentage.styl
#	browser/main/Detail/TagSelect.styl
This commit is contained in:
William Grant
2018-09-30 22:16:25 +02:00
46 changed files with 419 additions and 226 deletions

View File

@@ -292,6 +292,10 @@ export default class CodeEditor extends React.Component {
this.editor.on('cursorActivity', this.editorActivityHandler)
this.editor.on('changes', this.editorActivityHandler)
}
this.setState({
clientWidth: this.refs.root.clientWidth
})
}
expandSnippet (line, cursor, cm, snippets) {
@@ -441,6 +445,14 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('extraKeys', this.defaultKeyMap)
}
if (this.state.clientWidth !== this.refs.root.clientWidth) {
this.setState({
clientWidth: this.refs.root.clientWidth
})
needRefresh = true
}
if (needRefresh) {
this.editor.refresh()
}

View File

@@ -17,8 +17,11 @@ import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const { remote } = require('electron')
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote
@@ -27,6 +30,8 @@ const fileUrl = require('file-url')
const dialog = remote.dialog
const uri2path = require('file-uri-to-path')
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
@@ -161,7 +166,6 @@ const scrollBarDarkStyle = `
}
`
const { shell } = require('electron')
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
@@ -219,8 +223,32 @@ export default class MarkdownPreview extends React.Component {
}
}
handleContextMenu (e) {
this.props.onContextMenu(e)
handleContextMenu (event) {
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event)
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a') {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
context.popup([
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
])
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
}
}
handleDoubleClick (e) {

View File

@@ -74,24 +74,22 @@ const NoteItem = ({
? note.title
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
</div>
{['ALL', 'STORAGE'].includes(viewType) &&
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
<div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div
title={
viewType === 'ALL'
? storageName
: viewType === 'STORAGE' ? folderName : null
}
styleName='item-middle-app-meta-label'
>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div>}
</div>
</div>
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0

View File

@@ -368,13 +368,13 @@ body[data-theme="monokai"]
.item-title
.item-title-icon
.item-bottom-time
color $ui-monokai-text-color
color $ui-monokai-active-color
.item-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-monokai-text-color
&:hover
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
color #c0392b
color #f92672
.item-bottom-tagList-item
background-color alpha(#fff, 20%)

View File

@@ -52,13 +52,13 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.percentageBar
background-color #f92672
background-color: $ui-monokai-borderColor
.progressBar
background-color: #373831
background-color $ui-monokai-active-color
.percentageText
color #fdf6e3
color $ui-monokai-text-color
body[data-theme="dracula"]
.percentageBar
@@ -68,4 +68,4 @@ body[data-theme="dracula"]
background-color: $ui-dracula-active-color
.percentageText
color $ui-dracula-text-color
color $ui-dracula-text-color

View File

@@ -148,7 +148,8 @@ class Markdown {
}
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'))
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
this.md.use(require('./markdown-it-frontmatter'))
const deflate = require('markdown-it-plantuml/lib/deflate')

View File

@@ -759,6 +759,7 @@ class SnippetNoteDetail extends React.Component {
<TagSelect
ref='tags'
value={this.state.note.tags}
data={data}
onChange={(e) => this.handleChange(e)}
/>
</div>

View File

@@ -88,6 +88,11 @@ class TagSelect extends React.Component {
this.refs.newTag.input.focus()
}
handleTagLabelClick (tag) {
const { router } = this.context
router.push(`/tags/${tag}`)
}
handleTagRemoveButtonClick (tag) {
this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1)
@@ -182,7 +187,7 @@ class TagSelect extends React.Component {
<span styleName='tag'
key={tag}
>
<span styleName='tag-label'>#{tag}</span>
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
>
@@ -228,6 +233,10 @@ class TagSelect extends React.Component {
}
}
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),

View File

@@ -39,8 +39,9 @@
.tag-label
font-size 13px
color: $ui-text-color
color $ui-text-color
padding 4px 16px 4px 8px
cursor pointer
body[data-theme="dark"]
.tag
@@ -67,7 +68,7 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.tag
background-color $ui-monokai-button-backgroundColor
background-color $ui-monokai-tag-backgroundColor
.tag-removeButton
border-color $ui-button--focus-borderColor
@@ -85,4 +86,4 @@ body[data-theme="dracula"]
background-color transparent
.tag-label
color $ui-dracula-borderColor
color $ui-dracula-borderColor

View File

@@ -59,9 +59,9 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.control-toggleModeButton
background-color #272822
background-color #373831
.active
background-color #1EC38B
background-color #f92672
box-shadow 2px 0px 7px #222222
body[data-theme="dracula"]

View File

@@ -56,7 +56,7 @@ class Main extends React.Component {
init () {
dataApi
.addStorage({
name: 'My Storage',
name: 'My Storage Location',
path: path.join(remote.app.getPath('home'), 'Boostnote')
})
.then(data => {

View File

@@ -519,7 +519,7 @@ class NoteList extends React.Component {
click: this.cloneNote.bind(this)
}, {
label: copyNoteLink,
click: this.copyNoteLink(note)
click: this.copyNoteLink.bind(this, note)
})
if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) {

View File

@@ -24,7 +24,7 @@ export const DEFAULT_CONFIG = {
amaEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M'
},
ui: {
language: 'en',
@@ -197,6 +197,7 @@ function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
config.hotkey[key] = config.hotkey[key].replace(/Opt/g, 'Alt')
})
return config
}

View File

@@ -11,6 +11,118 @@ import i18n from 'browser/lib/i18n'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments'
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
/**
* @description
* Create a Image element to get the real size of image.
* @param {File} file the File object dropped.
* @returns {Promise<Image>} Image element created
*/
function getImage (file) {
return new Promise((resolve) => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
}
/**
* @description
* Get the orientation info from iamges's EXIF data.
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
* Other: reserved
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
* @param {File} file the File object dropped.
* @returns {Promise<Number>} Orientation info
*/
function getOrientation (file) {
const getData = arrayBuffer => {
const view = new DataView(arrayBuffer)
// Not start with SOI(Start of image) Marker return fail value
if (view.getUint16(0, false) !== 0xFFD8) return -2
const length = view.byteLength
let offset = 2
while (offset < length) {
const marker = view.getUint16(offset, false)
offset += 2
// Loop and seed for APP1 Marker
if (marker === 0xFFE1) {
// return fail value if it isn't EXIF data
if (view.getUint32(offset += 2, false) !== 0x45786966) {
return -1
}
// Read TIFF header,
// First 2bytes defines byte align of TIFF data.
// If it is 0x4949="II", it means "Intel" type byte align.
// If it is 0x4d4d="MM", it means "Motorola" type byte align
const little = view.getUint16(offset += 6, false) === 0x4949
offset += view.getUint32(offset + 4, little)
const tags = view.getUint16(offset, little) // Get TAG number
offset += 2
for (let i = 0; i < tags; i++) {
// Loop to find Orientation TAG and return the value
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
return view.getUint16(offset + (i * 12) + 8, little)
}
}
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
break
} else {
offset += view.getUint16(offset, false)
}
}
return -1
}
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = event => resolve(getData(event.target.result))
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
})
}
/**
* @description
* Rotate image file to correct direction.
* Create a canvas and draw the image with correct direction, then export to base64 format.
* @param {*} file the File object dropped.
* @return {String} Base64 encoded image.
*/
function fixRotate (file) {
return Promise.all([getImage(file), getOrientation(file)])
.then(([img, orientation]) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (orientation > 4 && orientation < 9) {
canvas.width = img.height
canvas.height = img.width
} else {
canvas.width = img.width
canvas.height = img.height
}
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
default: break
}
ctx.drawImage(img, 0, 0)
return canvas.toDataURL()
})
}
/**
* @description
@@ -38,26 +150,34 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
}
try {
if (!fs.existsSync(sourceFilePath)) {
reject('source file does not exist')
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
if (!fs.existsSync(sourceFilePath) && !isBase64) {
return reject('source file does not exist')
}
const targetStorage = findStorage.findStorage(storageKey)
const inputFileStream = fs.createReadStream(sourceFilePath)
let destinationName
if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
} else {
destinationName = path.basename(sourceFilePath)
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
}
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
inputFileStream.pipe(outputFile)
inputFileStream.on('end', () => {
resolve(destinationName)
})
if (isBase64) {
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
const dataBuffer = new Buffer(base64Data, 'base64')
outputFile.write(dataBuffer, () => {
resolve(destinationName)
})
} else {
const inputFileStream = fs.createReadStream(sourceFilePath)
inputFileStream.pipe(outputFile)
inputFileStream.on('end', () => {
resolve(destinationName)
})
}
} catch (e) {
return reject(e)
}
@@ -137,10 +257,17 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const filePath = file.path
const originalFileName = path.basename(filePath)
const fileType = file['type']
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
const showPreview = fileType.startsWith('image')
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
const isImage = fileType.startsWith('image')
let promise
if (isImage) {
promise = fixRotate(file).then(base64data => {
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
})
} else {
promise = copyAttachment(filePath, storageKey, noteKey)
}
promise.then((fileName) => {
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
codeEditor.insertAttachmentMd(imageMd)
})
}

View File

@@ -43,7 +43,7 @@ class Blog extends React.Component {
this.handleSettingError = (err) => {
this.setState({BlogAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
this.oldBlog = this.state.config.blog
@@ -70,7 +70,7 @@ class Blog extends React.Component {
this.props.haveToSave({
tab: 'Blog',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
}

View File

@@ -68,9 +68,9 @@
:global
.alert
display inline-block
position absolute
top 60px
right 15px
position fixed
top 130px
right 100px
font-size 14px
.success
color #1EC38B

View File

@@ -23,18 +23,18 @@ class Crowdfunding extends React.Component {
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
<p>{i18n.__('Dear everyone,')}</p>
<p>{i18n.__('Dear Boostnote users,')}</p>
<br />
<p>{i18n.__('Thank you for using Boostnote!')}</p>
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
<br />
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
<p>{i18n.__('To support our growing userbase, and satisfy community expectations,')}</p>
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
<br />
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
<p>{i18n.__('If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!')}</p>
<br />
<p>{i18n.__('Thanks,')}</p>
<p>{i18n.__('Boostnote maintainers')}</p>
<p>{i18n.__('The Boostnote Team')}</p>
<br />
<button styleName='cf-link'>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>

View File

@@ -2,6 +2,7 @@
height 35px
box-sizing border-box
padding 2.5px 15px
display flex
&:hover
background-color darken(white, 3%)
@@ -18,7 +19,10 @@
border-left solid 2px transparent
padding 0 10px
line-height 30px
float left
flex 1
white-space nowrap
text-overflow ellipsis
overflow hidden
.folderItem-left-danger
color $danger-color
font-weight bold
@@ -52,7 +56,8 @@
outline none
.folderItem-right
float right
-webkit-box-flex: 1
white-space nowrap
.folderItem-right-button
vertical-align middle

View File

@@ -30,7 +30,7 @@ class HotkeyTab extends React.Component {
this.handleSettingError = (err) => {
this.setState({keymapAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
this.oldHotkey = this.state.config.hotkey
@@ -79,7 +79,7 @@ class HotkeyTab extends React.Component {
this.props.haveToSave({
tab: 'Hotkey',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
}
@@ -117,7 +117,7 @@ class HotkeyTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}

View File

@@ -9,13 +9,17 @@
box-sizing border-box
border-bottom $default-border
margin-bottom 5px
display flex
.header-label
float left
cursor pointer
&:hover
.header-label-editButton
opacity 1
flex 1
white-space nowrap
text-overflow ellipsis
overflow hidden
.header-label-path
color $ui-inactive-text-color
@@ -38,8 +42,8 @@
outline none
.header-control
float right
-webkit-box-flex: 1
white-space nowrap
.header-control-button
width 30px
height 25px

View File

@@ -70,7 +70,7 @@ class StoragesTab extends React.Component {
})
return (
<div styleName='list'>
<div styleName='header'>{i18n.__('Storages')}</div>
<div styleName='header'>{i18n.__('Storage Locations')}</div>
{storageList.length > 0
? storageList
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>

View File

@@ -40,7 +40,7 @@ class UiTab extends React.Component {
this.handleSettingError = (err) => {
this.setState({UiAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
message: err.message != null ? err.message : i18n.__('An error occurred!')
}})
}
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -125,7 +125,7 @@ class UiTab extends React.Component {
this.props.haveToSave({
tab: 'UI',
type: 'warning',
message: i18n.__('You have to save!')
message: i18n.__('Unsaved Changes!')
})
}
})
@@ -499,7 +499,7 @@ class UiTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Code block Theme')}</div>
<div styleName='group-section-label'>{i18n.__('Code Block Theme')}</div>
<div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme'

View File

@@ -379,7 +379,7 @@ $ui-monokai-active-color = #f92672
$ui-monokai-borderColor = #373831
$ui-monokai-tag-backgroundColor = #f92672
$ui-monokai-tag-backgroundColor = #373831
$ui-monokai-button-backgroundColor = #373831
$ui-monokai-button--active-color = white