mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +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 defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -94,16 +96,16 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let { mode, value } = this.props
|
||||
let { mode, value, theme, fontSize } = this.props
|
||||
this.value = value
|
||||
let el = ReactDOM.findDOMNode(this)
|
||||
let editor = this.editor = ace.edit(el)
|
||||
editor.$blockScrolling = Infinity
|
||||
editor.renderer.setShowGutter(true)
|
||||
editor.setTheme('ace/theme/xcode')
|
||||
editor.setTheme('ace/theme/' + theme)
|
||||
editor.moveCursorTo(0, 0)
|
||||
editor.setReadOnly(!!this.props.readOnly)
|
||||
editor.setFontSize('14')
|
||||
editor.setFontSize(fontSize)
|
||||
|
||||
editor.on('blur', this.blurHandler)
|
||||
|
||||
@@ -135,9 +137,9 @@ export default class CodeEditor extends React.Component {
|
||||
: 'text'
|
||||
session.setMode('ace/mode/' + syntaxMode)
|
||||
|
||||
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||
session.setOption('useWorker', false)
|
||||
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||
session.setTabSize(this.props.indentSize)
|
||||
session.setOption('useWorker', true)
|
||||
session.setUseWrapMode(true)
|
||||
session.setValue(_.isString(value) ? value : '')
|
||||
|
||||
@@ -154,7 +156,8 @@ export default class CodeEditor extends React.Component {
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let { value } = this.props
|
||||
this.value = value
|
||||
var session = this.editor.getSession()
|
||||
let editor = this.editor
|
||||
let session = this.editor.getSession()
|
||||
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
let mode = _.find(modes, {name: this.props.mode})
|
||||
@@ -163,23 +166,18 @@ export default class CodeEditor extends React.Component {
|
||||
: 'text'
|
||||
session.setMode('ace/mode' + syntaxMode)
|
||||
}
|
||||
}
|
||||
|
||||
handleConfigApply (e, config) {
|
||||
// this.setState({
|
||||
// fontSize: config['editor-font-size'],
|
||||
// fontFamily: config['editor-font-family'],
|
||||
// indentType: config['editor-indent-type'],
|
||||
// indentSize: config['editor-indent-size'],
|
||||
// themeSyntax: config['theme-syntax']
|
||||
// }, function () {
|
||||
// var editor = this.editor
|
||||
// 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)
|
||||
// })
|
||||
if (prevProps.theme !== this.props.theme) {
|
||||
editor.setTheme('ace/theme/' + this.props.theme)
|
||||
}
|
||||
if (prevProps.fontSize !== this.props.fontSize) {
|
||||
editor.setFontSize(this.props.fontSize)
|
||||
}
|
||||
if (prevProps.indentSize !== this.props.indentSize) {
|
||||
session.setTabSize(this.props.indentSize)
|
||||
}
|
||||
if (prevProps.indentType !== this.props.indentType) {
|
||||
session.setUseSoftTabs(this.props.indentType === 'space')
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
@@ -222,8 +220,10 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className } = this.props
|
||||
|
||||
let { className, fontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
@@ -231,8 +231,7 @@ export default class CodeEditor extends React.Component {
|
||||
: `CodeEditor ${className}`
|
||||
}
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
fontFamily: fontFamily.join(', ')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -249,7 +248,12 @@ CodeEditor.propTypes = {
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
readOnly: false
|
||||
readOnly: false,
|
||||
theme: 'xcode',
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
}
|
||||
|
||||
export default CodeEditor
|
||||
|
||||
@@ -27,19 +27,22 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
let newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
if (newStatus === 'CODE') {
|
||||
this.refs.code.focus()
|
||||
} else {
|
||||
this.refs.code.blur()
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
})
|
||||
let { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
let newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
if (newStatus === 'CODE') {
|
||||
this.refs.code.focus()
|
||||
} else {
|
||||
this.refs.code.blur()
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -59,7 +62,14 @@ class MarkdownEditor extends React.Component {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={className == null
|
||||
@@ -72,15 +82,23 @@ class MarkdownEditor extends React.Component {
|
||||
ref='code'
|
||||
mode='markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
indentType={config.editor.indentType}
|
||||
indentSize={editorIndentSize}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
: 'preview--hide'
|
||||
}
|
||||
style={this.props.ignorePreviewPointerEvents
|
||||
? {pointerEvents: 'none'} : {}
|
||||
}
|
||||
style={previewStyle}
|
||||
fontSize={config.preview.fontSize}
|
||||
fontFamily={config.preview.fontFamily}
|
||||
codeBlockTheme={config.preview.theme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
tabIndex='0'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import markdown from 'browser/lib/markdown'
|
||||
import _ from 'lodash'
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const { shell } = require('electron')
|
||||
@@ -8,6 +9,15 @@ const goExternal = function (e) {
|
||||
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 {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -30,7 +40,12 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
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 () {
|
||||
@@ -38,7 +53,14 @@ export default class MarkdownPreview extends React.Component {
|
||||
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 = `
|
||||
<style>
|
||||
@font-face {
|
||||
@@ -51,6 +73,18 @@ export default class MarkdownPreview extends React.Component {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
${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>
|
||||
<link rel="stylesheet" href="../node_modules/highlight.js/styles/xcode.css" id="hljs-css">
|
||||
<link rel="stylesheet" href="../resources/katex.min.css">
|
||||
|
||||
@@ -188,7 +188,6 @@ ol
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
||||
padding 0.2em 0.4em
|
||||
background-color #f7f7f7
|
||||
border-radius 3px
|
||||
@@ -213,9 +212,9 @@ pre
|
||||
border none
|
||||
margin -5px
|
||||
&>span.lineNumber
|
||||
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
||||
display none
|
||||
float left
|
||||
font-size 0.85em
|
||||
margin 0 0.5em 0 -0.5em
|
||||
border-right 1px solid
|
||||
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 () {
|
||||
let { folders } = this.props
|
||||
let { storages } = this.props
|
||||
let { optionIndex } = this.state
|
||||
|
||||
optionIndex++
|
||||
@@ -184,25 +184,41 @@ class FolderSelect extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { className, folders, value } = this.props
|
||||
let currentFolder = _.find(folders, {key: value})
|
||||
let optionList = folders.map((folder, index) => {
|
||||
return (
|
||||
<div styleName={index === this.state.optionIndex
|
||||
? 'search-optionList-item--active'
|
||||
: 'search-optionList-item'
|
||||
}
|
||||
key={folder.key}
|
||||
onClick={(e) => this.handleOptionClick(folder.key)(e)}
|
||||
>
|
||||
<i style={{color: folder.color}}
|
||||
className='fa fa-fw fa-cube'
|
||||
/>
|
||||
{folder.name}
|
||||
</div>
|
||||
)
|
||||
let { className, storages, value } = this.props
|
||||
let splitted = value.split('-')
|
||||
let storageKey = splitted.shift()
|
||||
let folderKey = splitted.shift()
|
||||
let options = []
|
||||
storages.forEach((storage, index) => {
|
||||
storage.folders.forEach((folder) => {
|
||||
options.push({
|
||||
storage: storage,
|
||||
folder: folder
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 (
|
||||
<div className={_.isString(className)
|
||||
? 'FolderSelect ' + className
|
||||
@@ -239,10 +255,11 @@ class FolderSelect extends React.Component {
|
||||
</div>
|
||||
: <div styleName='idle'>
|
||||
<div styleName='idle-label'>
|
||||
<i style={{color: currentFolder.color}}
|
||||
className='fa fa-fw fa-cube'
|
||||
/>
|
||||
{currentFolder.name}
|
||||
<span styleName='idle-label-name'
|
||||
style={{borderColor: currentOption.folder.color}}
|
||||
>
|
||||
{currentOption.storage.name}/{currentOption.folder.name}
|
||||
</span>
|
||||
</div>
|
||||
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
border solid 1px transparent
|
||||
line-height 34px
|
||||
vertical-align middle
|
||||
border-radius 5px
|
||||
border-radius 2px
|
||||
transition 0.15s
|
||||
user-select none
|
||||
&:hover
|
||||
@@ -27,6 +27,10 @@
|
||||
right 20px
|
||||
overflow ellipsis
|
||||
|
||||
.idle-label-name
|
||||
border-left solid 4px transparent
|
||||
padding 2px 5px
|
||||
|
||||
.idle-caret
|
||||
absolute right top
|
||||
height 34px
|
||||
@@ -53,11 +57,11 @@
|
||||
border $ui-border
|
||||
z-index 200
|
||||
background-color white
|
||||
border-radius 5px
|
||||
border-radius 2px
|
||||
|
||||
.search-optionList-item
|
||||
height 34px
|
||||
width 120px
|
||||
width 250px
|
||||
box-sizing border-box
|
||||
padding 0 5px
|
||||
overflow ellipsis
|
||||
@@ -72,3 +76,6 @@
|
||||
&:hover
|
||||
background-color $ui-button--active-backgroundColor
|
||||
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 styles from './NoteDetail.styl'
|
||||
import MarkdownEditor from 'browser/components/MarkdownEditor'
|
||||
import queue from 'browser/main/lib/queue'
|
||||
import StarButton from './StarButton'
|
||||
import TagSelect from './TagSelect'
|
||||
import FolderSelect from './FolderSelect'
|
||||
import Repository from 'browser/lib/Repository'
|
||||
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 {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
note: Object.assign({}, props.note),
|
||||
note: Object.assign({
|
||||
title: '',
|
||||
content: ''
|
||||
}, props.note),
|
||||
isDispatchQueued: false
|
||||
}
|
||||
this.dispatchTimer = null
|
||||
@@ -84,86 +91,74 @@ class NoteDetail extends React.Component {
|
||||
|
||||
note.content = this.refs.content.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({
|
||||
note,
|
||||
isDispatchQueued: true
|
||||
note
|
||||
}, () => {
|
||||
this.queueDispatch()
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
cancelDispatchQueue () {
|
||||
if (this.dispatchTimer != null) {
|
||||
window.clearTimeout(this.dispatchTimer)
|
||||
this.dispatchTimer = null
|
||||
}
|
||||
}
|
||||
save () {
|
||||
let { note, dispatch } = this.props
|
||||
|
||||
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({
|
||||
type: 'SAVE_NOTE',
|
||||
repository: repoKey,
|
||||
note: note
|
||||
type: 'UPDATE_NOTE',
|
||||
note: this.state.note
|
||||
})
|
||||
queue.save(repoKey, note)
|
||||
|
||||
dataApi
|
||||
.updateNote(note.storage, note.folder, note.key, this.state.note)
|
||||
}
|
||||
|
||||
handleFolderChange (e) {
|
||||
|
||||
}
|
||||
|
||||
handleStarButtonClick (e) {
|
||||
let { note } = this.state
|
||||
let { dispatch } = this.props
|
||||
|
||||
let isStarred = note._repository.starred.some((starredKey) => starredKey === note.key)
|
||||
note.isStarred = !note.isStarred
|
||||
|
||||
if (isStarred) {
|
||||
Repository
|
||||
.find(note._repository.key)
|
||||
.then((repo) => {
|
||||
return repo.unstarNote(note.key)
|
||||
})
|
||||
this.setState({
|
||||
note
|
||||
}, () => {
|
||||
this.save()
|
||||
})
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'UNSTAR_NOTE',
|
||||
repository: note._repository.key,
|
||||
note: note.key
|
||||
})
|
||||
} else {
|
||||
Repository
|
||||
.find(note._repository.key)
|
||||
.then((repo) => {
|
||||
return repo.starNote(note.key)
|
||||
})
|
||||
exportAsFile () {
|
||||
|
||||
dispatch({
|
||||
type: 'STAR_NOTE',
|
||||
repository: note._repository.key,
|
||||
note: note.key
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleShareButtonClick (e) {
|
||||
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 () {
|
||||
let { storages, config } = this.props
|
||||
let { note } = this.state
|
||||
let isStarred = note._repository.starred.some((starredKey) => starredKey === note.key)
|
||||
let folders = note._repository.folders
|
||||
|
||||
return (
|
||||
<div className='NoteDetail'
|
||||
@@ -174,15 +169,11 @@ class NoteDetail extends React.Component {
|
||||
<div styleName='info-left'>
|
||||
|
||||
<div styleName='info-left-top'>
|
||||
<StarButton styleName='info-left-top-starButton'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={isStarred}
|
||||
/>
|
||||
<FolderSelect styleName='info-left-top-folderSelect'
|
||||
value={this.state.note.folder}
|
||||
value={this.state.note.storage + '-' + this.state.note.folder}
|
||||
ref='folder'
|
||||
folders={folders}
|
||||
onChange={() => this.handleChange()}
|
||||
storages={storages}
|
||||
onChange={(e) => this.handleFolderChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='info-left-bottom'>
|
||||
@@ -195,13 +186,18 @@ class NoteDetail extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<button styleName='info-right-button'>
|
||||
<i className='fa fa-clipboard fa-fw'/>
|
||||
</button>
|
||||
<button styleName='info-right-button'>
|
||||
<StarButton styleName='info-right-button'
|
||||
onClick={(e) => this.handleStarButtonClick(e)}
|
||||
isActive={note.isStarred}
|
||||
/>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleShareButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-share-alt fa-fw'/>
|
||||
</button>
|
||||
<button styleName='info-right-button'>
|
||||
<button styleName='info-right-button'
|
||||
onClick={(e) => this.handleContextButtonClick(e)}
|
||||
>
|
||||
<i className='fa fa-ellipsis-v'/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -210,6 +206,7 @@ class NoteDetail extends React.Component {
|
||||
<MarkdownEditor
|
||||
ref='content'
|
||||
styleName='body-noteEditor'
|
||||
config={config}
|
||||
value={this.state.note.content}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
|
||||
|
||||
@@ -14,22 +14,16 @@ $info-height = 75px
|
||||
|
||||
.info-left
|
||||
float left
|
||||
padding 0 5px
|
||||
|
||||
.info-left-top
|
||||
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
|
||||
display inline-block
|
||||
height 34px
|
||||
width 120px
|
||||
width 200px
|
||||
vertical-align middle
|
||||
|
||||
.info-left-bottom
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
.root
|
||||
position relative
|
||||
color $ui-inactive-text-color
|
||||
font-size 18px
|
||||
text-align center
|
||||
background-color transparent
|
||||
border none
|
||||
padding 0
|
||||
transition transform 0.15s
|
||||
&:focus
|
||||
color $ui-active-color
|
||||
&:hover
|
||||
color $ui-text-color
|
||||
transform rotate(-72deg)
|
||||
|
||||
.root--active
|
||||
@extend .root
|
||||
color $ui-active-color
|
||||
transform rotate(-72deg)
|
||||
&:hover
|
||||
color $ui-active-color
|
||||
|
||||
@@ -35,7 +35,9 @@ class TagSelect extends React.Component {
|
||||
removeLastTag () {
|
||||
let { value } = this.props
|
||||
|
||||
value = value.slice()
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.pop()
|
||||
value = _.uniq(value)
|
||||
|
||||
@@ -60,7 +62,9 @@ class TagSelect extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
value = value.slice()
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
value.push(newTag)
|
||||
value = _.uniq(value)
|
||||
|
||||
@@ -93,20 +97,22 @@ class TagSelect extends React.Component {
|
||||
render () {
|
||||
let { value, className } = this.props
|
||||
|
||||
let tagList = value.map((tag) => {
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||
let tagList = _.isArray(value)
|
||||
? value.map((tag) => {
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
>
|
||||
<i className='fa fa-times fa-fw'/>
|
||||
</button>
|
||||
<span styleName='tag-label'>{tag}</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
|
||||
>
|
||||
<i className='fa fa-times fa-fw'/>
|
||||
</button>
|
||||
<span styleName='tag-label'>{tag}</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
@@ -134,7 +140,7 @@ class TagSelect extends React.Component {
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
@@ -13,23 +13,25 @@ class Detail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { repositories, location } = this.props
|
||||
let { storages, location, notes, config } = this.props
|
||||
let note = null
|
||||
if (location.query.key != null) {
|
||||
let splitted = location.query.key.split('-')
|
||||
let repoKey = splitted.shift()
|
||||
let storageKey = splitted.shift()
|
||||
let folderKey = splitted.shift()
|
||||
let noteKey = splitted.shift()
|
||||
let repo = _.find(repositories, {key: repoKey})
|
||||
if (_.isObject(repo) && _.isArray(repo.notes)) {
|
||||
note = _.find(repo.notes, {key: noteKey})
|
||||
}
|
||||
|
||||
note = _.find(notes, {
|
||||
storage: storageKey,
|
||||
folder: folderKey,
|
||||
key: noteKey
|
||||
})
|
||||
}
|
||||
|
||||
if (note == null) {
|
||||
return (
|
||||
<div className='Detail'
|
||||
<div styleName='root'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
tabIndex='0'
|
||||
>
|
||||
<div styleName='empty'>
|
||||
@@ -42,9 +44,10 @@ class Detail extends React.Component {
|
||||
return (
|
||||
<NoteDetail
|
||||
note={note}
|
||||
config={config}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'repositories',
|
||||
'storages',
|
||||
'style',
|
||||
'ignorePreviewPointerEvents'
|
||||
])}
|
||||
@@ -55,7 +58,7 @@ class Detail extends React.Component {
|
||||
|
||||
Detail.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array,
|
||||
storages: PropTypes.array,
|
||||
style: PropTypes.shape({
|
||||
left: PropTypes.number
|
||||
}),
|
||||
|
||||
@@ -6,7 +6,7 @@ import SideNav from './SideNav'
|
||||
import TopBar from './TopBar'
|
||||
import NoteList from './NoteList'
|
||||
import Detail from './Detail'
|
||||
import Repository from 'browser/lib/Repository'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import StatusBar from './StatusBar'
|
||||
import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
@@ -27,9 +27,13 @@ class Main extends React.Component {
|
||||
let { dispatch } = this.props
|
||||
|
||||
// Reload all data
|
||||
Repository.loadAll()
|
||||
.then((allData) => {
|
||||
dispatch({type: 'INIT_ALL', data: allData})
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,12 +87,17 @@ class Main extends React.Component {
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, ['dispatch', 'repositories', 'config', 'location'])}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'storages',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<TopBar
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'repositories',
|
||||
'storages',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
@@ -100,7 +109,8 @@ class Main extends React.Component {
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'repositories',
|
||||
'storages',
|
||||
'notes',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
@@ -117,7 +127,8 @@ class Main extends React.Component {
|
||||
style={{left: this.state.listWidth + 1}}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'repositories',
|
||||
'storages',
|
||||
'notes',
|
||||
'config',
|
||||
'params',
|
||||
'location'
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
float left
|
||||
overflow ellipsis
|
||||
|
||||
.item-info-left-folder
|
||||
border-left 4px solid transparent
|
||||
padding 2px 5px
|
||||
|
||||
.item-info-right
|
||||
float right
|
||||
|
||||
|
||||
@@ -26,30 +26,32 @@ class NoteList extends React.Component {
|
||||
router.replace({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: `${this.notes[0]._repository.key}-${this.notes[0].key}`
|
||||
key: this.notes[0].uniqueKey
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Auto scroll
|
||||
let splitted = location.query.key.split('-')
|
||||
let repoKey = splitted[0]
|
||||
let noteKey = splitted[1]
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return repoKey === note._repository.key && noteKey === note.key
|
||||
})
|
||||
if (targetIndex > -1) {
|
||||
let list = this.refs.root
|
||||
let item = list.childNodes[targetIndex]
|
||||
if (_.isString(location.query.key)) {
|
||||
let splitted = location.query.key.split('/')
|
||||
let repoKey = splitted[0]
|
||||
let noteKey = splitted[1]
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return repoKey === note.storage && noteKey === note.key
|
||||
})
|
||||
if (targetIndex > -1) {
|
||||
let list = this.refs.root
|
||||
let item = list.childNodes[targetIndex]
|
||||
|
||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
let overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
|
||||
if (overflowBelow) {
|
||||
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
|
||||
}
|
||||
let overflowAbove = list.scrollTop > item.offsetTop
|
||||
if (overflowAbove) {
|
||||
list.scrollTop = item.offsetTop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +70,7 @@ class NoteList extends React.Component {
|
||||
let repoKey = splitted[0]
|
||||
let noteKey = splitted[1]
|
||||
let targetIndex = _.findIndex(this.notes, (note) => {
|
||||
return repoKey === note._repository.key && noteKey === note.key
|
||||
return repoKey === note.storage && noteKey === note.key
|
||||
})
|
||||
|
||||
if (targetIndex === 0) {
|
||||
@@ -80,7 +82,7 @@ class NoteList extends React.Component {
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
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({
|
||||
pathname: location.pathname,
|
||||
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)
|
||||
}
|
||||
|
||||
handleNoteClick (key) {
|
||||
handleNoteClick (uniqueKey) {
|
||||
return (e) => {
|
||||
let { router } = this.context
|
||||
let { location } = this.props
|
||||
@@ -200,42 +202,50 @@ class NoteList extends React.Component {
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
query: {
|
||||
key: key
|
||||
key: uniqueKey
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { location } = this.props
|
||||
let notes = this.notes = this.getNotes().sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||
let { location, storages, notes } = this.props
|
||||
this.notes = notes
|
||||
// this.notes = this.getNotes()
|
||||
|
||||
let noteElements = notes
|
||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||
.map((note) => {
|
||||
let folder = _.find(note._repository.folders, {key: note.folder})
|
||||
let tagElements = note.tags.map((tag) => {
|
||||
return (
|
||||
<span styleName='item-tagList-item'
|
||||
key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
let key = `${note._repository.key}-${note.key}`
|
||||
let isActive = location.query.key === key
|
||||
let storage = _.find(storages, {key: note.storage})
|
||||
let folder = _.find(storage.folders, {key: note.folder})
|
||||
let tagElements = _.isArray(note.tags)
|
||||
? note.tags.map((tag) => {
|
||||
return (
|
||||
<span styleName='item-tagList-item'
|
||||
key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
: []
|
||||
let isActive = location.query.key === note.uniqueKey
|
||||
return (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
key={key}
|
||||
onClick={(e) => this.handleNoteClick(key)(e)}
|
||||
key={note.uniqueKey}
|
||||
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)}
|
||||
>
|
||||
<div styleName='item-border'/>
|
||||
<div styleName='item-info'>
|
||||
|
||||
<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 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
|
||||
margin-left 5px
|
||||
|
||||
.repositoryList
|
||||
.storageList
|
||||
absolute left right
|
||||
bottom 44px
|
||||
top 178px
|
||||
overflow-y auto
|
||||
|
||||
.repositoryList-empty
|
||||
.storageList-empty
|
||||
padding 0 10px
|
||||
margin-top 15px
|
||||
line-height 24px
|
||||
@@ -68,10 +68,10 @@
|
||||
line-height 32px
|
||||
padding 0
|
||||
|
||||
.root-folded
|
||||
.root--folded
|
||||
@extend .root
|
||||
width 44px
|
||||
.repositoryList-empty
|
||||
.storageList-empty
|
||||
white-space nowrap
|
||||
transform rotate(90deg)
|
||||
.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 styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
import Preferences from '../modals/Preferences'
|
||||
import RepositorySection from './RepositorySection'
|
||||
import NewRepositoryModal from '../modals/NewRepositoryModal'
|
||||
import PreferencesModal from '../modals/PreferencesModal'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import StorageItem from './StorageItem'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
const Menu = remote.Menu
|
||||
const MenuItem = remote.MenuItem
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
handleMenuButtonClick (e) {
|
||||
var menu = new Menu()
|
||||
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)
|
||||
openModal(PreferencesModal)
|
||||
}
|
||||
|
||||
handleHomeButtonClick (e) {
|
||||
let { router } = this.context
|
||||
router.push('/repositories')
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
handleStarredButtonClick (e) {
|
||||
@@ -57,25 +36,22 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { repositories, dispatch, location, config } = this.props
|
||||
let { storages, location, config } = this.props
|
||||
|
||||
let isFolded = config.isSideNavFolded
|
||||
let isHomeActive = location.pathname.match(/^\/home$/)
|
||||
let isStarredActive = location.pathname.match(/^\/starred$/)
|
||||
|
||||
let repositorieElements = repositories
|
||||
.map((repo) => {
|
||||
return <RepositorySection
|
||||
key={repo.key}
|
||||
repository={repo}
|
||||
dispatch={dispatch}
|
||||
isFolded={isFolded}
|
||||
/>
|
||||
})
|
||||
let storageList = storages.map((storage) => {
|
||||
return <StorageItem
|
||||
key={storage.key}
|
||||
storage={storage}
|
||||
location={location}
|
||||
/>
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='SideNav'
|
||||
styleName={isFolded ? 'root-folded' : 'root'}
|
||||
styleName={isFolded ? 'root--folded' : 'root'}
|
||||
tabIndex='1'
|
||||
>
|
||||
<div styleName='top'>
|
||||
@@ -102,9 +78,9 @@ class SideNav extends React.Component {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div styleName='repositoryList'>
|
||||
{repositories.length > 0 ? repositorieElements : (
|
||||
<div styleName='repositoryList-empty'>No repository mount.</div>
|
||||
<div styleName='storageList'>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storageList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -127,7 +103,7 @@ SideNav.contextTypes = {
|
||||
|
||||
SideNav.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
repositories: PropTypes.array,
|
||||
storages: PropTypes.array,
|
||||
config: PropTypes.shape({
|
||||
isSideNavFolded: PropTypes.bool
|
||||
}),
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { PropTypes } from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './TopBar.styl'
|
||||
import activityRecord from 'browser/lib/activityRecord'
|
||||
import Repository from 'browser/lib/Repository'
|
||||
import _ from 'lodash'
|
||||
import Commander from 'browser/main/lib/Commander'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
|
||||
const OSX = window.process.platform === 'darwin'
|
||||
|
||||
@@ -33,65 +33,31 @@ class TopBar extends React.Component {
|
||||
}
|
||||
|
||||
handleNewPostButtonClick (e) {
|
||||
activityRecord.emit('ARTICLE_CREATE')
|
||||
|
||||
let { params, repositories } = this.props
|
||||
|
||||
let folderKey = params.folderKey
|
||||
let repositoryKey = params.repositoryKey
|
||||
if (folderKey == null) {
|
||||
let repository = _.find(repositories, {key: repositoryKey})
|
||||
if (repository == null) {
|
||||
repository = repositories[0]
|
||||
}
|
||||
if (repository != null) {
|
||||
repositoryKey = repository.key
|
||||
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)
|
||||
let { storages, params, dispatch } = this.props
|
||||
let storage = _.find(storages, {key: params.storageKey})
|
||||
if (storage == null) storage = storages[0]
|
||||
if (storage == null) throw new Error('No storage to create a note')
|
||||
let folder = _.find(storage.folders, {key: params.folderKey})
|
||||
if (folder == null) folder = storage.folders[0]
|
||||
if (folder == null) throw new Error('No folder to craete a note')
|
||||
// activityRecord.emit('ARTICLE_CREATE')
|
||||
console.log(storage, folder)
|
||||
dataApi
|
||||
.createNote(storage.key, folder.key, {
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
.then((note) => {
|
||||
let { dispatch, location } = this.props
|
||||
let { router } = this.context
|
||||
dispatch({
|
||||
type: 'ADD_NOTE',
|
||||
repository: repositoryKey,
|
||||
type: 'CREATE_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) {
|
||||
}
|
||||
|
||||
handleLinksButton (e) {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { config } = this.props
|
||||
return (
|
||||
@@ -124,7 +90,7 @@ class TopBar extends React.Component {
|
||||
onClick={(e) => this.handleNewPostButtonClick(e)}>
|
||||
<i className='fa fa-plus'/>
|
||||
<span styleName='left-control-newPostButton-tooltip'>
|
||||
New Post {OSX ? '⌘' : '^'} + n
|
||||
New Note {OSX ? '⌘' : '^'} + n
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -137,11 +103,7 @@ class TopBar extends React.Component {
|
||||
>
|
||||
?<span styleName='left-control-newPostButton-tooltip'>How to use</span>
|
||||
</button>
|
||||
<button styleName='right-linksButton'
|
||||
onClick={(e) => this.handleLinksButton(e)}
|
||||
>
|
||||
<img src='../resources/app.png' width='44' height='44'/>
|
||||
</button>
|
||||
|
||||
</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 React from 'react'
|
||||
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 fetchConfig from '../lib/fetchConfig'
|
||||
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
|
||||
@@ -84,11 +84,10 @@ ReactDOM.render((
|
||||
<IndexRedirect to='/home'/>
|
||||
<Route path='home'/>
|
||||
<Route path='starred'/>
|
||||
<Route path='repositories'>
|
||||
<Route path='storages'>
|
||||
<IndexRedirect to='/home'/>
|
||||
<Route path=':repositoryKey'>
|
||||
<Route path=':storageKey'>
|
||||
<IndexRoute/>
|
||||
<Route path='settings'/>
|
||||
<Route path='folders/:folderKey'/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultConfig = {
|
||||
zoom: 1,
|
||||
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) {
|
||||
@@ -16,6 +40,7 @@ function validate (config) {
|
||||
}
|
||||
|
||||
function _save (config) {
|
||||
console.log(config)
|
||||
window.localStorage.setItem('config', JSON.stringify(config))
|
||||
}
|
||||
|
||||
@@ -23,7 +48,7 @@ function get () {
|
||||
let config = window.localStorage.getItem('config')
|
||||
|
||||
try {
|
||||
config = JSON.parse(config)
|
||||
config = Object.assign({}, defaultConfig, JSON.parse(config))
|
||||
if (!validate(config)) throw new Error('INVALID CONFIG')
|
||||
} catch (err) {
|
||||
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'
|
||||
|
||||
let tasks = []
|
||||
|
||||
function _save (task, repoKey, note) {
|
||||
function _save (task, storageKey, note) {
|
||||
note = Object.assign({}, note)
|
||||
delete note._repository
|
||||
delete note._storage
|
||||
|
||||
task.status = 'process'
|
||||
|
||||
Repository
|
||||
.find(repoKey)
|
||||
.then((repo) => {
|
||||
return repo.updateNote(note.key, note)
|
||||
Storage
|
||||
.find(storageKey)
|
||||
.then((storage) => {
|
||||
return storage.updateNote(note.key, note)
|
||||
})
|
||||
.then((note) => {
|
||||
tasks.splice(tasks.indexOf(task), 1)
|
||||
@@ -25,8 +25,8 @@ function _save (task, repoKey, note) {
|
||||
})
|
||||
}
|
||||
|
||||
const queueSaving = function (repoKey, note) {
|
||||
let key = `${repoKey}-${note.key}`
|
||||
const queueSaving = function (storageKey, note) {
|
||||
let key = `${storageKey}-${note.key}`
|
||||
|
||||
let taskIndex = _.findIndex(tasks, {
|
||||
type: 'SAVE_NOTE',
|
||||
@@ -47,7 +47,7 @@ const queueSaving = function (repoKey, note) {
|
||||
}
|
||||
|
||||
task.timer = window.setTimeout(() => {
|
||||
_save(task, repoKey, note)
|
||||
_save(task, storageKey, note)
|
||||
}, 1500)
|
||||
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 _ from 'lodash'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
function storages (state = [], action) {
|
||||
console.info('REDUX >> ', action)
|
||||
switch (action.type) {
|
||||
case 'INIT_ALL':
|
||||
action.data.forEach((repo) => {
|
||||
repo.notes.forEach((note) => {
|
||||
note._repository = repo
|
||||
})
|
||||
})
|
||||
return action.data.slice()
|
||||
case 'ADD_REPOSITORY':
|
||||
return action.storages
|
||||
case 'ADD_STORAGE':
|
||||
{
|
||||
let repos = state.slice()
|
||||
let storages = state.slice()
|
||||
|
||||
repos.push(action.repository)
|
||||
storages.push(action.storage)
|
||||
|
||||
return repos
|
||||
}
|
||||
case 'REMOVE_REPOSITORY':
|
||||
{
|
||||
let repos = state.slice()
|
||||
|
||||
let targetIndex = _.findIndex(repos, {key: action.key})
|
||||
if (targetIndex > -1) {
|
||||
repos.splice(targetIndex, 1)
|
||||
}
|
||||
|
||||
return repos
|
||||
return storages
|
||||
}
|
||||
case 'ADD_FOLDER':
|
||||
case 'REMOVE_FOLDER':
|
||||
case 'UPDATE_STORAGE':
|
||||
{
|
||||
let repos = state.slice()
|
||||
let targetRepo = _.find(repos, {key: action.key})
|
||||
let storages = state.slice()
|
||||
storages = storages
|
||||
.filter((storage) => storage.key !== action.storage.key)
|
||||
storages.push(action.storage)
|
||||
|
||||
if (targetRepo == null) return state
|
||||
|
||||
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
|
||||
return storages
|
||||
}
|
||||
case 'EDIT_FOLDER':
|
||||
case 'REMOVE_STORAGE':
|
||||
{
|
||||
let repos = state.slice()
|
||||
let targetRepo = _.find(repos, {key: action.key})
|
||||
let storages = state.slice()
|
||||
storages = storages
|
||||
.filter((storage) => storage.key !== action.key)
|
||||
|
||||
if (targetRepo == null) return state
|
||||
|
||||
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
|
||||
return storages
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function notes (state = [], action) {
|
||||
switch (action.type) {
|
||||
case 'INIT_ALL':
|
||||
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':
|
||||
{
|
||||
let repos = state.slice()
|
||||
let targetRepo = _.find(repos, {key: action.repository})
|
||||
let notes = state.slice()
|
||||
notes = notes
|
||||
.filter((note) => note.storage !== action.storage.key || note.folder !== action.key)
|
||||
|
||||
if (targetRepo == null) return state
|
||||
|
||||
let targetFolderIndex = _.findIndex(targetRepo.folders, {key: action.folder})
|
||||
if (targetFolderIndex > -1) {
|
||||
targetRepo.folders.splice(targetFolderIndex, 1)
|
||||
}
|
||||
|
||||
return repos
|
||||
return notes
|
||||
}
|
||||
case 'ADD_NOTE':
|
||||
case 'CREATE_NOTE':
|
||||
{
|
||||
let repos = state.slice()
|
||||
let targetRepo = _.find(repos, {key: action.repository})
|
||||
|
||||
if (targetRepo == null) return state
|
||||
action.note._repository = targetRepo
|
||||
targetRepo.notes.push(action.note)
|
||||
|
||||
return repos
|
||||
let notes = state.slice()
|
||||
notes.push(action.note)
|
||||
return notes
|
||||
}
|
||||
case 'SAVE_NOTE':
|
||||
case 'UPDATE_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.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
|
||||
let notes = state.slice()
|
||||
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
|
||||
notes.push(action.note)
|
||||
return notes
|
||||
}
|
||||
}
|
||||
return state
|
||||
@@ -191,12 +98,15 @@ function config (state = defaultConfig, action) {
|
||||
return Object.assign({}, state)
|
||||
case 'SET_CONFIG':
|
||||
return Object.assign({}, state, action.config)
|
||||
case 'SET_UI':
|
||||
return Object.assign({}, state, action.config)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
let reducer = combineReducers({
|
||||
repositories,
|
||||
storages,
|
||||
notes,
|
||||
config,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$brand-color = #6AA5E9
|
||||
|
||||
$danger-color = red
|
||||
$danger-lighten-color = #FFE5E6
|
||||
$danger-color = #c9302c
|
||||
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||
|
||||
// Layouts
|
||||
$statusBar-height = 24px
|
||||
@@ -65,16 +65,32 @@ colorDefaultButton()
|
||||
// Primary button(Brand color)
|
||||
$primary-button-background = $brand-color
|
||||
$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()
|
||||
color white
|
||||
background-color $primary-button-background
|
||||
&:hover
|
||||
background-color $primary-button-background--hover
|
||||
&:active
|
||||
background-color $primary-button-background--active
|
||||
&: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
|
||||
@@ -101,7 +117,7 @@ navButtonColor()
|
||||
|
||||
$modal-z-index = 1002
|
||||
$modal-background = white
|
||||
$modal-margin = 64px auto 0
|
||||
$modal-margin = 64px auto 64px
|
||||
$modal-border-radius = 5px
|
||||
|
||||
modal()
|
||||
@@ -111,3 +127,4 @@ modal()
|
||||
background-color $modal-background
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
box-shadow 2px 2px 10px gray
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@import '../vars'
|
||||
@import '../mixins/*'
|
||||
global-reset()
|
||||
@import '../shared/*'
|
||||
@import './modal/*'
|
||||
@import '../theme/*'
|
||||
|
||||
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||
@@ -86,3 +84,26 @@ textarea.block-input
|
||||
|
||||
#content
|
||||
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
|
||||
Reference in New Issue
Block a user