diff --git a/browser/main/Main.js b/browser/main/Main.js index 33e770a0..8c688833 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -10,6 +10,8 @@ import dataApi from 'browser/main/lib/dataApi' import StatusBar from './StatusBar' import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' +import modal from 'browser/main/lib/modal' +import InitModal from 'browser/main/modals/InitModal' class Main extends React.Component { constructor (props) { @@ -34,6 +36,10 @@ class Main extends React.Component { storages: data.storages, notes: data.notes }) + + if (data.storages.length < 1) { + modal.open(InitModal) + } }) } diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index b353a043..075ac8e1 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -22,7 +22,7 @@ const defaultConfig = { fontFamily: 'Monaco, Consolas', indentType: 'space', indentSize: '4', - switchPreview: 'RIGHTCLICK' + switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR }, preview: { fontSize: '14', diff --git a/browser/main/lib/dataApi.js b/browser/main/lib/dataApi.js index 010e03d4..8c475f07 100644 --- a/browser/main/lib/dataApi.js +++ b/browser/main/lib/dataApi.js @@ -3,6 +3,7 @@ const CSON = require('season') const path = require('path') const _ = require('lodash') const sander = require('sander') +const consts = require('browser/lib/consts') let storages = [] let notes = [] @@ -67,6 +68,7 @@ class Storage { let initialStorage = { folders: [] } + return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage)) } else throw err }) @@ -127,10 +129,11 @@ function init () { let caches try { caches = JSON.parse(localStorage.getItem('storages')) + if (!_.isArray(caches)) throw new Error('Cached data is not valid.') } catch (e) { console.error(e) caches = [] - localStorage.getItem('storages', JSON.stringify(caches)) + localStorage.setItem('storages', JSON.stringify(caches)) } return caches.map((cache) => { @@ -236,15 +239,14 @@ function addStorage (input) { note.folder = folder.key _notes.push(Note.forge(note)) }) - - notes = notes.slice().concat(_notes) }) - return Promise.all(notes) - .then((notes) => { + return Promise.all(_notes) + .then((_notes) => { + notes = notes.concat(_notes) let data = { storage: storage, - notes: notes + notes: _notes } return isFolderRemoved ? storage.saveData().then(() => data) @@ -255,6 +257,17 @@ function addStorage (input) { storages = storages.filter((storage) => storage.key !== data.storage.key) storages.push(data.storage) _saveCaches() + + if (data.storage.data.folders.length < 1) { + return createFolder(data.storage.key, { + name: 'Default', + color: consts.FOLDER_COLORS[0] + }).then(() => data) + } + + return data + }) + .then((data) => { return { storage: data.storage.toJSON(), notes: data.notes.map((note) => note.toJSON()) @@ -269,6 +282,88 @@ function removeStorage (key) { return Promise.resolve(true) } +function migrateFromV5 (key, data) { + let oldFolders = data.folders + let oldArticles = data.articles + let storage = _.find(storages, {key: key}) + if (storage == null) throw new Error('Storage doesn\'t exist.') + + let migrateFolders = oldFolders.map((oldFolder) => { + let folderKey = keygen() + while (storage.data.folders.some((folder) => folder.key === folderKey)) { + folderKey = keygen() + } + let newFolder = { + key: folderKey, + name: oldFolder.name, + color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] + } + storage.data.folders.push(newFolder) + let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key) + let folderNotes = [] + articles.forEach((article) => { + let noteKey = keygen() + while (notes.some((note) => note.storage === key && note.folder === folderKey && note.key === noteKey)) { + key = keygen() + } + if (article.mode === 'markdown') { + let newNote = new Note({ + tags: article.tags, + createdAt: article.createdAt, + updatedAt: article.updatedAt, + folder: folderKey, + storage: key, + type: 'MARKDOWN_NOTE', + isStarred: false, + title: article.title, + content: '# ' + article.title + '\n\n' + article.content, + key: noteKey + }) + notes.push(newNote) + folderNotes.push(newNote) + } else { + let newNote = new Note({ + tags: article.tags, + createdAt: article.createdAt, + updatedAt: article.updatedAt, + folder: folderKey, + storage: key, + type: 'SNIPPET_NOTE', + isStarred: false, + title: article.title, + description: article.title, + key: noteKey, + snippets: [{ + name: article.mode, + mode: article.mode, + content: article.content + }] + }) + notes.push(newNote) + folderNotes.push(newNote) + } + }) + + return sander + .writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({ + notes: folderNotes.map((note) => { + let json = note.toJSON() + delete json.storage + return json + }) + })) + }) + return Promise.all(migrateFolders) + .then(() => storage.saveData()) + .then(() => { + return { + storage: storage.toJSON(), + notes: notes.filter((note) => note.storage === key) + .map((note) => note.toJSON()) + } + }) +} + function createFolder (key, input) { let storage = _.find(storages, {key: key}) if (storage == null) throw new Error('Storage doesn\'t exist.') @@ -441,5 +536,6 @@ export default { createSnippetNote, updateNote, removeNote, - moveNote + moveNote, + migrateFromV5 } diff --git a/browser/main/modals/InitModal.js b/browser/main/modals/InitModal.js new file mode 100644 index 00000000..739f3e5f --- /dev/null +++ b/browser/main/modals/InitModal.js @@ -0,0 +1,212 @@ +import React, { PropTypes } from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './InitModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import store from 'browser/main/store' +import { hashHistory } from 'react-router' +import _ from 'lodash' + +const CSON = require('season') +const path = require('path') +const electron = require('electron') +const { remote } = electron + +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 InitModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + path: path.join(remote.app.getPath('home'), 'Boostnote'), + migrationRequested: true, + isLoading: true, + data: null, + legacyStorageExists: false + } + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handlePathChange (e) { + this.setState({ + path: e.target.value + }) + } + + componentDidMount () { + let data = null + try { + data = CSON.readFileSync(path.join(remote.app.getPath('userData'), 'local.json')) + } catch (err) { + if (err.code === 'ENOENT') { + return + } + console.error(err) + } + let newState = { + isLoading: false + } + if (data != null) { + newState.legacyStorageExists = true + newState.data = data + } + this.setState(newState) + } + + handlePathBrowseButtonClick (e) { + browseFolder() + .then((targetPath) => { + if (targetPath.length > 0) { + this.setState({ + path: targetPath + }) + } + }) + .catch((err) => { + console.error('BrowseFAILED') + console.error(err) + }) + } + + handleSubmitButtonClick (e) { + dataApi + .addStorage({ + name: 'My Storage', + path: this.state.path + }) + .then((data) => { + if (this.state.migrationRequested && _.isObject(this.state.data) && _.isArray(this.state.data.folders) && _.isArray(this.state.data.articles)) { + return dataApi.migrateFromV5(data.storage.key, this.state.data) + } + return data + }) + .then((data) => { + store.dispatch({ + type: 'ADD_STORAGE', + storage: data.storage, + notes: data.notes + }) + + let defaultMarkdownNote = dataApi + .createMarkdownNote(data.storage.key, data.storage.folders[0].key, { + title: 'Welcome to Boostnote :)', + content: '# Welcome to Boostnote :)\nThis is a markdown note.\n\nClick to edit this note.' + }) + .then((note) => { + store.dispatch({ + type: 'CREATE_NOTE', + note: note + }) + }) + let defaultSnippetNote = dataApi + .createSnippetNote(data.storage.key, data.storage.folders[0].key, { + title: 'Snippet note example', + description: 'Snippet note example\nYou can store a series of snippet as a single note like Gist.', + snippets: [ + { + name: 'example.html', + mode: 'html', + content: '\n\n

