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
\nHello 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 (
+
+
+
+
+
+
+ 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':