diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index c7128600..ea1d2db4 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -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 (
) @@ -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 diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index c3ee4733..bf520bfb 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -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 (
this.handleChange(e)} /> this.handleContextMenu(e)} tabIndex='0' diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index edcbadce..8619cddf 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -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 = ` diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 70eb65b0..e31a1919 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -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 diff --git a/browser/lib/Repository.js b/browser/lib/Repository.js deleted file mode 100644 index 083f5c5a..00000000 --- a/browser/lib/Repository.js +++ /dev/null @@ -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 (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 diff --git a/browser/main/Detail/FolderSelect.js b/browser/main/Detail/FolderSelect.js index ec38d63f..eff89d10 100644 --- a/browser/main/Detail/FolderSelect.js +++ b/browser/main/Detail/FolderSelect.js @@ -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 ( -
this.handleOptionClick(folder.key)(e)} - > -   - {folder.name} -
- ) + 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 ( +
this.handleOptionClick(option.folder.key)(e)} + > + + {option.storage.name}/{option.folder.name} + +
+ ) + }) + return (
:
-   - {currentFolder.name} + + {currentOption.storage.name}/{currentOption.folder.name} +
diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl index 2593334b..64ecb35b 100644 --- a/browser/main/Detail/FolderSelect.styl +++ b/browser/main/Detail/FolderSelect.styl @@ -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 diff --git a/browser/main/Detail/NoteDetail.js b/browser/main/Detail/NoteDetail.js index 3264203b..25f989ab 100644 --- a/browser/main/Detail/NoteDetail.js +++ b/browser/main/Detail/NoteDetail.js @@ -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 (
- this.handleStarButtonClick(e)} - isActive={isStarred} - /> this.handleChange()} + storages={storages} + onChange={(e) => this.handleFolderChange(e)} />
@@ -195,13 +186,18 @@ class NoteDetail extends React.Component {
- - -
@@ -210,6 +206,7 @@ class NoteDetail extends React.Component { this.handleChange(e)} ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents} diff --git a/browser/main/Detail/NoteDetail.styl b/browser/main/Detail/NoteDetail.styl index 31ab252f..1b8ebe83 100644 --- a/browser/main/Detail/NoteDetail.styl +++ b/browser/main/Detail/NoteDetail.styl @@ -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 diff --git a/browser/main/Detail/StarButton.styl b/browser/main/Detail/StarButton.styl index 655f9b30..add3a354 100644 --- a/browser/main/Detail/StarButton.styl +++ b/browser/main/Detail/StarButton.styl @@ -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 diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index 8c872a44..06ba4f31 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -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 ( - - - {tag} - - ) - }) + + {tag} + + ) + }) + : [] return (
@@ -42,9 +44,10 @@ class Detail extends React.Component { return ( { - 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)} > { - 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 ( - - {tag} - - ) - }) - 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 ( + + {tag} + + ) + }) + : [] + let isActive = location.query.key === note.uniqueKey return (
this.handleNoteClick(key)(e)} + key={note.uniqueKey} + onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)} >
- {folder.name} + + {storage.name}/{folder.name} +
diff --git a/browser/main/SideNav/FolderItem.js b/browser/main/SideNav/FolderItem.js deleted file mode 100644 index 937c0397..00000000 --- a/browser/main/SideNav/FolderItem.js +++ /dev/null @@ -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 ( -
this.handleClick(e)} - onContextMenu={(e) => this.handleContextButtonClick(e)} - > -
- - {folder.name} -
-
- -
-
- ) - } - - 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 ( -
- this.handleNameInputChange(e)} - onBlur={(e) => this.handleNameInputBlur(e)} - disabled={this.state.isUpdating} - /> -
- ) - } - - 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) diff --git a/browser/main/SideNav/FolderItem.styl b/browser/main/SideNav/FolderItem.styl deleted file mode 100644 index d2d16d50..00000000 --- a/browser/main/SideNav/FolderItem.styl +++ /dev/null @@ -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 diff --git a/browser/main/SideNav/RepositorySection.js b/browser/main/SideNav/RepositorySection.js deleted file mode 100644 index a643d9e8..00000000 --- a/browser/main/SideNav/RepositorySection.js +++ /dev/null @@ -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 ( - - ) - }) - - let toggleButtonIconClassName = this.state.isOpen - ? 'fa fa-minus' - : 'fa fa-plus' - - return ( -
-
this.handleHeaderClick(e)} - onContextMenu={(e) => this.handleContextButtonClick(e)} - > -
- - {repository.name} -
- -
- - -
-
- {this.state.isOpen &&
- {folderElements} - - {this.state.isCreatingFolder - ?
- this.handleNewFolderFormChange(e)} - onBlur={(e) => this.handleNameInputBlur(e)} - /> -
- : - } -
} -
- ) - } -} - -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) diff --git a/browser/main/SideNav/RepositorySection.styl b/browser/main/SideNav/RepositorySection.styl deleted file mode 100644 index ac6b6135..00000000 --- a/browser/main/SideNav/RepositorySection.styl +++ /dev/null @@ -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 diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 5d772855..6d7e8417 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -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 diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js new file mode 100644 index 00000000..8bed86cd --- /dev/null +++ b/browser/main/SideNav/StorageItem.js @@ -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 + }) + + let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$')) + + return ( +
+
+ + +
+ {this.state.isOpen && +
+ {folderList} +
+ } +
+ ) + } +} + +StorageItem.propTypes = { +} + +export default CSSModules(StorageItem, styles) diff --git a/browser/main/SideNav/StorageItem.styl b/browser/main/SideNav/StorageItem.styl new file mode 100644 index 00000000..7901ca98 --- /dev/null +++ b/browser/main/SideNav/StorageItem.styl @@ -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 diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 05411f16..d12ab9c9 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -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 - }) + let storageList = storages.map((storage) => { + return + }) return (
@@ -102,9 +78,9 @@ class SideNav extends React.Component {
-
- {repositories.length > 0 ? repositorieElements : ( -
No repository mount.
+
+ {storageList.length > 0 ? storageList : ( +
No storage mount.
)}
@@ -127,7 +103,7 @@ SideNav.contextTypes = { SideNav.propTypes = { dispatch: PropTypes.func, - repositories: PropTypes.array, + storages: PropTypes.array, config: PropTypes.shape({ isSideNavFolded: PropTypes.bool }), diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index 76d71988..a74d7157 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -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)}> - New Post {OSX ? '⌘' : '^'} + n + New Note {OSX ? '⌘' : '^'} + n
@@ -137,11 +103,7 @@ class TopBar extends React.Component { > ?How to use - +
) diff --git a/browser/main/global.styl b/browser/main/global.styl new file mode 100644 index 00000000..cfeb0495 --- /dev/null +++ b/browser/main/global.styl @@ -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 diff --git a/browser/main/index.js b/browser/main/index.js index fedef081..9e0abe3f 100644 --- a/browser/main/index.js +++ b/browser/main/index.js @@ -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(( - + - + - diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index e715da61..dbc8c59d 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -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.') diff --git a/browser/main/lib/dataApi.js b/browser/main/lib/dataApi.js new file mode 100644 index 00000000..2a19a257 --- /dev/null +++ b/browser/main/lib/dataApi.js @@ -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 +} diff --git a/browser/main/lib/queue.js b/browser/main/lib/queue.js index 8e70ae0c..e4adcbfb 100644 --- a/browser/main/lib/queue.js +++ b/browser/main/lib/queue.js @@ -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) } diff --git a/browser/main/modals/Preferences/AppSettingTab.js b/browser/main/modals/Preferences/AppSettingTab.js deleted file mode 100644 index 51ddef2e..00000000 --- a/browser/main/modals/Preferences/AppSettingTab.js +++ /dev/null @@ -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 - ?

- {keymapAlert.message} -

- : null - let aceThemeList = ace.require('ace/ext/themelist') - let hljsThemeList = hljsTheme() - - return ( -
-
-
Editor
-
- - this.handleConfigKeyDown(e)} type='text'/> -
-
- - this.handleConfigKeyDown(e)} type='text'/> -
-
- -
- type - - size - -
-
-
Preview
-
- - this.handleConfigKeyDown(e)} type='text'/> -
-
- - this.handleConfigKeyDown(e)} type='text'/> -
-
- - -
-
- -
- { - global.process.platform === 'win32' - ? ( -
- -
- ) - : null - } -
Theme
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
Hotkey
-
- - this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleMain')} type='text'/> -
-
- - this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/> -
-
- - {keymapAlertElement} -
-
-
    -
  • 0 to 9
  • -
  • A to Z
  • -
  • F1 to F24
  • -
  • Punctuations like ~, !, @, #, $, etc.
  • -
  • Plus
  • -
  • Space
  • -
  • Backspace
  • -
  • Delete
  • -
  • Insert
  • -
  • Return (or Enter as alias)
  • -
  • Up, Down, Left and Right
  • -
  • Home and End
  • -
  • PageUp and PageDown
  • -
  • Escape (or Esc for short)
  • -
  • VolumeUp, VolumeDown and VolumeMute
  • -
  • MediaNextTrack, MediaPreviousTrack, MediaStop and MediaPlayPause
  • -
-
-
-
- ) - } -} - -AppSettingTab.propTypes = { - user: PropTypes.shape({ - name: PropTypes.string - }), - dispatch: PropTypes.func -} diff --git a/browser/main/modals/Preferences/ContactTab.js b/browser/main/modals/Preferences/ContactTab.js deleted file mode 100644 index edc1dd9a..00000000 --- a/browser/main/modals/Preferences/ContactTab.js +++ /dev/null @@ -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 ( -
-
Contact
-

- - Issues: https://github.com/BoostIO/Boostnote/issues -

-
- ) - } -} diff --git a/browser/main/modals/Preferences/index.js b/browser/main/modals/Preferences/index.js deleted file mode 100644 index 77222077..00000000 --- a/browser/main/modals/Preferences/index.js +++ /dev/null @@ -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) => ( - - )) - - return ( -
-
-
Setting
- -
- -
- {navButtons} -
- - {content} -
- ) - } - - renderContent () { - let { user, dispatch } = this.props - - switch (this.state.currentTab) { - case CONTACT: - return ( - - ) - case APP: - default: - return ( - - ) - } - } -} - -Preferences.propTypes = { - user: PropTypes.shape({ - name: PropTypes.string - }), - dispatch: PropTypes.func -} - -export default connect((x) => x)(Preferences) diff --git a/browser/main/modals/PreferencesModal/ConfigTab.js b/browser/main/modals/PreferencesModal/ConfigTab.js new file mode 100644 index 00000000..4ff94320 --- /dev/null +++ b/browser/main/modals/PreferencesModal/ConfigTab.js @@ -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 + ?

+ {keymapAlert.message} +

+ : null + let aceThemeList = ace.require('ace/ext/themelist') + let hljsThemeList = hljsTheme() + let { config } = this.state + + return ( +
+
+
Hotkey
+
+
Toggle Main
+
+ this.handleHotkeyChange(e)} + ref='toggleMain' + value={config.hotkey.toggleMain} + type='text' + /> +
+
+
+
Toggle Finder(popup)
+
+ this.handleHotkeyChange(e)} + ref='toggleFinder' + value={config.hotkey.toggleFinder} + type='text' + /> +
+
+
+ + + {keymapAlertElement} +
+ {this.state.isHotkeyHintOpen && +
+

Available Keys

+
    +
  • 0 to 9
  • +
  • A to Z
  • +
  • F1 to F24
  • +
  • Punctuations like ~, !, @, #, $, etc.
  • +
  • Plus
  • +
  • Space
  • +
  • Backspace
  • +
  • Delete
  • +
  • Insert
  • +
  • Return (or Enter as alias)
  • +
  • Up, Down, Left and Right
  • +
  • Home and End
  • +
  • PageUp and PageDown
  • +
  • Escape (or Esc for short)
  • +
  • VolumeUp, VolumeDown and VolumeMute
  • +
  • MediaNextTrack, MediaPreviousTrack, MediaStop and MediaPlayPause
  • +
+
+ } +
+ +
+
UI
+
+
Theme
+
+ +
+
+ { + global.process.platform === 'win32' + ?
+ +
+ : null + } +
Editor
+ +
+
+ Editor Theme +
+
+ +
+
+
+
+ Editor Font Size +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ Editor Font Family +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ Editor Indent Style +
+
+   + +
+
+ +
+
+ Switching Preview +
+
+ +
+
+
Preview
+
+
+ Preview Font Size +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
+ Preview Font Family +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+
+
Code block Theme
+
+ +
+
+
+ +
+ +
+ +
+
+
+ ) + } +} + +ConfigTab.propTypes = { + user: PropTypes.shape({ + name: PropTypes.string + }), + dispatch: PropTypes.func +} + +export default CSSModules(ConfigTab, styles) diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl new file mode 100644 index 00000000..b847ba8a --- /dev/null +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -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 + + diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js new file mode 100644 index 00000000..ee1bdef9 --- /dev/null +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -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 ( +
+
+ +
Boostnote {appVersion}
+
Made by MAISIN&CO.
+
+ +
+ ) + } +} + +InfoTab.propTypes = { +} + +export default CSSModules(InfoTab, styles) diff --git a/browser/main/modals/PreferencesModal/InfoTab.styl b/browser/main/modals/PreferencesModal/InfoTab.styl new file mode 100644 index 00000000..11d8f198 --- /dev/null +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/PreferencesModal.styl b/browser/main/modals/PreferencesModal/PreferencesModal.styl new file mode 100644 index 00000000..f48c93e9 --- /dev/null +++ b/browser/main/modals/PreferencesModal/PreferencesModal.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js new file mode 100644 index 00000000..292095ed --- /dev/null +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -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 ( +
+
+ + this.handleEditChange(e)} + /> +
+
+ + +
+
+ ) + } + + 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 ( +
+
+ Are you sure to delete this folder? +
+
+ + +
+
+ ) + } + + 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 ( +
this.handleEditButtonClick(e)} + > +
+ {folder.name} + ({folder.key}) +
+
+ + +
+
+ + ) + } + + 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 + }) + return ( +
+
+   + {storage.name}  + ({storage.path}) +
+ + + +
+
+
+ {folderList.length > 0 + ? folderList + :
No Folders
+ } +
+
+ ) + } +} + +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) diff --git a/browser/main/modals/PreferencesModal/StorageItem.styl b/browser/main/modals/PreferencesModal/StorageItem.styl new file mode 100644 index 00000000..724109d7 --- /dev/null +++ b/browser/main/modals/PreferencesModal/StorageItem.styl @@ -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() diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js new file mode 100644 index 00000000..f9a7183c --- /dev/null +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -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 + }) + return ( +
+ {storageList.length > 0 + ? storageList + :
No storage found.
+ } +
+ +
+
+ ) + } + + 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 ( +
+ +
Add Storage
+ +
+ +
+
+ Name +
+
+ this.handleAddStorageChange(e)} + /> +
+
+ +
+
Type
+
+ +
+ 3rd party cloud integration(such as Google Drive and Dropbox) will be available soon. +
+
+
+ +
+
Location +
+
+ this.handleAddStorageChange(e)} + /> + +
+
+ +
+ + +
+ +
+ +
+ ) + } + + renderContent () { + switch (this.state.page) { + case 'ADD_STORAGE': + case 'ADD_FOLDER': + return this.renderAddStorage() + case 'LIST': + default: + return this.renderList() + } + } + + render () { + return ( +
+ {this.renderContent()} +
+ ) + } +} + +StoragesTab.propTypes = { + dispatch: PropTypes.func +} + +export default CSSModules(StoragesTab, styles) diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl new file mode 100644 index 00000000..513048c5 --- /dev/null +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -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 diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js new file mode 100644 index 00000000..adbd1869 --- /dev/null +++ b/browser/main/modals/PreferencesModal/index.js @@ -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 + case 'CONFIG': + return ( + + ) + case 'STORAGES': + default: + return ( + + ) + } + } + + 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 ( + + ) + }) + + return ( +
+
+ {navButtons} +
+
+ {content} +
+
+ ) + } +} + +Preferences.propTypes = { + dispatch: PropTypes.func +} + +export default connect((x) => x)(CSSModules(Preferences, styles)) diff --git a/browser/main/store.js b/browser/main/store.js index 8050466d..93060405 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -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 }) diff --git a/browser/styles/index.styl b/browser/styles/index.styl index 2a809e97..9322259f 100644 --- a/browser/styles/index.styl +++ b/browser/styles/index.styl @@ -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 diff --git a/browser/styles/main/index.styl b/browser/styles/main/index.styl index 46d6b19c..84389e6b 100644 --- a/browser/styles/main/index.styl +++ b/browser/styles/main/index.styl @@ -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 + diff --git a/browser/styles/main/modal/DeleteArticleModal.styl b/browser/styles/main/modal/DeleteArticleModal.styl deleted file mode 100644 index 741224a8..00000000 --- a/browser/styles/main/modal/DeleteArticleModal.styl +++ /dev/null @@ -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%) diff --git a/browser/styles/main/modal/Preferences.styl b/browser/styles/main/modal/Preferences.styl deleted file mode 100644 index 9239f571..00000000 --- a/browser/styles/main/modal/Preferences.styl +++ /dev/null @@ -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() diff --git a/browser/styles/main/modal/modal.styl b/browser/styles/main/modal/modal.styl deleted file mode 100644 index 005ac7b3..00000000 --- a/browser/styles/main/modal/modal.styl +++ /dev/null @@ -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 diff --git a/package.json b/package.json index e11d8bf3..fd60f8e8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/webpack-skeleton.js b/webpack-skeleton.js index 1f46bc49..063a5c01 100644 --- a/webpack-skeleton.js +++ b/webpack-skeleton.js @@ -42,6 +42,7 @@ var config = { 'markdown-it-checkbox', 'season', 'devtron', + 'sander', { react: 'var React', 'react-dom': 'var ReactDOM',