1
0
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:
Dick Choi
2016-07-14 13:58:14 +09:00
parent 9ff70c4aef
commit 44f270f408
50 changed files with 2572 additions and 2496 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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">

View File

@@ -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

View File

@@ -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

View File

@@ -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'
/>&nbsp;
{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'
/>&nbsp;
{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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}),

View File

@@ -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'

View File

@@ -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

View File

@@ -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'>

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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)

View 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

View File

@@ -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
}),

View File

@@ -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
View 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

View File

@@ -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>

View File

@@ -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
View 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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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>
)
}
}

View File

@@ -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)

View 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>&nbsp;
<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'
/>&nbsp;
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)

View 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

View 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)

View 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

View 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

View 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'/>&nbsp;
{storage.name}&nbsp;
<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)

View 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()

View 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)

View 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

View 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))

View File

@@ -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
})

View File

@@ -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

View File

@@ -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

View File

@@ -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%)

View File

@@ -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()

View File

@@ -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

View File

@@ -52,6 +52,7 @@
"markdown-it-emoji": "^1.1.1",
"md5": "^2.0.0",
"moment": "^2.10.3",
"sander": "^0.5.1",
"season": "^5.3.0",
"superagent": "^1.2.0",
"superagent-promise": "^1.0.3"
@@ -66,7 +67,7 @@
"css-loader": "^0.19.0",
"devtron": "^1.1.0",
"electron-packager": "^6.0.0",
"electron-prebuilt": "^1.0.2",
"electron-prebuilt": "^1.1.3",
"electron-release": "^2.2.0",
"grunt": "^0.4.5",
"grunt-electron-installer": "^1.2.0",

View File

@@ -42,6 +42,7 @@ var config = {
'markdown-it-checkbox',
'season',
'devtron',
'sander',
{
react: 'var React',
'react-dom': 'var ReactDOM',