1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

add initialization modal & migration func

This commit is contained in:
Dick Choi
2016-07-23 12:14:01 +09:00
parent f628ed55cf
commit b2d34ab95d
9 changed files with 410 additions and 339 deletions

View File

@@ -10,6 +10,8 @@ import dataApi from 'browser/main/lib/dataApi'
import StatusBar from './StatusBar' import StatusBar from './StatusBar'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' 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 { class Main extends React.Component {
constructor (props) { constructor (props) {
@@ -34,6 +36,10 @@ class Main extends React.Component {
storages: data.storages, storages: data.storages,
notes: data.notes notes: data.notes
}) })
if (data.storages.length < 1) {
modal.open(InitModal)
}
}) })
} }

View File

@@ -22,7 +22,7 @@ const defaultConfig = {
fontFamily: 'Monaco, Consolas', fontFamily: 'Monaco, Consolas',
indentType: 'space', indentType: 'space',
indentSize: '4', indentSize: '4',
switchPreview: 'RIGHTCLICK' switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
}, },
preview: { preview: {
fontSize: '14', fontSize: '14',

View File

@@ -3,6 +3,7 @@ const CSON = require('season')
const path = require('path') const path = require('path')
const _ = require('lodash') const _ = require('lodash')
const sander = require('sander') const sander = require('sander')
const consts = require('browser/lib/consts')
let storages = [] let storages = []
let notes = [] let notes = []
@@ -67,6 +68,7 @@ class Storage {
let initialStorage = { let initialStorage = {
folders: [] folders: []
} }
return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage)) return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage))
} else throw err } else throw err
}) })
@@ -127,10 +129,11 @@ function init () {
let caches let caches
try { try {
caches = JSON.parse(localStorage.getItem('storages')) caches = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(caches)) throw new Error('Cached data is not valid.')
} catch (e) { } catch (e) {
console.error(e) console.error(e)
caches = [] caches = []
localStorage.getItem('storages', JSON.stringify(caches)) localStorage.setItem('storages', JSON.stringify(caches))
} }
return caches.map((cache) => { return caches.map((cache) => {
@@ -236,15 +239,14 @@ function addStorage (input) {
note.folder = folder.key note.folder = folder.key
_notes.push(Note.forge(note)) _notes.push(Note.forge(note))
}) })
notes = notes.slice().concat(_notes)
}) })
return Promise.all(notes) return Promise.all(_notes)
.then((notes) => { .then((_notes) => {
notes = notes.concat(_notes)
let data = { let data = {
storage: storage, storage: storage,
notes: notes notes: _notes
} }
return isFolderRemoved return isFolderRemoved
? storage.saveData().then(() => data) ? storage.saveData().then(() => data)
@@ -255,6 +257,17 @@ function addStorage (input) {
storages = storages.filter((storage) => storage.key !== data.storage.key) storages = storages.filter((storage) => storage.key !== data.storage.key)
storages.push(data.storage) storages.push(data.storage)
_saveCaches() _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 { return {
storage: data.storage.toJSON(), storage: data.storage.toJSON(),
notes: data.notes.map((note) => note.toJSON()) notes: data.notes.map((note) => note.toJSON())
@@ -269,6 +282,88 @@ function removeStorage (key) {
return Promise.resolve(true) 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) { function createFolder (key, input) {
let storage = _.find(storages, {key: key}) let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.') if (storage == null) throw new Error('Storage doesn\'t exist.')
@@ -441,5 +536,6 @@ export default {
createSnippetNote, createSnippetNote,
updateNote, updateNote,
removeNote, removeNote,
moveNote moveNote,
migrateFromV5
} }

View File

@@ -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: '<html>\n<body>\n<h1 id=\'hello\'>Hello World</h1>\n</body>\n</html>'
},
{
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 <div styleName='root--loading'>
<i styleName='spinner' className='fa fa-spin fa-spinner'/>
<div styleName='loadingMessage'>Preparing initialization...</div>
</div>
}
return (
<div styleName='root'>
<div styleName='header'>
<div styleName='header-title'>Initialize Storage</div>
</div>
<button styleName='closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>Close</button>
<div styleName='body'>
<div styleName='body-welcome'>
Welcome you!
</div>
<div styleName='body-description'>
Boostnote will use this directory as a default storage.
</div>
<div styleName='body-path'>
<input styleName='body-path-input'
placeholder='Select Folder'
value={this.state.path}
onChange={(e) => this.handlePathChange(e)}
/>
<button styleName='body-path-button'
onClick={(e) => this.handlePathBrowseButtonClick(e)}
>
...
</button>
</div>
<div styleName='body-migration'>
<label><input type='checkbox' checked={this.state.migrationRequested} onChange={(e) => this.handleMigrationRequestedChange(e)}/> Migrate old data from the legacy app v0.5</label>
</div>
<div styleName='body-control'>
<button styleName='body-control-createButton'
onClick={(e) => this.handleSubmitButtonClick(e)}
>Let's Go!</button>
</div>
</div>
</div>
)
}
}
InitModal.propTypes = {
}
export default CSSModules(InitModal, styles)

View File

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

View File

@@ -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 (
<div className='NewRepositoryModal'
styleName='root'
>
<div styleName='header'>
<div styleName='header-title'>New Repository</div>
<button styleName='header-closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>
<i className='fa fa-times'/>
</button>
</div>
<div styleName='body'>
<div styleName='body-section'>
<div styleName='body-section-label'>Repository Name</div>
<input styleName='body-section-input'
ref='nameInput'
value={this.state.name}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='body-section'>
<div styleName='body-section-label'>Repository Path</div>
<div styleName={!this.state.isPathSectionFocused ? 'body-section-path' : 'body-section-path--focus'}>
<input styleName='body-section-path-input'
ref='pathInput'
value={this.state.path}
style={styles.body_section_path_input}
onFocus={(e) => this.handlePathFocus(e)}
onBlur={(e) => this.handlePathBlur(e)}
disabled={this.state.isBrowsingPath}
onChange={(e) => this.handleChange(e)}
/>
<button styleName='body-section-path-button'
onClick={(e) => this.handleBrowseButtonClick(e)}
onFocus={(e) => this.handlePathFocus(e)}
onBlur={(e) => this.handlePathBlur(e)}
disabled={this.state.isBrowsingPath}
>
...
</button>
</div>
</div>
{
this.state.error != null && (
<div styleName='body-error'>
{this.state.error}
</div>
)
}
</div>
<div styleName='footer'>
<button styleName='footer-cancelButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>
<i className='fa fa-times'/> Cancel
</button>
<button styleName='footer-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
<i className='fa fa-check'/> Confirm
</button>
</div>
</div>
)
}
}
NewRepositoryModal.propTypes = {
close: PropTypes.func
}
export default CSSModules(NewRepositoryModal, styles)

View File

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

View File

@@ -208,7 +208,7 @@ class StorageItem extends React.Component {
let { storage } = this.props let { storage } = this.props
let input = { let input = {
name: 'Untitled', 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) dataApi.createFolder(storage.key, input)

View File

@@ -44,10 +44,7 @@ function notes (state = [], action) {
return action.notes return action.notes
case 'ADD_STORAGE': case 'ADD_STORAGE':
{ {
let notes = state.slice() let notes = state.concat(action.notes)
notes.concat(action.notes)
return notes return notes
} }
case 'REMOVE_STORAGE': case 'REMOVE_STORAGE':