Merge branch 'master' into master
@@ -19,5 +19,8 @@
|
||||
"FileReader": true,
|
||||
"localStorage": true,
|
||||
"fetch": true
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 6
|
||||
- 7
|
||||
script:
|
||||
- npm run lint && npm run test
|
||||
- yarn jest
|
||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||
after_success:
|
||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||
|
||||
@@ -3,12 +3,10 @@ import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import path from 'path'
|
||||
import copyImage from 'browser/main/lib/dataApi/copyImage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import fs from 'fs'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
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'
|
||||
@@ -275,23 +273,13 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
handleDropImage (e) {
|
||||
e.preventDefault()
|
||||
const ValidImageTypes = ['image/gif', 'image/jpeg', 'image/png']
|
||||
|
||||
const file = e.dataTransfer.files[0]
|
||||
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)
|
||||
})
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
|
||||
}
|
||||
|
||||
insertImageMd (imageMd) {
|
||||
insertAttachmentMd (imageMd) {
|
||||
this.editor.replaceSelection(imageMd)
|
||||
}
|
||||
|
||||
@@ -317,24 +305,8 @@ export default class CodeEditor extends React.Component {
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
const reader = new FileReader()
|
||||
let base64data
|
||||
|
||||
reader.readAsDataURL(blob)
|
||||
reader.onloadend = () => {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
const imageName = Math.random().toString(36).slice(-16)
|
||||
const storagePath = findStorage(this.props.storageKey).path
|
||||
const imageDir = path.join(storagePath, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const imagePath = path.join(imageDir, `${imageName}.png`)
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = `})`
|
||||
this.insertImageMd(imageMd)
|
||||
}
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import {findStorage} from 'browser/lib/findStorage'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -223,7 +223,7 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, value, config, storageKey } = this.props
|
||||
const {className, value, config, storageKey, noteKey} = this.props
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -263,6 +263,7 @@ class MarkdownEditor extends React.Component {
|
||||
displayLineNumbers={config.editor.displayLineNumbers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
@@ -293,6 +294,7 @@ class MarkdownEditor extends React.Component {
|
||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,9 +13,11 @@ import htmlTextHelper from 'browser/lib/htmlTextHelper'
|
||||
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 { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
const dialog = remote.dialog
|
||||
@@ -216,8 +218,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme)
|
||||
const body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||
|
||||
files.forEach((file) => {
|
||||
file = file.replace('file://', '')
|
||||
@@ -226,6 +230,13 @@ export default class MarkdownPreview extends React.Component {
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||
|
||||
let styles = ''
|
||||
files.forEach((file) => {
|
||||
@@ -398,13 +409,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
|
||||
let renderedHTML = this.markdown.render(value)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (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)
|
||||
})
|
||||
|
||||
@@ -416,12 +425,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
|
||||
el.src = this.markdown.normalizeLinkText(el.src)
|
||||
if (!/\/:storage/.test(el.src)) return
|
||||
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
|
||||
@@ -88,7 +88,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, value, storageKey } = this.props
|
||||
const {config, value, storageKey, noteKey} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
@@ -115,6 +115,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
/>
|
||||
@@ -138,6 +139,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -321,3 +321,76 @@ body[data-theme="solarized-dark"]
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha($ui-monokai-noteList-backgroundColor, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-wrapper
|
||||
border-color alpha($ui-monokai-button-backgroundColor, 60%)
|
||||
|
||||
.item--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.item-wrapper
|
||||
border-color transparent
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-monokai-text-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
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
.item-title
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-icon
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-title-empty
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
@@ -212,3 +212,61 @@ body[data-theme="solarized-dark"]
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
&:hover
|
||||
transition 0.15s
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
transition 0.15s
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.item-simple--active
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
.item-simple-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
.item-simple-right
|
||||
float right
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
@@ -41,3 +41,14 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.notification-area
|
||||
background-color none
|
||||
|
||||
.notification-link
|
||||
color $ui-monokai-text-color
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
@@ -51,7 +51,7 @@ const SideNavFilter = ({
|
||||
</button>
|
||||
|
||||
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
|
||||
onClick={handleTrashedButtonClick}
|
||||
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu}
|
||||
>
|
||||
<div styleName='iconWrap'>
|
||||
<img src={isTrashedActive
|
||||
@@ -60,7 +60,7 @@ const SideNavFilter = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||
<span styleName='menu-button-label'>{i18n.__('Trash')}</span>
|
||||
<span styleName='counters'>{counterDelNote}</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -222,4 +222,46 @@ body[data-theme="solarized-dark"]
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
.menu-button-label
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.menu-button
|
||||
&:active
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-star--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.menu-button-trash--active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
@@ -138,3 +138,22 @@ body[data-theme="solarized-dark"]
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.folderList-item
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.folderList-item--active
|
||||
@extend .folderList-item
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:active
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
@@ -9,16 +9,26 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Function} handleClickTagListItem
|
||||
* @param {Function} handleClickNarrowToTag
|
||||
* @param {bool} isActive
|
||||
* @param {bool} isRelated
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, isActive, count}) => (
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'> {count}</span>
|
||||
</span>
|
||||
</button>
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||
<div styleName='tagList-itemContainer'>
|
||||
{isRelated
|
||||
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
|
||||
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
|
||||
</button>
|
||||
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
|
||||
}
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'>{count}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
TagListItem.propTypes = {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.tagList-itemContainer
|
||||
display flex
|
||||
|
||||
.tagList-item
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
background-color transparent
|
||||
@@ -20,9 +24,16 @@
|
||||
color $ui-button-default-color
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
|
||||
.tagList-itemNarrow
|
||||
composes tagList-item
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-active
|
||||
background-color $ui-button-default--active-backgroundColor
|
||||
display flex
|
||||
flex 1
|
||||
width 100%
|
||||
height 26px
|
||||
padding 0
|
||||
@@ -36,10 +47,16 @@
|
||||
background-color alpha($ui-button-default--active-backgroundColor, 60%)
|
||||
transition 0.2s
|
||||
|
||||
.tagList-itemNarrow-active
|
||||
composes tagList-item-active
|
||||
flex none
|
||||
width 20px
|
||||
padding 0 4px
|
||||
|
||||
.tagList-item-name
|
||||
display block
|
||||
flex 1
|
||||
padding 0 15px
|
||||
padding 0 8px 0 4px
|
||||
height 26px
|
||||
line-height 26px
|
||||
border-width 0 0 0 2px
|
||||
@@ -49,7 +66,10 @@
|
||||
text-overflow ellipsis
|
||||
|
||||
.tagList-item-count
|
||||
padding 0 3px
|
||||
float right
|
||||
line-height 26px
|
||||
padding-right 15px
|
||||
font-size 13px
|
||||
|
||||
body[data-theme="white"]
|
||||
.tagList-item
|
||||
|
||||
@@ -47,5 +47,15 @@ body[data-theme="solarized-dark"]
|
||||
.progressBar
|
||||
background-color: #2aa198
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.percentageBar
|
||||
background-color #f92672
|
||||
|
||||
.progressBar
|
||||
background-color: #373831
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
@@ -371,3 +371,30 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
themeMonokaiTableHead = themeMonokaiTableEven
|
||||
themeMonokaiTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
table
|
||||
thead
|
||||
tr
|
||||
background-color themeMonokaiTableHead
|
||||
th
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color themeMonokaiTableOdd
|
||||
tr:nth-child(2n)
|
||||
background-color themeMonokaiTableEven
|
||||
td
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
75
browser/lib/Languages.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const languages = [
|
||||
{
|
||||
name: 'Albanian',
|
||||
locale: 'sq'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-CN)',
|
||||
locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
name: 'Chinese (zh-TW)',
|
||||
locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
locale: 'da'
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
locale: 'en'
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
locale: 'fr'
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
locale: 'de'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
locale: 'hu'
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
locale: 'ja'
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
locale: 'ko'
|
||||
},
|
||||
{
|
||||
name: 'Norwegian',
|
||||
locale: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
locale: 'pt'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
locale: 'ru'
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
locale: 'es-ES'
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
getLocales () {
|
||||
return languages.reduce(function (localeList, locale) {
|
||||
localeList.push(locale.locale)
|
||||
return localeList
|
||||
}, [])
|
||||
},
|
||||
getLanguages () {
|
||||
return languages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const path = require('path')
|
||||
const { remote } = require('electron')
|
||||
const { app } = remote
|
||||
const { getLocales } = require('./Languages.js')
|
||||
|
||||
// load package for localization
|
||||
const i18n = new (require('i18n-2'))({
|
||||
// setup some locales - other locales default to the first locale
|
||||
locales: [ 'da', 'de', 'en', 'es-ES', 'fr', 'hu', 'ja', 'ko', 'pl', 'pt-BR', 'pt-PT', 'ru', 'sq', 'zh-CN', 'zh-TW' ],
|
||||
locales: getLocales(),
|
||||
extension: '.json',
|
||||
directory: process.env.NODE_ENV === 'production'
|
||||
? path.join(app.getAppPath(), './locales')
|
||||
|
||||
@@ -5,7 +5,7 @@ import math from '@rokt33r/markdown-it-math'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import {lastFindInArray} from './utils'
|
||||
import { lastFindInArray } from './utils'
|
||||
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -234,10 +234,6 @@ class Markdown {
|
||||
if (!_.isString(content)) content = ''
|
||||
return this.md.render(content)
|
||||
}
|
||||
|
||||
normalizeLinkText (linkText) {
|
||||
return this.md.normalizeLinkText(linkText)
|
||||
}
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
|
||||
@@ -30,3 +30,10 @@ body[data-theme="solarized-dark"]
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -133,3 +133,29 @@ body[data-theme="dark"]
|
||||
color $ui-dark-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-dark-text-color
|
||||
&:hover
|
||||
color white
|
||||
background-color $ui-monokai-button--hover-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.search-optionList
|
||||
color white
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.search-optionList-item
|
||||
&:hover
|
||||
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%)
|
||||
|
||||
.search-optionList-item--active
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
&:hover
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-monokai-inactive-text-color
|
||||
|
||||
@@ -215,3 +215,43 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-ark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-infoButton-panel
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-infoButton-panel-trash
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.modification-date
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.modification-date-desc
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-defaul-count
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub-count
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-default
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.infoPanel-sub
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.infoPanel-noteLink
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
|
||||
[id=export-wrap]
|
||||
button
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
background-color alpha($ui-monokai-borderColor, 20%)
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -289,6 +289,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
@@ -298,6 +299,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
config={config}
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
onChange={this.handleUpdateContent.bind(this)}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
/>
|
||||
|
||||
@@ -71,3 +71,8 @@ body[data-theme="solarized-dark"]
|
||||
.root
|
||||
border-left 1px solid $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
@@ -98,3 +98,7 @@ body[data-theme="solarized-dark"]
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
@@ -152,4 +152,21 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.tabList
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.body .description textarea
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
border 1px solid $ui-monokai-borderColor
|
||||
|
||||
.tabList
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
@@ -81,4 +81,20 @@ body[data-theme="solarized-dark"]
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
background-color transparent
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -56,3 +56,10 @@ body[data-theme="solarized-dark"]
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #272822
|
||||
.active
|
||||
background-color #1EC38B
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
@@ -15,6 +15,7 @@ import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { hashHistory } from 'react-router'
|
||||
import store from 'browser/main/store'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLocales } from 'browser/lib/Languages'
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -143,7 +144,8 @@ class Main extends React.Component {
|
||||
const supportedThemes = [
|
||||
'dark',
|
||||
'white',
|
||||
'solarized-dark'
|
||||
'solarized-dark',
|
||||
'monokai'
|
||||
]
|
||||
|
||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||
@@ -152,24 +154,7 @@ class Main extends React.Component {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
const supportedLanguages = [
|
||||
'sq',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'da',
|
||||
'fr',
|
||||
'de',
|
||||
'hu',
|
||||
'ja',
|
||||
'ko',
|
||||
'no',
|
||||
'pl',
|
||||
'pt',
|
||||
'ru',
|
||||
'es-ES'
|
||||
]
|
||||
|
||||
if (supportedLanguages.indexOf(config.ui.language) !== -1) {
|
||||
if (getLocales().indexOf(config.ui.language) !== -1) {
|
||||
i18n.setLocale(config.ui.language)
|
||||
} else {
|
||||
i18n.setLocale('en')
|
||||
|
||||
@@ -74,4 +74,8 @@ body[data-theme="dark"]
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
@@ -113,4 +113,28 @@ body[data-theme="solarized-dark"]
|
||||
.control-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.control-sortBy-select
|
||||
&:hover
|
||||
transition 0.2s
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-button
|
||||
color $ui-monokai-inactive-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-button--active
|
||||
color $ui-monokai-text-color
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -343,11 +343,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
if (location.pathname.match(/\/tags/)) {
|
||||
const listOfTags = params.tagname.split(' ')
|
||||
return data.noteMap.map(note => {
|
||||
return note
|
||||
}).filter(note => {
|
||||
return note.tags.includes(params.tagname)
|
||||
})
|
||||
}).filter(note => listOfTags.every(tag => note.tags.includes(tag)))
|
||||
}
|
||||
|
||||
return this.getContextNotes()
|
||||
@@ -456,12 +455,19 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleDragStart (e, note) {
|
||||
const { selectedNoteKeys } = this.state
|
||||
let { selectedNoteKeys } = this.state
|
||||
const noteKey = getNoteKey(note)
|
||||
|
||||
if (!selectedNoteKeys.includes(noteKey)) {
|
||||
selectedNoteKeys = []
|
||||
selectedNoteKeys.push(noteKey)
|
||||
}
|
||||
|
||||
const notes = this.notes.map((note) => Object.assign({}, note))
|
||||
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
|
||||
const noteData = JSON.stringify(selectedNotes)
|
||||
e.dataTransfer.setData('note', noteData)
|
||||
this.setState({ selectedNoteKeys: [] })
|
||||
this.selectNextNote()
|
||||
}
|
||||
|
||||
handleNoteContextMenu (e, uniqueKey) {
|
||||
@@ -916,7 +922,7 @@ class NoteList extends React.Component {
|
||||
if (note.isTrashed !== true || location.pathname === '/trashed') return true
|
||||
})
|
||||
|
||||
moment.locale('en', {
|
||||
moment.updateLocale('en', {
|
||||
relativeTime: {
|
||||
future: 'in %s',
|
||||
past: '%s ago',
|
||||
|
||||
@@ -117,3 +117,8 @@ body[data-theme="solarized-dark"]
|
||||
.root, .root--folded
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
border-right 1px solid $ui-solarized-dark-borderColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--folded
|
||||
background-color $ui-monokai-backgroundColor
|
||||
border-right 1px solid $ui-monokai-borderColor
|
||||
|
||||
@@ -145,20 +145,27 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location, config } = this.props
|
||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
(tag, name) => ({name, size: tag.size})),
|
||||
['name']
|
||||
)
|
||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||
), ['name'])
|
||||
if (config.sortTagsBy === 'COUNTER') {
|
||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||
}
|
||||
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
||||
tagList = tagList.filter(
|
||||
tag => tag.related
|
||||
)
|
||||
}
|
||||
return (
|
||||
tagList.map(tag => {
|
||||
return (
|
||||
<TagListItem
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
isRelated={tag.related}
|
||||
key={tag.name}
|
||||
count={tag.size}
|
||||
/>
|
||||
@@ -167,10 +174,30 @@ class SideNav extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
getRelatedTags (activeTags, noteMap) {
|
||||
if (activeTags.length === 0) {
|
||||
return new Set()
|
||||
}
|
||||
const relatedNotes = noteMap.map(
|
||||
note => ({key: note.key, tags: note.tags})
|
||||
).filter(
|
||||
note => activeTags.every(tag => note.tags.includes(tag))
|
||||
)
|
||||
let relatedTags = new Set()
|
||||
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
||||
return relatedTags
|
||||
}
|
||||
|
||||
getTagActive (path, tag) {
|
||||
return this.getActiveTags(path).includes(tag)
|
||||
}
|
||||
|
||||
getActiveTags (path) {
|
||||
const pathSegments = path.split('/')
|
||||
const pathTag = pathSegments[pathSegments.length - 1]
|
||||
return pathTag === tag
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
@@ -192,6 +219,19 @@ class SideNav extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleClickNarrowToTag (tag) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let listOfTags = this.getActiveTags(location.pathname)
|
||||
const indexOfTag = listOfTags.indexOf(tag)
|
||||
if (indexOfTag > -1) {
|
||||
listOfTags.splice(indexOfTag, 1)
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
const { dispatch } = this.props
|
||||
const deletionPromises = entries.map((note) => {
|
||||
|
||||
@@ -69,3 +69,14 @@ body[data-theme="dark"]
|
||||
navDarkButtonColor()
|
||||
border-color $ui-dark-borderColor
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
navButtonColor()
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-monokai-active-color
|
||||
&:active
|
||||
color $ui-monokai-active-color
|
||||
|
||||
@@ -234,3 +234,25 @@ body[data-theme="solarized-dark"]
|
||||
input
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-monokai-borderColor
|
||||
.control-search
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-monokai-inactive-text-color
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -134,4 +134,10 @@ body[data-theme="solarized-dark"]
|
||||
.sortableItemHelper
|
||||
color: $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color $ui-monokai-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-monokai-text-color
|
||||
|
||||
|
||||
@@ -135,6 +135,8 @@ function set (updates) {
|
||||
document.body.setAttribute('data-theme', 'white')
|
||||
} else if (newConfig.ui.theme === 'solarized-dark') {
|
||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||
} else if (newConfig.ui.theme === 'monokai') {
|
||||
document.body.setAttribute('data-theme', 'monokai')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
204
browser/main/lib/dataApi/attachmentManagement.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const uniqueSlug = require('unique-slug')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const findStorage = require('browser/lib/findStorage')
|
||||
const mdurl = require('mdurl')
|
||||
const escapeStringRegexp = require('escape-string-regexp')
|
||||
|
||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||
const DESTINATION_FOLDER = 'attachments'
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Copies a copy of an attachment to the storage folder specified by the given key and return the generated attachment name.
|
||||
* Renames the file to match a unique file name.
|
||||
*
|
||||
* @param {String} sourceFilePath The source path of the attachment to be copied
|
||||
* @param {String} storageKey Storage key of the destination storage
|
||||
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
|
||||
* @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used
|
||||
* @return {Promise<String>} name (inclusive extension) of the generated file
|
||||
*/
|
||||
function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!sourceFilePath) {
|
||||
reject('sourceFilePath has to be given')
|
||||
}
|
||||
|
||||
if (!storageKey) {
|
||||
reject('storageKey has to be given')
|
||||
}
|
||||
|
||||
if (!noteKey) {
|
||||
reject('noteKey has to be given')
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(sourceFilePath)) {
|
||||
reject('source file does not exist')
|
||||
}
|
||||
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
|
||||
const inputFile = fs.createReadStream(sourceFilePath)
|
||||
let destinationName
|
||||
if (useRandomName) {
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
|
||||
} else {
|
||||
destinationName = path.basename(sourceFilePath)
|
||||
}
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||
inputFile.pipe(outputFile)
|
||||
resolve(destinationName)
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
||||
let destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER)
|
||||
if (!fs.existsSync(destinationDir)) {
|
||||
fs.mkdirSync(destinationDir)
|
||||
}
|
||||
destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey)
|
||||
if (!fs.existsSync(destinationDir)) {
|
||||
fs.mkdirSync(destinationDir)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files.
|
||||
* @param {String} renderedHTML HTML in that the links should be fixed
|
||||
* @param {String} storagePath Path of the current storage
|
||||
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
|
||||
*/
|
||||
function fixLocalURLS (renderedHTML, storagePath) {
|
||||
return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Generates the markdown code for a given attachment
|
||||
* @param {String} fileName Name of the attachment
|
||||
* @param {String} path Path of the attachment
|
||||
* @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported
|
||||
* @returns {String} Generated markdown code
|
||||
*/
|
||||
function generateAttachmentMarkdown (fileName, path, showPreview) {
|
||||
return `${showPreview ? '!' : ''}[${fileName}](${path})`
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Handles the drop-event of a file. Includes the necessary markdown code and copies the file to the corresponding storage folder.
|
||||
* The method calls {CodeEditor#insertAttachmentMd()} to include the generated markdown at the needed place!
|
||||
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||
* @param {String} storageKey Key of the current storage
|
||||
* @param {String} noteKey Key of the current note
|
||||
* @param {Event} dropEvent DropEvent
|
||||
*/
|
||||
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
const file = dropEvent.dataTransfer.files[0]
|
||||
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)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
|
||||
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
|
||||
* @param {String} storageKey Key of the current storage
|
||||
* @param {String} noteKey Key of the current note
|
||||
* @param {DataTransferItem} dataTransferItem Part of the past-event
|
||||
*/
|
||||
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
|
||||
if (!codeEditor) {
|
||||
throw new Error('codeEditor has to be given')
|
||||
}
|
||||
if (!storageKey) {
|
||||
throw new Error('storageKey has to be given')
|
||||
}
|
||||
|
||||
if (!noteKey) {
|
||||
throw new Error('noteKey has to be given')
|
||||
}
|
||||
if (!dataTransferItem) {
|
||||
throw new Error('dataTransferItem has to be given')
|
||||
}
|
||||
|
||||
const blob = dataTransferItem.getAsFile()
|
||||
const reader = new FileReader()
|
||||
let base64data
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
|
||||
const imageName = `${uniqueSlug()}.png`
|
||||
const imagePath = path.join(destinationDir, imageName)
|
||||
|
||||
reader.onloadend = function () {
|
||||
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
|
||||
base64data += base64data.replace('+', ' ')
|
||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||
fs.writeFile(imagePath, binaryData, 'binary')
|
||||
const imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
}
|
||||
reader.readAsDataURL(blob)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns all attachment paths of the given markdown
|
||||
* @param {String} markdownContent content in which the attachment paths should be found
|
||||
* @returns {String[]} Array of the relativ paths (starting with :storage) of the attachments of the given markdown
|
||||
*/
|
||||
function getAttachmentsInContent (markdownContent) {
|
||||
const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep)
|
||||
const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g')
|
||||
return preparedInput.match(regexp)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns an array of the absolute paths of the attachments referenced in the given markdown code
|
||||
* @param {String} markdownContent content in which the attachment paths should be found
|
||||
* @param {String} storagePath path of the current storage
|
||||
* @returns {String[]} Absolute paths of the referenced attachments
|
||||
*/
|
||||
function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
||||
const temp = getAttachmentsInContent(markdownContent)
|
||||
const result = []
|
||||
for (const relativePath of temp) {
|
||||
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Deletes all :storage and noteKey references from the given input.
|
||||
* @param input Input in which the references should be deleted
|
||||
* @param noteKey Key of the current note
|
||||
* @returns {String} Input without the references
|
||||
*/
|
||||
function removeStorageAndNoteReferences (input, noteKey) {
|
||||
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), DESTINATION_FOLDER)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
copyAttachment,
|
||||
fixLocalURLS,
|
||||
generateAttachmentMarkdown,
|
||||
handleAttachmentDrop,
|
||||
handlePastImageEvent,
|
||||
getAttachmentsInContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
removeStorageAndNoteReferences,
|
||||
STORAGE_FOLDER_PLACEHOLDER,
|
||||
DESTINATION_FOLDER
|
||||
}
|
||||
@@ -2,6 +2,8 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
// TODO: ehhc: delete this
|
||||
|
||||
/**
|
||||
* @description Copy an image and return the path.
|
||||
* @param {String} filePath
|
||||
@@ -21,8 +23,12 @@ function copyImage (filePath, storageKey, rename = true) {
|
||||
const imageDir = path.join(targetStorage.path, 'images')
|
||||
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
|
||||
const outputImage = fs.createWriteStream(path.join(imageDir, basename))
|
||||
outputImage.on('error', reject)
|
||||
inputImage.on('error', reject)
|
||||
inputImage.on('end', () => {
|
||||
resolve(basename)
|
||||
})
|
||||
inputImage.pipe(outputImage)
|
||||
resolve(basename)
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||
import {findStorage} from 'browser/lib/findStorage'
|
||||
import filenamify from 'filenamify'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||
const IMAGES_FOLDER_NAME = 'images'
|
||||
|
||||
/**
|
||||
* Export note together with images
|
||||
*
|
||||
@@ -28,21 +24,7 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
|
||||
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
||||
dstFilename = filenamify(dstFilename, {replacement: '_'})
|
||||
if (!path.extname(dstFilename)) {
|
||||
dstFilename += path.extname(srcFilename)
|
||||
}
|
||||
|
||||
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
||||
|
||||
exportTasks.push({
|
||||
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
||||
dst: dstRelativePath
|
||||
})
|
||||
|
||||
return ``
|
||||
})
|
||||
let exportedData = noteContent
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks)
|
||||
|
||||
@@ -68,6 +68,8 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
return noteData
|
||||
})
|
||||
.then(function moveImages (noteData) {
|
||||
if (oldStorage.path === newStorage.path) return noteData
|
||||
|
||||
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
||||
let match = searchImagesRegex.exec(noteData.content)
|
||||
|
||||
@@ -75,6 +77,7 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
|
||||
while (match != null) {
|
||||
const [, filename] = match
|
||||
const oldPath = path.join(oldStorage.path, 'images', filename)
|
||||
// TODO: ehhc: attachmentManagement
|
||||
moveTasks.push(
|
||||
copyImage(oldPath, noteData.storage, false)
|
||||
.then(() => {
|
||||
|
||||
@@ -102,3 +102,29 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.control-confirmButton
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
modalMonokai()
|
||||
width 500px
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-folder-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-folder-input
|
||||
border 1px solid $ui-input--create-folder-modal
|
||||
color white
|
||||
|
||||
.description
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-confirmButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
@@ -81,3 +81,19 @@ body[data-theme="solarized-dark"]
|
||||
.description
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
background-color transparent
|
||||
|
||||
.header
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.control-button
|
||||
border-color $ui-monokai-borderColor
|
||||
color $ui-monokai-text-color
|
||||
background-color transparent
|
||||
&:focus
|
||||
colorDarkPrimaryButton()
|
||||
|
||||
.description
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -133,6 +133,11 @@ colorSolarizedDarkControl()
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
colorMonokaiControl()
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
@@ -189,4 +194,29 @@ body[data-theme="solarized-dark"]
|
||||
select, .group-section-control-input
|
||||
colorSolarizedDarkControl()
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-header
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-rightButton
|
||||
colorMonokaiPrimaryButton()
|
||||
.group-hint
|
||||
colorMonokaiControl()
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorMonokaiControl()
|
||||
|
||||
@@ -33,4 +33,10 @@ body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
p
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-monokai-text-color
|
||||
|
||||
@@ -126,3 +126,26 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.folderItem
|
||||
&:hover
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
|
||||
.folderItem-left-key
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.folderItem-left-colorButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
.folderItem-right-button
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
.folderItem-right-confirmButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
@@ -68,3 +68,10 @@ body[data-theme="solarized-dark"]
|
||||
.list
|
||||
a
|
||||
color $ui-solarized-dark-active-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-monokai-active-color
|
||||
|
||||
@@ -116,3 +116,26 @@ body[data-theme="solarized-dark"]
|
||||
&:hover
|
||||
color white
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
background-color transparent
|
||||
.top-bar
|
||||
background-color transparent
|
||||
border-color $ui-monokai-borderColor
|
||||
p
|
||||
color $ui-monokai-text-color
|
||||
.nav
|
||||
background-color transparent
|
||||
border-color $ui-monokai-borderColor
|
||||
.nav-button
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.nav-button--active
|
||||
@extend .nav-button
|
||||
color $ui-monokai-button--active-color
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
&:hover
|
||||
color white
|
||||
|
||||
@@ -199,3 +199,40 @@ body[data-theme="solarized-dark"]
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.folderList-item
|
||||
border-bottom $ui-monokai-borderColor
|
||||
|
||||
.folderList-empty
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.list-empty
|
||||
color $ui-monokai-text-color
|
||||
.list-control-addStorageButton
|
||||
border-color $ui-monokai-button-backgroundColor
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.addStorage-header
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.addStorage-body-section-name-input
|
||||
border-color $$ui-monokai-borderColor
|
||||
|
||||
.addStorage-body-section-type-description
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.addStorage-body-section-path-button
|
||||
colorPrimaryButton()
|
||||
.addStorage-body-control
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.addStorage-body-control-createButton
|
||||
colorDarkPrimaryButton()
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
@@ -10,6 +10,7 @@ import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLanguages } from 'browser/lib/Languages'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
@@ -65,6 +66,7 @@ class UiTab extends React.Component {
|
||||
language: this.refs.uiLanguage.value,
|
||||
showCopyNotification: this.refs.showCopyNotification.checked,
|
||||
confirmDeletion: this.refs.confirmDeletion.checked,
|
||||
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
|
||||
disableDirectWrite: this.refs.uiD2w != null
|
||||
? this.refs.uiD2w.checked
|
||||
: false
|
||||
@@ -170,6 +172,7 @@ class UiTab extends React.Component {
|
||||
<option value='default'>{i18n.__('Default')}</option>
|
||||
<option value='white'>{i18n.__('White')}</option>
|
||||
<option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
|
||||
<option value='monokai'>{i18n.__('Monokai')}</option>
|
||||
<option value='dark'>{i18n.__('Dark')}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -182,22 +185,9 @@ class UiTab extends React.Component {
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
ref='uiLanguage'
|
||||
>
|
||||
<option value='sq'>{i18n.__('Albanian')}</option>
|
||||
<option value='zh-CN'>{i18n.__('Chinese (zh-CN)')}</option>
|
||||
<option value='zh-TW'>{i18n.__('Chinese (zh-TW)')}</option>
|
||||
<option value='da'>{i18n.__('Danish')}</option>
|
||||
<option value='en'>{i18n.__('English')}</option>
|
||||
<option value='fr'>{i18n.__('French')}</option>
|
||||
<option value='de'>{i18n.__('German')}</option>
|
||||
<option value='hu'>{i18n.__('Hungarian')}</option>
|
||||
<option value='ja'>{i18n.__('Japanese')}</option>
|
||||
<option value='ko'>{i18n.__('Korean')}</option>
|
||||
<option value='no'>{i18n.__('Norwegian')}</option>
|
||||
<option value='pl'>{i18n.__('Polish')}</option>
|
||||
<option value='pt-BR'>{i18n.__('Portuguese (Brazil)')}</option>
|
||||
<option value='pt-PT'>{i18n.__('Portuguese (Portugal)')}</option>
|
||||
<option value='ru'>{i18n.__('Russian')}</option>
|
||||
<option value='es-ES'>{i18n.__('Spanish')}</option>
|
||||
{
|
||||
getLanguages().map((language) => <option value={language.locale} key={language.locale}>{i18n.__(language.name)}</option>)
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,6 +212,16 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Show a confirmation dialog when deleting notes')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showOnlyRelatedTags}
|
||||
ref='showOnlyRelatedTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show only related tags')}
|
||||
</label>
|
||||
</div>
|
||||
{
|
||||
global.process.platform === 'win32'
|
||||
? <div styleName='group-checkBoxSection'>
|
||||
|
||||
@@ -118,6 +118,16 @@ colorSolarizedDarkPrimaryButton()
|
||||
&:active:hover
|
||||
background-color $dark-primary-button-background--active
|
||||
|
||||
colorMonokaiPrimaryButton()
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
border none
|
||||
&:hover
|
||||
background-color $dark-primary-button-background--hover
|
||||
&:active
|
||||
&:active:hover
|
||||
background-color $dark-primary-button-background--active
|
||||
|
||||
|
||||
// Danger button(Brand color)
|
||||
$danger-button-background = #c9302c
|
||||
@@ -348,3 +358,29 @@ modalSolarizedDark()
|
||||
background-color $ui-solarized-dark-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
|
||||
/******* Monokai theme ********/
|
||||
$ui-monokai-backgroundColor = #272822
|
||||
$ui-monokai-noteList-backgroundColor = #272822
|
||||
$ui-monokai-noteDetail-backgroundColor = #272822
|
||||
|
||||
$ui-monokai-text-color = #f8f8f2
|
||||
$ui-monokai-active-color = #f92672
|
||||
|
||||
$ui-monokai-borderColor = #373831
|
||||
|
||||
$ui-monokai-tag-backgroundColor = #f92672
|
||||
|
||||
$ui-monokai-button-backgroundColor = #373831
|
||||
$ui-monokai-button--active-color = white
|
||||
$ui-monokai-button--active-backgroundColor = #f92672
|
||||
$ui-monokai-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%)
|
||||
$ui-monokai-button--focus-borderColor = lighten(#369DCD, 25%)
|
||||
|
||||
modalmonokai()
|
||||
position relative
|
||||
z-index $modal-z-index
|
||||
width 100%
|
||||
background-color $ui-monokai-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
@@ -87,7 +87,7 @@
|
||||
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote es utilizado en alrededor de 200 países y regiones diferentes por una increíble comunidad de desarrolladores.",
|
||||
"To continue supporting this growth, and to satisfy community expectations,": "Para continuar apoyando este crecimiento y satisfacer las expectativas de la comunidad,",
|
||||
"we would like to invest more time and resources in this project.": "nos gustaría invertir más tiempo y recursos en este proyecto.",
|
||||
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "Si te gusta este proyecto y ves potencial en él, ¡puedes ayudar apoyándonos en OpenCollective!",
|
||||
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "Si te gusta este proyecto y ves su potencial, ¡puedes ayudar apoyándonos en OpenCollective!",
|
||||
"Thanks,": "Gracias,",
|
||||
"Boostnote maintainers": "Equipo de Boostnote",
|
||||
"Support via OpenCollective": "Contribuir vía OpenCollective",
|
||||
@@ -149,5 +149,5 @@
|
||||
"Sanitization": "Saneamiento",
|
||||
"Only allow secure html tags (recommended)": "Solo permitir etiquetas html seguras (recomendado)",
|
||||
"Allow styles": "Permitir estilos",
|
||||
"Allow dangerous html tags": "Permitir etiques html peligrosas"
|
||||
"Allow dangerous html tags": "Permitir etiquetas html peligrosas"
|
||||
}
|
||||
|
||||
@@ -1,135 +1,135 @@
|
||||
{
|
||||
"Notes": "筆記",
|
||||
"Tags": "標籤",
|
||||
"Preferences": "首選項",
|
||||
"Make a note": "新建筆記",
|
||||
"Preferences": "偏好設定",
|
||||
"Make a note": "做點筆記",
|
||||
"Ctrl": "Ctrl",
|
||||
"Ctrl(^)": "Ctrl",
|
||||
"to create a new note": "新建筆記",
|
||||
"to create a new note": "新增筆記",
|
||||
"Toggle Mode": "切換模式",
|
||||
"Trash": "廢紙簍",
|
||||
"MODIFICATION DATE": "更改時間",
|
||||
"Words": "單詞",
|
||||
"MODIFICATION DATE": "修改時間",
|
||||
"Words": "單字",
|
||||
"Letters": "字數",
|
||||
"STORAGE": "本地儲存",
|
||||
"STORAGE": "本機儲存空間",
|
||||
"FOLDER": "資料夾",
|
||||
"CREATION DATE": "創建時間",
|
||||
"NOTE LINK": "筆記鏈接",
|
||||
"CREATION DATE": "建立時間",
|
||||
"NOTE LINK": "筆記連結",
|
||||
".md": ".md",
|
||||
".txt": ".txt",
|
||||
".html": ".html",
|
||||
"Print": "列印",
|
||||
"Your preferences for Boostnote": "個性設置",
|
||||
"Storages": "本地儲存",
|
||||
"Add Storage Location": "添加一個本地儲存位置",
|
||||
"Add Folder": "新建資料夾",
|
||||
"Open Storage folder": "打開一個本地儲存位置",
|
||||
"Unlink": "取消鏈接",
|
||||
"Your preferences for Boostnote": "Boostnote 偏好設定",
|
||||
"Storages": "本機儲存空間",
|
||||
"Add Storage Location": "新增一個本機儲存位置",
|
||||
"Add Folder": "新增資料夾",
|
||||
"Open Storage folder": "開啟儲存資料夾",
|
||||
"Unlink": "解除連結",
|
||||
"Edit": "編輯",
|
||||
"Delete": "刪除",
|
||||
"Interface": "界面",
|
||||
"Interface Theme": "主題",
|
||||
"Default": "默認",
|
||||
"Default": "預設",
|
||||
"White": "White",
|
||||
"Solarized Dark": "Solarized Dark",
|
||||
"Dark": "Dark",
|
||||
"Show a confirmation dialog when deleting notes": "刪除筆記的時候,顯示確認框",
|
||||
"Show a confirmation dialog when deleting notes": "刪除筆記的時候,顯示確認對話框",
|
||||
"Editor Theme": "編輯器主題",
|
||||
"Editor Font Size": "編輯器字型大小",
|
||||
"Editor Font Family": "編輯器字體",
|
||||
"Editor Indent Style": "縮進風格",
|
||||
"Editor Indent Style": "縮排風格",
|
||||
"Spaces": "空格",
|
||||
"Tabs": "Tabs",
|
||||
"Switch to Preview": "快速切換到預覽界面",
|
||||
"When Editor Blurred": "當編輯器失去焦點的時候,切換到預覽界面",
|
||||
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點的時候預覽,雙擊切換到編輯界面",
|
||||
"On Right Click": "右鍵點擊切換兩個界面",
|
||||
"Switch to Preview": "切回預覽頁面的時機",
|
||||
"When Editor Blurred": "當編輯器失去焦點時",
|
||||
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,雙擊切換到編輯畫面",
|
||||
"On Right Click": "點擊右鍵切換兩個頁面",
|
||||
"Editor Keymap": "編輯器 Keymap",
|
||||
"default": "默認",
|
||||
"default": "預設",
|
||||
"vim": "vim",
|
||||
"emacs": "emacs",
|
||||
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 設置好快捷鍵後,記得重啟设置好快捷键后,记得重启boostnote",
|
||||
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 請重新開啟 Boostnote 以完成設定。",
|
||||
"Show line numbers in the editor": "在編輯器中顯示行號",
|
||||
"Allow editor to scroll past the last line": "允許編輯器滾動到最後一行",
|
||||
"Bring in web page title when pasting URL on editor": "粘貼網頁鏈接的時候,顯示為網頁標題",
|
||||
"Preview": "預覽器",
|
||||
"Preview Font Size": "預覽器字型大小",
|
||||
"Preview Font Family": "預覽器字體",
|
||||
"Code block Theme": "代碼塊主題",
|
||||
"Allow preview to scroll past the last line": "允許預覽器滾動到最後一行",
|
||||
"Show line numbers for preview code blocks": "在預覽器中顯示行號",
|
||||
"LaTeX Inline Open Delimiter": "LaTeX 單行開頭分隔符",
|
||||
"LaTeX Inline Close Delimiter": "LaTeX 單行結尾分隔符",
|
||||
"LaTeX Block Open Delimiter": "LaTeX 多行開頭分隔符",
|
||||
"LaTeX Block Close Delimiter": "LaTeX 多行結尾分隔符",
|
||||
"Community": "社區",
|
||||
"Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行",
|
||||
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題",
|
||||
"Preview": "預覽頁面",
|
||||
"Preview Font Size": "預覽頁面字型大小",
|
||||
"Preview Font Family": "預覽頁面字體",
|
||||
"Code block Theme": "程式碼區塊主題",
|
||||
"Allow preview to scroll past the last line": "允許預覽頁面捲軸捲動超過最後一行",
|
||||
"Show line numbers for preview code blocks": "在預覽頁面的程式碼區塊中顯示行號",
|
||||
"LaTeX Inline Open Delimiter": "LaTeX 單行開頭符號",
|
||||
"LaTeX Inline Close Delimiter": "LaTeX 單行結尾符號",
|
||||
"LaTeX Block Open Delimiter": "LaTeX 多行開頭符號",
|
||||
"LaTeX Block Close Delimiter": "LaTeX 多行結尾符號",
|
||||
"Community": "社群",
|
||||
"Subscribe to Newsletter": "訂閱郵件",
|
||||
"GitHub": "GitHub",
|
||||
"Blog": "部落格",
|
||||
"Facebook Group": "Facebook Group",
|
||||
"Facebook Group": "Facebook 社團",
|
||||
"Twitter": "Twitter",
|
||||
"About": "關於",
|
||||
"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.": "一款專門為程式設計師朋友量身打造的開源筆記軟體",
|
||||
"Website": "官網",
|
||||
"Development": "開發",
|
||||
" : Development configurations for Boostnote.": " : Boostnote的開發配置",
|
||||
" : Development configurations for Boostnote.": " : Boostnote 的開發組態",
|
||||
"Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO",
|
||||
"License: GPL v3": "License: GPL v3",
|
||||
"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 收集匿名數據只為了提升軟體使用體驗,絕對不收集任何個人信息(包括筆記內容)",
|
||||
"You can see how it works on ": "你可以看看它的源碼是如何運作的 ",
|
||||
"You can choose to enable or disable this option.": "你可以選擇開啟或不開啟這個功能",
|
||||
"Enable analytics to help improve Boostnote": "允許對數據進行分析,幫助我們改進Boostnote",
|
||||
"Crowdfunding": "眾籌",
|
||||
"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 choose to enable or disable this option.": "你可以選擇啟用或禁用這項功能",
|
||||
"Enable analytics to help improve Boostnote": "允許數據分析以協助我們改進 Boostnote",
|
||||
"Crowdfunding": "群眾募資",
|
||||
"Dear everyone,": "親愛的用戶:",
|
||||
"Thank you for using Boostnote!": "謝謝你使用Boostnote!",
|
||||
"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,": "為了繼續支持這種發展,和滿足社區的期待,",
|
||||
"Thank you for using Boostnote!": "謝謝你使用 Boostnote!",
|
||||
"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,": "為了繼續支持這種發展,和滿足社群的期待,",
|
||||
"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!": "如果你喜歡這款軟體並且看好它的潛力, 請在OpenCollective上支持我們!",
|
||||
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "如果你喜歡這款軟體並且看好它的潛力, 請在 OpenCollective 上支持我們!",
|
||||
"Thanks,": "十分感謝!",
|
||||
"Boostnote maintainers": "Boostnote的維護人員",
|
||||
"Support via OpenCollective": "在OpenCollective上支持我們",
|
||||
"Boostnote maintainers": "Boostnote 的維護人員",
|
||||
"Support via OpenCollective": "在 OpenCollective 上支持我們",
|
||||
"Language": "語言",
|
||||
"English": "English",
|
||||
"German": "German",
|
||||
"French": "French",
|
||||
"Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製\" 提示",
|
||||
"Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製到剪貼簿\" 的通知",
|
||||
"All Notes": "所有筆記",
|
||||
"Starred": "星標收藏",
|
||||
"Starred": "星號收藏",
|
||||
"Are you sure to ": "你確定要 ",
|
||||
" delete": " 刪除",
|
||||
"this folder?": "這個資料夾嗎?",
|
||||
"Confirm": "確認",
|
||||
"Cancel": "取消",
|
||||
"Markdown Note": "Markdown筆記",
|
||||
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "創建文檔,清單,代碼塊甚至是Latex格式文檔",
|
||||
"Snippet Note": "代碼筆記",
|
||||
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "創建代碼片段,支持多種語法代碼片段",
|
||||
"Tab to switch format": "使用Tab鍵切換格式",
|
||||
"Updated": "更新時間",
|
||||
"Created": "創建時間",
|
||||
"Alphabetically": "A~Z排序",
|
||||
"Default View": "默認視圖",
|
||||
"Compressed View": "列表視圖",
|
||||
"Search": "搜索",
|
||||
"Markdown Note": "Markdown 筆記",
|
||||
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "建立文件、清單,也可以使用程式碼區塊甚至是 Latex 區塊。",
|
||||
"Snippet Note": "程式碼片段筆記",
|
||||
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "建立程式碼區塊片段。數個程式碼區塊可以合在同一個筆記裡。",
|
||||
"Tab to switch format": "使用 Tab 鍵切換格式",
|
||||
"Updated": "依更新時間排序",
|
||||
"Created": "依建立時間排序",
|
||||
"Alphabetically": "依字母排序",
|
||||
"Default View": "預設顯示",
|
||||
"Compressed View": "緊密顯示",
|
||||
"Search": "搜尋",
|
||||
"Blog Type": "部落格類型",
|
||||
"Blog Address": "部落格地址",
|
||||
"Save": "保存",
|
||||
"Blog Address": "部落格網址",
|
||||
"Save": "儲存",
|
||||
"Auth": "Auth",
|
||||
"Authentication Method": "認證方法",
|
||||
"JWT": "JWT",
|
||||
"USER": "USER",
|
||||
"Token": "Token",
|
||||
"Storage": "本地儲存",
|
||||
"Storage": "本機儲存空間",
|
||||
"Hotkeys": "快捷鍵",
|
||||
"Show/Hide Boostnote": "顯示/隱藏 Boostnote",
|
||||
"Restore": "恢復",
|
||||
"Restore": "還原",
|
||||
"Permanent Delete": "永久刪除",
|
||||
"Confirm note deletion": "確認刪除筆記",
|
||||
"This will permanently remove this note.": "永久地刪除這條筆記",
|
||||
"Successfully applied!": "設置成功",
|
||||
"This will permanently remove this note.": "這將會永久地刪除這條筆記",
|
||||
"Successfully applied!": "設定成功",
|
||||
"Albanian": "Albanian",
|
||||
"Chinese (zh-CN)": "简体中文",
|
||||
"Chinese (zh-TW)": "繁體中文",
|
||||
@@ -142,11 +142,11 @@
|
||||
"Spanish": "Spanish",
|
||||
"You have to save!": "你必須儲存一下!",
|
||||
"Russian": "Russian",
|
||||
"Editor Rulers": "Editor Rulers",
|
||||
"Enable": "開啟",
|
||||
"Disable": "關閉",
|
||||
"Sanitization": "代碼處理",
|
||||
"Only allow secure html tags (recommended)": "只允許安全的html標籤(推薦)",
|
||||
"Editor Rulers": "編輯器中顯示垂直尺規",
|
||||
"Enable": "啟用",
|
||||
"Disable": "禁用",
|
||||
"Sanitization": "過濾 HTML 程式碼",
|
||||
"Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)",
|
||||
"Allow styles": "允許樣式",
|
||||
"Allow dangerous html tags": "允許危險的html標籤"
|
||||
"Allow dangerous html tags": "允許危險的 HTML 標籤"
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"striptags": "^2.2.1",
|
||||
"superagent": "^1.2.0",
|
||||
"superagent-promise": "^1.0.3",
|
||||
"unique-slug": "2.0.0",
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 589 B |
|
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 423 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 423 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 701 B |
@@ -1,19 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TagListItem renders correctly 1`] = `
|
||||
<button
|
||||
className="tagList-item"
|
||||
onClick={[Function]}
|
||||
<div
|
||||
className="tagList-itemContainer"
|
||||
>
|
||||
<span
|
||||
className="tagList-item-name"
|
||||
<div
|
||||
className="tagList-itemNarrow"
|
||||
/>
|
||||
<button
|
||||
className="tagList-item"
|
||||
onClick={[Function]}
|
||||
>
|
||||
# Test
|
||||
<span
|
||||
className="tagList-item-count"
|
||||
className="tagList-item-name"
|
||||
>
|
||||
|
||||
# Test
|
||||
<span
|
||||
className="tagList-item-count"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
262
tests/dataApi/attachmentManagement.test.js
Normal file
@@ -0,0 +1,262 @@
|
||||
'use strict'
|
||||
|
||||
jest.mock('fs')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const findStorage = require('browser/lib/findStorage')
|
||||
jest.mock('unique-slug')
|
||||
const uniqueSlug = require('unique-slug')
|
||||
const mdurl = require('mdurl')
|
||||
|
||||
const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement')
|
||||
|
||||
it('should test that copyAttachment should throw an error if sourcePath or storageKey or noteKey are undefined', function () {
|
||||
systemUnderTest.copyAttachment(undefined, 'storageKey').then(() => {}, error => {
|
||||
expect(error).toBe('sourceFilePath has to be given')
|
||||
})
|
||||
systemUnderTest.copyAttachment(null, 'storageKey', 'noteKey').then(() => {}, error => {
|
||||
expect(error).toBe('sourceFilePath has to be given')
|
||||
})
|
||||
systemUnderTest.copyAttachment('source', undefined, 'noteKey').then(() => {}, error => {
|
||||
expect(error).toBe('storageKey has to be given')
|
||||
})
|
||||
systemUnderTest.copyAttachment('source', null, 'noteKey').then(() => {}, error => {
|
||||
expect(error).toBe('storageKey has to be given')
|
||||
})
|
||||
systemUnderTest.copyAttachment('source', 'storageKey', null).then(() => {}, error => {
|
||||
expect(error).toBe('noteKey has to be given')
|
||||
})
|
||||
systemUnderTest.copyAttachment('source', 'storageKey', undefined).then(() => {}, error => {
|
||||
expect(error).toBe('noteKey has to be given')
|
||||
})
|
||||
})
|
||||
|
||||
it('should test that copyAttachment should throw an error if sourcePath dosen\'t exists', function () {
|
||||
fs.existsSync = jest.fn()
|
||||
fs.existsSync.mockReturnValue(false)
|
||||
|
||||
systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey').then(() => {}, error => {
|
||||
expect(error).toBe('source file does not exist')
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('should test that copyAttachment works correctly assuming correct working of fs', function () {
|
||||
const dummyExtension = '.ext'
|
||||
const sourcePath = 'path' + dummyExtension
|
||||
const storageKey = 'storageKey'
|
||||
const noteKey = 'noteKey'
|
||||
const dummyUniquePath = 'dummyPath'
|
||||
const dummyStorage = {path: 'dummyStoragePath'}
|
||||
|
||||
fs.existsSync = jest.fn()
|
||||
fs.existsSync.mockReturnValue(true)
|
||||
fs.createReadStream = jest.fn()
|
||||
fs.createReadStream.mockReturnValue({pipe: jest.fn()})
|
||||
fs.createWriteStream = jest.fn()
|
||||
|
||||
findStorage.findStorage = jest.fn()
|
||||
findStorage.findStorage.mockReturnValue(dummyStorage)
|
||||
uniqueSlug.mockReturnValue(dummyUniquePath)
|
||||
|
||||
systemUnderTest.copyAttachment(sourcePath, storageKey, noteKey).then(
|
||||
function (newFileName) {
|
||||
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
|
||||
expect(fs.createReadStream).toHaveBeenCalledWith(sourcePath)
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(sourcePath)
|
||||
expect(fs.createReadStream().pipe).toHaveBeenCalled()
|
||||
expect(fs.createWriteStream).toHaveBeenCalledWith(path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey, dummyUniquePath + dummyExtension))
|
||||
expect(newFileName).toBe(dummyUniquePath + dummyExtension)
|
||||
})
|
||||
})
|
||||
|
||||
it('should test that copyAttachment creates a new folder if the attachment folder doesn\'t exist', function () {
|
||||
const dummyStorage = {path: 'dummyStoragePath'}
|
||||
const noteKey = 'noteKey'
|
||||
const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER)
|
||||
const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey)
|
||||
|
||||
fs.existsSync = jest.fn()
|
||||
fs.existsSync.mockReturnValueOnce(true)
|
||||
fs.existsSync.mockReturnValueOnce(false)
|
||||
fs.existsSync.mockReturnValueOnce(false)
|
||||
fs.mkdirSync = jest.fn()
|
||||
|
||||
findStorage.findStorage = jest.fn()
|
||||
findStorage.findStorage.mockReturnValue(dummyStorage)
|
||||
uniqueSlug.mockReturnValue('dummyPath')
|
||||
|
||||
systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey').then(
|
||||
function () {
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(attachmentFolderPath)
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(attachmentFolderPath)
|
||||
expect(fs.existsSync).toHaveBeenLastCalledWith(attachmentFolderNoteKyPath)
|
||||
expect(fs.mkdirSync).toHaveBeenLastCalledWith(attachmentFolderNoteKyPath)
|
||||
})
|
||||
})
|
||||
|
||||
it('should test that copyAttachment don\'t uses a random file name if not intended ', function () {
|
||||
const dummyStorage = {path: 'dummyStoragePath'}
|
||||
|
||||
fs.existsSync = jest.fn()
|
||||
fs.existsSync.mockReturnValueOnce(true)
|
||||
fs.existsSync.mockReturnValueOnce(false)
|
||||
fs.mkdirSync = jest.fn()
|
||||
|
||||
findStorage.findStorage = jest.fn()
|
||||
findStorage.findStorage.mockReturnValue(dummyStorage)
|
||||
uniqueSlug.mockReturnValue('dummyPath')
|
||||
|
||||
systemUnderTest.copyAttachment('path', 'storageKey', 'noteKey', false).then(
|
||||
function (newFileName) {
|
||||
expect(newFileName).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace the all ":storage" path with the actual storage path', function () {
|
||||
const storageFolder = systemUnderTest.DESTINATION_FOLDER
|
||||
const testInput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href=":storage' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const storagePath = '<<dummyStoragePath>>'
|
||||
const expectedOutput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.fixLocalURLS(testInput, storagePath)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should test that generateAttachmentMarkdown works correct both with previews and without', function () {
|
||||
const fileName = 'fileName'
|
||||
const path = 'path'
|
||||
let expected = ``
|
||||
let actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, true)
|
||||
expect(actual).toEqual(expected)
|
||||
expected = `[${fileName}](${path})`
|
||||
actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false)
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should test that getAttachmentsInContent finds all attachments', function () {
|
||||
const testInput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.getAttachmentsInContent(testInput)
|
||||
const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
|
||||
it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute paths', function () {
|
||||
const dummyStoragePath = 'dummyStoragePath'
|
||||
const testInput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath)
|
||||
const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp',
|
||||
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx',
|
||||
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
|
||||
it('should remove the all ":storage" and noteKey references', function () {
|
||||
const storageFolder = systemUnderTest.DESTINATION_FOLDER
|
||||
const noteKey = 'noteKey'
|
||||
const testInput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const expectedOutput =
|
||||
'<html>\n' +
|
||||
' <head>\n' +
|
||||
' //header\n' +
|
||||
' </head>\n' +
|
||||
' <body data-theme="default">\n' +
|
||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||
' <p data-line="2">\n' +
|
||||
' <img src="' + storageFolder + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href="' + storageFolder + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src="' + storageFolder + path.sep + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
@@ -8714,6 +8714,12 @@ uniqs@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
|
||||
|
||||
unique-slug@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
|
||||
dependencies:
|
||||
imurmurhash "^0.1.4"
|
||||
|
||||
unique-string@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
|
||||
|
||||