mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
rewite whole code
add dataApi renew PreferencesModal
This commit is contained in:
@@ -5,6 +5,8 @@ import _ from 'lodash'
|
|||||||
|
|
||||||
const ace = window.ace
|
const ace = window.ace
|
||||||
|
|
||||||
|
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
|
||||||
export default class CodeEditor extends React.Component {
|
export default class CodeEditor extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -94,16 +96,16 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
let { mode, value } = this.props
|
let { mode, value, theme, fontSize } = this.props
|
||||||
this.value = value
|
this.value = value
|
||||||
let el = ReactDOM.findDOMNode(this)
|
let el = ReactDOM.findDOMNode(this)
|
||||||
let editor = this.editor = ace.edit(el)
|
let editor = this.editor = ace.edit(el)
|
||||||
editor.$blockScrolling = Infinity
|
editor.$blockScrolling = Infinity
|
||||||
editor.renderer.setShowGutter(true)
|
editor.renderer.setShowGutter(true)
|
||||||
editor.setTheme('ace/theme/xcode')
|
editor.setTheme('ace/theme/' + theme)
|
||||||
editor.moveCursorTo(0, 0)
|
editor.moveCursorTo(0, 0)
|
||||||
editor.setReadOnly(!!this.props.readOnly)
|
editor.setReadOnly(!!this.props.readOnly)
|
||||||
editor.setFontSize('14')
|
editor.setFontSize(fontSize)
|
||||||
|
|
||||||
editor.on('blur', this.blurHandler)
|
editor.on('blur', this.blurHandler)
|
||||||
|
|
||||||
@@ -135,9 +137,9 @@ export default class CodeEditor extends React.Component {
|
|||||||
: 'text'
|
: 'text'
|
||||||
session.setMode('ace/mode/' + syntaxMode)
|
session.setMode('ace/mode/' + syntaxMode)
|
||||||
|
|
||||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
session.setTabSize(this.props.indentSize)
|
||||||
session.setOption('useWorker', false)
|
session.setOption('useWorker', true)
|
||||||
session.setUseWrapMode(true)
|
session.setUseWrapMode(true)
|
||||||
session.setValue(_.isString(value) ? value : '')
|
session.setValue(_.isString(value) ? value : '')
|
||||||
|
|
||||||
@@ -154,7 +156,8 @@ export default class CodeEditor extends React.Component {
|
|||||||
componentDidUpdate (prevProps, prevState) {
|
componentDidUpdate (prevProps, prevState) {
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
this.value = value
|
this.value = value
|
||||||
var session = this.editor.getSession()
|
let editor = this.editor
|
||||||
|
let session = this.editor.getSession()
|
||||||
|
|
||||||
if (prevProps.mode !== this.props.mode) {
|
if (prevProps.mode !== this.props.mode) {
|
||||||
let mode = _.find(modes, {name: this.props.mode})
|
let mode = _.find(modes, {name: this.props.mode})
|
||||||
@@ -163,23 +166,18 @@ export default class CodeEditor extends React.Component {
|
|||||||
: 'text'
|
: 'text'
|
||||||
session.setMode('ace/mode' + syntaxMode)
|
session.setMode('ace/mode' + syntaxMode)
|
||||||
}
|
}
|
||||||
}
|
if (prevProps.theme !== this.props.theme) {
|
||||||
|
editor.setTheme('ace/theme/' + this.props.theme)
|
||||||
handleConfigApply (e, config) {
|
}
|
||||||
// this.setState({
|
if (prevProps.fontSize !== this.props.fontSize) {
|
||||||
// fontSize: config['editor-font-size'],
|
editor.setFontSize(this.props.fontSize)
|
||||||
// fontFamily: config['editor-font-family'],
|
}
|
||||||
// indentType: config['editor-indent-type'],
|
if (prevProps.indentSize !== this.props.indentSize) {
|
||||||
// indentSize: config['editor-indent-size'],
|
session.setTabSize(this.props.indentSize)
|
||||||
// themeSyntax: config['theme-syntax']
|
}
|
||||||
// }, function () {
|
if (prevProps.indentType !== this.props.indentType) {
|
||||||
// var editor = this.editor
|
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||||
// editor.setTheme('ace/theme/' + this.state.themeSyntax)
|
}
|
||||||
|
|
||||||
// var session = editor.getSession()
|
|
||||||
// session.setUseSoftTabs(this.state.indentType === 'space')
|
|
||||||
// session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange (e) {
|
handleChange (e) {
|
||||||
@@ -222,8 +220,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className } = this.props
|
let { className, fontFamily } = this.props
|
||||||
|
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||||
|
? [fontFamily].concat(defaultEditorFontFamily)
|
||||||
|
: defaultEditorFontFamily
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className == null
|
className={className == null
|
||||||
@@ -231,8 +231,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
: `CodeEditor ${className}`
|
: `CodeEditor ${className}`
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontFamily: fontFamily.join(', ')
|
||||||
fontFamily: 'monospace'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -249,7 +248,12 @@ CodeEditor.propTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CodeEditor.defaultProps = {
|
CodeEditor.defaultProps = {
|
||||||
readOnly: false
|
readOnly: false,
|
||||||
|
theme: 'xcode',
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Monaco, Consolas',
|
||||||
|
indentSize: 4,
|
||||||
|
indentType: 'space'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CodeEditor
|
export default CodeEditor
|
||||||
|
|||||||
@@ -27,19 +27,22 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenu (e) {
|
handleContextMenu (e) {
|
||||||
let newStatus = this.state.status === 'PREVIEW'
|
let { config } = this.props
|
||||||
? 'CODE'
|
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||||
: 'PREVIEW'
|
let newStatus = this.state.status === 'PREVIEW'
|
||||||
this.setState({
|
? 'CODE'
|
||||||
status: newStatus
|
: 'PREVIEW'
|
||||||
}, () => {
|
this.setState({
|
||||||
if (newStatus === 'CODE') {
|
status: newStatus
|
||||||
this.refs.code.focus()
|
}, () => {
|
||||||
} else {
|
if (newStatus === 'CODE') {
|
||||||
this.refs.code.blur()
|
this.refs.code.focus()
|
||||||
this.refs.preview.focus()
|
} else {
|
||||||
}
|
this.refs.code.blur()
|
||||||
})
|
this.refs.preview.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus () {
|
focus () {
|
||||||
@@ -59,7 +62,14 @@ class MarkdownEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, value } = this.props
|
let { className, value, config } = this.props
|
||||||
|
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||||
|
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||||
|
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||||
|
|
||||||
|
let previewStyle = {}
|
||||||
|
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className == null
|
<div className={className == null
|
||||||
@@ -72,15 +82,23 @@ class MarkdownEditor extends React.Component {
|
|||||||
ref='code'
|
ref='code'
|
||||||
mode='markdown'
|
mode='markdown'
|
||||||
value={value}
|
value={value}
|
||||||
|
theme={config.editor.theme}
|
||||||
|
fontFamily={config.editor.fontFamily}
|
||||||
|
fontSize={editorFontSize}
|
||||||
|
indentType={config.editor.indentType}
|
||||||
|
indentSize={editorIndentSize}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||||
? 'preview'
|
? 'preview'
|
||||||
: 'preview--hide'
|
: 'preview--hide'
|
||||||
}
|
}
|
||||||
style={this.props.ignorePreviewPointerEvents
|
style={previewStyle}
|
||||||
? {pointerEvents: 'none'} : {}
|
fontSize={config.preview.fontSize}
|
||||||
}
|
fontFamily={config.preview.fontFamily}
|
||||||
|
codeBlockTheme={config.preview.theme}
|
||||||
|
codeBlockFontFamily={config.editor.fontFamily}
|
||||||
|
lineNumber={config.preview.lineNumber}
|
||||||
ref='preview'
|
ref='preview'
|
||||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import markdown from 'browser/lib/markdown'
|
import markdown from 'browser/lib/markdown'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||||
const { shell } = require('electron')
|
const { shell } = require('electron')
|
||||||
@@ -8,6 +9,15 @@ const goExternal = function (e) {
|
|||||||
shell.openExternal(e.target.href)
|
shell.openExternal(e.target.href)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||||
|
if (!OSX) {
|
||||||
|
defaultFontFamily.unshift('\'Microsoft YaHei\'')
|
||||||
|
defaultFontFamily.unshift('meiryo')
|
||||||
|
}
|
||||||
|
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
export default class MarkdownPreview extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -30,7 +40,12 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
if (prevProps.value !== this.props.value ||
|
||||||
|
prevProps.fontFamily !== this.props.fontFamily ||
|
||||||
|
prevProps.fontSize !== this.props.fontSize ||
|
||||||
|
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||||
|
prevProps.lineNumber !== this.props.lineNumber
|
||||||
|
) this.rewriteIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteIframe () {
|
rewriteIframe () {
|
||||||
@@ -38,7 +53,14 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
el.removeEventListener('click', goExternal)
|
el.removeEventListener('click', goExternal)
|
||||||
})
|
})
|
||||||
|
|
||||||
let { value } = this.props
|
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber } = this.props
|
||||||
|
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||||
|
? [fontFamily].concat(defaultFontFamily)
|
||||||
|
: defaultFontFamily
|
||||||
|
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||||
|
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
|
||||||
|
: defaultCodeBlockFontFamily
|
||||||
|
|
||||||
this.refs.root.contentWindow.document.head.innerHTML = `
|
this.refs.root.contentWindow.document.head.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -51,6 +73,18 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
${markdownStyle}
|
${markdownStyle}
|
||||||
|
body {
|
||||||
|
font-family: ${fontFamily.join(', ')};
|
||||||
|
font-size: ${fontSize}px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: ${codeBlockFontFamily.join(', ')};
|
||||||
|
}
|
||||||
|
.lineNumber {
|
||||||
|
${lineNumber && 'display: block !important;'}
|
||||||
|
font-family: ${codeBlockFontFamily.join(', ')};
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="../node_modules/highlight.js/styles/xcode.css" id="hljs-css">
|
<link rel="stylesheet" href="../node_modules/highlight.js/styles/xcode.css" id="hljs-css">
|
||||||
<link rel="stylesheet" href="../resources/katex.min.css">
|
<link rel="stylesheet" href="../resources/katex.min.css">
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ ol
|
|||||||
&>li>ul, &>li>ol
|
&>li>ul, &>li>ol
|
||||||
margin 0
|
margin 0
|
||||||
code
|
code
|
||||||
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
|
||||||
padding 0.2em 0.4em
|
padding 0.2em 0.4em
|
||||||
background-color #f7f7f7
|
background-color #f7f7f7
|
||||||
border-radius 3px
|
border-radius 3px
|
||||||
@@ -213,9 +212,9 @@ pre
|
|||||||
border none
|
border none
|
||||||
margin -5px
|
margin -5px
|
||||||
&>span.lineNumber
|
&>span.lineNumber
|
||||||
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
|
||||||
display none
|
display none
|
||||||
float left
|
float left
|
||||||
|
font-size 0.85em
|
||||||
margin 0 0.5em 0 -0.5em
|
margin 0 0.5em 0 -0.5em
|
||||||
border-right 1px solid
|
border-right 1px solid
|
||||||
text-align right
|
text-align right
|
||||||
|
|||||||
@@ -1,636 +0,0 @@
|
|||||||
const keygen = require('browser/lib/keygen')
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const CSON = require('season')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const consts = require('browser/lib/consts')
|
|
||||||
|
|
||||||
let repositories = []
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Repository
|
|
||||||
*
|
|
||||||
* ## Life cycle
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // Add reposiotry
|
|
||||||
* var repo = new Repository({name: 'test', path: '/Users/somebody/test'})
|
|
||||||
* repo
|
|
||||||
* .mount()
|
|
||||||
* .then(() => repo.load())
|
|
||||||
*
|
|
||||||
* // Update Cached
|
|
||||||
* repo.saveCache({name: 'renamed'})
|
|
||||||
*
|
|
||||||
* // Update JSON
|
|
||||||
* repo.saveJSON({author: 'Other user'})
|
|
||||||
*
|
|
||||||
* // Remove repository
|
|
||||||
* repo.unmount()
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## `this.cached`
|
|
||||||
*
|
|
||||||
* Information of a Repository. It stores in `repoStats` key of localStorage
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* this.cached = {
|
|
||||||
* "key":"523280a789404d430ce571ab9148fdd1343336cb",
|
|
||||||
* "name":"test",
|
|
||||||
* "path":"/Users/dickchoi/test"
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## `this.json`
|
|
||||||
*
|
|
||||||
* Parsed object from `boostrepo.json`. See `boostrepo.json`.
|
|
||||||
*
|
|
||||||
* ## Repository file structure
|
|
||||||
*
|
|
||||||
* The root directory of a Repository. It has several files and folders.
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* root
|
|
||||||
* |- data
|
|
||||||
* |-note1.cson
|
|
||||||
* |-note2.cson
|
|
||||||
* |-note3.cson
|
|
||||||
* |- boostrepo.json
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* #### `boostrepo.json`
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* {
|
|
||||||
* name: String,
|
|
||||||
* author: String, // Same convention of package.json, `John Doe <email@example.com> (http://example.com)`
|
|
||||||
* remotes: [{
|
|
||||||
* name: String,
|
|
||||||
* url: String, // url of git remote
|
|
||||||
* branch: String // if branch isn't set, it will try to use `master` branch.
|
|
||||||
* }],
|
|
||||||
* folders: [{
|
|
||||||
* key: String // Unique sha1 hash key to identify folder,
|
|
||||||
* name: String,
|
|
||||||
* color: String // All CSS color formats available.
|
|
||||||
* }]
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* #### `data` directory
|
|
||||||
*
|
|
||||||
* Every note will be saved here as a single CSON file to `git diff` efficiently.
|
|
||||||
* > This is because CSON supports Multiline string.
|
|
||||||
* File name of each cson file will be used to identify note.
|
|
||||||
* Commonly, Boostnote will automatically generate sha1 key and use it as a file name when creating a new note.
|
|
||||||
*
|
|
||||||
* ##### `note.cson`
|
|
||||||
*
|
|
||||||
* ```cson
|
|
||||||
* tags: [String] // tags
|
|
||||||
* folder: String // hash key of folder
|
|
||||||
* mode: String // syntax mode
|
|
||||||
* title: String
|
|
||||||
* content: String
|
|
||||||
* createdAt: Date
|
|
||||||
* updatedAt: Date
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ### Full data
|
|
||||||
*
|
|
||||||
* Full data of a Repository. It includes not only information also all data(notes). Redux will directly handle this data.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* var repo = Object.assign({}, repoJSON, notesData, cached, {status: 'READY'})
|
|
||||||
*
|
|
||||||
* // repo
|
|
||||||
* {
|
|
||||||
* {...repoJSON}
|
|
||||||
* "key": "523280a789404d430ce571ab9148fdd1343336cb",
|
|
||||||
* "name": "test",
|
|
||||||
* "path": "/Users/dickchoi/test",
|
|
||||||
* "notes": [], // See `note.cson`
|
|
||||||
* "status": "READY" // If Boostnote failed to fetch data, It will be set `ERROR`
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Repository {
|
|
||||||
constructor (cached) {
|
|
||||||
this.cached = _.pick(cached, ['key', 'name', 'path'])
|
|
||||||
this.status = 'IDLE'
|
|
||||||
this.isMount = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload and set all data of a repository
|
|
||||||
* @return {Promise} full data of a repository
|
|
||||||
*/
|
|
||||||
load () {
|
|
||||||
if (!_.isString(this.cached.key)) {
|
|
||||||
console.warn('The key of this repository doesn\'t seem to be valid. You should input this data to Redux reducer after setting the key properly.')
|
|
||||||
}
|
|
||||||
let name = this.cached.name
|
|
||||||
let targetPath = this.cached.path
|
|
||||||
|
|
||||||
targetPath = path.join(targetPath)
|
|
||||||
let dataPath = path.join(targetPath, 'data')
|
|
||||||
|
|
||||||
let initializeRepository = () => {
|
|
||||||
let resolveDataDirectory = this.constructor.resolveDirectory(path.join(targetPath, 'data'))
|
|
||||||
let resolveBoostrepoJSON = this.constructor.resolveJSON(path.join(targetPath, 'boostrepo.json'), {name: name})
|
|
||||||
return Promise.all([resolveDataDirectory, resolveBoostrepoJSON])
|
|
||||||
.then((data) => {
|
|
||||||
this.json = data[1]
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchNotes = () => {
|
|
||||||
let noteNames = fs.readdirSync(dataPath)
|
|
||||||
let notes = noteNames
|
|
||||||
.map((noteName) => path.join(dataPath, noteName))
|
|
||||||
.filter((notePath) => CSON.isObjectPath(notePath))
|
|
||||||
.map((notePath) => {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
CSON.readFile(notePath, function (err, obj) {
|
|
||||||
if (err != null) {
|
|
||||||
console.log(err)
|
|
||||||
return resolve(null)
|
|
||||||
}
|
|
||||||
obj.key = path.basename(notePath, '.cson')
|
|
||||||
return resolve(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.filter((note) => note != null)
|
|
||||||
|
|
||||||
return Promise.all(notes)
|
|
||||||
.then((notes) => {
|
|
||||||
this.notes = notes
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.constructor.resolveDirectory(targetPath)
|
|
||||||
.then(initializeRepository)
|
|
||||||
.then(fetchNotes)
|
|
||||||
.then(() => {
|
|
||||||
this.status = 'READY'
|
|
||||||
|
|
||||||
return this.getData()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.status = 'ERROR'
|
|
||||||
this.error = err
|
|
||||||
console.error(err)
|
|
||||||
return this
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add repository to localStorage
|
|
||||||
* If key doesn't exist, it will generate it.
|
|
||||||
* If a cache having same key extists in localStorage, override it.
|
|
||||||
*
|
|
||||||
* @return {Promise} added cache data
|
|
||||||
*/
|
|
||||||
mount () {
|
|
||||||
let allCached = this.constructor.getAllCached()
|
|
||||||
|
|
||||||
// If key is not set, generate new one.
|
|
||||||
if (!_.isString(this.cached.key)) {
|
|
||||||
this.cached.key = keygen()
|
|
||||||
// Repeat utill generated key becomes unique.
|
|
||||||
while (_.findIndex(allCached, {key: this.cached.key}) > -1) {
|
|
||||||
this.cached.key = keygen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetCachedIndex = _.findIndex(allCached, {key: this.cached.key})
|
|
||||||
if (targetCachedIndex > -1) {
|
|
||||||
allCached.splice(targetCachedIndex, 1, this.cached)
|
|
||||||
} else {
|
|
||||||
allCached.push(this.cached)
|
|
||||||
}
|
|
||||||
this.constructor.saveAllCached(allCached)
|
|
||||||
this.isMount = true
|
|
||||||
|
|
||||||
// Put in `repositories` array if it isn't in.
|
|
||||||
let targetIndex = _.findIndex(repositories, (repo) => {
|
|
||||||
this.cached.key === repo.cached.key
|
|
||||||
})
|
|
||||||
if (targetIndex < 0) {
|
|
||||||
repositories.push(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(this.cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove repository from localStorage
|
|
||||||
* @return {Promise} remoded cache data
|
|
||||||
*/
|
|
||||||
unmount () {
|
|
||||||
let allCached = this.constructor.getAllCached()
|
|
||||||
|
|
||||||
let targetCachedIndex = _.findIndex(allCached, {key: this.cached.key})
|
|
||||||
if (targetCachedIndex > -1) {
|
|
||||||
allCached.splice(targetCachedIndex, 1)
|
|
||||||
}
|
|
||||||
this.constructor.saveAllCached(allCached)
|
|
||||||
this.isMount = false
|
|
||||||
|
|
||||||
// Discard from `repositories` array if it is in.
|
|
||||||
let targetIndex = _.findIndex(repositories, (repo) => {
|
|
||||||
this.cached.key === repo.cached.key
|
|
||||||
})
|
|
||||||
if (targetIndex > -1) {
|
|
||||||
repositories.splice(targetIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(this.cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all data of a repository
|
|
||||||
*
|
|
||||||
* If data is not ready, try to load it.
|
|
||||||
*
|
|
||||||
* @return {Promise} all data of a repository
|
|
||||||
*/
|
|
||||||
getData () {
|
|
||||||
function carbonCopy (obj) {
|
|
||||||
return JSON.parse(JSON.stringify(obj))
|
|
||||||
}
|
|
||||||
if (this.status === 'IDLE') {
|
|
||||||
return this.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.status === 'ERROR') {
|
|
||||||
return Promise.resolve(carbonCopy(Object.assign({}, this.json, this.cached, {
|
|
||||||
status: this.status
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(carbonCopy(Object.assign({}, this.json, this.cached, {
|
|
||||||
status: this.status,
|
|
||||||
notes: this.notes
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save Cached
|
|
||||||
* @param {Object} newCached
|
|
||||||
* @return {Promies} updated Cached
|
|
||||||
*/
|
|
||||||
saveCached (newCached) {
|
|
||||||
if (_.isObject(newCached)) {
|
|
||||||
Object.assign(this.cached, _.pick(newCached, ['name', 'path']))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMount) {
|
|
||||||
this.mount()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(this.cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save JSON
|
|
||||||
* @param {Object} newJSON
|
|
||||||
* @return {Promise} updated JSON
|
|
||||||
*/
|
|
||||||
saveJSON (newJSON) {
|
|
||||||
let jsonPath = path.join(this.cached.path, 'boostrepo.json')
|
|
||||||
if (_.isObject(newJSON)) {
|
|
||||||
Object.assign(this.json, newJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
CSON
|
|
||||||
.writeFile(jsonPath, this.json, (err) => {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(this.json)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save both Cached and JSON
|
|
||||||
* @return {Promise} resolve array have cached and JSON
|
|
||||||
*/
|
|
||||||
save () {
|
|
||||||
return Promise.all([this.saveCached(), this.saveJSON()])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a folder
|
|
||||||
* @param {Object} newFolder
|
|
||||||
* @return {Promise} new folder
|
|
||||||
*/
|
|
||||||
addFolder (newFolder) {
|
|
||||||
let { folders } = this.json
|
|
||||||
|
|
||||||
newFolder = _.pick(newFolder, ['color', 'name'])
|
|
||||||
if (!_.isString(newFolder.name) || newFolder.name.trim().length === 0) newFolder.name = 'unnamed'
|
|
||||||
else newFolder.name = newFolder.name.trim()
|
|
||||||
|
|
||||||
if (!_.isString(newFolder.color)) newFolder.color = this.constructor.randomColor()
|
|
||||||
|
|
||||||
newFolder.key = keygen()
|
|
||||||
while (_.findIndex(folders, {key: newFolder.key}) > -1) {
|
|
||||||
newFolder.key = keygen()
|
|
||||||
}
|
|
||||||
|
|
||||||
folders.push(newFolder)
|
|
||||||
|
|
||||||
return this.saveJSON(this.json)
|
|
||||||
.then(() => newFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a folder
|
|
||||||
* @param {String} folderKey [description]
|
|
||||||
* @param {Object} override [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
updateFolder (folderKey, override) {
|
|
||||||
let { folders } = this.json
|
|
||||||
|
|
||||||
let targetFolder = _.find(folders, {key: folderKey})
|
|
||||||
if (targetFolder == null) {
|
|
||||||
return this.addFolder(override)
|
|
||||||
}
|
|
||||||
if (_.isString(override.name) && override.name.trim().length > 0) targetFolder.name = override.name.trim()
|
|
||||||
if (_.isString(override.color)) targetFolder.color = override.color
|
|
||||||
|
|
||||||
return this.saveJSON(this.json)
|
|
||||||
.then(() => targetFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a folder
|
|
||||||
* @param {String} folderKey
|
|
||||||
* @return {Promise} removed folder
|
|
||||||
*/
|
|
||||||
removeFolder (folderKey) {
|
|
||||||
let { folders } = this.json
|
|
||||||
let targetFolder = null
|
|
||||||
|
|
||||||
let targetIndex = _.findIndex(folders, {key: folderKey})
|
|
||||||
if (targetIndex > -1) {
|
|
||||||
targetFolder = folders.splice(targetIndex, 1)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.saveJSON(null)
|
|
||||||
.then(() => targetFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
addNote (newNote) {
|
|
||||||
newNote = Object.assign({}, _.pick(newNote, ['content', 'tags', 'folder', 'mode', 'title']))
|
|
||||||
|
|
||||||
if (!this.constructor.validateNote(newNote)) {
|
|
||||||
return Promise.reject(new Error('Invalid input'))
|
|
||||||
}
|
|
||||||
|
|
||||||
newNote.key = keygen()
|
|
||||||
while (_.find(this.notes, {key: newNote.key}) != null) {
|
|
||||||
newNote.key = keygen()
|
|
||||||
}
|
|
||||||
newNote.createdAt = new Date()
|
|
||||||
newNote.updatedAt = new Date()
|
|
||||||
this.notes.push(newNote)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
CSON.writeFile(path.join(this.cached.path, 'data', newNote.key + '.cson'), _.omit(newNote, ['key']), function (err) {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(newNote)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateNote (note) {
|
|
||||||
if (!_.isString(note.title)) return false
|
|
||||||
if (!_.isString(note.content)) return false
|
|
||||||
if (!_.isArray(note.tags)) return false
|
|
||||||
if (!note.tags.every((tag) => _.isString(tag))) return false
|
|
||||||
if (!_.isString(note.folder)) return false
|
|
||||||
if (!_.isString(note.mode)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
getNote (noteKey) {
|
|
||||||
let note = _.find(this.notes, {key: noteKey})
|
|
||||||
|
|
||||||
return Promise.resolve(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNote (noteKey, override) {
|
|
||||||
if (!this.constructor.validateNote(override)) {
|
|
||||||
return Promise.reject(new Error('Invalid input'))
|
|
||||||
}
|
|
||||||
|
|
||||||
override.updatedAt = new Date()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
CSON.writeFile(path.join(this.cached.path, 'data', noteKey + '.cson'), _.omit(override, ['key']), function (err) {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
override.key = noteKey
|
|
||||||
resolve(override)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
starNote (noteKey) {
|
|
||||||
let note = _.find(this.notes, {key: noteKey})
|
|
||||||
if (note != null) {
|
|
||||||
let json = this.json
|
|
||||||
json.starred.push(noteKey)
|
|
||||||
json.starred = _.uniq(json.starred)
|
|
||||||
|
|
||||||
return this.saveJSON(json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unstarNote (noteKey) {
|
|
||||||
let note = _.find(this.notes, {key: noteKey})
|
|
||||||
if (note != null) {
|
|
||||||
let json = this.json
|
|
||||||
json.starred = json.starred
|
|
||||||
.filter((starredKey) => starredKey !== noteKey)
|
|
||||||
json.starred = _.uniq(json.starred)
|
|
||||||
|
|
||||||
return this.saveJSON(json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNote (noteKey) {
|
|
||||||
let noteIndex = _.findIndex(this.notes, {key: noteKey})
|
|
||||||
|
|
||||||
if (noteIndex < 0) return Promise.resolve(null)
|
|
||||||
|
|
||||||
let note = this.notes[noteIndex]
|
|
||||||
this.notes.splice(noteIndex, 1)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.unlink(path.join(this.cached.path, 'data', noteKey + '.cson'), function (err) {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(note)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllNotes () {
|
|
||||||
return Promise.resolve(this.notes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
static generateDefaultJSON (override) {
|
|
||||||
return Object.assign({
|
|
||||||
name: 'unnamed',
|
|
||||||
remotes: [],
|
|
||||||
folders: [{
|
|
||||||
key: keygen(),
|
|
||||||
name: 'general',
|
|
||||||
color: this.randomColor()
|
|
||||||
}],
|
|
||||||
starred: []
|
|
||||||
}, override)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Resolve Repository JSON
|
|
||||||
*
|
|
||||||
* Fetch and parse JSON from given path.
|
|
||||||
* If boostrepo.json doesn't exist, create new one.
|
|
||||||
*
|
|
||||||
* @param {String} targetPath [description]
|
|
||||||
* @param {Object} defaultOverrides Overrided to default value of RepoJSON when creating new one.
|
|
||||||
* @return {Promise} resolving parsed data
|
|
||||||
*/
|
|
||||||
static resolveJSON (targetPath, defaultOverrides) {
|
|
||||||
return (new Promise((resolve, reject) => {
|
|
||||||
let writeNew = () => {
|
|
||||||
let newRepoJSON = this.generateDefaultJSON(defaultOverrides)
|
|
||||||
CSON.writeFile(targetPath, newRepoJSON, (err) => {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(newRepoJSON)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// If JSON doesn't exist, make a new one.
|
|
||||||
if (CSON.resolve(targetPath) == null) {
|
|
||||||
writeNew()
|
|
||||||
} else {
|
|
||||||
CSON.readFile(targetPath, (err, obj) => {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
if (obj == null) {
|
|
||||||
writeNew()
|
|
||||||
} else {
|
|
||||||
resolve(obj)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Resolve directory.
|
|
||||||
*
|
|
||||||
* Make sure the directory exists.
|
|
||||||
* If directory doesn't exist, it will try to make a new one.
|
|
||||||
* If failed, it return rejected promise
|
|
||||||
*
|
|
||||||
* @param {String} targetPath Target path of directory
|
|
||||||
* @return {Promise} resolving targetPath
|
|
||||||
*/
|
|
||||||
static resolveDirectory (targetPath) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
// check the directory exists
|
|
||||||
fs.stat(targetPath, function (err, stat) {
|
|
||||||
// Reject errors except `ENOENT`
|
|
||||||
if (err != null && err.code !== 'ENOENT') {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle no suchfile error only
|
|
||||||
// Make new Folder by given path
|
|
||||||
if (err != null) {
|
|
||||||
return fs.mkdir(targetPath, function (err, stat) {
|
|
||||||
// If failed to make a new directory, reject it.
|
|
||||||
if (err != null) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
resolve(targetPath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the target is not a directory
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return reject(new Error(targetPath + ' path is not a directory'))
|
|
||||||
}
|
|
||||||
resolve(targetPath)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all repository stats from localStorage
|
|
||||||
* it is stored to `repositories` key.
|
|
||||||
* if the data is corrupted, re-intialize it.
|
|
||||||
*
|
|
||||||
* @return {Array} registered repositories
|
|
||||||
* ```
|
|
||||||
* [{
|
|
||||||
* key: String,
|
|
||||||
* name: String,
|
|
||||||
* path: String // path of repository
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
static getAllCached () {
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = JSON.parse(localStorage.getItem('repositories'))
|
|
||||||
if (!_.isArray(data)) {
|
|
||||||
throw new Error('Data is corrupted. it must be an array.')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
data = []
|
|
||||||
this.saveAllCached(data)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
static saveAllCached (allCached) {
|
|
||||||
console.info('cach updated > ', allCached)
|
|
||||||
localStorage.setItem('repositories', JSON.stringify(allCached))
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadAll () {
|
|
||||||
repositories = []
|
|
||||||
return Promise.all(this.getAllCached().map((cached) => {
|
|
||||||
let repo = new this(cached)
|
|
||||||
repositories.push(repo)
|
|
||||||
|
|
||||||
return repo.load()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
static list () {
|
|
||||||
return Promise.resolve(repositories)
|
|
||||||
}
|
|
||||||
|
|
||||||
static find (repoKey) {
|
|
||||||
let repository = _.find(repositories, {cached: {key: repoKey}})
|
|
||||||
return Promise.resolve(repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
static randomColor () {
|
|
||||||
return consts.FOLDER_COLORS[Math.floor(Math.random() * consts.FOLDER_COLORS.length)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Repository
|
|
||||||
@@ -128,7 +128,7 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextOption () {
|
nextOption () {
|
||||||
let { folders } = this.props
|
let { storages } = this.props
|
||||||
let { optionIndex } = this.state
|
let { optionIndex } = this.state
|
||||||
|
|
||||||
optionIndex++
|
optionIndex++
|
||||||
@@ -184,25 +184,41 @@ class FolderSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { className, folders, value } = this.props
|
let { className, storages, value } = this.props
|
||||||
let currentFolder = _.find(folders, {key: value})
|
let splitted = value.split('-')
|
||||||
let optionList = folders.map((folder, index) => {
|
let storageKey = splitted.shift()
|
||||||
return (
|
let folderKey = splitted.shift()
|
||||||
<div styleName={index === this.state.optionIndex
|
let options = []
|
||||||
? 'search-optionList-item--active'
|
storages.forEach((storage, index) => {
|
||||||
: 'search-optionList-item'
|
storage.folders.forEach((folder) => {
|
||||||
}
|
options.push({
|
||||||
key={folder.key}
|
storage: storage,
|
||||||
onClick={(e) => this.handleOptionClick(folder.key)(e)}
|
folder: folder
|
||||||
>
|
})
|
||||||
<i style={{color: folder.color}}
|
})
|
||||||
className='fa fa-fw fa-cube'
|
|
||||||
/>
|
|
||||||
{folder.name}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
|
||||||
|
|
||||||
|
let optionList = options
|
||||||
|
.map((option, index) => {
|
||||||
|
return (
|
||||||
|
<div styleName={index === this.state.optionIndex
|
||||||
|
? 'search-optionList-item--active'
|
||||||
|
: 'search-optionList-item'
|
||||||
|
}
|
||||||
|
key={option.storage.key + '-' + option.folder.key}
|
||||||
|
onClick={(e) => this.handleOptionClick(option.folder.key)(e)}
|
||||||
|
>
|
||||||
|
<span styleName='search-optionList-item-name'
|
||||||
|
style={{borderColor: option.folder.color}}
|
||||||
|
>
|
||||||
|
{option.storage.name}/{option.folder.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_.isString(className)
|
<div className={_.isString(className)
|
||||||
? 'FolderSelect ' + className
|
? 'FolderSelect ' + className
|
||||||
@@ -239,10 +255,11 @@ class FolderSelect extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
: <div styleName='idle'>
|
: <div styleName='idle'>
|
||||||
<div styleName='idle-label'>
|
<div styleName='idle-label'>
|
||||||
<i style={{color: currentFolder.color}}
|
<span styleName='idle-label-name'
|
||||||
className='fa fa-fw fa-cube'
|
style={{borderColor: currentOption.folder.color}}
|
||||||
/>
|
>
|
||||||
{currentFolder.name}
|
{currentOption.storage.name}/{currentOption.folder.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
|
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
border solid 1px transparent
|
border solid 1px transparent
|
||||||
line-height 34px
|
line-height 34px
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
border-radius 5px
|
border-radius 2px
|
||||||
transition 0.15s
|
transition 0.15s
|
||||||
user-select none
|
user-select none
|
||||||
&:hover
|
&:hover
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
right 20px
|
right 20px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
|
|
||||||
|
.idle-label-name
|
||||||
|
border-left solid 4px transparent
|
||||||
|
padding 2px 5px
|
||||||
|
|
||||||
.idle-caret
|
.idle-caret
|
||||||
absolute right top
|
absolute right top
|
||||||
height 34px
|
height 34px
|
||||||
@@ -53,11 +57,11 @@
|
|||||||
border $ui-border
|
border $ui-border
|
||||||
z-index 200
|
z-index 200
|
||||||
background-color white
|
background-color white
|
||||||
border-radius 5px
|
border-radius 2px
|
||||||
|
|
||||||
.search-optionList-item
|
.search-optionList-item
|
||||||
height 34px
|
height 34px
|
||||||
width 120px
|
width 250px
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
padding 0 5px
|
padding 0 5px
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
@@ -72,3 +76,6 @@
|
|||||||
&:hover
|
&:hover
|
||||||
background-color $ui-button--active-backgroundColor
|
background-color $ui-button--active-backgroundColor
|
||||||
color $ui-button--active-color
|
color $ui-button--active-color
|
||||||
|
.search-optionList-item-name
|
||||||
|
border-left solid 4px transparent
|
||||||
|
padding 2px 5px
|
||||||
|
|||||||
@@ -2,19 +2,26 @@ import React, { PropTypes } from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './NoteDetail.styl'
|
import styles from './NoteDetail.styl'
|
||||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||||
import queue from 'browser/main/lib/queue'
|
|
||||||
import StarButton from './StarButton'
|
import StarButton from './StarButton'
|
||||||
import TagSelect from './TagSelect'
|
import TagSelect from './TagSelect'
|
||||||
import FolderSelect from './FolderSelect'
|
import FolderSelect from './FolderSelect'
|
||||||
import Repository from 'browser/lib/Repository'
|
|
||||||
import Commander from 'browser/main/lib/Commander'
|
import Commander from 'browser/main/lib/Commander'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { remote } = electron
|
||||||
|
const Menu = remote.Menu
|
||||||
|
const MenuItem = remote.MenuItem
|
||||||
|
|
||||||
class NoteDetail extends React.Component {
|
class NoteDetail extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
note: Object.assign({}, props.note),
|
note: Object.assign({
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
}, props.note),
|
||||||
isDispatchQueued: false
|
isDispatchQueued: false
|
||||||
}
|
}
|
||||||
this.dispatchTimer = null
|
this.dispatchTimer = null
|
||||||
@@ -84,86 +91,74 @@ class NoteDetail extends React.Component {
|
|||||||
|
|
||||||
note.content = this.refs.content.value
|
note.content = this.refs.content.value
|
||||||
note.tags = this.refs.tags.value
|
note.tags = this.refs.tags.value
|
||||||
note.folder = this.refs.folder.value
|
note.title = this.findTitle(note.content)
|
||||||
|
note.updatedAt = new Date()
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
note,
|
note
|
||||||
isDispatchQueued: true
|
|
||||||
}, () => {
|
}, () => {
|
||||||
this.queueDispatch()
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelDispatchQueue () {
|
save () {
|
||||||
if (this.dispatchTimer != null) {
|
let { note, dispatch } = this.props
|
||||||
window.clearTimeout(this.dispatchTimer)
|
|
||||||
this.dispatchTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queueDispatch () {
|
|
||||||
this.cancelDispatchQueue()
|
|
||||||
|
|
||||||
this.dispatchTimer = window.setTimeout(() => {
|
|
||||||
this.dispatch()
|
|
||||||
this.setState({
|
|
||||||
isDispatchQueued: false
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch () {
|
|
||||||
let { note } = this.state
|
|
||||||
note = Object.assign({}, note)
|
|
||||||
let repoKey = note._repository.key
|
|
||||||
note.title = this.findTitle(note.content)
|
|
||||||
|
|
||||||
let { dispatch } = this.props
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SAVE_NOTE',
|
type: 'UPDATE_NOTE',
|
||||||
repository: repoKey,
|
note: this.state.note
|
||||||
note: note
|
|
||||||
})
|
})
|
||||||
queue.save(repoKey, note)
|
|
||||||
|
dataApi
|
||||||
|
.updateNote(note.storage, note.folder, note.key, this.state.note)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderChange (e) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarButtonClick (e) {
|
handleStarButtonClick (e) {
|
||||||
let { note } = this.state
|
let { note } = this.state
|
||||||
let { dispatch } = this.props
|
|
||||||
|
|
||||||
let isStarred = note._repository.starred.some((starredKey) => starredKey === note.key)
|
note.isStarred = !note.isStarred
|
||||||
|
|
||||||
if (isStarred) {
|
this.setState({
|
||||||
Repository
|
note
|
||||||
.find(note._repository.key)
|
}, () => {
|
||||||
.then((repo) => {
|
this.save()
|
||||||
return repo.unstarNote(note.key)
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
dispatch({
|
exportAsFile () {
|
||||||
type: 'UNSTAR_NOTE',
|
|
||||||
repository: note._repository.key,
|
|
||||||
note: note.key
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Repository
|
|
||||||
.find(note._repository.key)
|
|
||||||
.then((repo) => {
|
|
||||||
return repo.starNote(note.key)
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch({
|
}
|
||||||
type: 'STAR_NOTE',
|
|
||||||
repository: note._repository.key,
|
handleShareButtonClick (e) {
|
||||||
note: note.key
|
let menu = new Menu()
|
||||||
})
|
menu.append(new MenuItem({
|
||||||
}
|
label: 'Export as a File',
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Export to Web',
|
||||||
|
disabled: true,
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextButtonClick (e) {
|
||||||
|
let menu = new Menu()
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: 'Delete',
|
||||||
|
click: (e) => this.handlePreferencesButtonClick(e)
|
||||||
|
}))
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
let { storages, config } = this.props
|
||||||
let { note } = this.state
|
let { note } = this.state
|
||||||
let isStarred = note._repository.starred.some((starredKey) => starredKey === note.key)
|
|
||||||
let folders = note._repository.folders
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='NoteDetail'
|
<div className='NoteDetail'
|
||||||
@@ -174,15 +169,11 @@ class NoteDetail extends React.Component {
|
|||||||
<div styleName='info-left'>
|
<div styleName='info-left'>
|
||||||
|
|
||||||
<div styleName='info-left-top'>
|
<div styleName='info-left-top'>
|
||||||
<StarButton styleName='info-left-top-starButton'
|
|
||||||
onClick={(e) => this.handleStarButtonClick(e)}
|
|
||||||
isActive={isStarred}
|
|
||||||
/>
|
|
||||||
<FolderSelect styleName='info-left-top-folderSelect'
|
<FolderSelect styleName='info-left-top-folderSelect'
|
||||||
value={this.state.note.folder}
|
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||||
ref='folder'
|
ref='folder'
|
||||||
folders={folders}
|
storages={storages}
|
||||||
onChange={() => this.handleChange()}
|
onChange={(e) => this.handleFolderChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-left-bottom'>
|
<div styleName='info-left-bottom'>
|
||||||
@@ -195,13 +186,18 @@ class NoteDetail extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div styleName='info-right'>
|
<div styleName='info-right'>
|
||||||
<button styleName='info-right-button'>
|
<StarButton styleName='info-right-button'
|
||||||
<i className='fa fa-clipboard fa-fw'/>
|
onClick={(e) => this.handleStarButtonClick(e)}
|
||||||
</button>
|
isActive={note.isStarred}
|
||||||
<button styleName='info-right-button'>
|
/>
|
||||||
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleShareButtonClick(e)}
|
||||||
|
>
|
||||||
<i className='fa fa-share-alt fa-fw'/>
|
<i className='fa fa-share-alt fa-fw'/>
|
||||||
</button>
|
</button>
|
||||||
<button styleName='info-right-button'>
|
<button styleName='info-right-button'
|
||||||
|
onClick={(e) => this.handleContextButtonClick(e)}
|
||||||
|
>
|
||||||
<i className='fa fa-ellipsis-v'/>
|
<i className='fa fa-ellipsis-v'/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,6 +206,7 @@ class NoteDetail extends React.Component {
|
|||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
ref='content'
|
ref='content'
|
||||||
styleName='body-noteEditor'
|
styleName='body-noteEditor'
|
||||||
|
config={config}
|
||||||
value={this.state.note.content}
|
value={this.state.note.content}
|
||||||
onChange={(e) => this.handleChange(e)}
|
onChange={(e) => this.handleChange(e)}
|
||||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||||
|
|||||||
@@ -14,22 +14,16 @@ $info-height = 75px
|
|||||||
|
|
||||||
.info-left
|
.info-left
|
||||||
float left
|
float left
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
.info-left-top
|
.info-left-top
|
||||||
height 40px
|
height 40px
|
||||||
line-height 40px
|
line-height 40px
|
||||||
|
|
||||||
.info-left-top-starButton
|
|
||||||
display inline-block
|
|
||||||
height 40px
|
|
||||||
width 40px
|
|
||||||
line-height 40px
|
|
||||||
vertical-align top
|
|
||||||
|
|
||||||
.info-left-top-folderSelect
|
.info-left-top-folderSelect
|
||||||
display inline-block
|
display inline-block
|
||||||
height 34px
|
height 34px
|
||||||
width 120px
|
width 200px
|
||||||
vertical-align middle
|
vertical-align middle
|
||||||
|
|
||||||
.info-left-bottom
|
.info-left-bottom
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
.root
|
.root
|
||||||
position relative
|
position relative
|
||||||
color $ui-inactive-text-color
|
|
||||||
font-size 18px
|
|
||||||
text-align center
|
|
||||||
background-color transparent
|
|
||||||
border none
|
|
||||||
padding 0
|
padding 0
|
||||||
transition transform 0.15s
|
transition transform 0.15s
|
||||||
&:focus
|
|
||||||
color $ui-active-color
|
|
||||||
&:hover
|
&:hover
|
||||||
color $ui-text-color
|
|
||||||
transform rotate(-72deg)
|
transform rotate(-72deg)
|
||||||
|
|
||||||
.root--active
|
.root--active
|
||||||
@extend .root
|
@extend .root
|
||||||
color $ui-active-color
|
color $ui-active-color
|
||||||
transform rotate(-72deg)
|
transform rotate(-72deg)
|
||||||
&:hover
|
|
||||||
color $ui-active-color
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ class TagSelect extends React.Component {
|
|||||||
removeLastTag () {
|
removeLastTag () {
|
||||||
let { value } = this.props
|
let { value } = this.props
|
||||||
|
|
||||||
value = value.slice()
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
value.pop()
|
value.pop()
|
||||||
value = _.uniq(value)
|
value = _.uniq(value)
|
||||||
|
|
||||||
@@ -60,7 +62,9 @@ class TagSelect extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value = value.slice()
|
value = _.isArray(value)
|
||||||
|
? value.slice()
|
||||||
|
: []
|
||||||
value.push(newTag)
|
value.push(newTag)
|
||||||
value = _.uniq(value)
|
value = _.uniq(value)
|
||||||
|
|
||||||
@@ -93,20 +97,22 @@ class TagSelect extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
let { value, className } = this.props
|
let { value, className } = this.props
|
||||||
|
|
||||||
let tagList = value.map((tag) => {
|
let tagList = _.isArray(value)
|
||||||
return (
|
? value.map((tag) => {
|
||||||
<span styleName='tag'
|
return (
|
||||||
key={tag}
|
<span styleName='tag'
|
||||||
>
|
key={tag}
|
||||||
<button styleName='tag-removeButton'
|
|
||||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
|
||||||
>
|
>
|
||||||
<i className='fa fa-times fa-fw'/>
|
<button styleName='tag-removeButton'
|
||||||
</button>
|
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||||
<span styleName='tag-label'>{tag}</span>
|
>
|
||||||
</span>
|
<i className='fa fa-times fa-fw'/>
|
||||||
)
|
</button>
|
||||||
})
|
<span styleName='tag-label'>{tag}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={_.isString(className)
|
<div className={_.isString(className)
|
||||||
@@ -134,7 +140,7 @@ class TagSelect extends React.Component {
|
|||||||
|
|
||||||
TagSelect.propTypes = {
|
TagSelect.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.arrayOf(PropTypes.string).isRequired,
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
onChange: PropTypes.func
|
onChange: PropTypes.func
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,23 +13,25 @@ class Detail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { repositories, location } = this.props
|
let { storages, location, notes, config } = this.props
|
||||||
let note = null
|
let note = null
|
||||||
if (location.query.key != null) {
|
if (location.query.key != null) {
|
||||||
let splitted = location.query.key.split('-')
|
let splitted = location.query.key.split('-')
|
||||||
let repoKey = splitted.shift()
|
let storageKey = splitted.shift()
|
||||||
|
let folderKey = splitted.shift()
|
||||||
let noteKey = splitted.shift()
|
let noteKey = splitted.shift()
|
||||||
let repo = _.find(repositories, {key: repoKey})
|
|
||||||
if (_.isObject(repo) && _.isArray(repo.notes)) {
|
note = _.find(notes, {
|
||||||
note = _.find(repo.notes, {key: noteKey})
|
storage: storageKey,
|
||||||
}
|
folder: folderKey,
|
||||||
|
key: noteKey
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return (
|
return (
|
||||||
<div className='Detail'
|
<div styleName='root'
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
styleName='root'
|
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
>
|
>
|
||||||
<div styleName='empty'>
|
<div styleName='empty'>
|
||||||
@@ -42,9 +44,10 @@ class Detail extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<NoteDetail
|
<NoteDetail
|
||||||
note={note}
|
note={note}
|
||||||
|
config={config}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'repositories',
|
'storages',
|
||||||
'style',
|
'style',
|
||||||
'ignorePreviewPointerEvents'
|
'ignorePreviewPointerEvents'
|
||||||
])}
|
])}
|
||||||
@@ -55,7 +58,7 @@ class Detail extends React.Component {
|
|||||||
|
|
||||||
Detail.propTypes = {
|
Detail.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
repositories: PropTypes.array,
|
storages: PropTypes.array,
|
||||||
style: PropTypes.shape({
|
style: PropTypes.shape({
|
||||||
left: PropTypes.number
|
left: PropTypes.number
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import SideNav from './SideNav'
|
|||||||
import TopBar from './TopBar'
|
import TopBar from './TopBar'
|
||||||
import NoteList from './NoteList'
|
import NoteList from './NoteList'
|
||||||
import Detail from './Detail'
|
import Detail from './Detail'
|
||||||
import Repository from 'browser/lib/Repository'
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
import StatusBar from './StatusBar'
|
import StatusBar from './StatusBar'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
@@ -27,9 +27,13 @@ class Main extends React.Component {
|
|||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
|
|
||||||
// Reload all data
|
// Reload all data
|
||||||
Repository.loadAll()
|
dataApi.init()
|
||||||
.then((allData) => {
|
.then((data) => {
|
||||||
dispatch({type: 'INIT_ALL', data: allData})
|
dispatch({
|
||||||
|
type: 'INIT_ALL',
|
||||||
|
storages: data.storages,
|
||||||
|
notes: data.notes
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +87,17 @@ class Main extends React.Component {
|
|||||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||||
>
|
>
|
||||||
<SideNav
|
<SideNav
|
||||||
{..._.pick(this.props, ['dispatch', 'repositories', 'config', 'location'])}
|
{..._.pick(this.props, [
|
||||||
|
'dispatch',
|
||||||
|
'storages',
|
||||||
|
'config',
|
||||||
|
'location'
|
||||||
|
])}
|
||||||
/>
|
/>
|
||||||
<TopBar
|
<TopBar
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'repositories',
|
'storages',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'params',
|
||||||
'location'
|
'location'
|
||||||
@@ -100,7 +109,8 @@ class Main extends React.Component {
|
|||||||
<NoteList style={{width: this.state.listWidth}}
|
<NoteList style={{width: this.state.listWidth}}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'repositories',
|
'storages',
|
||||||
|
'notes',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'params',
|
||||||
'location'
|
'location'
|
||||||
@@ -117,7 +127,8 @@ class Main extends React.Component {
|
|||||||
style={{left: this.state.listWidth + 1}}
|
style={{left: this.state.listWidth + 1}}
|
||||||
{..._.pick(this.props, [
|
{..._.pick(this.props, [
|
||||||
'dispatch',
|
'dispatch',
|
||||||
'repositories',
|
'storages',
|
||||||
|
'notes',
|
||||||
'config',
|
'config',
|
||||||
'params',
|
'params',
|
||||||
'location'
|
'location'
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
float left
|
float left
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
|
|
||||||
|
.item-info-left-folder
|
||||||
|
border-left 4px solid transparent
|
||||||
|
padding 2px 5px
|
||||||
|
|
||||||
.item-info-right
|
.item-info-right
|
||||||
float right
|
float right
|
||||||
|
|
||||||
|
|||||||
@@ -26,30 +26,32 @@ class NoteList extends React.Component {
|
|||||||
router.replace({
|
router.replace({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: `${this.notes[0]._repository.key}-${this.notes[0].key}`
|
key: this.notes[0].uniqueKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto scroll
|
// Auto scroll
|
||||||
let splitted = location.query.key.split('-')
|
if (_.isString(location.query.key)) {
|
||||||
let repoKey = splitted[0]
|
let splitted = location.query.key.split('/')
|
||||||
let noteKey = splitted[1]
|
let repoKey = splitted[0]
|
||||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
let noteKey = splitted[1]
|
||||||
return repoKey === note._repository.key && noteKey === note.key
|
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
})
|
return repoKey === note.storage && noteKey === note.key
|
||||||
if (targetIndex > -1) {
|
})
|
||||||
let list = this.refs.root
|
if (targetIndex > -1) {
|
||||||
let item = list.childNodes[targetIndex]
|
let list = this.refs.root
|
||||||
|
let item = list.childNodes[targetIndex]
|
||||||
|
|
||||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||||
if (overflowBelow) {
|
if (overflowBelow) {
|
||||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||||
}
|
}
|
||||||
let overflowAbove = list.scrollTop > item.offsetTop
|
let overflowAbove = list.scrollTop > item.offsetTop
|
||||||
if (overflowAbove) {
|
if (overflowAbove) {
|
||||||
list.scrollTop = item.offsetTop
|
list.scrollTop = item.offsetTop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,7 @@ class NoteList extends React.Component {
|
|||||||
let repoKey = splitted[0]
|
let repoKey = splitted[0]
|
||||||
let noteKey = splitted[1]
|
let noteKey = splitted[1]
|
||||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||||
return repoKey === note._repository.key && noteKey === note.key
|
return repoKey === note.storage && noteKey === note.key
|
||||||
})
|
})
|
||||||
|
|
||||||
if (targetIndex === 0) {
|
if (targetIndex === 0) {
|
||||||
@@ -80,7 +82,7 @@ class NoteList extends React.Component {
|
|||||||
router.push({
|
router.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: `${this.notes[targetIndex]._repository.key}-${this.notes[targetIndex].key}`
|
key: this.notes[targetIndex].uniqueKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -108,7 +110,7 @@ class NoteList extends React.Component {
|
|||||||
router.push({
|
router.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: `${this.notes[targetIndex]._repository.key}-${this.notes[targetIndex].key}`
|
key: this.notes[targetIndex].uniqueKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -192,7 +194,7 @@ class NoteList extends React.Component {
|
|||||||
.filter((note) => note.folder === folderKey)
|
.filter((note) => note.folder === folderKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNoteClick (key) {
|
handleNoteClick (uniqueKey) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let { router } = this.context
|
let { router } = this.context
|
||||||
let { location } = this.props
|
let { location } = this.props
|
||||||
@@ -200,42 +202,50 @@ class NoteList extends React.Component {
|
|||||||
router.push({
|
router.push({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
query: {
|
query: {
|
||||||
key: key
|
key: uniqueKey
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { location } = this.props
|
let { location, storages, notes } = this.props
|
||||||
let notes = this.notes = this.getNotes().sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
this.notes = notes
|
||||||
|
// this.notes = this.getNotes()
|
||||||
|
|
||||||
let noteElements = notes
|
let noteElements = notes
|
||||||
|
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||||
.map((note) => {
|
.map((note) => {
|
||||||
let folder = _.find(note._repository.folders, {key: note.folder})
|
let storage = _.find(storages, {key: note.storage})
|
||||||
let tagElements = note.tags.map((tag) => {
|
let folder = _.find(storage.folders, {key: note.folder})
|
||||||
return (
|
let tagElements = _.isArray(note.tags)
|
||||||
<span styleName='item-tagList-item'
|
? note.tags.map((tag) => {
|
||||||
key={tag}>
|
return (
|
||||||
{tag}
|
<span styleName='item-tagList-item'
|
||||||
</span>
|
key={tag}>
|
||||||
)
|
{tag}
|
||||||
})
|
</span>
|
||||||
let key = `${note._repository.key}-${note.key}`
|
)
|
||||||
let isActive = location.query.key === key
|
})
|
||||||
|
: []
|
||||||
|
let isActive = location.query.key === note.uniqueKey
|
||||||
return (
|
return (
|
||||||
<div styleName={isActive
|
<div styleName={isActive
|
||||||
? 'item--active'
|
? 'item--active'
|
||||||
: 'item'
|
: 'item'
|
||||||
}
|
}
|
||||||
key={key}
|
key={note.uniqueKey}
|
||||||
onClick={(e) => this.handleNoteClick(key)(e)}
|
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)}
|
||||||
>
|
>
|
||||||
<div styleName='item-border'/>
|
<div styleName='item-border'/>
|
||||||
<div styleName='item-info'>
|
<div styleName='item-info'>
|
||||||
|
|
||||||
<div styleName='item-info-left'>
|
<div styleName='item-info-left'>
|
||||||
<i className='fa fa-cube fa-fw' style={{color: folder.color}}/> {folder.name}
|
<span styleName='item-info-left-folder'
|
||||||
|
style={{borderColor: folder.color}}
|
||||||
|
>
|
||||||
|
{storage.name}/{folder.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div styleName='item-info-right'>
|
<div styleName='item-info-right'>
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
|
||||||
import styles from './FolderItem.styl'
|
|
||||||
import store from 'browser/main/store'
|
|
||||||
import Repository from 'browser/lib/Repository'
|
|
||||||
import consts from 'browser/lib/consts'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { Menu, MenuItem } = remote
|
|
||||||
|
|
||||||
class FolderItem extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditing: false,
|
|
||||||
isUpdating: false,
|
|
||||||
name: props.folder.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColorButtonClick (color) {
|
|
||||||
return (e) => {
|
|
||||||
let { repository, folder } = this.props
|
|
||||||
this.setState({
|
|
||||||
isUpdating: true
|
|
||||||
}, () => {
|
|
||||||
Repository.find(repository.key)
|
|
||||||
.then((repository) => {
|
|
||||||
console.log(repository)
|
|
||||||
return repository.updateFolder(folder.key, {color: color})
|
|
||||||
})
|
|
||||||
.then((folder) => {
|
|
||||||
store.dispatch({
|
|
||||||
type: 'EDIT_FOLDER',
|
|
||||||
key: repository.key,
|
|
||||||
folder: folder
|
|
||||||
})
|
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
isUpdating: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
isUpdating: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextButtonClick (e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (this.state.isUpdating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var menu = new Menu()
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'New Note'
|
|
||||||
}))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Rename',
|
|
||||||
click: () => this.handleRenameButtonClick(e)
|
|
||||||
}))
|
|
||||||
var colorMenu = new Menu()
|
|
||||||
consts.FOLDER_COLORS.forEach((color, index) => {
|
|
||||||
colorMenu.append(new MenuItem({
|
|
||||||
label: consts.FOLDER_COLOR_NAMES[index],
|
|
||||||
click: (e) => this.handleColorButtonClick(color)(e)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Recolor',
|
|
||||||
submenu: colorMenu
|
|
||||||
}))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Delete',
|
|
||||||
click: () => this.handleDeleteButtonClick(e)
|
|
||||||
}))
|
|
||||||
|
|
||||||
menu.popup(remote.getCurrentWindow())
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRenameButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
isEditing: true,
|
|
||||||
name: this.props.folder.name
|
|
||||||
}, () => {
|
|
||||||
this.refs.nameInput.focus()
|
|
||||||
this.refs.nameInput.select()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteButtonClick (e) {
|
|
||||||
let { repository, folder } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isUpdating: true
|
|
||||||
}, () => {
|
|
||||||
Repository.find(repository.key)
|
|
||||||
.then((repository) => {
|
|
||||||
console.log(repository)
|
|
||||||
return repository.removeFolder(folder.key)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
store.dispatch({
|
|
||||||
type: 'REMOVE_FOLDER',
|
|
||||||
repository: repository.key,
|
|
||||||
folder: folder.key
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({
|
|
||||||
isUpdating: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick (e) {
|
|
||||||
let { folder, repository } = this.props
|
|
||||||
let { router } = this.context
|
|
||||||
|
|
||||||
router.push('/repositories/' + repository.key + '/folders/' + folder.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIdle () {
|
|
||||||
let { folder, repository, isFolded } = this.props
|
|
||||||
let { router } = this.context
|
|
||||||
|
|
||||||
let isActive = router.isActive('/repositories/' + repository.key + '/folders/' + folder.key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div styleName={isFolded
|
|
||||||
? isActive ? 'root--folded--active' : 'root--folded'
|
|
||||||
: isActive ? 'root--active' : 'root'
|
|
||||||
}
|
|
||||||
onClick={(e) => this.handleClick(e)}
|
|
||||||
onContextMenu={(e) => this.handleContextButtonClick(e)}
|
|
||||||
>
|
|
||||||
<div styleName='label'>
|
|
||||||
<i styleName='label-icon'
|
|
||||||
className='fa fa-cube'
|
|
||||||
style={{color: folder.color}}
|
|
||||||
/>
|
|
||||||
<span styleName='label-name'>{folder.name}</span>
|
|
||||||
</div>
|
|
||||||
<div styleName='control'>
|
|
||||||
<button styleName='control-button'
|
|
||||||
onClick={(e) => this.handleContextButtonClick(e)}
|
|
||||||
>
|
|
||||||
<i className='fa fa-ellipsis-v'/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNameInputChange (e) {
|
|
||||||
this.setState({
|
|
||||||
name: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNameInputBlur (e) {
|
|
||||||
let { folder, repository } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isUpdating: true
|
|
||||||
}, () => {
|
|
||||||
Repository.find(repository.key)
|
|
||||||
.then((repository) => {
|
|
||||||
console.log(repository)
|
|
||||||
return repository.updateFolder(folder.key, {name: this.state.name})
|
|
||||||
})
|
|
||||||
.then((folder) => {
|
|
||||||
store.dispatch({
|
|
||||||
type: 'EDIT_FOLDER',
|
|
||||||
key: repository.key,
|
|
||||||
folder: folder
|
|
||||||
})
|
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
isUpdating: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({
|
|
||||||
isEditing: false,
|
|
||||||
isUpdating: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEdit () {
|
|
||||||
let { isFolded } = this.props
|
|
||||||
return (
|
|
||||||
<div styleName={isFolded
|
|
||||||
? 'root--edit--folded'
|
|
||||||
: 'root--edit'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input styleName='nameInput'
|
|
||||||
ref='nameInput'
|
|
||||||
value={this.state.name}
|
|
||||||
onChange={(e) => this.handleNameInputChange(e)}
|
|
||||||
onBlur={(e) => this.handleNameInputBlur(e)}
|
|
||||||
disabled={this.state.isUpdating}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return this.state.isEditing ? this.renderEdit() : this.renderIdle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderItem.contextTypes = {
|
|
||||||
router: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderItem.propTypes = {
|
|
||||||
folder: PropTypes.shape({
|
|
||||||
name: PropTypes.string,
|
|
||||||
color: PropTypes.string
|
|
||||||
}),
|
|
||||||
repository: PropTypes.shape({
|
|
||||||
key: PropTypes.string
|
|
||||||
}),
|
|
||||||
isFolded: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CSSModules(FolderItem, styles)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
.root
|
|
||||||
height 33px
|
|
||||||
width 100%
|
|
||||||
position relative
|
|
||||||
cursor pointer
|
|
||||||
navButtonColor()
|
|
||||||
|
|
||||||
.root--active
|
|
||||||
@extend .root
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
.control-button
|
|
||||||
opacity 1
|
|
||||||
color white
|
|
||||||
&:hover
|
|
||||||
background-color alpha(white, 30%)
|
|
||||||
&:active, &:hover:active
|
|
||||||
background-color alpha(white, 15%)
|
|
||||||
|
|
||||||
.label
|
|
||||||
position absolute
|
|
||||||
left 0
|
|
||||||
top 0
|
|
||||||
bottom 0
|
|
||||||
right 48px
|
|
||||||
padding-left 20px
|
|
||||||
line-height 33px
|
|
||||||
overflow-x hidden
|
|
||||||
|
|
||||||
.label-name
|
|
||||||
margin-left 5px
|
|
||||||
|
|
||||||
.control
|
|
||||||
position absolute
|
|
||||||
top 0
|
|
||||||
bottom 0
|
|
||||||
right 5px
|
|
||||||
width 24px
|
|
||||||
|
|
||||||
.control-button
|
|
||||||
opacity 0
|
|
||||||
navButtonColor()
|
|
||||||
width 24px
|
|
||||||
height 24px
|
|
||||||
margin-top 4.5px
|
|
||||||
border-radius 5px
|
|
||||||
transition opacity 0.15s
|
|
||||||
|
|
||||||
.root--edit
|
|
||||||
@extend .root
|
|
||||||
|
|
||||||
.nameInput
|
|
||||||
absolute top bottom
|
|
||||||
left 10px
|
|
||||||
right 10px
|
|
||||||
height 33px
|
|
||||||
padding 0 10px
|
|
||||||
border-radius 5px
|
|
||||||
border $ui-border
|
|
||||||
outline none
|
|
||||||
background-color white
|
|
||||||
z-index 1
|
|
||||||
&:focus
|
|
||||||
border-color $ui-input--focus-borderColor
|
|
||||||
&:disabled
|
|
||||||
background-color $ui-input--disabled-backgroundColor
|
|
||||||
|
|
||||||
.root--folded
|
|
||||||
@extend .root
|
|
||||||
width 44px - 1
|
|
||||||
&:hover .label-name
|
|
||||||
width 100px
|
|
||||||
.label
|
|
||||||
padding-left 0
|
|
||||||
text-align center
|
|
||||||
right 0
|
|
||||||
.label-icon
|
|
||||||
width 44px - 1
|
|
||||||
.label-name
|
|
||||||
position fixed
|
|
||||||
height 34px
|
|
||||||
left 44px
|
|
||||||
width 0
|
|
||||||
box-sizing border-box
|
|
||||||
margin-left 0
|
|
||||||
overflow ellipsis
|
|
||||||
background-color $ui-tooltip-backgroundColor
|
|
||||||
z-index 10
|
|
||||||
color white
|
|
||||||
line-height 34px
|
|
||||||
border-top-right-radius 5px
|
|
||||||
border-bottom-right-radius 5px
|
|
||||||
transition width 0.15s
|
|
||||||
pointer-events none
|
|
||||||
.control
|
|
||||||
display none
|
|
||||||
|
|
||||||
.root--folded--active
|
|
||||||
@extend .root--folded
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
|
|
||||||
.root--edit--folded
|
|
||||||
@extend .root--edit
|
|
||||||
.nameInput
|
|
||||||
position fixed
|
|
||||||
top inherit
|
|
||||||
bottom inherit
|
|
||||||
width 100px
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
|
||||||
import styles from './RepositorySection.styl'
|
|
||||||
import Repository from 'browser/lib/Repository'
|
|
||||||
import FolderItem from './FolderItem'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const { remote } = electron
|
|
||||||
const { Menu, MenuItem } = remote
|
|
||||||
|
|
||||||
class RepositorySection extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isOpen: true,
|
|
||||||
isCreatingFolder: false,
|
|
||||||
isSaving: false,
|
|
||||||
newFolder: {
|
|
||||||
name: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getRepository () {
|
|
||||||
let { repository } = this.props
|
|
||||||
return Repository.find(repository.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUnlinkButtonClick () {
|
|
||||||
let { dispatch, repository } = this.props
|
|
||||||
|
|
||||||
this.getRepository()
|
|
||||||
.then((repositoryInstance) => {
|
|
||||||
return repositoryInstance.unmount()
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
dispatch({
|
|
||||||
type: 'REMOVE_REPOSITORY',
|
|
||||||
key: repository.key
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleButtonClick (e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.setState({
|
|
||||||
isOpen: !this.state.isOpen
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick (e) {
|
|
||||||
let { repository } = this.props
|
|
||||||
let { router } = this.context
|
|
||||||
router.push('/repositories/' + repository.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextButtonClick (e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
let menu = new Menu()
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'New Folder',
|
|
||||||
click: () => this.handleNewFolderButtonClick()
|
|
||||||
}))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Unmount',
|
|
||||||
click: () => this.handleUnlinkButtonClick()
|
|
||||||
}))
|
|
||||||
|
|
||||||
menu.popup(remote.getCurrentWindow())
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewFolderButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
isCreatingFolder: true,
|
|
||||||
newFolder: {
|
|
||||||
name: 'New Folder'
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
this.refs.nameInput.select()
|
|
||||||
this.refs.nameInput.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewFolderFormChange (e) {
|
|
||||||
let newFolder = this.state.newFolder
|
|
||||||
newFolder.name = this.refs.nameInput.value
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
newFolder
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNameInputBlur (e) {
|
|
||||||
let { dispatch, repository } = this.props
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isSaving: true
|
|
||||||
}, () => {
|
|
||||||
this.getRepository()
|
|
||||||
.then((repositoryInstance) => {
|
|
||||||
return repositoryInstance.addFolder({
|
|
||||||
name: this.state.newFolder.name
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then((folder) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'ADD_FOLDER',
|
|
||||||
key: repository.key,
|
|
||||||
folder: folder
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isCreatingFolder: false,
|
|
||||||
isSaving: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isCreatingFolder: false,
|
|
||||||
isSaving: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let { repository, isFolded } = this.props
|
|
||||||
let { router } = this.context
|
|
||||||
|
|
||||||
let isActive = router.isActive('/repositories/' + repository.key, true)
|
|
||||||
|
|
||||||
let folderElements = repository.folders.map((folder) => {
|
|
||||||
return (
|
|
||||||
<FolderItem
|
|
||||||
key={folder.key}
|
|
||||||
folder={folder}
|
|
||||||
repository={repository}
|
|
||||||
isFolded={isFolded}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
let toggleButtonIconClassName = this.state.isOpen
|
|
||||||
? 'fa fa-minus'
|
|
||||||
: 'fa fa-plus'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='RepositorySection'
|
|
||||||
styleName={isFolded ? 'root-folded' : 'root'}
|
|
||||||
>
|
|
||||||
<div styleName={isActive ? 'header--active' : 'header'}
|
|
||||||
onClick={(e) => this.handleHeaderClick(e)}
|
|
||||||
onContextMenu={(e) => this.handleContextButtonClick(e)}
|
|
||||||
>
|
|
||||||
<div styleName='header-name'>
|
|
||||||
<i className='fa fa-archive fa-fw'/>
|
|
||||||
<span styleName='header-name-label'>{repository.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div styleName='header-control'>
|
|
||||||
<button styleName='header-control-button'
|
|
||||||
onClick={(e) => this.handleContextButtonClick(e)}
|
|
||||||
>
|
|
||||||
<i className='fa fa-ellipsis-v fa-fw'/>
|
|
||||||
</button>
|
|
||||||
<button styleName='header-control-button--show'
|
|
||||||
onClick={(e) => this.handleToggleButtonClick(e)}
|
|
||||||
>
|
|
||||||
<i className={toggleButtonIconClassName}/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.isOpen && <div>
|
|
||||||
{folderElements}
|
|
||||||
|
|
||||||
{this.state.isCreatingFolder
|
|
||||||
? <div styleName='newFolderForm'>
|
|
||||||
<input styleName='newFolderForm-nameInput'
|
|
||||||
ref='nameInput'
|
|
||||||
disabled={this.state.isSaving}
|
|
||||||
value={this.state.newFolder.name}
|
|
||||||
onChange={(e) => this.handleNewFolderFormChange(e)}
|
|
||||||
onBlur={(e) => this.handleNameInputBlur(e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
: <button styleName='newFolderButton'
|
|
||||||
onClick={(e) => this.handleNewFolderButtonClick(e)}
|
|
||||||
>
|
|
||||||
<i styleName='newFolderButton-icon' className='fa fa-plus fa-fw'/>
|
|
||||||
<span styleName='newFolderButton-label'>New Folder</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RepositorySection.contextTypes = {
|
|
||||||
router: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
RepositorySection.propTypes = {
|
|
||||||
repository: PropTypes.shape({
|
|
||||||
name: PropTypes.string,
|
|
||||||
folders: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
name: PropTypes.string
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
dispatch: PropTypes.func,
|
|
||||||
isFolded: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CSSModules(RepositorySection, styles)
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
.root
|
|
||||||
user-select none
|
|
||||||
color $nav-text-color
|
|
||||||
|
|
||||||
.header
|
|
||||||
position relative
|
|
||||||
width 100%
|
|
||||||
height 33px
|
|
||||||
cursor pointer
|
|
||||||
text-align left
|
|
||||||
font-size 14px
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
&:hover .header-control-button
|
|
||||||
opacity 1
|
|
||||||
&:active
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
.header-control-button, .header-control-button--show
|
|
||||||
color white
|
|
||||||
|
|
||||||
.header--active, .header--active:hover, .header--active:active
|
|
||||||
@extend .header
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
color $ui-button--active-color
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--active-backgroundColor
|
|
||||||
.header-control-button,
|
|
||||||
.header-control-button--show
|
|
||||||
color white
|
|
||||||
opacity 1
|
|
||||||
&:hover
|
|
||||||
background-color alpha(white, 30%)
|
|
||||||
&:active
|
|
||||||
background-color alpha(white, 15%)
|
|
||||||
|
|
||||||
.header-name
|
|
||||||
position absolute
|
|
||||||
left 0
|
|
||||||
top 0
|
|
||||||
bottom 0
|
|
||||||
right 72px
|
|
||||||
padding-left 10px
|
|
||||||
line-height 33px
|
|
||||||
|
|
||||||
.header-name-label
|
|
||||||
margin-left 5px
|
|
||||||
|
|
||||||
.header-control
|
|
||||||
position absolute
|
|
||||||
top 0
|
|
||||||
bottom 0
|
|
||||||
right 5px
|
|
||||||
width 48px
|
|
||||||
|
|
||||||
.header-control-button
|
|
||||||
border none
|
|
||||||
background-color transparent
|
|
||||||
width 24px
|
|
||||||
height 24px
|
|
||||||
padding 0
|
|
||||||
margin-top 4.5px
|
|
||||||
border-radius 5px
|
|
||||||
opacity 0
|
|
||||||
color $ui-inactive-text-color
|
|
||||||
transition color background-color 0.15s
|
|
||||||
&:hover
|
|
||||||
background-color $ui-button--hover-backgroundColor
|
|
||||||
|
|
||||||
.header-control-button--show
|
|
||||||
@extend .header-control-button
|
|
||||||
opacity 1
|
|
||||||
|
|
||||||
.newFolderForm
|
|
||||||
width 100%
|
|
||||||
padding 0 15px
|
|
||||||
height 33px
|
|
||||||
|
|
||||||
.newFolderForm-nameInput
|
|
||||||
width 100%
|
|
||||||
height 33px
|
|
||||||
padding 0 10px
|
|
||||||
border-radius 5px
|
|
||||||
border $ui-border
|
|
||||||
outline none
|
|
||||||
&:focus
|
|
||||||
border-color $ui-input--focus-borderColor
|
|
||||||
&:disabled
|
|
||||||
background-color $ui-input--disabled-backgroundColor
|
|
||||||
|
|
||||||
.newFolderButton
|
|
||||||
navButtonColor()
|
|
||||||
height 34px
|
|
||||||
width 100%
|
|
||||||
border none
|
|
||||||
padding 0 0 0 20px
|
|
||||||
text-align left
|
|
||||||
line-height 34px
|
|
||||||
|
|
||||||
.newFolderButton-label
|
|
||||||
margin-left 0
|
|
||||||
|
|
||||||
.root-folded
|
|
||||||
@extend .root
|
|
||||||
width 44px - 1
|
|
||||||
.header, .header--active
|
|
||||||
width 44px - 1
|
|
||||||
text-align center
|
|
||||||
overflow hidden
|
|
||||||
&:hover
|
|
||||||
.header-name-label
|
|
||||||
width 134px
|
|
||||||
padding-left 34px
|
|
||||||
.header-control
|
|
||||||
width 35px
|
|
||||||
padding-right 5px
|
|
||||||
.header-name
|
|
||||||
width 44px - 1
|
|
||||||
padding-left 0
|
|
||||||
.header-name-label
|
|
||||||
position fixed
|
|
||||||
display inline-block
|
|
||||||
height 34px
|
|
||||||
left 44px - 1
|
|
||||||
width 0
|
|
||||||
box-sizing border-box
|
|
||||||
margin-left 0
|
|
||||||
overflow ellipsis
|
|
||||||
background-color $ui-tooltip-backgroundColor
|
|
||||||
z-index 10
|
|
||||||
color white
|
|
||||||
line-height 34px
|
|
||||||
border-top-right-radius 5px
|
|
||||||
border-bottom-right-radius 5px
|
|
||||||
transition width 0.15s
|
|
||||||
pointer-events none
|
|
||||||
.header-control
|
|
||||||
position fixed
|
|
||||||
width 0
|
|
||||||
height 33px
|
|
||||||
top inherit
|
|
||||||
bottom inherit
|
|
||||||
z-index 11
|
|
||||||
left 44px - 1
|
|
||||||
box-sizing border-box
|
|
||||||
overflow hidden
|
|
||||||
.header-control-button
|
|
||||||
display none
|
|
||||||
.header-control-button--show
|
|
||||||
float right
|
|
||||||
background-color $ui-tooltip-button-backgroundColor
|
|
||||||
&:hover
|
|
||||||
background-color $ui-tooltip-button--hover-backgroundColor
|
|
||||||
.newFolderButton
|
|
||||||
width 44px - 1
|
|
||||||
padding 0
|
|
||||||
&:hover .newFolderButton-label
|
|
||||||
width 100px
|
|
||||||
.newFolderButton-icon
|
|
||||||
text-align center
|
|
||||||
width 44px - 1
|
|
||||||
.newFolderButton-label
|
|
||||||
position fixed
|
|
||||||
display inline-block
|
|
||||||
height 34px
|
|
||||||
left 44px
|
|
||||||
width 0
|
|
||||||
box-sizing border-box
|
|
||||||
margin-left 0
|
|
||||||
overflow ellipsis
|
|
||||||
background-color $ui-tooltip-backgroundColor
|
|
||||||
z-index 10
|
|
||||||
color white
|
|
||||||
line-height 34px
|
|
||||||
border-top-right-radius 5px
|
|
||||||
border-bottom-right-radius 5px
|
|
||||||
transition width 0.15s
|
|
||||||
pointer-events none
|
|
||||||
font-size 14px
|
|
||||||
text-align center
|
|
||||||
.newFolderForm-nameInput
|
|
||||||
position fixed
|
|
||||||
width 100px
|
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
.menu-button-label
|
.menu-button-label
|
||||||
margin-left 5px
|
margin-left 5px
|
||||||
|
|
||||||
.repositoryList
|
.storageList
|
||||||
absolute left right
|
absolute left right
|
||||||
bottom 44px
|
bottom 44px
|
||||||
top 178px
|
top 178px
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
|
|
||||||
.repositoryList-empty
|
.storageList-empty
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
margin-top 15px
|
margin-top 15px
|
||||||
line-height 24px
|
line-height 24px
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
line-height 32px
|
line-height 32px
|
||||||
padding 0
|
padding 0
|
||||||
|
|
||||||
.root-folded
|
.root--folded
|
||||||
@extend .root
|
@extend .root
|
||||||
width 44px
|
width 44px
|
||||||
.repositoryList-empty
|
.storageList-empty
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
transform rotate(90deg)
|
transform rotate(90deg)
|
||||||
.top-menu
|
.top-menu
|
||||||
|
|||||||
95
browser/main/SideNav/StorageItem.js
Normal file
95
browser/main/SideNav/StorageItem.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './StorageItem.styl'
|
||||||
|
import { hashHistory } from 'react-router'
|
||||||
|
|
||||||
|
class StorageItem extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOpen: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
isOpen: !this.state.isOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHeaderInfoClick (e) {
|
||||||
|
let { storage } = this.props
|
||||||
|
hashHistory.push('/storages/' + storage.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderButtonClick (folderKey) {
|
||||||
|
return (e) => {
|
||||||
|
let { storage } = this.props
|
||||||
|
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { storage, location } = this.props
|
||||||
|
let folderList = storage.folders.map((folder) => {
|
||||||
|
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))
|
||||||
|
return <button styleName={isActive
|
||||||
|
? 'folderList-item--active'
|
||||||
|
: 'folderList-item'
|
||||||
|
}
|
||||||
|
key={folder.key}
|
||||||
|
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
|
||||||
|
>
|
||||||
|
<span styleName='folderList-item-name'
|
||||||
|
style={{borderColor: folder.color}}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
})
|
||||||
|
|
||||||
|
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='root'
|
||||||
|
key={storage.key}
|
||||||
|
>
|
||||||
|
<div styleName={isActive
|
||||||
|
? 'header--active'
|
||||||
|
: 'header'
|
||||||
|
}>
|
||||||
|
<button styleName='header-toggleButton'
|
||||||
|
onMouseDown={(e) => this.handleToggleButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className={this.state.isOpen
|
||||||
|
? 'fa fa-caret-down'
|
||||||
|
: 'fa fa-caret-right'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button styleName='header-info'
|
||||||
|
onClick={(e) => this.handleHeaderInfoClick(e)}
|
||||||
|
>
|
||||||
|
<span styleName='header-info-name'>
|
||||||
|
{storage.name}
|
||||||
|
</span>
|
||||||
|
<span styleName='header-info-path'>
|
||||||
|
({storage.path})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{this.state.isOpen &&
|
||||||
|
<div styleName='folderList' >
|
||||||
|
{folderList}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageItem.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(StorageItem, styles)
|
||||||
89
browser/main/SideNav/StorageItem.styl
Normal file
89
browser/main/SideNav/StorageItem.styl
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.root
|
||||||
|
width 100%
|
||||||
|
user-select none
|
||||||
|
.header
|
||||||
|
position relative
|
||||||
|
height 30px
|
||||||
|
width 100%
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
&:active
|
||||||
|
.header-toggleButton
|
||||||
|
color white
|
||||||
|
.header--active
|
||||||
|
@extend .header
|
||||||
|
.header-info
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
.header-toggleButton
|
||||||
|
color white
|
||||||
|
&:active
|
||||||
|
color white
|
||||||
|
|
||||||
|
.header-toggleButton
|
||||||
|
position absolute
|
||||||
|
left 0
|
||||||
|
width 25px
|
||||||
|
height 30px
|
||||||
|
padding 0
|
||||||
|
border none
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
color $ui-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-active-color
|
||||||
|
|
||||||
|
.header-info
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
height 30px
|
||||||
|
padding-left 25px
|
||||||
|
padding-right 10px
|
||||||
|
line-height 30px
|
||||||
|
cursor pointer
|
||||||
|
font-size 14px
|
||||||
|
border none
|
||||||
|
overflow ellipsis
|
||||||
|
text-align left
|
||||||
|
background-color transparent
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
&:active
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
|
||||||
|
.header-info-path
|
||||||
|
font-size 10px
|
||||||
|
margin 0 5px
|
||||||
|
|
||||||
|
.folderList-item
|
||||||
|
display block
|
||||||
|
width 100%
|
||||||
|
height 3 0px
|
||||||
|
background-color transparent
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
padding 0
|
||||||
|
margin 2px 0
|
||||||
|
text-align left
|
||||||
|
border none
|
||||||
|
font-size 14px
|
||||||
|
&:hover
|
||||||
|
background-color $ui-button--hover-backgroundColor
|
||||||
|
&:active
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
.folderList-item--active
|
||||||
|
@extend .folderList-item
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
&:hover
|
||||||
|
color $ui-button--active-color
|
||||||
|
background-color $ui-button--active-backgroundColor
|
||||||
|
.folderList-item-name
|
||||||
|
display block
|
||||||
|
padding 0 10px
|
||||||
|
height 30px
|
||||||
|
line-height 30px
|
||||||
|
border-width 0 0 0 6px
|
||||||
|
border-style solid
|
||||||
|
border-color transparent
|
||||||
@@ -2,43 +2,22 @@ import React, { PropTypes } from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
import { openModal } from 'browser/main/lib/modal'
|
import { openModal } from 'browser/main/lib/modal'
|
||||||
import Preferences from '../modals/Preferences'
|
import PreferencesModal from '../modals/PreferencesModal'
|
||||||
import RepositorySection from './RepositorySection'
|
|
||||||
import NewRepositoryModal from '../modals/NewRepositoryModal'
|
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import StorageItem from './StorageItem'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const { remote } = electron
|
const { remote } = electron
|
||||||
const Menu = remote.Menu
|
|
||||||
const MenuItem = remote.MenuItem
|
|
||||||
|
|
||||||
class SideNav extends React.Component {
|
class SideNav extends React.Component {
|
||||||
// TODO: should not use electron stuff v0.7
|
// TODO: should not use electron stuff v0.7
|
||||||
handleMenuButtonClick (e) {
|
handleMenuButtonClick (e) {
|
||||||
var menu = new Menu()
|
openModal(PreferencesModal)
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Preferences',
|
|
||||||
click: (e) => this.handlePreferencesButtonClick(e)
|
|
||||||
}))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({
|
|
||||||
label: 'Mount Repository',
|
|
||||||
click: (e) => this.handleNewRepositoryButtonClick(e)
|
|
||||||
}))
|
|
||||||
menu.popup(remote.getCurrentWindow())
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewRepositoryButtonClick (e) {
|
|
||||||
openModal(NewRepositoryModal)
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePreferencesButtonClick (e) {
|
|
||||||
openModal(Preferences)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHomeButtonClick (e) {
|
handleHomeButtonClick (e) {
|
||||||
let { router } = this.context
|
let { router } = this.context
|
||||||
router.push('/repositories')
|
router.push('/home')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStarredButtonClick (e) {
|
handleStarredButtonClick (e) {
|
||||||
@@ -57,25 +36,22 @@ class SideNav extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { repositories, dispatch, location, config } = this.props
|
let { storages, location, config } = this.props
|
||||||
|
|
||||||
let isFolded = config.isSideNavFolded
|
let isFolded = config.isSideNavFolded
|
||||||
let isHomeActive = location.pathname.match(/^\/home$/)
|
let isHomeActive = location.pathname.match(/^\/home$/)
|
||||||
let isStarredActive = location.pathname.match(/^\/starred$/)
|
let isStarredActive = location.pathname.match(/^\/starred$/)
|
||||||
|
let storageList = storages.map((storage) => {
|
||||||
let repositorieElements = repositories
|
return <StorageItem
|
||||||
.map((repo) => {
|
key={storage.key}
|
||||||
return <RepositorySection
|
storage={storage}
|
||||||
key={repo.key}
|
location={location}
|
||||||
repository={repo}
|
/>
|
||||||
dispatch={dispatch}
|
})
|
||||||
isFolded={isFolded}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='SideNav'
|
<div className='SideNav'
|
||||||
styleName={isFolded ? 'root-folded' : 'root'}
|
styleName={isFolded ? 'root--folded' : 'root'}
|
||||||
tabIndex='1'
|
tabIndex='1'
|
||||||
>
|
>
|
||||||
<div styleName='top'>
|
<div styleName='top'>
|
||||||
@@ -102,9 +78,9 @@ class SideNav extends React.Component {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div styleName='repositoryList'>
|
<div styleName='storageList'>
|
||||||
{repositories.length > 0 ? repositorieElements : (
|
{storageList.length > 0 ? storageList : (
|
||||||
<div styleName='repositoryList-empty'>No repository mount.</div>
|
<div styleName='storageList-empty'>No storage mount.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,7 +103,7 @@ SideNav.contextTypes = {
|
|||||||
|
|
||||||
SideNav.propTypes = {
|
SideNav.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
repositories: PropTypes.array,
|
storages: PropTypes.array,
|
||||||
config: PropTypes.shape({
|
config: PropTypes.shape({
|
||||||
isSideNavFolded: PropTypes.bool
|
isSideNavFolded: PropTypes.bool
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { PropTypes } from 'react'
|
|||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './TopBar.styl'
|
import styles from './TopBar.styl'
|
||||||
import activityRecord from 'browser/lib/activityRecord'
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
import Repository from 'browser/lib/Repository'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import Commander from 'browser/main/lib/Commander'
|
import Commander from 'browser/main/lib/Commander'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
|
||||||
const OSX = window.process.platform === 'darwin'
|
const OSX = window.process.platform === 'darwin'
|
||||||
|
|
||||||
@@ -33,65 +33,31 @@ class TopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNewPostButtonClick (e) {
|
handleNewPostButtonClick (e) {
|
||||||
activityRecord.emit('ARTICLE_CREATE')
|
let { storages, params, dispatch } = this.props
|
||||||
|
let storage = _.find(storages, {key: params.storageKey})
|
||||||
let { params, repositories } = this.props
|
if (storage == null) storage = storages[0]
|
||||||
|
if (storage == null) throw new Error('No storage to create a note')
|
||||||
let folderKey = params.folderKey
|
let folder = _.find(storage.folders, {key: params.folderKey})
|
||||||
let repositoryKey = params.repositoryKey
|
if (folder == null) folder = storage.folders[0]
|
||||||
if (folderKey == null) {
|
if (folder == null) throw new Error('No folder to craete a note')
|
||||||
let repository = _.find(repositories, {key: repositoryKey})
|
// activityRecord.emit('ARTICLE_CREATE')
|
||||||
if (repository == null) {
|
console.log(storage, folder)
|
||||||
repository = repositories[0]
|
dataApi
|
||||||
}
|
.createNote(storage.key, folder.key, {
|
||||||
if (repository != null) {
|
title: '',
|
||||||
repositoryKey = repository.key
|
content: ''
|
||||||
folderKey = repository.folders[0] != null && repository.folders[0].key
|
|
||||||
}
|
|
||||||
if (folderKey == null) throw new Error('no folder exists')
|
|
||||||
}
|
|
||||||
|
|
||||||
let newNote = {
|
|
||||||
title: 'New Note',
|
|
||||||
content: '',
|
|
||||||
folder: folderKey,
|
|
||||||
tags: [],
|
|
||||||
mode: 'markdown'
|
|
||||||
}
|
|
||||||
|
|
||||||
Repository
|
|
||||||
.find(repositoryKey)
|
|
||||||
.then((repo) => {
|
|
||||||
return repo.addNote(newNote)
|
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
let { dispatch, location } = this.props
|
|
||||||
let { router } = this.context
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'ADD_NOTE',
|
type: 'CREATE_NOTE',
|
||||||
repository: repositoryKey,
|
|
||||||
note: note
|
note: note
|
||||||
})
|
})
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: location.pathname,
|
|
||||||
query: {
|
|
||||||
key: `${note._repository.key}-${note.key}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Commander.fire('note-detail:focus')
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTutorialButtonClick (e) {
|
handleTutorialButtonClick (e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLinksButton (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { config } = this.props
|
let { config } = this.props
|
||||||
return (
|
return (
|
||||||
@@ -124,7 +90,7 @@ class TopBar extends React.Component {
|
|||||||
onClick={(e) => this.handleNewPostButtonClick(e)}>
|
onClick={(e) => this.handleNewPostButtonClick(e)}>
|
||||||
<i className='fa fa-plus'/>
|
<i className='fa fa-plus'/>
|
||||||
<span styleName='left-control-newPostButton-tooltip'>
|
<span styleName='left-control-newPostButton-tooltip'>
|
||||||
New Post {OSX ? '⌘' : '^'} + n
|
New Note {OSX ? '⌘' : '^'} + n
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,11 +103,7 @@ class TopBar extends React.Component {
|
|||||||
>
|
>
|
||||||
?<span styleName='left-control-newPostButton-tooltip'>How to use</span>
|
?<span styleName='left-control-newPostButton-tooltip'>How to use</span>
|
||||||
</button>
|
</button>
|
||||||
<button styleName='right-linksButton'
|
|
||||||
onClick={(e) => this.handleLinksButton(e)}
|
|
||||||
>
|
|
||||||
<img src='../resources/app.png' width='44' height='44'/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
73
browser/main/global.styl
Normal file
73
browser/main/global.styl
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
global-reset()
|
||||||
|
|
||||||
|
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||||
|
|
||||||
|
html, body
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
|
body
|
||||||
|
font-family DEFAULT_FONTS
|
||||||
|
color textColor
|
||||||
|
font-size fontSize
|
||||||
|
font-weight 400
|
||||||
|
|
||||||
|
button, input, select, textarea
|
||||||
|
font-family DEFAULT_FONTS
|
||||||
|
|
||||||
|
div, span, a, button, input, textarea
|
||||||
|
box-sizing border-box
|
||||||
|
|
||||||
|
a
|
||||||
|
color $brand-color
|
||||||
|
&:hover
|
||||||
|
color lighten($brand-color, 5%)
|
||||||
|
&:visited
|
||||||
|
color $brand-color
|
||||||
|
|
||||||
|
hr
|
||||||
|
border-top none
|
||||||
|
border-bottom solid 1px $border-color
|
||||||
|
margin 15px 0
|
||||||
|
|
||||||
|
button
|
||||||
|
font-weight 400
|
||||||
|
cursor pointer
|
||||||
|
font-size 12px
|
||||||
|
&:focus, &.focus
|
||||||
|
outline none
|
||||||
|
|
||||||
|
.noSelect
|
||||||
|
noSelect()
|
||||||
|
|
||||||
|
.text-center
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
margin-bottom 15px
|
||||||
|
&>label
|
||||||
|
display block
|
||||||
|
margin-bottom 5px
|
||||||
|
|
||||||
|
textarea.block-input
|
||||||
|
resize vertical
|
||||||
|
height 125px
|
||||||
|
border-radius 5px
|
||||||
|
padding 5px 10px
|
||||||
|
|
||||||
|
#content
|
||||||
|
fullsize()
|
||||||
|
|
||||||
|
modalZIndex= 1000
|
||||||
|
modalBackColor = transparentify(white, 65%)
|
||||||
|
|
||||||
|
.ModalBase
|
||||||
|
fixed top left bottom right
|
||||||
|
z-index modalZIndex
|
||||||
|
&.hide
|
||||||
|
display none
|
||||||
|
.modalBack
|
||||||
|
absolute top left bottom right
|
||||||
|
background-color modalBackColor
|
||||||
|
z-index modalZIndex + 1
|
||||||
@@ -3,7 +3,7 @@ import Main from './Main'
|
|||||||
import store from './store'
|
import store from './store'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
require('!!style!css!stylus?sourceMap!../styles/main/index.styl')
|
require('!!style!css!stylus?sourceMap!./global.styl')
|
||||||
import activityRecord from 'browser/lib/activityRecord'
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
import fetchConfig from '../lib/fetchConfig'
|
import fetchConfig from '../lib/fetchConfig'
|
||||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||||
@@ -84,11 +84,10 @@ ReactDOM.render((
|
|||||||
<IndexRedirect to='/home'/>
|
<IndexRedirect to='/home'/>
|
||||||
<Route path='home'/>
|
<Route path='home'/>
|
||||||
<Route path='starred'/>
|
<Route path='starred'/>
|
||||||
<Route path='repositories'>
|
<Route path='storages'>
|
||||||
<IndexRedirect to='/home'/>
|
<IndexRedirect to='/home'/>
|
||||||
<Route path=':repositoryKey'>
|
<Route path=':storageKey'>
|
||||||
<IndexRoute/>
|
<IndexRoute/>
|
||||||
<Route path='settings'/>
|
|
||||||
<Route path='folders/:folderKey'/>
|
<Route path='folders/:folderKey'/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,9 +1,33 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
isSideNavFolded: false,
|
isSideNavFolded: false,
|
||||||
listWidth: 250
|
listWidth: 250,
|
||||||
|
hotkey: {
|
||||||
|
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
||||||
|
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
theme: 'default',
|
||||||
|
disableDirectWrite: false
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
theme: 'xcode',
|
||||||
|
fontSize: '14',
|
||||||
|
fontFamily: 'Monaco, Consolas',
|
||||||
|
indentType: 'space',
|
||||||
|
indentSize: '4',
|
||||||
|
switchPreview: 'RIGHTCLICK'
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
fontSize: '14',
|
||||||
|
fontFamily: 'Lato',
|
||||||
|
codeBlockTheme: 'xcode',
|
||||||
|
lineNumber: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate (config) {
|
function validate (config) {
|
||||||
@@ -16,6 +40,7 @@ function validate (config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _save (config) {
|
function _save (config) {
|
||||||
|
console.log(config)
|
||||||
window.localStorage.setItem('config', JSON.stringify(config))
|
window.localStorage.setItem('config', JSON.stringify(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +48,7 @@ function get () {
|
|||||||
let config = window.localStorage.getItem('config')
|
let config = window.localStorage.getItem('config')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
config = JSON.parse(config)
|
config = Object.assign({}, defaultConfig, JSON.parse(config))
|
||||||
if (!validate(config)) throw new Error('INVALID CONFIG')
|
if (!validate(config)) throw new Error('INVALID CONFIG')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Boostnote resets the malformed configuration.')
|
console.warn('Boostnote resets the malformed configuration.')
|
||||||
|
|||||||
380
browser/main/lib/dataApi.js
Normal file
380
browser/main/lib/dataApi.js
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
const keygen = require('browser/lib/keygen')
|
||||||
|
const CSON = require('season')
|
||||||
|
const path = require('path')
|
||||||
|
const _ = require('lodash')
|
||||||
|
const sander = require('sander')
|
||||||
|
|
||||||
|
let storages = []
|
||||||
|
let notes = []
|
||||||
|
|
||||||
|
let queuedTasks = []
|
||||||
|
|
||||||
|
function queueSaveFolder (storageKey, folderKey) {
|
||||||
|
let storage = _.find(storages, {key: storageKey})
|
||||||
|
if (storage == null) throw new Error('Failed to queue: Storage doesn\'t exist.')
|
||||||
|
|
||||||
|
let targetTasks = queuedTasks.filter((task) => task.storage === storageKey && task.folder === folderKey)
|
||||||
|
targetTasks.forEach((task) => {
|
||||||
|
clearTimeout(task.timer)
|
||||||
|
})
|
||||||
|
queuedTasks = queuedTasks.filter((task) => task.storage !== storageKey || task.folder !== folderKey)
|
||||||
|
|
||||||
|
let newTimer = setTimeout(() => {
|
||||||
|
let folderNotes = notes.filter((note) => note.storage === storageKey && note.folder === folderKey)
|
||||||
|
sander
|
||||||
|
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
|
||||||
|
notes: folderNotes
|
||||||
|
}))
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
queuedTasks.push({
|
||||||
|
storage: storageKey,
|
||||||
|
folder: folderKey,
|
||||||
|
timer: newTimer
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
constructor (cache) {
|
||||||
|
this.key = cache.key
|
||||||
|
this.cache = cache
|
||||||
|
}
|
||||||
|
|
||||||
|
loadJSONData () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let data = CSON.readFileSync(path.join(this.cache.path, 'boostnote.json'))
|
||||||
|
this.data = data
|
||||||
|
resolve(this)
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON () {
|
||||||
|
return Object.assign({}, this.cache, this.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
initStorage () {
|
||||||
|
return this.loadJSONData()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err.code)
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
let initialStorage = {
|
||||||
|
folders: []
|
||||||
|
}
|
||||||
|
return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage))
|
||||||
|
} else throw err
|
||||||
|
})
|
||||||
|
.then(() => this.loadJSONData())
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData () {
|
||||||
|
return sander
|
||||||
|
.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(this.data))
|
||||||
|
.then(() => this)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCache () {
|
||||||
|
_saveCaches()
|
||||||
|
}
|
||||||
|
|
||||||
|
static forge (cache) {
|
||||||
|
let instance = new this(cache)
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Note {
|
||||||
|
constructor (note) {
|
||||||
|
this.storage = note.storage
|
||||||
|
this.folder = note.folder
|
||||||
|
this.key = note.key
|
||||||
|
this.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
|
||||||
|
this.data = note
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON () {
|
||||||
|
return Object.assign({}, this.data, {
|
||||||
|
uniqueKey: this.uniqueKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
save () {
|
||||||
|
let storage = _.find(storages, {key: this.storage})
|
||||||
|
if (storage == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
|
||||||
|
let folder = _.find(storage.data.folders, {key: this.folder})
|
||||||
|
if (folder == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
|
||||||
|
|
||||||
|
// FS MUST BE MANIPULATED BY ASYNC METHOD
|
||||||
|
queueSaveFolder(storage.key, folder.key)
|
||||||
|
return Promise.resolve(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
static forge (note) {
|
||||||
|
let instance = new this(note)
|
||||||
|
|
||||||
|
return Promise.resolve(instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
let fetchStorages = function () {
|
||||||
|
let caches
|
||||||
|
try {
|
||||||
|
caches = JSON.parse(localStorage.getItem('storages'))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
caches = []
|
||||||
|
localStorage.getItem('storages', JSON.stringify(caches))
|
||||||
|
}
|
||||||
|
|
||||||
|
return caches.map((cache) => {
|
||||||
|
return Storage
|
||||||
|
.forge(cache)
|
||||||
|
.loadJSONData()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
console.error('Failed to load a storage JSON File: %s', cache)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchNotes = function (storages) {
|
||||||
|
let notes = []
|
||||||
|
let modifiedStorages = []
|
||||||
|
storages
|
||||||
|
.forEach((storage) => {
|
||||||
|
storage.data.folders.forEach((folder) => {
|
||||||
|
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = CSON.readFileSync(dataPath)
|
||||||
|
} catch (e) {
|
||||||
|
// Remove folder if fetching failed.
|
||||||
|
console.error('Failed to load data: %s', dataPath)
|
||||||
|
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
|
||||||
|
if (modifiedStorages.some((modified) => modified.key === storage.key)) modifiedStorages.push(storage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.notes.forEach((note) => {
|
||||||
|
note.storage = storage.key
|
||||||
|
note.folder = folder.key
|
||||||
|
notes.push(Note.forge(note))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
return Promise
|
||||||
|
.all(modifiedStorages.map((storage) => storage.saveData()))
|
||||||
|
.then(() => Promise.all(notes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(fetchStorages())
|
||||||
|
.then((_storages) => {
|
||||||
|
storages = _storages.filter((storage) => {
|
||||||
|
if (!_.isObject(storage)) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
_saveCaches()
|
||||||
|
|
||||||
|
return storages
|
||||||
|
})
|
||||||
|
.then(fetchNotes)
|
||||||
|
.then((_notes) => {
|
||||||
|
notes = _notes
|
||||||
|
return {
|
||||||
|
storages: storages.map((storage) => storage.toJSON()),
|
||||||
|
notes: notes.map((note) => note.toJSON())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function _saveCaches () {
|
||||||
|
localStorage.setItem('storages', JSON.stringify(storages.map((storage) => storage.cache)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStorage (input) {
|
||||||
|
if (!_.isString(input.path) || !input.path.match(/^\//)) {
|
||||||
|
return Promise.reject(new Error('Path must be absolute.'))
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = keygen()
|
||||||
|
while (storages.some((storage) => storage.key === key)) {
|
||||||
|
key = keygen()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage
|
||||||
|
.forge({
|
||||||
|
name: input.name,
|
||||||
|
key: key,
|
||||||
|
type: input.type,
|
||||||
|
path: input.path
|
||||||
|
})
|
||||||
|
.initStorage()
|
||||||
|
.then((storage) => {
|
||||||
|
let _notes = []
|
||||||
|
let isFolderRemoved = false
|
||||||
|
storage.data.folders.forEach((folder) => {
|
||||||
|
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = CSON.readFileSync(dataPath)
|
||||||
|
} catch (e) {
|
||||||
|
// Remove folder if fetching failed.
|
||||||
|
console.error('Failed to load data: %s', dataPath)
|
||||||
|
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
|
||||||
|
isFolderRemoved = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
data.notes.forEach((note) => {
|
||||||
|
note.storage = storage.key
|
||||||
|
note.folder = folder.key
|
||||||
|
_notes.push(Note.forge(note))
|
||||||
|
})
|
||||||
|
|
||||||
|
notes = notes.slice().concat(_notes)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(notes)
|
||||||
|
.then((notes) => {
|
||||||
|
let data = {
|
||||||
|
storage: storage,
|
||||||
|
notes: notes
|
||||||
|
}
|
||||||
|
return isFolderRemoved
|
||||||
|
? storage.saveData().then(() => data)
|
||||||
|
: data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
storages = storages.filter((storage) => storage.key !== data.storage.key)
|
||||||
|
storages.push(data.storage)
|
||||||
|
_saveCaches()
|
||||||
|
return {
|
||||||
|
storage: data.storage.toJSON(),
|
||||||
|
notes: data.notes.map((note) => note.toJSON())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStorage (key) {
|
||||||
|
storages = storages.filter((storage) => storage.key !== key)
|
||||||
|
_saveCaches()
|
||||||
|
notes = notes.filter((note) => note.storage !== key)
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolder (key, input) {
|
||||||
|
let storage = _.find(storages, {key: key})
|
||||||
|
if (storage == null) throw new Error('Storage doesn\'t exist.')
|
||||||
|
|
||||||
|
let folderKey = keygen()
|
||||||
|
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
|
||||||
|
folderKey = keygen()
|
||||||
|
}
|
||||||
|
|
||||||
|
let newFolder = {
|
||||||
|
key: folderKey,
|
||||||
|
name: input.name,
|
||||||
|
color: input.color
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultData = {notes: []}
|
||||||
|
// FS MUST BE MANIPULATED BY ASYNC METHOD
|
||||||
|
return sander
|
||||||
|
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify(defaultData))
|
||||||
|
.then(() => {
|
||||||
|
storage.data.folders.push(newFolder)
|
||||||
|
return storage
|
||||||
|
.saveData()
|
||||||
|
.then((storage) => storage.toJSON())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFolder (storageKey, folderKey, input) {
|
||||||
|
let storage = _.find(storages, {key: storageKey})
|
||||||
|
if (storage == null) throw new Error('Storage doesn\'t exist.')
|
||||||
|
let folder = _.find(storage.data.folders, {key: folderKey})
|
||||||
|
folder.color = input.color
|
||||||
|
folder.name = input.name
|
||||||
|
|
||||||
|
return storage
|
||||||
|
.saveData()
|
||||||
|
.then((storage) => storage.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFolder (storageKey, folderKey) {
|
||||||
|
let storage = _.find(storages, {key: storageKey})
|
||||||
|
if (storage == null) throw new Error('Storage doesn\'t exist.')
|
||||||
|
storage.data.folders = storage.data.folders.filter((folder) => folder.key !== folderKey)
|
||||||
|
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey)
|
||||||
|
|
||||||
|
// FS MUST BE MANIPULATED BY ASYNC METHOD
|
||||||
|
return sander
|
||||||
|
.rimraf(path.join(storage.cache.path, folderKey))
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.code === 'ENOENT') return true
|
||||||
|
else throw err
|
||||||
|
})
|
||||||
|
.then(() => storage.saveData())
|
||||||
|
.then((storage) => storage.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNote (storageKey, folderKey, input) {
|
||||||
|
let key = keygen()
|
||||||
|
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
|
||||||
|
key = keygen()
|
||||||
|
}
|
||||||
|
|
||||||
|
let newNote = new Note(Object.assign({
|
||||||
|
type: 'MARKDOWN_NOTE',
|
||||||
|
tags: [],
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
}, input, {
|
||||||
|
storage: storageKey,
|
||||||
|
folder: folderKey,
|
||||||
|
key: key,
|
||||||
|
isStarred: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}))
|
||||||
|
notes.push(newNote)
|
||||||
|
|
||||||
|
return newNote
|
||||||
|
.save()
|
||||||
|
.then(() => newNote.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNote (storageKey, folderKey, noteKey, input) {
|
||||||
|
let note = _.find(notes, {
|
||||||
|
key: noteKey,
|
||||||
|
storage: storageKey,
|
||||||
|
folder: folderKey
|
||||||
|
})
|
||||||
|
note.data.title = input.title
|
||||||
|
note.data.tags = input.tags
|
||||||
|
note.data.content = input.content
|
||||||
|
note.data.updatedAt = input.updatedAt
|
||||||
|
|
||||||
|
return note.save()
|
||||||
|
.then(() => note.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNote (storageKey, folderKey, noteKey, input) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
init,
|
||||||
|
addStorage,
|
||||||
|
removeStorage,
|
||||||
|
createFolder,
|
||||||
|
updateFolder,
|
||||||
|
removeFolder,
|
||||||
|
createNote,
|
||||||
|
updateNote,
|
||||||
|
removeNote
|
||||||
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import Repository from 'browser/lib/Repository'
|
import Storage from 'browser/lib/Storage'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
let tasks = []
|
let tasks = []
|
||||||
|
|
||||||
function _save (task, repoKey, note) {
|
function _save (task, storageKey, note) {
|
||||||
note = Object.assign({}, note)
|
note = Object.assign({}, note)
|
||||||
delete note._repository
|
delete note._storage
|
||||||
|
|
||||||
task.status = 'process'
|
task.status = 'process'
|
||||||
|
|
||||||
Repository
|
Storage
|
||||||
.find(repoKey)
|
.find(storageKey)
|
||||||
.then((repo) => {
|
.then((storage) => {
|
||||||
return repo.updateNote(note.key, note)
|
return storage.updateNote(note.key, note)
|
||||||
})
|
})
|
||||||
.then((note) => {
|
.then((note) => {
|
||||||
tasks.splice(tasks.indexOf(task), 1)
|
tasks.splice(tasks.indexOf(task), 1)
|
||||||
@@ -25,8 +25,8 @@ function _save (task, repoKey, note) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const queueSaving = function (repoKey, note) {
|
const queueSaving = function (storageKey, note) {
|
||||||
let key = `${repoKey}-${note.key}`
|
let key = `${storageKey}-${note.key}`
|
||||||
|
|
||||||
let taskIndex = _.findIndex(tasks, {
|
let taskIndex = _.findIndex(tasks, {
|
||||||
type: 'SAVE_NOTE',
|
type: 'SAVE_NOTE',
|
||||||
@@ -47,7 +47,7 @@ const queueSaving = function (repoKey, note) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
task.timer = window.setTimeout(() => {
|
task.timer = window.setTimeout(() => {
|
||||||
_save(task, repoKey, note)
|
_save(task, storageKey, note)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
tasks.push(task)
|
tasks.push(task)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import fetchConfig from 'browser/lib/fetchConfig'
|
|
||||||
import hljsTheme from 'browser/lib/hljsThemes'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
|
||||||
const ipc = electron.ipcRenderer
|
|
||||||
const remote = electron.remote
|
|
||||||
const ace = window.ace
|
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
|
||||||
|
|
||||||
export default class AppSettingTab extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
let keymap = Object.assign({}, remote.getGlobal('keymap'))
|
|
||||||
let config = Object.assign({}, fetchConfig())
|
|
||||||
let userName = props.user != null ? props.user.name : null
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
user: {
|
|
||||||
name: userName,
|
|
||||||
alert: null
|
|
||||||
},
|
|
||||||
userAlert: null,
|
|
||||||
keymap: keymap,
|
|
||||||
keymapAlert: null,
|
|
||||||
config: config,
|
|
||||||
configAlert: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.handleSettingDone = () => {
|
|
||||||
this.setState({keymapAlert: {
|
|
||||||
type: 'success',
|
|
||||||
message: 'Successfully done!'
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
this.handleSettingError = (err) => {
|
|
||||||
this.setState({keymapAlert: {
|
|
||||||
type: 'error',
|
|
||||||
message: err.message != null ? err.message : 'Error occurs!'
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
|
||||||
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
|
|
||||||
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
|
|
||||||
}
|
|
||||||
|
|
||||||
submitHotKey () {
|
|
||||||
ipc.send('hotkeyUpdated', this.state.keymap)
|
|
||||||
}
|
|
||||||
|
|
||||||
submitConfig () {
|
|
||||||
ipc.send('configUpdated', this.state.config)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveButtonClick (e) {
|
|
||||||
this.submitHotKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConfigSaveButtonClick (e) {
|
|
||||||
this.submitConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown (e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
this.submitHotKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConfigKeyDown (e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
this.submitConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLineNumberingClick (e) {
|
|
||||||
let config = this.state.config
|
|
||||||
|
|
||||||
config['preview-line-number'] = e.target.checked
|
|
||||||
this.setState({
|
|
||||||
config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDisableDirectWriteClick (e) {
|
|
||||||
let config = this.state.config
|
|
||||||
config['disable-direct-write'] = e.target.checked
|
|
||||||
this.setState({
|
|
||||||
config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let keymapAlert = this.state.keymapAlert
|
|
||||||
let keymapAlertElement = keymapAlert != null
|
|
||||||
? <p className={`alert ${keymapAlert.type}`}>
|
|
||||||
{keymapAlert.message}
|
|
||||||
</p>
|
|
||||||
: null
|
|
||||||
let aceThemeList = ace.require('ace/ext/themelist')
|
|
||||||
let hljsThemeList = hljsTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='AppSettingTab content'>
|
|
||||||
<div className='section'>
|
|
||||||
<div className='sectionTitle'>Editor</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Editor Font Size</label>
|
|
||||||
<input valueLink={this.linkState('config.editor-font-size')} onKeyDown={(e) => this.handleConfigKeyDown(e)} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Editor Font Family</label>
|
|
||||||
<input valueLink={this.linkState('config.editor-font-family')} onKeyDown={(e) => this.handleConfigKeyDown(e)} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionMultiSelect'>
|
|
||||||
<label>Editor Indent Style</label>
|
|
||||||
<div className='sectionMultiSelect-input'>
|
|
||||||
type
|
|
||||||
<select valueLink={this.linkState('config.editor-indent-type')}>
|
|
||||||
<option value='space'>Space</option>
|
|
||||||
<option value='tab'>Tab</option>
|
|
||||||
</select>
|
|
||||||
size
|
|
||||||
<select valueLink={this.linkState('config.editor-indent-size')}>
|
|
||||||
<option value='2'>2</option>
|
|
||||||
<option value='4'>4</option>
|
|
||||||
<option value='8'>8</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='sectionTitle'>Preview</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Preview Font Size</label>
|
|
||||||
<input valueLink={this.linkState('config.preview-font-size')} onKeyDown={(e) => this.handleConfigKeyDown(e)} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Preview Font Family</label>
|
|
||||||
<input valueLink={this.linkState('config.preview-font-family')} onKeyDown={(e) => this.handleConfigKeyDown(e)} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionSelect'>
|
|
||||||
<label>Switching Preview</label>
|
|
||||||
<select valueLink={this.linkState('config.switch-preview')}>
|
|
||||||
<option value='blur'>When Editor Blurred</option>
|
|
||||||
<option value='rightclick'>When Right Clicking</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='sectionCheck'>
|
|
||||||
<label><input onChange={e => this.handleLineNumberingClick(e)} checked={this.state.config['preview-line-number']} type='checkbox'/>Code block line numbering</label>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
global.process.platform === 'win32'
|
|
||||||
? (
|
|
||||||
<div className='sectionCheck'>
|
|
||||||
<label><input onChange={e => this.handleDisableDirectWriteClick(e)} checked={this.state.config['disable-direct-write']} disabled={OSX} type='checkbox'/>Disable Direct Write<span className='sectionCheck-warn'>It will be applied after restarting</span></label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<div className='sectionTitle'>Theme</div>
|
|
||||||
<div className='sectionSelect'>
|
|
||||||
<label>UI Theme</label>
|
|
||||||
<select valueLink={this.linkState('config.theme-ui')}>
|
|
||||||
<option value='light'>Light</option>
|
|
||||||
<option value='dark'>Dark</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='sectionSelect'>
|
|
||||||
<label>Code block Theme</label>
|
|
||||||
<select valueLink={this.linkState('config.theme-code')}>
|
|
||||||
{
|
|
||||||
hljsThemeList.map((theme) => {
|
|
||||||
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='sectionSelect'>
|
|
||||||
<label>Editor Theme</label>
|
|
||||||
<select valueLink={this.linkState('config.theme-syntax')}>
|
|
||||||
{
|
|
||||||
aceThemeList.themes.map((theme) => {
|
|
||||||
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='sectionConfirm'>
|
|
||||||
<button onClick={(e) => this.handleConfigSaveButtonClick(e)}>Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='section'>
|
|
||||||
<div className='sectionTitle'>Hotkey</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Toggle Main</label>
|
|
||||||
<input onKeyDown={(e) => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleMain')} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Toggle Finder(popup)</label>
|
|
||||||
<input onKeyDown={(e) => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionConfirm'>
|
|
||||||
<button onClick={(e) => this.handleSaveButtonClick(e)}>Save</button>
|
|
||||||
{keymapAlertElement}
|
|
||||||
</div>
|
|
||||||
<div className='description'>
|
|
||||||
<ul>
|
|
||||||
<li><code>0</code> to <code>9</code></li>
|
|
||||||
<li><code>A</code> to <code>Z</code></li>
|
|
||||||
<li><code>F1</code> to <code>F24</code></li>
|
|
||||||
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
|
|
||||||
<li><code>Plus</code></li>
|
|
||||||
<li><code>Space</code></li>
|
|
||||||
<li><code>Backspace</code></li>
|
|
||||||
<li><code>Delete</code></li>
|
|
||||||
<li><code>Insert</code></li>
|
|
||||||
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
|
|
||||||
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
|
|
||||||
<li><code>Home</code> and <code>End</code></li>
|
|
||||||
<li><code>PageUp</code> and <code>PageDown</code></li>
|
|
||||||
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
|
|
||||||
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
|
|
||||||
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppSettingTab.propTypes = {
|
|
||||||
user: PropTypes.shape({
|
|
||||||
name: PropTypes.string
|
|
||||||
}),
|
|
||||||
dispatch: PropTypes.func
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
|
|
||||||
export default class ContactTab extends React.Component {
|
|
||||||
componentDidMount () {
|
|
||||||
let titleInput = ReactDOM.findDOMNode(this.refs.title)
|
|
||||||
if (titleInput != null) titleInput.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='ContactTab content'>
|
|
||||||
<div className='title'>Contact</div>
|
|
||||||
<p>
|
|
||||||
- Issues: <a href='https://github.com/BoostIO/Boostnote/issues'>https://github.com/BoostIO/Boostnote/issues</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import AppSettingTab from './AppSettingTab'
|
|
||||||
import ContactTab from './ContactTab'
|
|
||||||
import { closeModal } from 'browser/main/lib/modal'
|
|
||||||
|
|
||||||
const APP = 'APP'
|
|
||||||
const CONTACT = 'CONTACT'
|
|
||||||
|
|
||||||
class Preferences extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
currentTab: APP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switchTeam (teamId) {
|
|
||||||
this.setState({currentTeamId: teamId})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNavButtonClick (tab) {
|
|
||||||
return (e) => {
|
|
||||||
this.setState({currentTab: tab})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let content = this.renderContent()
|
|
||||||
|
|
||||||
let tabs = [
|
|
||||||
{target: APP, label: 'Preferences'},
|
|
||||||
{target: CONTACT, label: 'Contact'}
|
|
||||||
]
|
|
||||||
|
|
||||||
let navButtons = tabs.map((tab) => (
|
|
||||||
<button key={tab.target} onClick={(e) => this.handleNavButtonClick(tab.target)(e)} className={this.state.currentTab === tab.target ? 'active' : ''}>{tab.label}</button>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='Preferences modal'>
|
|
||||||
<div className='header'>
|
|
||||||
<div className='title'>Setting</div>
|
|
||||||
<button onClick={(e) => closeModal()} className='closeBtn'>Done</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='nav'>
|
|
||||||
{navButtons}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent () {
|
|
||||||
let { user, dispatch } = this.props
|
|
||||||
|
|
||||||
switch (this.state.currentTab) {
|
|
||||||
case CONTACT:
|
|
||||||
return (
|
|
||||||
<ContactTab/>
|
|
||||||
)
|
|
||||||
case APP:
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<AppSettingTab
|
|
||||||
user={user}
|
|
||||||
dispatch={dispatch}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Preferences.propTypes = {
|
|
||||||
user: PropTypes.shape({
|
|
||||||
name: PropTypes.string
|
|
||||||
}),
|
|
||||||
dispatch: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect((x) => x)(Preferences)
|
|
||||||
416
browser/main/modals/PreferencesModal/ConfigTab.js
Normal file
416
browser/main/modals/PreferencesModal/ConfigTab.js
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './ConfigTab.styl'
|
||||||
|
import fetchConfig from 'browser/lib/fetchConfig'
|
||||||
|
import hljsTheme from 'browser/lib/hljsThemes'
|
||||||
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
import store from 'browser/main/store'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
const remote = electron.remote
|
||||||
|
const ace = window.ace
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
class ConfigTab extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isHotkeyHintOpen: false,
|
||||||
|
config: props.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.handleSettingDone = () => {
|
||||||
|
this.setState({keymapAlert: {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Successfully done!'
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
this.handleSettingError = (err) => {
|
||||||
|
this.setState({keymapAlert: {
|
||||||
|
type: 'error',
|
||||||
|
message: err.message != null ? err.message : 'Error occurs!'
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||||
|
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||||
|
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitHotKey () {
|
||||||
|
ipc.send('hotkeyUpdated', this.state.keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitConfig () {
|
||||||
|
ipc.send('configUpdated', this.state.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveButtonClick (e) {
|
||||||
|
this.submitHotKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigSaveButtonClick (e) {
|
||||||
|
this.submitConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.submitHotKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigKeyDown (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.submitConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLineNumberingClick (e) {
|
||||||
|
let config = this.state.config
|
||||||
|
|
||||||
|
config['preview-line-number'] = e.target.checked
|
||||||
|
this.setState({
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisableDirectWriteClick (e) {
|
||||||
|
let config = this.state.config
|
||||||
|
config['disable-direct-write'] = e.target.checked
|
||||||
|
this.setState({
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHintToggleButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
isHotkeyHintOpen: !this.state.isHotkeyHintOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHotkeyChange (e) {
|
||||||
|
let { config } = this.state
|
||||||
|
config.hotkey = {
|
||||||
|
toggleFinder: this.refs.toggleFinder.value,
|
||||||
|
toggleMain: this.refs.toggleMain.value
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUIChange (e) {
|
||||||
|
let { config } = this.state
|
||||||
|
|
||||||
|
config.ui = {
|
||||||
|
theme: this.refs.uiTheme.value,
|
||||||
|
disableDirectWrite: this.refs.uiD2w != null
|
||||||
|
? this.refs.uiD2w.checked
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
config.editor = {
|
||||||
|
theme: this.refs.editorTheme.value,
|
||||||
|
fontSize: this.refs.editorFontSize.value,
|
||||||
|
fontFamily: this.refs.editorFontFamily.value,
|
||||||
|
indentType: this.refs.editorIndentType.value,
|
||||||
|
indentSize: this.refs.editorIndentSize.value,
|
||||||
|
switchPreview: this.refs.editorSwitchPreview.value
|
||||||
|
}
|
||||||
|
config.preview = {
|
||||||
|
fontSize: this.refs.previewFontSize.value,
|
||||||
|
fontFamily: this.refs.previewFontFamily.value,
|
||||||
|
codeBlockTheme: this.refs.previewCodeBlockTheme.value,
|
||||||
|
lineNumber: this.refs.previewLineNumber.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveUIClick (e) {
|
||||||
|
let newConfig = {
|
||||||
|
ui: this.state.config.ui,
|
||||||
|
editor: this.state.config.editor,
|
||||||
|
preview: this.state.config.preview
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigManager.set(newConfig)
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: 'SET_UI',
|
||||||
|
config: newConfig
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let keymapAlert = this.state.keymapAlert
|
||||||
|
let keymapAlertElement = keymapAlert != null
|
||||||
|
? <p className={`alert ${keymapAlert.type}`}>
|
||||||
|
{keymapAlert.message}
|
||||||
|
</p>
|
||||||
|
: null
|
||||||
|
let aceThemeList = ace.require('ace/ext/themelist')
|
||||||
|
let hljsThemeList = hljsTheme()
|
||||||
|
let { config } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='group'>
|
||||||
|
<div styleName='group-header'>Hotkey</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>Toggle Main</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
onChange={(e) => this.handleHotkeyChange(e)}
|
||||||
|
ref='toggleMain'
|
||||||
|
value={config.hotkey.toggleMain}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>Toggle Finder(popup)</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
onChange={(e) => this.handleHotkeyChange(e)}
|
||||||
|
ref='toggleFinder'
|
||||||
|
value={config.hotkey.toggleFinder}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-control'>
|
||||||
|
<button styleName='group-control-leftButton'
|
||||||
|
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||||
|
>
|
||||||
|
{this.state.isHotkeyHintOpen
|
||||||
|
? 'Hide Hint'
|
||||||
|
: 'Show Hint'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<button styleName='group-control-rightButton'
|
||||||
|
onClick={(e) => this.handleSaveButtonClick(e)}>Save Hotkey
|
||||||
|
</button>
|
||||||
|
{keymapAlertElement}
|
||||||
|
</div>
|
||||||
|
{this.state.isHotkeyHintOpen &&
|
||||||
|
<div styleName='group-hint'>
|
||||||
|
<p>Available Keys</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>0</code> to <code>9</code></li>
|
||||||
|
<li><code>A</code> to <code>Z</code></li>
|
||||||
|
<li><code>F1</code> to <code>F24</code></li>
|
||||||
|
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
|
||||||
|
<li><code>Plus</code></li>
|
||||||
|
<li><code>Space</code></li>
|
||||||
|
<li><code>Backspace</code></li>
|
||||||
|
<li><code>Delete</code></li>
|
||||||
|
<li><code>Insert</code></li>
|
||||||
|
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
|
||||||
|
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
|
||||||
|
<li><code>Home</code> and <code>End</code></li>
|
||||||
|
<li><code>PageUp</code> and <code>PageDown</code></li>
|
||||||
|
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
|
||||||
|
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
|
||||||
|
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='group'>
|
||||||
|
<div styleName='group-header'>UI</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>Theme</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select value={config.ui.theme}
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
ref='uiTheme'
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<option value='default'>Light</option>
|
||||||
|
<option value='dark'>Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
global.process.platform === 'win32'
|
||||||
|
? <div styleName='group-checkBoxSection'>
|
||||||
|
<label>
|
||||||
|
<input onChange={(e) => this.handleUIChange(e)}
|
||||||
|
checked={this.state.config.ui.disableDirectWrite}
|
||||||
|
refs='uiD2w'
|
||||||
|
disabled={OSX}
|
||||||
|
type='checkbox'
|
||||||
|
/>
|
||||||
|
Disable Direct Write(It will be applied after restarting)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<div styleName='group-header2'>Editor</div>
|
||||||
|
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Editor Theme
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select value={config.editor.theme}
|
||||||
|
ref='editorTheme'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
aceThemeList.themes.map((theme) => {
|
||||||
|
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Editor Font Size
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
ref='editorFontSize'
|
||||||
|
value={config.editor.fontSize}
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Editor Font Family
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
ref='editorFontFamily'
|
||||||
|
value={config.editor.fontFamily}
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Editor Indent Style
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select value={config.editor.indentSize}
|
||||||
|
ref='editorIndentSize'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
>
|
||||||
|
<option value='1'>1</option>
|
||||||
|
<option value='2'>2</option>
|
||||||
|
<option value='4'>4</option>
|
||||||
|
<option value='8'>8</option>
|
||||||
|
</select>
|
||||||
|
<select value={config.editor.indentType}
|
||||||
|
ref='editorIndentType'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
>
|
||||||
|
<option value='space'>Spaces</option>
|
||||||
|
<option value='tab'>Tabs</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Switching Preview
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select value={config.editor.switchPreview}
|
||||||
|
ref='editorSwitchPreview'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
>
|
||||||
|
<option value='BLUR'>When Editor Blurred</option>
|
||||||
|
<option value='RIGHTCLICK'>When Right Clicking</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-header2'>Preview</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Preview Font Size
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
value={config.preview.fontSize}
|
||||||
|
ref='previewFontSize'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>
|
||||||
|
Preview Font Family
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<input styleName='group-section-control-input'
|
||||||
|
ref='previewFontFamily'
|
||||||
|
value={config.preview.fontFamily}
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
type='text'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-section'>
|
||||||
|
<div styleName='group-section-label'>Code block Theme</div>
|
||||||
|
<div styleName='group-section-control'>
|
||||||
|
<select value={config.preview.codeBlockTheme}
|
||||||
|
ref='previewCodeBlockTheme'
|
||||||
|
onChange={(e) => this.handleUIChange(e)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
hljsThemeList.map((theme) => {
|
||||||
|
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='group-checkBoxSection'>
|
||||||
|
<label>
|
||||||
|
<input onChange={(e) => this.handleUIChange(e)}
|
||||||
|
checked={this.state.config.preview.lineNumber}
|
||||||
|
ref='previewLineNumber'
|
||||||
|
type='checkbox'
|
||||||
|
/>
|
||||||
|
Code block line numbering
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='group-control'>
|
||||||
|
<button styleName='group-control-rightButton'
|
||||||
|
onClick={(e) => this.handleSaveUIClick(e)}
|
||||||
|
>
|
||||||
|
Save UI Config
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigTab.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
}),
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(ConfigTab, styles)
|
||||||
80
browser/main/modals/PreferencesModal/ConfigTab.styl
Normal file
80
browser/main/modals/PreferencesModal/ConfigTab.styl
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
.root
|
||||||
|
padding 15px
|
||||||
|
color $ui-text-color
|
||||||
|
.group
|
||||||
|
margin-bottom 45px
|
||||||
|
.group-header
|
||||||
|
font-size 24px
|
||||||
|
color $ui-text-color
|
||||||
|
padding 5px
|
||||||
|
border-bottom $default-border
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.group-header2
|
||||||
|
font-size 18px
|
||||||
|
color $ui-text-color
|
||||||
|
padding 5px
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.group-section
|
||||||
|
margin-bottom 15px
|
||||||
|
display flex
|
||||||
|
line-height 30px
|
||||||
|
.group-section-label
|
||||||
|
width 150px
|
||||||
|
text-align right
|
||||||
|
margin-right 10px
|
||||||
|
.group-section-control
|
||||||
|
flex 1
|
||||||
|
.group-section-control-input
|
||||||
|
height 30px
|
||||||
|
vertical-align middle
|
||||||
|
width 150px
|
||||||
|
font-size 12px
|
||||||
|
border solid 1px $border-color
|
||||||
|
border-radius 2px
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
|
.group-checkBoxSection
|
||||||
|
margin-bottom 15px
|
||||||
|
display flex
|
||||||
|
line-height 30px
|
||||||
|
padding-left 15px
|
||||||
|
|
||||||
|
.group-control
|
||||||
|
border-top $default-border
|
||||||
|
padding-top 10px
|
||||||
|
box-sizing border-box
|
||||||
|
height 40px
|
||||||
|
text-align right
|
||||||
|
.group-control-leftButton
|
||||||
|
float left
|
||||||
|
colorDefaultButton()
|
||||||
|
border $default-border
|
||||||
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
|
padding 0 15px
|
||||||
|
margin-right 5px
|
||||||
|
.group-control-rightButton
|
||||||
|
float right
|
||||||
|
colorPrimaryButton()
|
||||||
|
border none
|
||||||
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
|
padding 0 15px
|
||||||
|
margin-right 5px
|
||||||
|
.group-hint
|
||||||
|
border $ui-border
|
||||||
|
padding 10px 15px
|
||||||
|
margin 15px 0
|
||||||
|
border-radius 5px
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
ul
|
||||||
|
list-style inherit
|
||||||
|
padding-left 1em
|
||||||
|
line-height 1.2
|
||||||
|
p
|
||||||
|
line-height 1.2
|
||||||
|
|
||||||
|
|
||||||
39
browser/main/modals/PreferencesModal/InfoTab.js
Normal file
39
browser/main/modals/PreferencesModal/InfoTab.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './InfoTab.styl'
|
||||||
|
|
||||||
|
const appVersion = global.process.version
|
||||||
|
|
||||||
|
class InfoTab extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='top'>
|
||||||
|
<img styleName='icon' src='../resources/app.png' width='150' height='150'/>
|
||||||
|
<div styleName='appId'>Boostnote {appVersion}</div>
|
||||||
|
<div styleName='madeBy'>Made by MAISIN&CO.</div>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
- License : GPLv3
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
- Issue Tracker : <a href='https://github.com/BoostIO/Boostnote/issues'>https://github.com/BoostIO/Boostnote/issues</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoTab.propTypes = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(InfoTab, styles)
|
||||||
13
browser/main/modals/PreferencesModal/InfoTab.styl
Normal file
13
browser/main/modals/PreferencesModal/InfoTab.styl
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.root
|
||||||
|
padding 15px
|
||||||
|
white-space pre
|
||||||
|
line-height 1.4
|
||||||
|
color $ui-text-color
|
||||||
|
.top
|
||||||
|
text-align center
|
||||||
|
margin-bottom 25px
|
||||||
|
.appId
|
||||||
|
font-size 18px
|
||||||
|
.madeBy
|
||||||
|
font-size 12px
|
||||||
|
$ui-inactive-text-color
|
||||||
37
browser/main/modals/PreferencesModal/PreferencesModal.styl
Normal file
37
browser/main/modals/PreferencesModal/PreferencesModal.styl
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
.root
|
||||||
|
modal()
|
||||||
|
max-width 540px
|
||||||
|
min-height 400px
|
||||||
|
height 80%
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.nav
|
||||||
|
absolute top left right
|
||||||
|
height 50px
|
||||||
|
background-color $ui-backgroundColor
|
||||||
|
border-bottom solid 1px $ui-borderColor
|
||||||
|
|
||||||
|
.nav-button
|
||||||
|
width 80px
|
||||||
|
height 50px
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
color #939395
|
||||||
|
font-size 14px
|
||||||
|
&:hover
|
||||||
|
color #515151
|
||||||
|
|
||||||
|
.nav-button--active
|
||||||
|
@extend .nav-button
|
||||||
|
color #6AA5E9
|
||||||
|
&:hover
|
||||||
|
color #6AA5E9
|
||||||
|
|
||||||
|
.nav-button-icon
|
||||||
|
display block
|
||||||
|
|
||||||
|
.content
|
||||||
|
absolute left right bottom
|
||||||
|
top 50px
|
||||||
|
overflow-y auto
|
||||||
299
browser/main/modals/PreferencesModal/StorageItem.js
Normal file
299
browser/main/modals/PreferencesModal/StorageItem.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './StorageItem.styl'
|
||||||
|
import consts from 'browser/lib/consts'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import store from 'browser/main/store'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const { shell, remote } = electron
|
||||||
|
const { Menu, MenuItem } = remote
|
||||||
|
|
||||||
|
class UnstyledFolderItem extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
status: 'IDLE',
|
||||||
|
folder: {
|
||||||
|
color: props.color,
|
||||||
|
name: props.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditChange (e) {
|
||||||
|
let { folder } = this.state
|
||||||
|
|
||||||
|
folder.name = this.refs.nameInput.value
|
||||||
|
this.setState({
|
||||||
|
folder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirmButtonClick (e) {
|
||||||
|
let { storage, folder } = this.props
|
||||||
|
dataApi
|
||||||
|
.updateFolder(storage.key, folder.key, {
|
||||||
|
color: this.state.folder.color,
|
||||||
|
name: this.state.folder.name
|
||||||
|
})
|
||||||
|
.then((storage) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UPDATE_STORAGE',
|
||||||
|
storage: storage
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
status: 'IDLE'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleColorButtonClick (e) {
|
||||||
|
var menu = new Menu()
|
||||||
|
|
||||||
|
consts.FOLDER_COLORS.forEach((color, index) => {
|
||||||
|
menu.append(new MenuItem({
|
||||||
|
label: consts.FOLDER_COLOR_NAMES[index],
|
||||||
|
click: (e) => {
|
||||||
|
let { folder } = this.state
|
||||||
|
folder.color = color
|
||||||
|
this.setState({
|
||||||
|
folder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.popup(remote.getCurrentWindow())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
status: 'IDLE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEdit (e) {
|
||||||
|
return (
|
||||||
|
<div styleName='folderList-item'>
|
||||||
|
<div styleName='folderList-item-left'>
|
||||||
|
<button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}}
|
||||||
|
onClick={(e) => this.handleColorButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-square'/>
|
||||||
|
</button>
|
||||||
|
<input styleName='folderList-item-left-nameInput'
|
||||||
|
value={this.state.folder.name}
|
||||||
|
ref='nameInput'
|
||||||
|
onChange={(e) => this.handleEditChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div styleName='folderList-item-right'>
|
||||||
|
<button styleName='folderList-item-right-confirmButton'
|
||||||
|
onClick={(e) => this.handleConfirmButtonClick(e)}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button styleName='folderList-item-right-button'
|
||||||
|
onClick={(e) => this.handleCancelButtonClick(e)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteConfirmButtonClick (e) {
|
||||||
|
let { storage, folder } = this.props
|
||||||
|
dataApi
|
||||||
|
.removeFolder(storage.key, folder.key)
|
||||||
|
.then((storage) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'REMOVE_FOLDER',
|
||||||
|
key: folder.key,
|
||||||
|
storage: storage
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDelete () {
|
||||||
|
return (
|
||||||
|
<div styleName='folderList-item'>
|
||||||
|
<div styleName='folderList-item-left'>
|
||||||
|
Are you sure to <span styleName='folderList-item-left-danger'>delete</span> this folder?
|
||||||
|
</div>
|
||||||
|
<div styleName='folderList-item-right'>
|
||||||
|
<button styleName='folderList-item-right-dangerButton'
|
||||||
|
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button styleName='folderList-item-right-button'
|
||||||
|
onClick={(e) => this.handleCancelButtonClick(e)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditButtonClick (e) {
|
||||||
|
let { folder } = this.props
|
||||||
|
this.setState({
|
||||||
|
status: 'EDIT',
|
||||||
|
folder: {
|
||||||
|
color: folder.color,
|
||||||
|
name: folder.name
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
this.refs.nameInput.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
status: 'DELETE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIdle () {
|
||||||
|
let { folder } = this.props
|
||||||
|
return (
|
||||||
|
<div styleName='folderList-item'
|
||||||
|
onDoubleClick={(e) => this.handleEditButtonClick(e)}
|
||||||
|
>
|
||||||
|
<div styleName='folderList-item-left'
|
||||||
|
style={{borderColor: folder.color}}
|
||||||
|
>
|
||||||
|
<span styleName='folderList-item-left-name'>{folder.name}</span>
|
||||||
|
<span styleName='folderList-item-left-key'>({folder.key})</span>
|
||||||
|
</div>
|
||||||
|
<div styleName='folderList-item-right'>
|
||||||
|
<button styleName='folderList-item-right-button'
|
||||||
|
onClick={(e) => this.handleEditButtonClick(e)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button styleName='folderList-item-right-button'
|
||||||
|
onClick={(e) => this.handleDeleteButtonClick(e)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
switch (this.state.status) {
|
||||||
|
case 'DELETE':
|
||||||
|
return this.renderDelete()
|
||||||
|
case 'EDIT':
|
||||||
|
return this.renderEdit()
|
||||||
|
case 'IDLE':
|
||||||
|
default:
|
||||||
|
return this.renderIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderItem = CSSModules(UnstyledFolderItem, styles)
|
||||||
|
|
||||||
|
class StorageItem extends React.Component {
|
||||||
|
handleNewFolderButtonClick (e) {
|
||||||
|
let { storage } = this.props
|
||||||
|
let input = {
|
||||||
|
name: 'Untitled',
|
||||||
|
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7)]
|
||||||
|
}
|
||||||
|
|
||||||
|
dataApi.createFolder(storage.key, input)
|
||||||
|
.then((storage) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'ADD_FOLDER',
|
||||||
|
storage: storage
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExternalButtonClick () {
|
||||||
|
let { storage } = this.props
|
||||||
|
shell.showItemInFolder(storage.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnlinkButtonClick (e) {
|
||||||
|
let { storage } = this.props
|
||||||
|
dataApi.removeStorage(storage.key)
|
||||||
|
.then(() => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'REMOVE_STORAGE',
|
||||||
|
key: storage.key
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { storage } = this.props
|
||||||
|
let folderList = storage.folders.map((folder) => {
|
||||||
|
return <FolderItem key={folder.key}
|
||||||
|
folder={folder}
|
||||||
|
storage={storage}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div styleName='root' key={storage.key}>
|
||||||
|
<div styleName='header'>
|
||||||
|
<i className='fa fa-folder-open'/>
|
||||||
|
{storage.name}
|
||||||
|
<span styleName='header-path'>({storage.path})</span>
|
||||||
|
<div styleName='header-control'>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
onClick={(e) => this.handleNewFolderButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-plus'/>
|
||||||
|
</button>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
onClick={(e) => this.handleExternalButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-external-link'/>
|
||||||
|
</button>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
onClick={(e) => this.handleUnlinkButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-unlink'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div styleName='folderList'>
|
||||||
|
{folderList.length > 0
|
||||||
|
? folderList
|
||||||
|
: <div styleName='folderList-empty'>No Folders</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageItem.propTypes = {
|
||||||
|
storage: PropTypes.shape({
|
||||||
|
key: PropTypes.string
|
||||||
|
}),
|
||||||
|
folder: PropTypes.shape({
|
||||||
|
key: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
name: PropTypes.string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(StorageItem, styles)
|
||||||
97
browser/main/modals/PreferencesModal/StorageItem.styl
Normal file
97
browser/main/modals/PreferencesModal/StorageItem.styl
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.root
|
||||||
|
position relative
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.header
|
||||||
|
height 35px
|
||||||
|
line-height 30px
|
||||||
|
padding 0 10px 5px
|
||||||
|
box-sizing border-box
|
||||||
|
border-bottom $default-border
|
||||||
|
margin-bottom 5px
|
||||||
|
|
||||||
|
.header-path
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
font-size 10px
|
||||||
|
margin 0 5px
|
||||||
|
|
||||||
|
.header-control
|
||||||
|
float right
|
||||||
|
|
||||||
|
.header-control-button
|
||||||
|
width 30px
|
||||||
|
height 25px
|
||||||
|
colorDefaultButton()
|
||||||
|
border-radius 2px
|
||||||
|
border $ui-border
|
||||||
|
margin-right 5px
|
||||||
|
&:last-child
|
||||||
|
margin-right 0
|
||||||
|
|
||||||
|
.folderList-item
|
||||||
|
height 35px
|
||||||
|
box-sizing border-box
|
||||||
|
padding 2.5px 15px
|
||||||
|
&:hover
|
||||||
|
background-color darken(white, 3%)
|
||||||
|
.folderList-item-left
|
||||||
|
height 30px
|
||||||
|
border-left solid 6px transparent
|
||||||
|
padding 0 10px
|
||||||
|
line-height 30px
|
||||||
|
float left
|
||||||
|
.folderList-item-left-danger
|
||||||
|
color $danger-color
|
||||||
|
font-weight bold
|
||||||
|
|
||||||
|
.folderList-item-left-key
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
font-size 10px
|
||||||
|
margin 0 5px
|
||||||
|
border none
|
||||||
|
|
||||||
|
.folderList-item-left-colorButton
|
||||||
|
colorDefaultButton()
|
||||||
|
height 25px
|
||||||
|
width 25px
|
||||||
|
line-height 23px
|
||||||
|
padding 0
|
||||||
|
box-sizing border-box
|
||||||
|
vertical-align middle
|
||||||
|
border $ui-border
|
||||||
|
border-radius 2px
|
||||||
|
margin-right 5px
|
||||||
|
margin-left -15px
|
||||||
|
|
||||||
|
.folderList-item-left-nameInput
|
||||||
|
height 25px
|
||||||
|
box-sizing border-box
|
||||||
|
vertical-align middle
|
||||||
|
border $ui-border
|
||||||
|
border-radius 2px
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
|
.folderList-item-right
|
||||||
|
float right
|
||||||
|
|
||||||
|
.folderList-item-right-button
|
||||||
|
vertical-align middle
|
||||||
|
height 25px
|
||||||
|
margin-top 2.5px
|
||||||
|
colorDefaultButton()
|
||||||
|
border-radius 2px
|
||||||
|
border $ui-border
|
||||||
|
margin-right 5px
|
||||||
|
padding 0 5px
|
||||||
|
&:last-child
|
||||||
|
margin-right 0
|
||||||
|
|
||||||
|
.folderList-item-right-confirmButton
|
||||||
|
@extend .folderList-item-right-button
|
||||||
|
border none
|
||||||
|
colorPrimaryButton()
|
||||||
|
|
||||||
|
.folderList-item-right-dangerButton
|
||||||
|
@extend .folderList-item-right-button
|
||||||
|
border none
|
||||||
|
colorDangerButton()
|
||||||
223
browser/main/modals/PreferencesModal/StoragesTab.js
Normal file
223
browser/main/modals/PreferencesModal/StoragesTab.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './StoragesTab.styl'
|
||||||
|
import dataApi from 'browser/main/lib/dataApi'
|
||||||
|
import StorageItem from './StorageItem'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
function browseFolder () {
|
||||||
|
let dialog = remote.dialog
|
||||||
|
|
||||||
|
let defaultPath = remote.app.getPath('home')
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dialog.showOpenDialog({
|
||||||
|
title: 'Select Directory',
|
||||||
|
defaultPath,
|
||||||
|
properties: ['openDirectory', 'createDirectory']
|
||||||
|
}, function (targetPaths) {
|
||||||
|
if (targetPaths == null) return resolve('')
|
||||||
|
resolve(targetPaths[0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoragesTab extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
page: 'LIST',
|
||||||
|
newStorage: {
|
||||||
|
name: 'Unnamed',
|
||||||
|
type: 'FILESYSTEM',
|
||||||
|
path: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddStorageButton (e) {
|
||||||
|
this.setState({
|
||||||
|
page: 'ADD_STORAGE',
|
||||||
|
newStorage: {
|
||||||
|
name: 'Unnamed',
|
||||||
|
type: 'FILESYSTEM',
|
||||||
|
path: ''
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
this.refs.addStorageName.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList () {
|
||||||
|
let { storages } = this.props
|
||||||
|
|
||||||
|
let storageList = storages.map((storage) => {
|
||||||
|
return <StorageItem
|
||||||
|
key={storage.key}
|
||||||
|
storage={storage}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div styleName='list'>
|
||||||
|
{storageList.length > 0
|
||||||
|
? storageList
|
||||||
|
: <div styleName='list-empty'>No storage found.</div>
|
||||||
|
}
|
||||||
|
<div styleName='list-control'>
|
||||||
|
<button styleName='list-control-addStorageButton'
|
||||||
|
onClick={(e) => this.handleAddStorageButton(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-plus'/> Add Storage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddStorageBrowseButtonClick (e) {
|
||||||
|
browseFolder()
|
||||||
|
.then((targetPath) => {
|
||||||
|
if (targetPath.length > 0) {
|
||||||
|
let { newStorage } = this.state
|
||||||
|
newStorage.path = targetPath
|
||||||
|
this.setState({
|
||||||
|
newStorage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('BrowseFAILED')
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddStorageChange (e) {
|
||||||
|
let { newStorage } = this.state
|
||||||
|
newStorage.name = this.refs.addStorageName.value
|
||||||
|
newStorage.path = this.refs.addStoragePath.value
|
||||||
|
this.setState({
|
||||||
|
newStorage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddStorageCreateButton (e) {
|
||||||
|
dataApi
|
||||||
|
.addStorage({
|
||||||
|
name: this.state.newStorage.name,
|
||||||
|
path: this.state.newStorage.path
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
dispatch({
|
||||||
|
type: 'ADD_STORAGE',
|
||||||
|
storage: data.storage,
|
||||||
|
notes: data.notes
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
page: 'LIST'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddStorageCancelButton (e) {
|
||||||
|
this.setState({
|
||||||
|
page: 'LIST'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAddStorage () {
|
||||||
|
return (
|
||||||
|
<div styleName='addStorage'>
|
||||||
|
|
||||||
|
<div styleName='addStorage-header'>Add Storage</div>
|
||||||
|
|
||||||
|
<div styleName='addStorage-body'>
|
||||||
|
|
||||||
|
<div styleName='addStorage-body-section'>
|
||||||
|
<div styleName='addStorage-body-section-label'>
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<div styleName='addStorage-body-section-name'>
|
||||||
|
<input styleName='addStorage-body-section-name-input'
|
||||||
|
ref='addStorageName'
|
||||||
|
value={this.state.newStorage.name}
|
||||||
|
onChange={(e) => this.handleAddStorageChange(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='addStorage-body-section'>
|
||||||
|
<div styleName='addStorage-body-section-label'>Type</div>
|
||||||
|
<div styleName='addStorage-body-section-type'>
|
||||||
|
<select styleName='addStorage-body-section-type-select'
|
||||||
|
value={this.state.newStorage.type}
|
||||||
|
readOnly
|
||||||
|
>
|
||||||
|
<option value='FILESYSTEM'>File System</option>
|
||||||
|
</select>
|
||||||
|
<div styleName='addStorage-body-section-type-description'>
|
||||||
|
3rd party cloud integration(such as Google Drive and Dropbox) will be available soon.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='addStorage-body-section'>
|
||||||
|
<div styleName='addStorage-body-section-label'>Location
|
||||||
|
</div>
|
||||||
|
<div styleName='addStorage-body-section-path'>
|
||||||
|
<input styleName='addStorage-body-section-path-input'
|
||||||
|
ref='addStoragePath'
|
||||||
|
placeholder='Select Folder'
|
||||||
|
value={this.state.newStorage.path}
|
||||||
|
onChange={(e) => this.handleAddStorageChange(e)}
|
||||||
|
/>
|
||||||
|
<button styleName='addStorage-body-section-path-button'
|
||||||
|
onClick={(e) => this.handleAddStorageBrowseButtonClick(e)}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='addStorage-body-control'>
|
||||||
|
<button styleName='addStorage-body-control-createButton'
|
||||||
|
onClick={(e) => this.handleAddStorageCreateButton(e)}
|
||||||
|
>Create</button>
|
||||||
|
<button styleName='addStorage-body-control-cancelButton'
|
||||||
|
onClick={(e) => this.handleAddStorageCancelButton(e)}
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent () {
|
||||||
|
switch (this.state.page) {
|
||||||
|
case 'ADD_STORAGE':
|
||||||
|
case 'ADD_FOLDER':
|
||||||
|
return this.renderAddStorage()
|
||||||
|
case 'LIST':
|
||||||
|
default:
|
||||||
|
return this.renderList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
{this.renderContent()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StoragesTab.propTypes = {
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(StoragesTab, styles)
|
||||||
115
browser/main/modals/PreferencesModal/StoragesTab.styl
Normal file
115
browser/main/modals/PreferencesModal/StoragesTab.styl
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
.root
|
||||||
|
padding 15px
|
||||||
|
color $ui-text-color
|
||||||
|
|
||||||
|
.list
|
||||||
|
margin-bottom 15px
|
||||||
|
font-size 14px
|
||||||
|
|
||||||
|
.folderList
|
||||||
|
padding 0 15px
|
||||||
|
|
||||||
|
.folderList-item
|
||||||
|
height 30px
|
||||||
|
line-height 30px
|
||||||
|
border-bottom $ui-border
|
||||||
|
|
||||||
|
.folderList-empty
|
||||||
|
height 30px
|
||||||
|
line-height 30px
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
|
||||||
|
.list-empty
|
||||||
|
height 30px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
.list-control
|
||||||
|
height 30px
|
||||||
|
.list-control-addStorageButton
|
||||||
|
height 30px
|
||||||
|
padding 0 15px
|
||||||
|
border $ui-border
|
||||||
|
colorDefaultButton()
|
||||||
|
border-radius 2px
|
||||||
|
|
||||||
|
.addStorage
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.addStorage-header
|
||||||
|
font-size 24px
|
||||||
|
color $ui-text-color
|
||||||
|
padding 5px
|
||||||
|
border-bottom $default-border
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.addStorage-body-section
|
||||||
|
margin-bottom 15px
|
||||||
|
display flex
|
||||||
|
line-height 30px
|
||||||
|
|
||||||
|
.addStorage-body-section-label
|
||||||
|
width 150px
|
||||||
|
text-align right
|
||||||
|
margin-right 10px
|
||||||
|
|
||||||
|
.addStorage-body-section-name
|
||||||
|
flex 1
|
||||||
|
.addStorage-body-section-name-input
|
||||||
|
height 30px
|
||||||
|
vertical-align middle
|
||||||
|
width 150px
|
||||||
|
font-size 12px
|
||||||
|
border solid 1px $border-color
|
||||||
|
border-radius 2px
|
||||||
|
padding 0 5px
|
||||||
|
|
||||||
|
.addStorage-body-section-type
|
||||||
|
flex 1
|
||||||
|
.addStorage-body-section-type-select
|
||||||
|
height 30px
|
||||||
|
.addStorage-body-section-type-description
|
||||||
|
margin 5px
|
||||||
|
font-size 12px
|
||||||
|
color $ui-inactive-text-color
|
||||||
|
line-height 16px
|
||||||
|
|
||||||
|
.addStorage-body-section-path
|
||||||
|
flex 1
|
||||||
|
.addStorage-body-section-path-input
|
||||||
|
height 30px
|
||||||
|
vertical-align middle
|
||||||
|
width 150px
|
||||||
|
font-size 12px
|
||||||
|
border-style solid
|
||||||
|
border-width 1px 0 1px 1px
|
||||||
|
border-color $border-color
|
||||||
|
border-top-left-radius 2px
|
||||||
|
border-bottom-left-radius 2px
|
||||||
|
padding 0 5px
|
||||||
|
.addStorage-body-section-path-button
|
||||||
|
height 30px
|
||||||
|
border none
|
||||||
|
border-top-right-radius 2px
|
||||||
|
border-bottom-right-radius 2px
|
||||||
|
colorPrimaryButton()
|
||||||
|
vertical-align middle
|
||||||
|
.addStorage-body-control
|
||||||
|
border-top $default-border
|
||||||
|
padding-top 10px
|
||||||
|
box-sizing border-box
|
||||||
|
height 40px
|
||||||
|
text-align right
|
||||||
|
|
||||||
|
.addStorage-body-control-createButton
|
||||||
|
colorPrimaryButton()
|
||||||
|
border none
|
||||||
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
|
padding 0 15px
|
||||||
|
margin-right 5px
|
||||||
|
.addStorage-body-control-cancelButton
|
||||||
|
colorDefaultButton()
|
||||||
|
border $default-border
|
||||||
|
border-radius 2px
|
||||||
|
height 30px
|
||||||
|
padding 0 15px
|
||||||
98
browser/main/modals/PreferencesModal/index.js
Normal file
98
browser/main/modals/PreferencesModal/index.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import ConfigTab from './ConfigTab'
|
||||||
|
import InfoTab from './InfoTab'
|
||||||
|
import StoragesTab from './StoragesTab'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './PreferencesModal.styl'
|
||||||
|
|
||||||
|
class Preferences extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
currentTab: 'STORAGES'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTeam (teamId) {
|
||||||
|
this.setState({currentTeamId: teamId})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNavButtonClick (tab) {
|
||||||
|
return (e) => {
|
||||||
|
this.setState({currentTab: tab})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent () {
|
||||||
|
let { dispatch, config, storages } = this.props
|
||||||
|
|
||||||
|
switch (this.state.currentTab) {
|
||||||
|
case 'INFO':
|
||||||
|
return <InfoTab/>
|
||||||
|
case 'CONFIG':
|
||||||
|
return (
|
||||||
|
<ConfigTab
|
||||||
|
dispatch={dispatch}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'STORAGES':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<StoragesTab
|
||||||
|
dispatch={dispatch}
|
||||||
|
storages={storages}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let content = this.renderContent()
|
||||||
|
|
||||||
|
let tabs = [
|
||||||
|
{target: 'STORAGES', label: 'Storages', icon: 'database'},
|
||||||
|
{target: 'CONFIG', label: 'Config', icon: 'cogs'},
|
||||||
|
{target: 'INFO', label: 'Info', icon: 'info-circle'}
|
||||||
|
]
|
||||||
|
|
||||||
|
let navButtons = tabs.map((tab) => {
|
||||||
|
let isActive = this.state.currentTab === tab.target
|
||||||
|
return (
|
||||||
|
<button styleName={isActive
|
||||||
|
? 'nav-button--active'
|
||||||
|
: 'nav-button'
|
||||||
|
}
|
||||||
|
key={tab.target}
|
||||||
|
onClick={(e) => this.handleNavButtonClick(tab.target)(e)}
|
||||||
|
>
|
||||||
|
<i styleName='nav-button-icon'
|
||||||
|
className={'fa fa-' + tab.icon}
|
||||||
|
/>
|
||||||
|
<span styleName='nav-button-label'>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div styleName='root'>
|
||||||
|
<div styleName='nav'>
|
||||||
|
{navButtons}
|
||||||
|
</div>
|
||||||
|
<div styleName='content'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Preferences.propTypes = {
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect((x) => x)(CSSModules(Preferences, styles))
|
||||||
@@ -1,176 +1,83 @@
|
|||||||
import { combineReducers, createStore } from 'redux'
|
import { combineReducers, createStore } from 'redux'
|
||||||
import _ from 'lodash'
|
|
||||||
import { routerReducer } from 'react-router-redux'
|
import { routerReducer } from 'react-router-redux'
|
||||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||||
|
|
||||||
/**
|
function storages (state = [], action) {
|
||||||
* Repositories
|
|
||||||
* ```
|
|
||||||
* repositories = [{
|
|
||||||
* key: String,
|
|
||||||
* name: String,
|
|
||||||
* path: String, // path of repository
|
|
||||||
* status: String, // status of repository [IDLE, LOADING, READY, ERROR]
|
|
||||||
* folders: {
|
|
||||||
* name: String,
|
|
||||||
* color: String
|
|
||||||
* },
|
|
||||||
* notes: [{
|
|
||||||
* key: String,
|
|
||||||
* title: String,
|
|
||||||
* content: String,
|
|
||||||
* folder: String,
|
|
||||||
* tags: [String],
|
|
||||||
* createdAt: Date,
|
|
||||||
* updatedAt: Date
|
|
||||||
* }]
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
const initialRepositories = []
|
|
||||||
|
|
||||||
function repositories (state = initialRepositories, action) {
|
|
||||||
console.info('REDUX >> ', action)
|
console.info('REDUX >> ', action)
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'INIT_ALL':
|
case 'INIT_ALL':
|
||||||
action.data.forEach((repo) => {
|
return action.storages
|
||||||
repo.notes.forEach((note) => {
|
case 'ADD_STORAGE':
|
||||||
note._repository = repo
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return action.data.slice()
|
|
||||||
case 'ADD_REPOSITORY':
|
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let storages = state.slice()
|
||||||
|
|
||||||
repos.push(action.repository)
|
storages.push(action.storage)
|
||||||
|
|
||||||
return repos
|
return storages
|
||||||
}
|
|
||||||
case 'REMOVE_REPOSITORY':
|
|
||||||
{
|
|
||||||
let repos = state.slice()
|
|
||||||
|
|
||||||
let targetIndex = _.findIndex(repos, {key: action.key})
|
|
||||||
if (targetIndex > -1) {
|
|
||||||
repos.splice(targetIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
}
|
||||||
case 'ADD_FOLDER':
|
case 'ADD_FOLDER':
|
||||||
|
case 'REMOVE_FOLDER':
|
||||||
|
case 'UPDATE_STORAGE':
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let storages = state.slice()
|
||||||
let targetRepo = _.find(repos, {key: action.key})
|
storages = storages
|
||||||
|
.filter((storage) => storage.key !== action.storage.key)
|
||||||
|
storages.push(action.storage)
|
||||||
|
|
||||||
if (targetRepo == null) return state
|
return storages
|
||||||
|
|
||||||
let targetFolderIndex = _.findIndex(targetRepo.folders, {key: action.folder.key})
|
|
||||||
if (targetFolderIndex < 0) {
|
|
||||||
targetRepo.folders.push(action.folder)
|
|
||||||
} else {
|
|
||||||
targetRepo.folders.splice(targetFolderIndex, 1, action.folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
}
|
||||||
case 'EDIT_FOLDER':
|
case 'REMOVE_STORAGE':
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let storages = state.slice()
|
||||||
let targetRepo = _.find(repos, {key: action.key})
|
storages = storages
|
||||||
|
.filter((storage) => storage.key !== action.key)
|
||||||
|
|
||||||
if (targetRepo == null) return state
|
return storages
|
||||||
|
}
|
||||||
let targetFolderIndex = _.findIndex(targetRepo.folders, {key: action.folder.key})
|
}
|
||||||
if (targetFolderIndex < 0) {
|
return state
|
||||||
targetRepo.folders.push(action.folder)
|
}
|
||||||
} else {
|
|
||||||
targetRepo.folders.splice(targetFolderIndex, 1, action.folder)
|
function notes (state = [], action) {
|
||||||
}
|
switch (action.type) {
|
||||||
|
case 'INIT_ALL':
|
||||||
return repos
|
return action.notes
|
||||||
|
case 'ADD_STORAGE':
|
||||||
|
{
|
||||||
|
let notes = state.slice()
|
||||||
|
|
||||||
|
notes.concat(action.notes)
|
||||||
|
|
||||||
|
return notes
|
||||||
|
}
|
||||||
|
case 'REMOVE_STORAGE':
|
||||||
|
{
|
||||||
|
let notes = state.slice()
|
||||||
|
notes = notes
|
||||||
|
.filter((note) => note.storage !== action.key)
|
||||||
|
|
||||||
|
return notes
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Remove a folder from the repository
|
|
||||||
* {
|
|
||||||
* type: 'REMOVE_FOLDER',
|
|
||||||
* repository: repositoryKey,
|
|
||||||
* folder: folderKey
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
case 'REMOVE_FOLDER':
|
case 'REMOVE_FOLDER':
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let notes = state.slice()
|
||||||
let targetRepo = _.find(repos, {key: action.repository})
|
notes = notes
|
||||||
|
.filter((note) => note.storage !== action.storage.key || note.folder !== action.key)
|
||||||
|
|
||||||
if (targetRepo == null) return state
|
return notes
|
||||||
|
|
||||||
let targetFolderIndex = _.findIndex(targetRepo.folders, {key: action.folder})
|
|
||||||
if (targetFolderIndex > -1) {
|
|
||||||
targetRepo.folders.splice(targetFolderIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
}
|
||||||
case 'ADD_NOTE':
|
case 'CREATE_NOTE':
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let notes = state.slice()
|
||||||
let targetRepo = _.find(repos, {key: action.repository})
|
notes.push(action.note)
|
||||||
|
return notes
|
||||||
if (targetRepo == null) return state
|
|
||||||
action.note._repository = targetRepo
|
|
||||||
targetRepo.notes.push(action.note)
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
}
|
||||||
case 'SAVE_NOTE':
|
case 'UPDATE_NOTE':
|
||||||
{
|
{
|
||||||
let repos = state.slice()
|
let notes = state.slice()
|
||||||
let targetRepo = _.find(repos, {key: action.repository})
|
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
|
||||||
|
notes.push(action.note)
|
||||||
if (targetRepo == null) return state
|
return notes
|
||||||
|
|
||||||
let targetNoteIndex = _.findIndex(targetRepo.notes, {key: action.note.key})
|
|
||||||
action.note.updatedAt = Date.now()
|
|
||||||
action.note._repository = targetRepo
|
|
||||||
|
|
||||||
if (targetNoteIndex > -1) {
|
|
||||||
targetRepo.notes.splice(targetNoteIndex, 1, action.note)
|
|
||||||
} else {
|
|
||||||
targetRepo.notes.push(action.note)
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
|
||||||
case 'STAR_NOTE':
|
|
||||||
{
|
|
||||||
let repos = state.slice()
|
|
||||||
let targetRepo = _.find(repos, {key: action.repository})
|
|
||||||
|
|
||||||
if (targetRepo == null) return state
|
|
||||||
|
|
||||||
let targetNoteIndex = _.findIndex(targetRepo.notes, {key: action.note})
|
|
||||||
if (targetNoteIndex > -1) {
|
|
||||||
targetRepo.starred.push(action.note)
|
|
||||||
targetRepo.starred = _.uniq(targetRepo.starred)
|
|
||||||
} else {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
|
||||||
case 'UNSTAR_NOTE':
|
|
||||||
{
|
|
||||||
let repos = state.slice()
|
|
||||||
let targetRepo = _.find(repos, {key: action.repository})
|
|
||||||
|
|
||||||
if (targetRepo == null) return state
|
|
||||||
|
|
||||||
targetRepo.starred = targetRepo.starred
|
|
||||||
.filter((starredKey) => starredKey !== action.note)
|
|
||||||
targetRepo.starred = _.uniq(targetRepo.starred)
|
|
||||||
|
|
||||||
return repos
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
@@ -191,12 +98,15 @@ function config (state = defaultConfig, action) {
|
|||||||
return Object.assign({}, state)
|
return Object.assign({}, state)
|
||||||
case 'SET_CONFIG':
|
case 'SET_CONFIG':
|
||||||
return Object.assign({}, state, action.config)
|
return Object.assign({}, state, action.config)
|
||||||
|
case 'SET_UI':
|
||||||
|
return Object.assign({}, state, action.config)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
let reducer = combineReducers({
|
let reducer = combineReducers({
|
||||||
repositories,
|
storages,
|
||||||
|
notes,
|
||||||
config,
|
config,
|
||||||
routing: routerReducer
|
routing: routerReducer
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
$brand-color = #6AA5E9
|
$brand-color = #6AA5E9
|
||||||
|
|
||||||
$danger-color = red
|
$danger-color = #c9302c
|
||||||
$danger-lighten-color = #FFE5E6
|
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
$statusBar-height = 24px
|
$statusBar-height = 24px
|
||||||
@@ -65,16 +65,32 @@ colorDefaultButton()
|
|||||||
// Primary button(Brand color)
|
// Primary button(Brand color)
|
||||||
$primary-button-background = $brand-color
|
$primary-button-background = $brand-color
|
||||||
$primary-button-background--hover = darken($brand-color, 5%)
|
$primary-button-background--hover = darken($brand-color, 5%)
|
||||||
$primary-button-background--hover = darken($brand-color, 10%)
|
$primary-button-background--active = darken($brand-color, 10%)
|
||||||
|
|
||||||
colorPrimaryButton()
|
colorPrimaryButton()
|
||||||
|
color white
|
||||||
background-color $primary-button-background
|
background-color $primary-button-background
|
||||||
&:hover
|
&:hover
|
||||||
background-color $primary-button-background--hover
|
background-color $primary-button-background--hover
|
||||||
&:active
|
&:active
|
||||||
background-color $primary-button-background--active
|
background-color $primary-button-background--active
|
||||||
&:active:hover
|
&:active:hover
|
||||||
background-color $primary-button-background--active
|
background-color $primary-button-background--activ
|
||||||
|
|
||||||
|
// Danger button(Brand color)
|
||||||
|
$danger-button-background = #c9302c
|
||||||
|
$danger-button-background--hover = darken(#c9302c, 5%)
|
||||||
|
$danger-button-background--active = darken(#c9302c, 10%)
|
||||||
|
|
||||||
|
colorDangerButton()
|
||||||
|
color white
|
||||||
|
background-color $danger-button-background
|
||||||
|
&:hover
|
||||||
|
background-color $danger-button-background--hover
|
||||||
|
&:active
|
||||||
|
background-color $danger-button-background--active
|
||||||
|
&:active:hover
|
||||||
|
background-color $danger-button-background--active
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nav
|
* Nav
|
||||||
@@ -101,7 +117,7 @@ navButtonColor()
|
|||||||
|
|
||||||
$modal-z-index = 1002
|
$modal-z-index = 1002
|
||||||
$modal-background = white
|
$modal-background = white
|
||||||
$modal-margin = 64px auto 0
|
$modal-margin = 64px auto 64px
|
||||||
$modal-border-radius = 5px
|
$modal-border-radius = 5px
|
||||||
|
|
||||||
modal()
|
modal()
|
||||||
@@ -111,3 +127,4 @@ modal()
|
|||||||
background-color $modal-background
|
background-color $modal-background
|
||||||
overflow hidden
|
overflow hidden
|
||||||
border-radius $modal-border-radius
|
border-radius $modal-border-radius
|
||||||
|
box-shadow 2px 2px 10px gray
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
@import '../vars'
|
@import '../vars'
|
||||||
@import '../mixins/*'
|
@import '../mixins/*'
|
||||||
global-reset()
|
global-reset()
|
||||||
@import '../shared/*'
|
|
||||||
@import './modal/*'
|
|
||||||
@import '../theme/*'
|
@import '../theme/*'
|
||||||
|
|
||||||
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||||
@@ -86,3 +84,26 @@ textarea.block-input
|
|||||||
|
|
||||||
#content
|
#content
|
||||||
fullsize()
|
fullsize()
|
||||||
|
|
||||||
|
modalZIndex= 1000
|
||||||
|
modalBackColor = transparentify(black, 65%)
|
||||||
|
|
||||||
|
.ModalBase
|
||||||
|
fixed top left bottom right
|
||||||
|
z-index modalZIndex
|
||||||
|
&.hide
|
||||||
|
display none
|
||||||
|
.modalBack
|
||||||
|
absolute top left bottom right
|
||||||
|
background-color modalBackColor
|
||||||
|
z-index modalZIndex + 1
|
||||||
|
.modal
|
||||||
|
position relative
|
||||||
|
width 650px
|
||||||
|
margin 50px auto 0
|
||||||
|
z-index modalZIndex + 2
|
||||||
|
background-color white
|
||||||
|
padding 15px
|
||||||
|
color #666666
|
||||||
|
border-radius 5px
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
.DeleteArticleModal.modal
|
|
||||||
width 350px !important
|
|
||||||
top 100px
|
|
||||||
user-select none
|
|
||||||
.title
|
|
||||||
font-size 24px
|
|
||||||
margin-bottom 15px
|
|
||||||
.message
|
|
||||||
font-size 14px
|
|
||||||
margin-bottom 15px
|
|
||||||
.control
|
|
||||||
text-align right
|
|
||||||
button
|
|
||||||
border-radius 5px
|
|
||||||
height 33px
|
|
||||||
padding 0 15px
|
|
||||||
font-size 14px
|
|
||||||
background-color white
|
|
||||||
border 1px solid borderColor
|
|
||||||
border-radius 5px
|
|
||||||
margin-left 5px
|
|
||||||
&:hover
|
|
||||||
background-color darken(white, 10%)
|
|
||||||
&:focus
|
|
||||||
border-color focusBorderColor
|
|
||||||
&.danger
|
|
||||||
border-color #E9432A
|
|
||||||
background-color #E9432A
|
|
||||||
color white
|
|
||||||
&:hover
|
|
||||||
background-color lighten(#E9432A, 15%)
|
|
||||||
&:focus
|
|
||||||
background-color lighten(#E9432A, 15%)
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
menuColor = #808080
|
|
||||||
menuBgColor = #E6E6E6
|
|
||||||
closeBtnBgColor = #1790C6
|
|
||||||
iptFocusBorderColor = #369DCD
|
|
||||||
|
|
||||||
.Preferences.modal
|
|
||||||
padding 0
|
|
||||||
border-radius 5px
|
|
||||||
overflow hidden
|
|
||||||
width 720px
|
|
||||||
height 600px
|
|
||||||
&>.header
|
|
||||||
absolute top left right
|
|
||||||
height 50px
|
|
||||||
border-bottom 1px solid borderColor
|
|
||||||
background-color menuBgColor
|
|
||||||
&>.title
|
|
||||||
font-size 22px
|
|
||||||
font-weight bold
|
|
||||||
float left
|
|
||||||
padding-left 30px
|
|
||||||
line-height 50px
|
|
||||||
&>.closeBtn
|
|
||||||
float right
|
|
||||||
font-size 14px
|
|
||||||
background-color closeBtnBgColor
|
|
||||||
color white
|
|
||||||
padding 0 15px
|
|
||||||
height 33px
|
|
||||||
margin-top 9px
|
|
||||||
margin-right 15px
|
|
||||||
border none
|
|
||||||
border-radius 5px
|
|
||||||
&>.nav
|
|
||||||
absolute left bottom
|
|
||||||
top 50px
|
|
||||||
width 180px
|
|
||||||
background-color menuBgColor
|
|
||||||
border-right 1px solid borderColor
|
|
||||||
&>button
|
|
||||||
width 100%
|
|
||||||
height 44px
|
|
||||||
font-size 18px
|
|
||||||
color menuColor
|
|
||||||
border none
|
|
||||||
background-color transparent
|
|
||||||
transition 0.1s
|
|
||||||
text-align left
|
|
||||||
padding-left 15px
|
|
||||||
&:hover
|
|
||||||
background-color darken(menuBgColor, 10%)
|
|
||||||
&.active, &:active
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
&>.content
|
|
||||||
absolute right bottom
|
|
||||||
top 50px
|
|
||||||
left 180px
|
|
||||||
overflow-y auto
|
|
||||||
&>.section
|
|
||||||
padding 10px 20px
|
|
||||||
border-bottom 1px solid borderColor
|
|
||||||
overflow-y auto
|
|
||||||
&:nth-last-child(1)
|
|
||||||
border-bottom none
|
|
||||||
&>.sectionTitle
|
|
||||||
font-size 18px
|
|
||||||
margin 10px 0 5px
|
|
||||||
color brandColor
|
|
||||||
&>.sectionCheck
|
|
||||||
margin-bottom 5px
|
|
||||||
height 33px
|
|
||||||
label
|
|
||||||
width 150px
|
|
||||||
padding-left 15px
|
|
||||||
line-height 33px
|
|
||||||
.sectionCheck-warn
|
|
||||||
font-size 12px
|
|
||||||
margin-left 10px
|
|
||||||
border-left 2px solid brandColor
|
|
||||||
padding-left 5px
|
|
||||||
&>.sectionInput
|
|
||||||
margin-bottom 5px
|
|
||||||
clearfix()
|
|
||||||
height 33px
|
|
||||||
label
|
|
||||||
width 150px
|
|
||||||
padding-left 15px
|
|
||||||
float left
|
|
||||||
line-height 33px
|
|
||||||
input
|
|
||||||
width 250px
|
|
||||||
float left
|
|
||||||
height 33px
|
|
||||||
border-radius 5px
|
|
||||||
border 1px solid borderColor
|
|
||||||
padding 0 10px
|
|
||||||
font-size 14px
|
|
||||||
outline none
|
|
||||||
&:focus
|
|
||||||
border-color iptFocusBorderColor
|
|
||||||
&>.sectionSelect
|
|
||||||
margin-bottom 5px
|
|
||||||
clearfix()
|
|
||||||
height 33px
|
|
||||||
label
|
|
||||||
width 150px
|
|
||||||
padding-left 15px
|
|
||||||
float left
|
|
||||||
line-height 33px
|
|
||||||
select
|
|
||||||
float left
|
|
||||||
width 200px
|
|
||||||
height 25px
|
|
||||||
margin-top 4px
|
|
||||||
border-radius 5px
|
|
||||||
border 1px solid borderColor
|
|
||||||
padding 0 10px
|
|
||||||
font-size 14px
|
|
||||||
outline none
|
|
||||||
&:focus
|
|
||||||
border-color iptFocusBorderColor
|
|
||||||
&>.sectionMultiSelect
|
|
||||||
margin-bottom 5px
|
|
||||||
clearfix()
|
|
||||||
height 33px
|
|
||||||
label
|
|
||||||
width 150px
|
|
||||||
padding-left 15px
|
|
||||||
float left
|
|
||||||
line-height 33px
|
|
||||||
.sectionMultiSelect-input
|
|
||||||
float left
|
|
||||||
select
|
|
||||||
width 80px
|
|
||||||
height 25px
|
|
||||||
margin-top 4px
|
|
||||||
border-radius 5px
|
|
||||||
border 1px solid borderColor
|
|
||||||
padding 0 10px
|
|
||||||
font-size 14px
|
|
||||||
outline none
|
|
||||||
margin-left 5px
|
|
||||||
margin-right 15px
|
|
||||||
&:focus
|
|
||||||
border-color iptFocusBorderColor
|
|
||||||
&>.sectionConfirm
|
|
||||||
clearfix()
|
|
||||||
padding 5px 15px
|
|
||||||
button
|
|
||||||
float right
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
border none
|
|
||||||
border-radius 5px
|
|
||||||
height 33px
|
|
||||||
padding 0 15px
|
|
||||||
font-size 14px
|
|
||||||
.alert
|
|
||||||
float right
|
|
||||||
width 250px
|
|
||||||
padding 10px 15px
|
|
||||||
margin 0 10px 0
|
|
||||||
.alert
|
|
||||||
color infoTextColor
|
|
||||||
background-color infoBackgroundColor
|
|
||||||
font-size 14px
|
|
||||||
padding 15px 15px
|
|
||||||
width 330px
|
|
||||||
border-radius 5px
|
|
||||||
margin 10px auto
|
|
||||||
&.error
|
|
||||||
color errorTextColor
|
|
||||||
background-color errorBackgroundColor
|
|
||||||
&.ContactTab
|
|
||||||
padding 10px
|
|
||||||
.title
|
|
||||||
font-size 18px
|
|
||||||
color brandColor
|
|
||||||
margin-top 10px
|
|
||||||
margin-bottom 10px
|
|
||||||
p
|
|
||||||
line-height 2
|
|
||||||
&.AppSettingTab
|
|
||||||
.description
|
|
||||||
marked()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
modalZIndex= 1000
|
|
||||||
modalBackColor = transparentify(black, 65%)
|
|
||||||
|
|
||||||
.ModalBase
|
|
||||||
fixed top left bottom right
|
|
||||||
z-index modalZIndex
|
|
||||||
&.hide
|
|
||||||
display none
|
|
||||||
.modalBack
|
|
||||||
absolute top left bottom right
|
|
||||||
background-color modalBackColor
|
|
||||||
z-index modalZIndex + 1
|
|
||||||
.modal
|
|
||||||
position relative
|
|
||||||
width 650px
|
|
||||||
margin 50px auto 0
|
|
||||||
z-index modalZIndex + 2
|
|
||||||
background-color white
|
|
||||||
padding 15px
|
|
||||||
color #666666
|
|
||||||
border-radius 5px
|
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"markdown-it-emoji": "^1.1.1",
|
"markdown-it-emoji": "^1.1.1",
|
||||||
"md5": "^2.0.0",
|
"md5": "^2.0.0",
|
||||||
"moment": "^2.10.3",
|
"moment": "^2.10.3",
|
||||||
|
"sander": "^0.5.1",
|
||||||
"season": "^5.3.0",
|
"season": "^5.3.0",
|
||||||
"superagent": "^1.2.0",
|
"superagent": "^1.2.0",
|
||||||
"superagent-promise": "^1.0.3"
|
"superagent-promise": "^1.0.3"
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
"css-loader": "^0.19.0",
|
"css-loader": "^0.19.0",
|
||||||
"devtron": "^1.1.0",
|
"devtron": "^1.1.0",
|
||||||
"electron-packager": "^6.0.0",
|
"electron-packager": "^6.0.0",
|
||||||
"electron-prebuilt": "^1.0.2",
|
"electron-prebuilt": "^1.1.3",
|
||||||
"electron-release": "^2.2.0",
|
"electron-release": "^2.2.0",
|
||||||
"grunt": "^0.4.5",
|
"grunt": "^0.4.5",
|
||||||
"grunt-electron-installer": "^1.2.0",
|
"grunt-electron-installer": "^1.2.0",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ var config = {
|
|||||||
'markdown-it-checkbox',
|
'markdown-it-checkbox',
|
||||||
'season',
|
'season',
|
||||||
'devtron',
|
'devtron',
|
||||||
|
'sander',
|
||||||
{
|
{
|
||||||
react: 'var React',
|
react: 'var React',
|
||||||
'react-dom': 'var ReactDOM',
|
'react-dom': 'var ReactDOM',
|
||||||
|
|||||||
Reference in New Issue
Block a user