diff --git a/.eslintrc b/.eslintrc index a460b507..1709c9d8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,5 +19,8 @@ "FileReader": true, "localStorage": true, "fetch": true + }, + "env": { + "jest": true } } diff --git a/.travis.yml b/.travis.yml index c68d1063..d9267f77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 872e9ad7..dfe072ef 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -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 = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` - 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) } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 83509184..2c98f18e 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -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} /> ) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index d1e6dba5..8d15cdb5 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -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' diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index c30f50da..27505a5a 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -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} /> ) diff --git a/browser/components/NoteItem.styl b/browser/components/NoteItem.styl index 4067a6cd..017ef6d0 100644 --- a/browser/components/NoteItem.styl +++ b/browser/components/NoteItem.styl @@ -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 diff --git a/browser/components/NoteItemSimple.styl b/browser/components/NoteItemSimple.styl index 3097b82c..91c28c2a 100644 --- a/browser/components/NoteItemSimple.styl +++ b/browser/components/NoteItemSimple.styl @@ -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 diff --git a/browser/components/RealtimeNotification.styl b/browser/components/RealtimeNotification.styl index 0f77acbb..0365d8c9 100644 --- a/browser/components/RealtimeNotification.styl +++ b/browser/components/RealtimeNotification.styl @@ -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 \ No newline at end of file diff --git a/browser/components/SideNavFilter.js b/browser/components/SideNavFilter.js index 06cfc19a..3a259ce7 100644 --- a/browser/components/SideNavFilter.js +++ b/browser/components/SideNavFilter.js @@ -51,7 +51,7 @@ const SideNavFilter = ({ diff --git a/browser/components/SideNavFilter.styl b/browser/components/SideNavFilter.styl index 8a9a350d..c1b378b8 100644 --- a/browser/components/SideNavFilter.styl +++ b/browser/components/SideNavFilter.styl @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/browser/components/StorageItem.styl b/browser/components/StorageItem.styl index 842f8d66..ece97008 100644 --- a/browser/components/StorageItem.styl +++ b/browser/components/StorageItem.styl @@ -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 \ No newline at end of file diff --git a/browser/components/TagListItem.js b/browser/components/TagListItem.js index ebef7df4..6cd50c9c 100644 --- a/browser/components/TagListItem.js +++ b/browser/components/TagListItem.js @@ -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}) => ( - +const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => ( +
+ {isRelated + ? + :
+ } + +
) TagListItem.propTypes = { diff --git a/browser/components/TagListItem.styl b/browser/components/TagListItem.styl index b35b30cf..555520b0 100644 --- a/browser/components/TagListItem.styl +++ b/browser/components/TagListItem.styl @@ -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 diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl index 329663f9..6116cd58 100644 --- a/browser/components/TodoListPercentage.styl +++ b/browser/components/TodoListPercentage.styl @@ -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 \ No newline at end of file diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index cc6d7d92..373b321e 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -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 \ No newline at end of file diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js new file mode 100644 index 00000000..09a1614e --- /dev/null +++ b/browser/lib/Languages.js @@ -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 + } +} + diff --git a/browser/lib/i18n.js b/browser/lib/i18n.js index f7bd96e9..e12e2aa6 100644 --- a/browser/lib/i18n.js +++ b/browser/lib/i18n.js @@ -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') diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 6f1f2f00..1ef488a7 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -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 diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl index d4c4100c..49a634f3 100644 --- a/browser/main/Detail/Detail.styl +++ b/browser/main/Detail/Detail.styl @@ -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 diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl index 31930fe6..cfdc2734 100644 --- a/browser/main/Detail/FolderSelect.styl +++ b/browser/main/Detail/FolderSelect.styl @@ -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 diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl index d90dea49..480441bd 100644 --- a/browser/main/Detail/InfoPanel.styl +++ b/browser/main/Detail/InfoPanel.styl @@ -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 diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 05883c0e..72f832e3 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -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} /> diff --git a/browser/main/Detail/MarkdownNoteDetail.styl b/browser/main/Detail/MarkdownNoteDetail.styl index ad20f0f2..b27dc80e 100644 --- a/browser/main/Detail/MarkdownNoteDetail.styl +++ b/browser/main/Detail/MarkdownNoteDetail.styl @@ -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 diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index bc3c9462..8d454203 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -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 \ No newline at end of file diff --git a/browser/main/Detail/SnippetNoteDetail.styl b/browser/main/Detail/SnippetNoteDetail.styl index 789d5186..f8ca48cc 100644 --- a/browser/main/Detail/SnippetNoteDetail.styl +++ b/browser/main/Detail/SnippetNoteDetail.styl @@ -152,4 +152,21 @@ body[data-theme="solarized-dark"] .tabList background-color $ui-solarized-dark-noteDetail-backgroundColor - color $ui-solarized-dark-text-color \ No newline at end of file + 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 \ No newline at end of file diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl index 18d4d2e0..0ff4c6a3 100644 --- a/browser/main/Detail/TagSelect.styl +++ b/browser/main/Detail/TagSelect.styl @@ -81,4 +81,20 @@ body[data-theme="solarized-dark"] .newTag border-color none background-color transparent - color $ui-solarized-dark-text-color \ No newline at end of file + 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 diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 185a780c..2e7ab5fa 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -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 diff --git a/browser/main/Main.js b/browser/main/Main.js index 14a56225..9c15a0fe 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -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') diff --git a/browser/main/NewNoteButton/NewNoteButton.styl b/browser/main/NewNoteButton/NewNoteButton.styl index 81ff7e8d..e8e4b5f0 100644 --- a/browser/main/NewNoteButton/NewNoteButton.styl +++ b/browser/main/NewNoteButton/NewNoteButton.styl @@ -74,4 +74,8 @@ body[data-theme="dark"] body[data-theme="solarized-dark"] .root, .root--expanded - background-color $ui-solarized-dark-noteList-backgroundColor \ No newline at end of file + background-color $ui-solarized-dark-noteList-backgroundColor + +body[data-theme="monokai"] + .root, .root--expanded + background-color $ui-monokai-noteList-backgroundColor diff --git a/browser/main/NoteList/NoteList.styl b/browser/main/NoteList/NoteList.styl index 312f5143..ea261208 100644 --- a/browser/main/NoteList/NoteList.styl +++ b/browser/main/NoteList/NoteList.styl @@ -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 \ No newline at end of file + 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 diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index e413c647..876de0c0 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -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', diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 666ae0cd..ecab70d0 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -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 diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index f8a65013..6b53478e 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -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 ( @@ -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) => { diff --git a/browser/main/StatusBar/StatusBar.styl b/browser/main/StatusBar/StatusBar.styl index 9f189fec..52cc4b02 100644 --- a/browser/main/StatusBar/StatusBar.styl +++ b/browser/main/StatusBar/StatusBar.styl @@ -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 diff --git a/browser/main/TopBar/TopBar.styl b/browser/main/TopBar/TopBar.styl index 0956571f..7654f66f 100644 --- a/browser/main/TopBar/TopBar.styl +++ b/browser/main/TopBar/TopBar.styl @@ -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 diff --git a/browser/main/global.styl b/browser/main/global.styl index 613c7611..7025163f 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -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 diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 3e1a2162..ee8a57c7 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -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') } diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js new file mode 100644 index 00000000..c2e7f6d6 --- /dev/null +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -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} 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 +} diff --git a/browser/main/lib/dataApi/copyImage.js b/browser/main/lib/dataApi/copyImage.js index 6a79b8b7..24053bdd 100644 --- a/browser/main/lib/dataApi/copyImage.js +++ b/browser/main/lib/dataApi/copyImage.js @@ -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) } diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 313bb85b..e4fec5f4 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -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 `![${dstFilename}](${dstRelativePath})` - }) + let exportedData = noteContent if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index 928d331b..cffb5c53 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -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(() => { diff --git a/browser/main/modals/CreateFolderModal.styl b/browser/main/modals/CreateFolderModal.styl index 45f2e852..1b96e123 100644 --- a/browser/main/modals/CreateFolderModal.styl +++ b/browser/main/modals/CreateFolderModal.styl @@ -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() diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index 748ab88c..db14133f 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index f6f7ace9..0e5f81fb 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -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() diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl index 930c33f0..3d4af539 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -33,4 +33,10 @@ body[data-theme="solarized-dark"] .root color $ui-solarized-dark-text-color p - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .root + color $ui-monokai-text-color + p + color $ui-monokai-text-color diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index acc4cbfb..8bcf2b02 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -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() diff --git a/browser/main/modals/PreferencesModal/InfoTab.styl b/browser/main/modals/PreferencesModal/InfoTab.styl index cc04a10f..491fc4d4 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.styl +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/PreferencesModal.styl b/browser/main/modals/PreferencesModal/PreferencesModal.styl index 57b5dbad..d21f6c28 100644 --- a/browser/main/modals/PreferencesModal/PreferencesModal.styl +++ b/browser/main/modals/PreferencesModal/PreferencesModal.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index 230f0aed..9804d7e7 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index a092129a..a607f548 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -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 { +
@@ -182,22 +185,9 @@ class UiTab extends React.Component { onChange={(e) => this.handleUIChange(e)} ref='uiLanguage' > - - - - - - - - - - - - - - - - + { + getLanguages().map((language) => ) + } @@ -222,6 +212,16 @@ class UiTab extends React.Component { {i18n.__('Show a confirmation dialog when deleting notes')} +
+ +
{ global.process.platform === 'win32' ?
diff --git a/browser/styles/index.styl b/browser/styles/index.styl index 6fb208b1..7d32e77a 100644 --- a/browser/styles/index.styl +++ b/browser/styles/index.styl @@ -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 \ No newline at end of file diff --git a/locales/es-ES.json b/locales/es-ES.json index ea20641b..d533579c 100644 --- a/locales/es-ES.json +++ b/locales/es-ES.json @@ -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" } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 06f40608..bc60cfcc 100755 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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 標籤" } diff --git a/package.json b/package.json index 038d4c00..82c90896 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/resources/app.png b/resources/app.png index ac817bc6..ee1b3c93 100755 Binary files a/resources/app.png and b/resources/app.png differ diff --git a/resources/boostnote-install.png b/resources/boostnote-install.png index 6bb31b4d..980e672d 100755 Binary files a/resources/boostnote-install.png and b/resources/boostnote-install.png differ diff --git a/resources/boostnote-install@2x.png b/resources/boostnote-install@2x.png index 6b4dd5fb..e45e137f 100755 Binary files a/resources/boostnote-install@2x.png and b/resources/boostnote-install@2x.png differ diff --git a/resources/dmg.png b/resources/dmg.png index 49196123..3d2cd438 100755 Binary files a/resources/dmg.png and b/resources/dmg.png differ diff --git a/resources/finder.png b/resources/finder.png index 4caf9249..7adb292a 100644 Binary files a/resources/finder.png and b/resources/finder.png differ diff --git a/resources/repository/top.png b/resources/repository/top.png index ac8dc699..29dfc033 100644 Binary files a/resources/repository/top.png and b/resources/repository/top.png differ diff --git a/resources/tray-icon-dark.png b/resources/tray-icon-dark.png index 4aa26bfd..20b47832 100644 Binary files a/resources/tray-icon-dark.png and b/resources/tray-icon-dark.png differ diff --git a/resources/tray-icon-dark@2x.png b/resources/tray-icon-dark@2x.png index 5e58d5ef..7d43788d 100644 Binary files a/resources/tray-icon-dark@2x.png and b/resources/tray-icon-dark@2x.png differ diff --git a/resources/tray-icon-default.png b/resources/tray-icon-default.png index 08725fb7..942b0220 100644 Binary files a/resources/tray-icon-default.png and b/resources/tray-icon-default.png differ diff --git a/resources/tray-icon-default@2x.png b/resources/tray-icon-default@2x.png index c2a7e17e..9cf088d9 100644 Binary files a/resources/tray-icon-default@2x.png and b/resources/tray-icon-default@2x.png differ diff --git a/resources/tray-icon.png b/resources/tray-icon.png index 08725fb7..942b0220 100644 Binary files a/resources/tray-icon.png and b/resources/tray-icon.png differ diff --git a/resources/tray-icon@2x.png b/resources/tray-icon@2x.png index c2a7e17e..9cf088d9 100644 Binary files a/resources/tray-icon@2x.png and b/resources/tray-icon@2x.png differ diff --git a/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap b/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap index 0805edcd..80293ab4 100644 --- a/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap +++ b/tests/components/__snapshots__/TagListItem.snapshot.test.js.snap @@ -1,19 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TagListItem renders correctly 1`] = ` - + +
`; diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js new file mode 100644 index 00000000..d58a8eb8 --- /dev/null +++ b/tests/dataApi/attachmentManagement.test.js @@ -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 = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const storagePath = '<>' + const expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + 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 = `![${fileName}](${path})` + 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 = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + 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 = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + 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 = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey) + expect(actual).toEqual(expectedOutput) +}) diff --git a/yarn.lock b/yarn.lock index 62b6600a..a0e4d96f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"