Hello World

\n\n' + }, + { + name: 'example.js', + mode: 'javascript', + content: 'var html = document.getElementById(\'hello\').innerHTML\n\nconsole.log(html)' + } + ] + }) + .then((note) => { + store.dispatch({ + type: 'CREATE_NOTE', + note: note + }) + }) + + return Promise.resolve(defaultSnippetNote) + .then(defaultMarkdownNote) + .then(() => data.storage) + }) + .then((storage) => { + hashHistory.push('/storages/' + storage.key) + this.props.close() + }) + } + + handleMigrationRequestedChange (e) { + this.setState({ + migrationRequested: e.target.checked + }) + } + + render () { + if (this.state.isLoading) { + return
+ +
Preparing initialization...
+
+ } + return ( +
+ +
+
Initialize Storage
+
+ +
+
+ Welcome you! +
+
+ Boostnote will use this directory as a default storage. +
+
+ this.handlePathChange(e)} + /> + +
+
+ +
+ +
+ +
+
+ +
+ ) + } +} + +InitModal.propTypes = { +} + +export default CSSModules(InitModal, styles) diff --git a/browser/main/modals/InitModal.styl b/browser/main/modals/InitModal.styl new file mode 100644 index 00000000..7c612f10 --- /dev/null +++ b/browser/main/modals/InitModal.styl @@ -0,0 +1,86 @@ +.root + modal() + max-width 540px + overflow hidden + position relative +.root--loading + @extend .root + text-align center +.spinner + font-size 100px + margin 35px auto + color $ui-text-color +.loadingMessage + color $ui-text-color + margin 15px auto 35px +.header + height 50px + font-size 18px + line-height 50px + padding 0 15px + background-color $ui-backgroundColor + border-bottom solid 1px $ui-borderColor + color $ui-text-color + +.closeButton + position absolute + top 10px + right 10px + height 30px + width 0 25px + border $ui-border + border-radius 2px + color $ui-text-color + colorDefaultButton() + +.body + padding 30px + +.body-welcome + text-align center + margin-bottom 25px + font-size 32px + color $ui-text-color + +.body-description + font-size 14px + color $ui-text-color + text-align center + margin-bottom 25px + +.body-path + margin 0 auto 25px + width 280px + +.body-path-input + height 30px + vertical-align middle + width 250px + 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 + +.body-path-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle +.body-migration + margin 0 auto 25px + text-align center + +.body-control + text-align center + +.body-control-createButton + colorPrimaryButton() + border none + border-radius 2px + height 40px + padding 0 25px diff --git a/browser/main/modals/NewRepositoryModal.js b/browser/main/modals/NewRepositoryModal.js deleted file mode 100644 index b777278a..00000000 --- a/browser/main/modals/NewRepositoryModal.js +++ /dev/null @@ -1,193 +0,0 @@ -import React, { PropTypes } from 'react' -import CSSModules from 'browser/lib/CSSModules' -import styles from './NewRepositoryModal.styl' -import Repository from 'browser/lib/Repository' -import store from 'browser/main/store' - -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 NewRepositoryModal extends React.Component { - constructor (props) { - super(props) - - this.state = { - name: '', - path: '', - isPathSectionFocused: false, - error: null, - isBrowsingPath: false - } - } - - componentDidMount () { - this.refs.nameInput.focus() - } - - handleCloseButtonClick (e) { - this.props.close() - } - - handlePathFocus (e) { - this.setState({ - isPathSectionFocused: true - }) - } - - handlePathBlur (e) { - if (e.relatedTarget !== this.refs.pathInput && e.relatedTarget !== this.refs.browseButton) { - this.setState({ - isPathSectionFocused: false - }) - } - } - - handleBrowseButtonClick (e) { - this.setState({ - isBrowsingPath: true - }, () => { - browseFolder() - .then((targetPath) => { - this.setState({ - path: targetPath, - isBrowsingPath: false - }) - }) - .catch((err) => { - console.error('BrowseFAILED') - console.error(err) - this.setState({ - isBrowsingPath: false - }) - }) - }) - } - - handleConfirmButtonClick (e) { - let targetPath = this.state.path - let name = this.state.name - - let repository = new Repository({ - name: name, - path: targetPath - }) - - repository - .mount() - .then(() => repository.load()) - .then((data) => { - store.dispatch({ - type: 'ADD_REPOSITORY', - repository: data - }) - this.props.close() - }) - .catch((err) => { - console.error(err) - this.setState({ - error: err.message - }) - }) - } - - handleChange (e) { - let name = this.refs.nameInput.value - let path = this.refs.pathInput.value - this.setState({ - name, - path - }) - } - - render () { - return ( -
-
-
New Repository
- -
-
-
-
Repository Name
- this.handleChange(e)} - /> -
- -
-
Repository Path
-
- this.handlePathFocus(e)} - onBlur={(e) => this.handlePathBlur(e)} - disabled={this.state.isBrowsingPath} - onChange={(e) => this.handleChange(e)} - /> - -
-
- { - this.state.error != null && ( -
- {this.state.error} -
- ) - } -
- -
- - -
-
- ) - } -} - -NewRepositoryModal.propTypes = { - close: PropTypes.func -} - -export default CSSModules(NewRepositoryModal, styles) diff --git a/browser/main/modals/NewRepositoryModal.styl b/browser/main/modals/NewRepositoryModal.styl deleted file mode 100644 index 9f555f9c..00000000 --- a/browser/main/modals/NewRepositoryModal.styl +++ /dev/null @@ -1,133 +0,0 @@ -$modal-width = 550px -$modal-header-color = #F2F2F2 -$body-button-background-color = #2BAC8F - -.root - modal() - width $modal-width - height 310px - -.header - height 50px - background-color $modal-header-color - -.header-title - font-size 24px - line-height 50px - padding-left 15px - -.header-closeButton - position absolute - top 8.5px - right 8.5px - width 33px - height 33px - font-size 20px - background-color transparent - border none - color #AAA - &:hover - color #4D4D4D - -.body - absolute left right - top 50px - bottom 50px - padding 35px 0 - -.body-section - height 33px - margin-bottom 15px - position relative - -.body-section-label - absolute top bottom left - width 175px - text-align right - line-height 33px - padding-right 15px - -.body-section-input - absolute top bottom - left 175px - width 315px - padding 0 10px - border $default-border - border-radius 5px - outline none - &:focus - border $active-border - -.body-section-path - absolute top bottom - left 175px - width 315px - padding 0 10px - border $default-border - border-radius 5px - outline none - overflow hidden - -.body-section-path--focus - @extend .body-section-path - border $active-border - -.body-section-path-input - absolute top left bottom - width 265px - border none - outline none - padding 0 10px - -.body-section-path-button - absolute top right bottom - width 50px - border none - border-left $default-border - outline none - color white - background-color $body-button-background-color - transition 0.15s - &:hover - background-color lighten($body-button-background-color, 7%) - &:disabled - background-color lighten(gray, 15%) - -.body-error - height 33px - margin 35px auto 0 - width 320px - border-radius 5px - text-align center - line-height 33px - color $danger-color - background-color $danger-lighten-color - -.footer - absolute left right bottom - height 50px - -.footer-cancelButton - position absolute - height 33px - right 85.5px - width 72px - top 8.5px - border-radius 5px - border $default-border - background-color darken(white, 0.03) - &:hover - background-color white - -.footer-confirmButton - position absolute - height 33px - right 8.5px - width 72px - top 8.5px - color white - border-radius 5px - border none - background-color #2BAC8F - &:hover - background-color lighten($body-button-background-color, 7%) diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index 292095ed..42f2f73b 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -208,7 +208,7 @@ class StorageItem extends React.Component { let { storage } = this.props let input = { name: 'Untitled', - color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7)] + color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] } dataApi.createFolder(storage.key, input) diff --git a/browser/main/store.js b/browser/main/store.js index 85dac683..2105ceec 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -44,10 +44,7 @@ function notes (state = [], action) { return action.notes case 'ADD_STORAGE': { - let notes = state.slice() - - notes.concat(action.notes) - + let notes = state.concat(action.notes) return notes } case 'REMOVE_STORAGE':