mirror of
https://github.com/BoostIO/BoostNote-App.git
synced 2024-10-04 08:07:41 +03:00
Initial commit
This commit is contained in:
commit
a4cc4901a8
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/*
|
||||
.DS_Store
|
||||
.env
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
*.log
|
34
app/index.js
Normal file
34
app/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const { app, BrowserWindow } = electron
|
||||
const path = require('path')
|
||||
|
||||
let mainWindow = null
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('ready', () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
frame: false
|
||||
})
|
||||
mainWindow.loadURL('file://' + path.join(__dirname, '/main.html'))
|
||||
mainWindow.webContents.on('new-window', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
mainWindow.on('close', (e) => {
|
||||
e.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
app.on('activate', (e) => {
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
})
|
||||
app.on('before-quit', (e) => {
|
||||
mainWindow.removeAllListeners()
|
||||
})
|
||||
})
|
39
app/main.html
Normal file
39
app/main.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Drafter</title>
|
||||
<style>
|
||||
#content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="../node_modules/octicons/build/octicons.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/lib/codemirror.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/mode/meta.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/addon/mode/overlay.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/addon/mode/loadmode.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/keymap/sublime.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/addon/runmode/runmode.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/codemirror/addon/edit/continuelist.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../node_modules/react/dist/react.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/react-dom/dist/react-dom.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/redux/dist/redux.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/react-redux/dist/react-redux.js"></script>
|
||||
<script type="text/javascript" src="../node_modules/immutable/dist/immutable.js"></script>
|
||||
|
||||
<script type="text/javascript" src="http://localhost:8080/assets/main.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
32
docs/database.md
Normal file
32
docs/database.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Database
|
||||
|
||||
## Sequences
|
||||
|
||||
### Initialization
|
||||
|
||||
When app started
|
||||
|
||||
DB 관리
|
||||
|
||||
파일과의 연결처리.
|
||||
항상 기동시 레벨 DB와 덤프파일간에 레플리케이션을 시도한다.
|
||||
파일 워치를 넣어두어서 해당파일이 바뀌면 레플리케이션을 시도한다.
|
||||
|
||||
스토리지 변경후 레플리케이트까지 10초 유예기간
|
||||
|
||||
*.padstorage 확장자
|
||||
|
||||
스토리지 = 데이터베이스
|
||||
작성자는 이메일로 구분한다.
|
||||
공유전에는 이메일을 입력하게한다.
|
||||
입력시 해당이메일을 글로벌 프로필로 사용할지 질문한다.
|
||||
|
||||
디폴트DB는 항상 계속 가져옴
|
||||
|
||||
|
||||
Keys|
|
||||
---|---
|
||||
folder_${path}/path|
|
||||
note_${createdAt}
|
||||
_local/resourcesPath|
|
||||
_local/remotes|
|
71
package.json
Normal file
71
package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "Inpad",
|
||||
"version": "0.1.0",
|
||||
"description": "A simple note app for developer",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "NODE_ENV=development electron app/index.js",
|
||||
"pack": "build --dir",
|
||||
"dist": "build",
|
||||
"webpack": "NODE_ENV=development webpack-dev-server --config webpack.config.js",
|
||||
"rebuild": "electron-rebuild"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"snippet",
|
||||
"note"
|
||||
],
|
||||
"author": "Sarah Seo <sarah.seo.0311@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.18.0",
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-loader": "^6.2.7",
|
||||
"babel-plugin-transform-class-properties": "^6.18.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"electron": "^1.4.5",
|
||||
"electron-builder": "^7.15.2",
|
||||
"electron-devtools-installer": "^2.0.1",
|
||||
"electron-rebuild": "^1.3.0",
|
||||
"file-loader": "^0.9.0",
|
||||
"pouchdb": "^6.0.7",
|
||||
"react-desktop": "^0.2.14",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-router-redux": "^4.0.6",
|
||||
"standard": "^8.5.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack": "^2.1.0-beta",
|
||||
"webpack-dev-server": "^2.1.0-beta"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.20.2",
|
||||
"github-markdown-css": "^2.4.1",
|
||||
"immutable": "^3.8.1",
|
||||
"leveldown": "^1.5.0",
|
||||
"lodash": "^4.16.6",
|
||||
"octicons": "^5.0.1",
|
||||
"react": "^15.3.2",
|
||||
"react-dom": "^15.3.2",
|
||||
"react-redux": "^4.4.5",
|
||||
"redux": "^3.6.0",
|
||||
"sander": "^0.5.1",
|
||||
"styled-components": "^1.0.10"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react",
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties"
|
||||
]
|
||||
}
|
||||
}
|
6
readme.md
Normal file
6
readme.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Inpad
|
||||
|
||||
A simple note app for developer.
|
||||
|
||||
## Resources
|
||||
|
57
src/components/Octicon.js
Normal file
57
src/components/Octicon.js
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import _ from 'lodash'
|
||||
import octicons from 'octicons'
|
||||
|
||||
const pulse = keyframes`
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
`
|
||||
const pulseStyle = `
|
||||
animation: ${pulse} 0.5s linear infinite;
|
||||
`
|
||||
|
||||
const Icon = styled.svg`
|
||||
display: inline-block;
|
||||
line-height: 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
fill: ${(p) => _.isString(p.color) ? p.color : 'inherit'};
|
||||
${(p) => p.pulse ? pulseStyle : ''}
|
||||
`
|
||||
|
||||
class Octicon extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { size, icon } = this.props
|
||||
const octicon = octicons[icon]
|
||||
return (
|
||||
<Icon
|
||||
{...this.props}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={octicon.options.viewBox}
|
||||
dangerouslySetInnerHTML={{__html: octicon.path}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Octicon.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
pulse: PropTypes.bool
|
||||
}
|
||||
|
||||
export default Octicon
|
44
src/main/App.js
Normal file
44
src/main/App.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { Router } from 'react-router'
|
||||
import routes from './routes'
|
||||
|
||||
Router.prototype.componentWillReceiveProps = function (nextProps) {
|
||||
let components = []
|
||||
function grabComponents (element) {
|
||||
if (element.props && element.props.component) {
|
||||
components.push(element.props.component)
|
||||
}
|
||||
if (element.props && element.props.children) {
|
||||
React.Children.forEach(element.props.children, grabComponents)
|
||||
}
|
||||
}
|
||||
grabComponents(nextProps.routes || nextProps.children)
|
||||
components.forEach(React.createElement)
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let { store, history } = this.props
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
{routes}
|
||||
</Router>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
}
|
||||
|
||||
export default App
|
149
src/main/Main.js
Normal file
149
src/main/Main.js
Normal file
@ -0,0 +1,149 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import styled, { ThemeProvider } from 'styled-components'
|
||||
import TitleBar from './TitleBar'
|
||||
import themes from './lib/themes'
|
||||
import Nav from './Nav'
|
||||
import { Map } from 'immutable'
|
||||
import StorageManager from './lib/StorageManager'
|
||||
|
||||
const Root = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const Body = styled.div`
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const Slider = styled.div`
|
||||
position: relative;
|
||||
width: 5px;
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
margin-left: -2px;
|
||||
margin-right: -2px;
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const SliderLine = styled.div`
|
||||
margin-left: 2px;
|
||||
width: 1px;
|
||||
background-color: ${(p) => p.active ? p.theme.activeBorderColor : p.theme.borderColor};
|
||||
`
|
||||
|
||||
const Content = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
class Main extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
navWidth: props.status.get('navWidth'),
|
||||
isSliderActive: false
|
||||
}
|
||||
|
||||
this.handleSliderMouseDown = () => {
|
||||
window.addEventListener('mouseup', this.handleSliderMouseUp)
|
||||
window.addEventListener('mousemove', this.handleSliderMouseMove)
|
||||
this.setState({
|
||||
isSliderActive: true
|
||||
})
|
||||
}
|
||||
|
||||
this.handleSliderMouseMove = (e) => {
|
||||
this.setState({
|
||||
navWidth: e.clientX
|
||||
})
|
||||
}
|
||||
|
||||
this.handleSliderMouseUp = (e) => {
|
||||
window.removeEventListener('mouseup', this.handleSliderMouseUp)
|
||||
window.removeEventListener('mousemove', this.handleSliderMouseMove)
|
||||
|
||||
this.setState({
|
||||
isSliderActive: false,
|
||||
navWidth: e.clientX
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('mouseup', this.handleSliderMouseUp)
|
||||
window.removeEventListener('mousemove', this.handleSliderMouseMove)
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
status: this.props.status
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props
|
||||
StorageManager.init()
|
||||
.then(() => {
|
||||
return StorageManager.loadAll()
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'LOAD_ALL_STORAGES',
|
||||
payload: {
|
||||
storageMap: data
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let { storageMap } = this.props
|
||||
return (
|
||||
<ThemeProvider theme={themes.default}>
|
||||
<Root>
|
||||
|
||||
<TitleBar />
|
||||
|
||||
<Body>
|
||||
<Nav storageMap={storageMap} width={this.state.navWidth} />
|
||||
|
||||
<Slider
|
||||
onMouseDown={this.handleSliderMouseDown}
|
||||
onMouseUp={this.handleSliderMouseUp}
|
||||
>
|
||||
<SliderLine
|
||||
active={this.state.isSliderActive}
|
||||
/>
|
||||
</Slider>
|
||||
|
||||
<Content>
|
||||
{this.props.children}
|
||||
</Content>
|
||||
</Body>
|
||||
|
||||
</Root>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
}
|
||||
|
||||
Main.childContextTypes = {
|
||||
status: PropTypes.instanceOf(Map)
|
||||
}
|
||||
|
||||
export default connect((x) => x)(Main)
|
110
src/main/Nav.js
Normal file
110
src/main/Nav.js
Normal file
@ -0,0 +1,110 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Octicon from 'components/Octicon'
|
||||
import { Map } from 'immutable'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
const Root = styled.div`
|
||||
position: relative;
|
||||
min-width: 150px;
|
||||
width: ${(p) => p.width}px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const StorageSection = styled.div`
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const NavButton = styled.a`
|
||||
${(p) => p.active ? p.theme.navButtonActive : p.theme.navButton}
|
||||
display: block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const FolderButton = styled(NavButton)`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
const BottomButton = styled.button`
|
||||
${(p) => p.theme.navButton}
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
class Nav extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { storageMap } = this.props
|
||||
const { router } = this.context
|
||||
|
||||
const storageList = storageMap
|
||||
.map((data, storageName) => {
|
||||
const folderList = data.folders
|
||||
.map((meta, folderName) => {
|
||||
const folderPath = `/storages/${storageName}/folders/${folderName}`
|
||||
|
||||
return <FolderButton
|
||||
key={folderName}
|
||||
href={'#' + folderPath}
|
||||
active={router.isActive(folderPath)}
|
||||
>
|
||||
{folderName}
|
||||
</FolderButton>
|
||||
})
|
||||
.toArray()
|
||||
const storagePath = `/storages/${storageName}/all-notes`
|
||||
const isStorageActive = router.isActive(storagePath)
|
||||
return <StorageSection
|
||||
key={storageName}
|
||||
>
|
||||
<NavButton
|
||||
href={'#' + storagePath}
|
||||
active={isStorageActive}
|
||||
>
|
||||
<Octicon icon='repo' size={12} color={isStorageActive && 'white'} /> {storageName}
|
||||
</NavButton>
|
||||
{folderList}
|
||||
</StorageSection>
|
||||
})
|
||||
.toArray()
|
||||
|
||||
return (
|
||||
<Root width={this.props.width}>
|
||||
{storageList}
|
||||
<BottomButton>
|
||||
<Octicon icon='plus' /> Add Folder
|
||||
</BottomButton>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Nav.propTypes = {
|
||||
}
|
||||
|
||||
Nav.contextTypes = {
|
||||
router: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
isActive: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
export default Nav
|
120
src/main/TitleBar.js
Normal file
120
src/main/TitleBar.js
Normal file
@ -0,0 +1,120 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TitleBar as MacTitleBar, Toolbar } from 'react-desktop/macOs'
|
||||
import Octicon from 'components/Octicon'
|
||||
|
||||
const { remote } = require('electron')
|
||||
|
||||
const SearchInput = styled.input`
|
||||
${(p) => p.theme.input}
|
||||
-webkit-app-region: no-drag;
|
||||
-webkit-user-select: none;
|
||||
margin: 0 2.5px;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
&:focus {
|
||||
border: ${(p) => p.theme.activeBorder}
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.button`
|
||||
${(p) => p.theme.button}
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 26px;
|
||||
-webkit-app-region: no-drag;
|
||||
-webkit-user-select: none;
|
||||
margin: 0 2.5px;
|
||||
`
|
||||
|
||||
const Seperator = styled.div`
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
height: 26px;
|
||||
`
|
||||
|
||||
const BordedTitleBar = styled(MacTitleBar)`
|
||||
border-bottom: ${(p) => p.theme.border};
|
||||
`
|
||||
|
||||
const Root = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
class TitleBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isFullscreen: false,
|
||||
search: ''
|
||||
}
|
||||
|
||||
this.handleChange = (e) => {
|
||||
this.setState({
|
||||
search: e.target.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseClick = () => {
|
||||
remote.getCurrentWindow().close()
|
||||
}
|
||||
|
||||
handleResizeClick () {
|
||||
let currentWindow = remote.getCurrentWindow()
|
||||
let isFullscreen = currentWindow.isFullScreen()
|
||||
|
||||
currentWindow.setFullScreen(!isFullscreen)
|
||||
|
||||
this.setState({
|
||||
isFullscreen: !isFullscreen
|
||||
})
|
||||
}
|
||||
|
||||
handleMinimizeClick = () => {
|
||||
remote.getCurrentWindow().minimize()
|
||||
}
|
||||
|
||||
handleMaximizeClick = () => {
|
||||
remote.getCurrentWindow().maximize()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Root>
|
||||
<BordedTitleBar
|
||||
inset
|
||||
controls
|
||||
transparent
|
||||
isFullscreen={this.state.isFullscreen}
|
||||
onCloseClick={this.handleCloseClick}
|
||||
onMinimizeClick={this.handleMinimizeClick}
|
||||
onMaximizeClick={this.handleMaximizeClick}
|
||||
onResizeClick={this.handleResizeClick.bind(this)}
|
||||
>
|
||||
<Toolbar height='36' horizontalAlignment='center'>
|
||||
<SearchInput
|
||||
placeholder='Search...'
|
||||
value={this.state.search}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<Button>
|
||||
<Octicon icon='plus' />
|
||||
</Button>
|
||||
<Seperator />
|
||||
<Button>
|
||||
<Octicon icon='settings' />
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</BordedTitleBar>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TitleBar.propTypes = {
|
||||
}
|
||||
|
||||
export default TitleBar
|
117
src/main/contents/NoteList/index.js
Normal file
117
src/main/contents/NoteList/index.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
import { Map } from 'immutable'
|
||||
import Octicon from 'components/Octicon'
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
`
|
||||
|
||||
const Left = styled.div`
|
||||
width: ${(p) => p.width}px;
|
||||
min-width: 150px;
|
||||
`
|
||||
|
||||
const LeftMenu = styled.div`
|
||||
border-bottom: ${(p) => p.theme.border}
|
||||
`
|
||||
|
||||
const LeftList = styled.div`
|
||||
|
||||
`
|
||||
|
||||
const LeftListItem = styled.div`
|
||||
|
||||
`
|
||||
|
||||
const Slider = styled.div`
|
||||
position: relative;
|
||||
width: 5px;
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
margin-left: -2px;
|
||||
margin-right: -2px;
|
||||
`
|
||||
|
||||
const SliderLine = styled.div`
|
||||
margin-left: 2px;
|
||||
width: 1px;
|
||||
background-color: ${(p) => p.active ? p.theme.activeBorderColor : p.theme.borderColor};
|
||||
`
|
||||
|
||||
const Detail = styled.div`
|
||||
`
|
||||
|
||||
class NoteList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
listWidth: props.status.get('noteListWidth')
|
||||
}
|
||||
|
||||
this.handleSliderMouseDown = () => {
|
||||
window.addEventListener('mouseup', this.handleSliderMouseUp)
|
||||
window.addEventListener('mousemove', this.handleSliderMouseMove)
|
||||
this.setState({
|
||||
isSliderActive: true
|
||||
})
|
||||
}
|
||||
|
||||
this.handleSliderMouseMove = (e) => {
|
||||
this.setState({
|
||||
listWidth: e.clientX - this.props.status.get('navWidth')
|
||||
})
|
||||
}
|
||||
|
||||
this.handleSliderMouseUp = (e) => {
|
||||
window.removeEventListener('mouseup', this.handleSliderMouseUp)
|
||||
window.removeEventListener('mousemove', this.handleSliderMouseMove)
|
||||
|
||||
this.setState({
|
||||
isSliderActive: false,
|
||||
listWidth: e.clientX - this.props.status.get('navWidth')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Root>
|
||||
<Left width={this.state.listWidth}>
|
||||
<LeftMenu>
|
||||
Sort By <select/>
|
||||
<button><Octicon icon='grabber' size='12' /></button>
|
||||
<button><Octicon icon='three-bars' size='12' /></button>
|
||||
</LeftMenu>
|
||||
<LeftList>
|
||||
</LeftList>
|
||||
</Left>
|
||||
<Slider
|
||||
onMouseDown={this.handleSliderMouseDown}
|
||||
onMouseUp={this.handleSliderMouseUp}
|
||||
>
|
||||
<SliderLine
|
||||
active={this.state.isSliderActive}
|
||||
/>
|
||||
</Slider>
|
||||
<Detail />
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NoteList.propTypes = {
|
||||
}
|
||||
|
||||
NoteList.contextTypes = {
|
||||
status: PropTypes.instanceOf(Map)
|
||||
}
|
||||
|
||||
export default connect((x) => x)(NoteList)
|
66
src/main/contents/StorageCreate.js
Normal file
66
src/main/contents/StorageCreate.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Octicon from 'components/Octicon'
|
||||
|
||||
const Root = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
`
|
||||
|
||||
class StorageCreate extends React.Component {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
isSaving: false
|
||||
}
|
||||
}
|
||||
|
||||
handleNameChange = (e) => {
|
||||
this.setState({
|
||||
name: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmClick = (e) => {
|
||||
this.setState({
|
||||
isSaving: true
|
||||
}, () => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Root>
|
||||
<div>Add Storage</div>
|
||||
<input
|
||||
value={this.state.name}
|
||||
onChange={this.handleNameChange}
|
||||
/>
|
||||
<div>
|
||||
<button onClick={this.handleConfirmClick}>
|
||||
{this.state.isSaving
|
||||
? <span>
|
||||
<Octicon icon='pulse' pulse /> Saving...
|
||||
</span>
|
||||
: <span>
|
||||
<Octicon icon='check' /> Confirm
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageCreate.propTypes = {
|
||||
}
|
||||
|
||||
export default StorageCreate
|
22
src/main/contents/StorageIndex.js
Normal file
22
src/main/contents/StorageIndex.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
class StorageIndex extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StorageIndex.propTypes = {
|
||||
}
|
||||
|
||||
export default StorageIndex
|
48
src/main/index.js
Normal file
48
src/main/index.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { AppContainer } from 'react-hot-loader'
|
||||
import App from './App'
|
||||
import store from './lib/redux/store'
|
||||
import { hashHistory } from 'react-router'
|
||||
import { syncHistoryWithStore } from 'react-router-redux'
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
installExtension(REACT_DEVELOPER_TOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
|
||||
document.addEventListener('drop', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
document.addEventListener('dragover', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
const history = syncHistoryWithStore(hashHistory, store)
|
||||
|
||||
let el = document.getElementById('content')
|
||||
|
||||
ReactDOM.render((
|
||||
<AppContainer>
|
||||
<App store={store} history={history} />
|
||||
</AppContainer>
|
||||
), el, function () {
|
||||
|
||||
})
|
||||
|
||||
// 강제적으로 App을 다시 불러와서 새롭게 렌더링합니다.
|
||||
// 고로 App을 바깥으로 빼둘 필요가 있습니다.
|
||||
if (module.hot) {
|
||||
module.hot.accept('./App', () => {
|
||||
let NextApp = require('./App').default
|
||||
ReactDOM.render((
|
||||
<AppContainer>
|
||||
<NextApp store={store} history={history} />
|
||||
</AppContainer>
|
||||
), el)
|
||||
})
|
||||
}
|
168
src/main/lib/StorageManager/index.js
Normal file
168
src/main/lib/StorageManager/index.js
Normal file
@ -0,0 +1,168 @@
|
||||
const sander = require('sander')
|
||||
const path = require('path')
|
||||
const PouchDB = require('pouchdb')
|
||||
const { OrderedMap, Map } = require('immutable')
|
||||
const util = require('../util')
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
const storagesPath = path.join(remote.app.getPath('userData'), 'storages')
|
||||
|
||||
let dbs
|
||||
|
||||
/**
|
||||
* Initialize db connection
|
||||
* If nothing is found, add a new connection
|
||||
*
|
||||
* @return {OrderedMap} All DB connections
|
||||
*/
|
||||
export function init () {
|
||||
return sander.readdir(storagesPath)
|
||||
// If `storages` doesn't exist, create it.
|
||||
.catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return sander.mkdir(storagesPath)
|
||||
.then(() => [])
|
||||
} else throw err
|
||||
})
|
||||
// If `storages/default` doesn't exist, create it.
|
||||
.then(function (dirNames) {
|
||||
if (!dirNames.some((dirName) => dirName === 'notebook')) {
|
||||
return sander.mkdir(storagesPath, 'notebook')
|
||||
.then(() => dirNames.push('notebook'))
|
||||
}
|
||||
return dirNames
|
||||
})
|
||||
.then(function initPouchDBs (dirNames) {
|
||||
dbs = dirNames.reduce(function (map, name) {
|
||||
return map.set(name, new PouchDB(path.join(storagesPath, name)))
|
||||
}, new OrderedMap())
|
||||
|
||||
return dbs
|
||||
})
|
||||
}
|
||||
|
||||
export function list () {
|
||||
if (dbs == null) return init()
|
||||
return Promise.resolve(new OrderedMap(dbs))
|
||||
}
|
||||
|
||||
/**
|
||||
* load dataMap from a storage
|
||||
*
|
||||
* @param {String} name [description]
|
||||
* @return {Map} return data map of a Storage
|
||||
* including `notes` and `folders` field
|
||||
*/
|
||||
export function load (name) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
|
||||
return db
|
||||
.allDocs({include_docs: true})
|
||||
.then((docs) => {
|
||||
return {
|
||||
notes: new Map([]),
|
||||
folders: new Map([['Notes', {}]])
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* load dataMaps from all storages and map them
|
||||
*
|
||||
* @return {OrderedMap} Data Map of all storages
|
||||
*/
|
||||
export function loadAll () {
|
||||
const promises = dbs
|
||||
.keySeq()
|
||||
.map((name) => {
|
||||
return load(name)
|
||||
// struct tuple
|
||||
.then((dataMap) => [name, dataMap])
|
||||
})
|
||||
// Promise.all only understands array
|
||||
.toArray()
|
||||
|
||||
return Promise.all(promises)
|
||||
// destruct tuple
|
||||
.then((storageMap) => new OrderedMap(storageMap))
|
||||
}
|
||||
|
||||
export function upsertFolder (name, folderPath) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
return db
|
||||
.put({
|
||||
_id: 'folder:' + folderPath
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteFolder (name, folderPath) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
return db
|
||||
.put({
|
||||
_id: 'folder:' + folderPath,
|
||||
_deleted: true
|
||||
})
|
||||
}
|
||||
|
||||
export function createNote (name, payload) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
|
||||
function genNoteId () {
|
||||
let id = 'note:' + util.randomBytes()
|
||||
return db.get(id)
|
||||
.then((doc) => {
|
||||
if (doc == null) return id
|
||||
return genNoteId()
|
||||
})
|
||||
}
|
||||
|
||||
return genNoteId()
|
||||
.then((noteId) => {
|
||||
return db
|
||||
.put({}, payload, {
|
||||
_id: noteId
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function updateNote (name, noteId, payload) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
|
||||
return db.get(noteId)
|
||||
.then((doc) => {
|
||||
return db
|
||||
.put({}, doc, payload, {
|
||||
_id: doc._id,
|
||||
_rev: doc._rev
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteNote (name, noteId) {
|
||||
const db = dbs.get(name)
|
||||
if (db == null) return Promise.reject(new Error('DB doesn\'t exist.'))
|
||||
|
||||
return db.get(noteId)
|
||||
.then((doc) => {
|
||||
return db
|
||||
.remove(doc)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
list,
|
||||
load,
|
||||
loadAll,
|
||||
upsertFolder,
|
||||
deleteFolder,
|
||||
createNote,
|
||||
updateNote,
|
||||
deleteNote
|
||||
}
|
40
src/main/lib/redux/store.js
Normal file
40
src/main/lib/redux/store.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { combineReducers, createStore } from 'redux'
|
||||
import { routerReducer } from 'react-router-redux'
|
||||
import { Map, OrderedMap } from 'immutable'
|
||||
function config (state = {}, action) {
|
||||
return state
|
||||
}
|
||||
|
||||
const defaultStatus = Map({
|
||||
navWidth: 150,
|
||||
noteListWidth: 200
|
||||
})
|
||||
|
||||
function status (state = defaultStatus, action) {
|
||||
// switch (action.type) {
|
||||
// case 'UPDATE_STATUS':
|
||||
// return Object.assign({}, state, action.payload)
|
||||
// }
|
||||
return state
|
||||
}
|
||||
|
||||
const defaultStorageMap = OrderedMap()
|
||||
|
||||
function storageMap (state = defaultStorageMap, action) {
|
||||
switch (action.type) {
|
||||
case 'LOAD_ALL_STORAGES':
|
||||
return action.payload.storageMap
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
let reducer = combineReducers({
|
||||
config,
|
||||
status,
|
||||
storageMap,
|
||||
routing: routerReducer
|
||||
})
|
||||
|
||||
let store = createStore(reducer)
|
||||
|
||||
export default store
|
68
src/main/lib/themes.js
Normal file
68
src/main/lib/themes.js
Normal file
@ -0,0 +1,68 @@
|
||||
const defaultUIColor = '#333'
|
||||
const defaultUIActiveColor = '#4F9DFB'
|
||||
const defaultBorderColor = '#DEDCDE'
|
||||
const defaultUIFontSize = '12px'
|
||||
const defaultUIFontFamily = 'Helvetica, Arial, sans-serif'
|
||||
|
||||
const defaultTheme = {
|
||||
borderColor: defaultBorderColor,
|
||||
border: 'solid 1px ' + defaultBorderColor,
|
||||
activeBorderColor: defaultUIActiveColor,
|
||||
activeBorder: 'solid 1px ' + defaultUIActiveColor,
|
||||
input: `
|
||||
border: solid 1px
|
||||
${defaultBorderColor};
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
background-color: #FCFCFC;
|
||||
color: ${defaultUIColor};
|
||||
font-size: ${defaultUIFontSize};
|
||||
font-family: ${defaultUIFontFamily};
|
||||
`,
|
||||
button: `
|
||||
border: solid 1px
|
||||
${defaultBorderColor};
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
background-color: #FCFCFC;
|
||||
color: ${defaultUIColor};
|
||||
font-size: ${defaultUIFontSize};
|
||||
font-family: ${defaultUIFontFamily};
|
||||
&:active {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
`,
|
||||
navButton: `
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
text-align: left;
|
||||
color: ${defaultUIColor};
|
||||
text-decoration: none;
|
||||
font-size: ${defaultUIFontSize};
|
||||
font-family: ${defaultUIFontFamily};
|
||||
&:hover {
|
||||
background-color: #EEE;
|
||||
}
|
||||
&:active {
|
||||
background-color: #DCDCDC;
|
||||
}
|
||||
`,
|
||||
navButtonActive: `
|
||||
display: block;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
font-size: ${defaultUIFontSize};
|
||||
font-family: ${defaultUIFontFamily};
|
||||
background-color: ${defaultUIActiveColor};
|
||||
color: white;
|
||||
`
|
||||
}
|
||||
|
||||
export default {
|
||||
default: defaultTheme
|
||||
}
|
17
src/main/lib/util.js
Normal file
17
src/main/lib/util.js
Normal file
@ -0,0 +1,17 @@
|
||||
const crypto = require('crypto')
|
||||
const _ = require('lodash')
|
||||
|
||||
const defaultLength = 10
|
||||
|
||||
export function randomBytes (length = defaultLength) {
|
||||
if (!_.isFinite(length)) length = defaultLength
|
||||
return crypto.randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
const util = {
|
||||
randomBytes
|
||||
}
|
||||
|
||||
export default util
|
||||
|
||||
module.exports = util
|
27
src/main/routes.js
Normal file
27
src/main/routes.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import { Route, IndexRedirect } from 'react-router'
|
||||
|
||||
import Main from './Main'
|
||||
import StorageIndex from './contents/StorageIndex'
|
||||
import NoteList from './contents/NoteList'
|
||||
import StorageCreate from './contents/StorageCreate'
|
||||
|
||||
const routes = (
|
||||
<Route path='/' component={Main}>
|
||||
|
||||
<IndexRedirect to='home' />
|
||||
<Route path='home' component={NoteList} />
|
||||
|
||||
<Route path='storages/:storageId'>
|
||||
<IndexRedirect to='all-notes' />
|
||||
<Route path='all-notes' component={NoteList} />
|
||||
<Route path='settings' component={StorageIndex} />
|
||||
<Route path='folders/:folderId' component={NoteList} />
|
||||
</Route>
|
||||
|
||||
<Route path='new-storage' component={StorageCreate} />
|
||||
|
||||
</Route>
|
||||
)
|
||||
|
||||
export default routes
|
72
webpack.config.js
Normal file
72
webpack.config.js
Normal file
@ -0,0 +1,72 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin')
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
main: [
|
||||
'react-hot-loader/patch',
|
||||
'webpack-dev-server/client?http://localhost:8080',
|
||||
'./src/main/index.js'
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx'],
|
||||
alias: {
|
||||
'components': path.join(__dirname, 'src/components'),
|
||||
'main': path.join(__dirname, 'src/main')
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new NodeTargetPlugin()
|
||||
],
|
||||
externals: [
|
||||
'electron',
|
||||
'styled-components',
|
||||
'pouchdb',
|
||||
'sander',
|
||||
'electron-devtools-installer',
|
||||
'octicons',
|
||||
{
|
||||
react: 'var React',
|
||||
'react-dom': 'var ReactDOM',
|
||||
'react-redux': 'var ReactRedux',
|
||||
'redux': 'var Redux',
|
||||
'immutable': 'var Immutable'
|
||||
}
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'react-hot-loader/webpack'
|
||||
},
|
||||
{
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
],
|
||||
include: path.join(__dirname, 'src')
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'compiled'),
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[name].map',
|
||||
libraryTarget: 'commonjs2',
|
||||
publicPath: 'http://localhost:8080/assets/'
|
||||
},
|
||||
devtool: 'eval',
|
||||
devServer: {
|
||||
hot: true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
Loading…
Reference in New Issue
Block a user