1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00
This commit is contained in:
Dick Choi
2016-08-14 01:34:32 +09:00
parent 6bb78d3216
commit 2cbe07b373
34 changed files with 1482 additions and 765 deletions

View File

@@ -1,44 +0,0 @@
import React, { PropTypes } from 'react'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import ModeIcon from 'browser/components/ModeIcon'
export default class FinderDetail extends React.Component {
render () {
let { activeArticle } = this.props
if (activeArticle != null) {
return (
<div className='FinderDetail'>
<div className='header'>
<div className='left'>
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
</div>
<div className='right'>
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
<i className='fa fa-clipboard fa-fw'/>
<span className='tooltip'>Copy to clipboard (Enter)</span>
</button>
</div>
</div>
<div className='content'>
{activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/>
: <CodeEditor readOnly article={activeArticle}/>
}
</div>
</div>
)
}
return (
<div className='FinderDetail'>
<div className='nothing'>Nothing selected</div>
</div>
)
}
}
FinderDetail.propTypes = {
activeArticle: PropTypes.shape(),
saveToClipboard: PropTypes.func
}

View File

@@ -1,16 +0,0 @@
import React, { PropTypes } from 'react'
export default class FinderInput extends React.Component {
render () {
return (
<div className='FinderInput'>
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
</div>
)
}
}
FinderInput.propTypes = {
handleSearchChange: PropTypes.func,
value: PropTypes.string
}

View File

@@ -1,71 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import { selectArticle } from './actions'
export default class FinderList extends React.Component {
componentDidUpdate () {
var index = this.props.articles.indexOf(this.props.activeArticle)
var el = ReactDOM.findDOMNode(this)
var li = el.querySelectorAll('li')[index]
if (li == null) {
return
}
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
if (overflowBelow) {
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
}
var overflowAbove = el.scrollTop > li.offsetTop
if (overflowAbove) {
el.scrollTop = li.offsetTop
}
}
handleArticleClick (article) {
return (e) => {
let { dispatch } = this.props
dispatch(selectArticle(article.key))
}
}
render () {
let articleElements = this.props.articles.map(function (article) {
if (article == null) {
return (
<li className={isActive ? 'active' : ''}>
<div className='articleItem'>Undefined</div>
<div className='divider'/>
</li>
)
}
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
return (
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
<div className='articleItem'>
<ModeIcon mode={article.mode}/> {article.title}</div>
<div className='divider'/>
</li>
)
}.bind(this))
return (
<div className='FinderList'>
<ul>
{articleElements}
</ul>
</div>
)
}
}
FinderList.propTypes = {
articles: PropTypes.array,
activeArticle: PropTypes.shape({
type: PropTypes.string,
key: PropTypes.string
}),
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,78 @@
$search-height = 50px
$nav-width = 175px
$list-width = 250px
.root
absolute top left right bottom
.search
height $search-height
padding 10px
box-sizing border-box
border-bottom $ui-border
text-align center
.search-input
height 30px
width 100%
margin 0 auto
font-size 18px
border none
outline none
text-align center
.result
absolute left right bottom
top $search-height
.result-nav
user-select none
absolute left top bottom
width $nav-width
background-color $ui-backgroundColor
.result-nav-filter
margin-bottom 5px
.result-nav-filter-option
height 25px
line-height 25px
padding 0 10px
label
cursor pointer
.result-nav-menu
navButtonColor()
height 40px
padding 0 10px
font-size 14px
width 100%
outline none
text-align left
line-height 40px
box-sizing border-box
cursor pointer
.result-nav-menu--active
@extend .result-nav-menu
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.result-nav-storageList
absolute bottom left right
top 80px + 50px + 10px
overflow-y auto
.result-list
user-select none
absolute top bottom
left $nav-width
width $list-width
border-width 0 1px
border-style solid
border-color $ui-borderColor
box-sizing border-box
overflow-y auto
.result-detail
absolute top bottom right
left $nav-width + $list-width

View File

@@ -0,0 +1,198 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import CodeEditor from 'browser/components/CodeEditor'
import modes from 'browser/lib/modes'
const electron = require('electron')
const { clipboard } = electron
const path = require('path')
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
}
return new window.Notification(title, options)
}
class NoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.note !== this.props.note) {
this.setState({
snippetIndex: 0
}, () => {
if (nextProps.note.type === 'SNIPPET_NOTE') {
nextProps.note.snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
}
})
}
}
selectPriorSnippet () {
let { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
})
}
}
selectNextSnippet () {
let { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
})
}
}
saveToClipboard () {
let { note } = this.props
if (note.type === 'MARKDOWN_NOTE') {
clipboard.writeText(note.content)
} else {
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
}
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
handleTabButtonClick (e, index) {
this.setState({
snippetIndex: index
})
}
render () {
let { note, config } = this.props
if (note == null) {
return (
<div styleName='root'>
</div>
)
}
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
if (note.type === 'SNIPPET_NOTE') {
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(e, index)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
</div>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
let mode = snippet.mode === 'text'
? null
: modes.filter((mode) => mode.name === snippet.mode)[0]
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
<div styleName='tabView-top'>
<input styleName='tabView-top-name'
placeholder='Filename including extensions...'
value={snippet.name}
readOnly
/>
<button styleName='tabView-top-mode'
>
{mode.label}
</button>
</div>
{snippet.mode === 'markdown'
? <MarkdownEditor styleName='tabView-content'
config={config}
value={snippet.content}
ref={'code-' + index}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
readOnly
ref={'code-' + index}
/>
}
</div>
})
return (
<div styleName='root'>
<div styleName='description'>
<textarea styleName='description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={note.description}
readOnly
/>
</div>
<div styleName='tabList'>
{tabList}
</div>
{viewList}
</div>
)
}
return (
<MarkdownPreview styleName='root'
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
value={note.content}
/>
)
}
}
NoteDetail.propTypes = {
}
export default CSSModules(NoteDetail, styles)

View File

@@ -0,0 +1,93 @@
.root
absolute top bottom left right
width 100%
height 100%
.description
absolute top left right
height 80px
border-bottom $ui-border
box-sizing border-box
.description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
box-sizing border-box
.tabList
absolute left right
top 80px
box-sizing border-box
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList-item
position relative
flex 1
border-right $ui-border
&:hover
.tabList-item-deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
.tabList-item--active
@extend .tabList-item
.tabList-item-button
border-color $brand-color
.tabList-item-button
width 100%
height 29px
navButtonColor()
border-left 4px solid transparent
.tabList-item-deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.tabList-plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-top
absolute top left right
height 30px
border-bottom $ui-border
display flex
box-sizing border-box
.tabView-top-name
flex 1
border none
padding 0 10px
font-size 14px
.tabView-top-mode
width 110px
padding 0
border none
border-left $ui-border
colorDefaultButton()
color $ui-inactive-text-color
pointer-events none
.tabView-content
absolute left right bottom
top 30px
box-sizing border-box
height 100%
width 100%

View File

@@ -0,0 +1,86 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteItem.styl'
import moment from 'moment'
import _ from 'lodash'
class NoteItem extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
handleClick (e) {
this.props.onClick(e)
}
render () {
let { note, folder, storage, isActive } = this.props
let tagList = _.isArray(note.tags)
? note.tags.map((tag) => {
return (
<span styleName='item-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
return (
<div styleName={isActive
? 'root--active'
: 'root'
}
key={note.uniqueKey}
onClick={(e) => this.handleClick(e)}
>
<div styleName='border'/>
<div styleName='info'>
<div styleName='info-left'>
<span styleName='info-left-folder'
style={{borderColor: folder.color}}
>
{folder.name}
<span styleName='info-left-folder-surfix'>in {storage.name}</span>
</span>
</div>
<div styleName='info-right'>
{moment(note.updatedAt).fromNow()}
</div>
</div>
<div styleName='title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='title-icon' className='fa fa-fw fa-code'/>
: <i styleName='title-icon' className='fa fa-fw fa-file-text-o'/>
}
{note.title.trim().length > 0
? note.title
: <span styleName='title-empty'>Empty</span>
}
</div>
<div styleName='tagList'>
<i styleName='tagList-icon'
className='fa fa-tags fa-fw'
/>
{tagList.length > 0
? tagList
: <span styleName='tagList-empty'>Not tagged yet</span>
}
</div>
</div>
)
}
}
NoteItem.propTypes = {
}
export default CSSModules(NoteItem, styles)

View File

@@ -0,0 +1,86 @@
.root
position relative
height 80px
border-bottom $ui-border
padding 0 5px
user-select none
cursor pointer
transition background-color 0.15s
&:hover
background-color alpha($ui-active-color, 10%)
.root--active
@extend .root
.border
border-color $ui-active-color
.border
absolute top bottom left right
border-style solid
border-width 2px
border-color transparent
transition 0.15s
.info
height 30px
clearfix()
font-size 12px
color $ui-inactive-text-color
line-height 30px
overflow-y hidden
.info-left
float left
overflow ellipsis
.info-left-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
.info-left-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.info-right
float right
.title
height 20px
line-height 20px
padding 0 5px 0 0
font-weight bold
overflow ellipsis
color $ui-text-color
.title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.title-empty
font-weight normal
color $ui-inactive-text-color
.tagList
height 30px
font-size 12px
line-height 30px
overflow ellipsis
.tagList-icon
vertical-align middle
color $ui-button-color
.tagList-item
margin 0 4px
padding 0 4px
height 20px
border-radius 3px
vertical-align middle
border-style solid
border-color $ui-button--focus-borderColor
border-width 0 0 0 3px
background-color $ui-backgroundColor
.tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -0,0 +1,89 @@
import React, { PropTypes } from 'react'
import NoteItem from './NoteItem'
import _ from 'lodash'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.state = {
range: 0
}
}
componentWillReceiveProps (nextProps) {
if (this.props.search !== nextProps.search) {
this.resetScroll()
}
}
componentDidUpdate () {
let { index } = this.props
if (index > -1) {
let list = this.refs.root
let item = list.childNodes[index]
if (item == null) return null
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
}
}
}
resetScroll () {
this.refs.root.scrollTop = 0
this.setState({
range: 0
})
}
handleScroll (e) {
let { notes } = this.props
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
this.setState({
range: this.state.range + 1
})
}
}
render () {
let { storages, notes, index } = this.props
let notesList = notes
.slice(0, 10 + 10 * this.state.range)
.map((note, _index) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
return (
<NoteItem
note={note}
key={`${note.storage}-${note.folder}-${note.key}`}
storage={storage}
folder={folder}
isActive={index === _index}
onClick={(e) => this.props.handleNoteClick(e, _index)}
/>
)
})
return (
<div className={this.props.className}
onScroll={(e) => this.handleScroll(e)}
ref='root'
>
{notesList}
</div>
)
}
}
NoteList.propTypes = {
}
export default NoteList

View File

@@ -0,0 +1,72 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageSection.styl'
class StorageSection extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleHeaderClick (e) {
let { storage } = this.props
this.props.handleStorageButtonClick(e, storage.key)
}
handleFolderClick (e, folder) {
let { storage } = this.props
this.props.handleFolderButtonClick(e, storage.key, folder.key)
}
render () {
let { storage, filter } = this.props
let folderList = storage.folders
.map((folder) => {
return (
<button styleName={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key
? 'folderList-item--active'
: 'folderList-item'
}
style={{borderColor: folder.color}}
key={folder.key}
onClick={(e) => this.handleFolderClick(e, folder)}
>
{folder.name}
</button>
)
})
return (
<div styleName='root'>
<div styleName='header'>
<button styleName='header-toggleButton'
onClick={(e) => this.handleToggleButtonClick(e)}
><i className='fa fa-caret-down'/></button>
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
? 'header-name--active'
: 'header-name'
}
onClick={(e) => this.handleHeaderClick(e)}
>{storage.name}</button>
</div>
{this.state.isOpen &&
<div styleName='folderList'>
{folderList}
</div>
}
</div>
)
}
}
StorageSection.propTypes = {
}
export default CSSModules(StorageSection, styles)

View File

@@ -0,0 +1,59 @@
.root
position relative
.header
height 30px
.header-toggleButton
absolute top left
width 25px
height 30px
navButtonColor()
border none
outline none
.header-name
display block
height 30px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 30px
box-sizing border-box
cursor pointer
outline none
.header-name--active
@extend .header-name
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.folderList-item
display block
width 100%
height 30px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 30px
box-sizing border-box
cursor pointer
outline none
padding 0 10px
margin 2px 0
height 30px
line-height 30px
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item--active
@extend .folderList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor

View File

@@ -1,39 +0,0 @@
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
export const REFRESH_DATA = 'REFRESH_DATA'
export function selectArticle (key) {
return {
type: SELECT_ARTICLE,
data: { key }
}
}
export function searchArticle (input) {
return {
type: SEARCH_ARTICLE,
data: { input }
}
}
export function refreshData (data) {
console.log('refreshing data')
let { folders, articles } = data
return {
type: REFRESH_DATA,
data: {
articles,
folders
}
}
}
export default {
SELECT_ARTICLE,
SEARCH_ARTICLE,
REFRESH_DATA,
selectArticle,
searchArticle,
refreshData
}

View File

@@ -1,85 +1,86 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import reducer from './reducer'
import { createStore } from 'redux'
import FinderInput from './FinderInput'
import FinderList from './FinderList'
import FinderDetail from './FinderDetail'
import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash'
import dataStore from 'browser/lib/dataStore'
import fetchConfig from '../lib/fetchConfig'
import ipc from './ipcClient'
import store from './store'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FinderMain.styl'
import StorageSection from './StorageSection'
import NoteList from './NoteList'
import NoteDetail from './NoteDetail'
const electron = require('electron')
const { clipboard, ipcRenderer, remote } = electron
const path = require('path')
let config = fetchConfig()
applyConfig(config)
ipcRenderer.on('config-apply', function (e, newConfig) {
config = newConfig
applyConfig(config)
})
function applyConfig () {
let body = document.body
body.setAttribute('data-theme', config['theme-ui'])
let hljsCss = document.getElementById('hljs-css')
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
}
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
const { remote } = electron
const { Menu } = remote
function hideFinder () {
ipcRenderer.send('hide-finder')
}
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
remote.getCurrentWindow().minimize()
return
}
return new window.Notification(title, options)
if (process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
}
remote.getCurrentWindow().hide()
}
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TEXT_FILTER = 'TEXT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
class FinderMain extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
index: 0,
filter: {
includeSnippet: true,
includeMarkdown: false,
type: 'ALL',
storage: null,
folder: null
}
}
this.focusHandler = (e) => this.handleWindowFocus(e)
this.blurHandler = (e) => this.handleWindowBlur(e)
}
componentDidMount () {
this.keyDownHandler = e => this.handleKeyDown(e)
document.addEventListener('keydown', this.keyDownHandler)
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
this.focusHandler = e => {
let { dispatch } = this.props
dispatch(searchArticle(''))
dispatch(selectArticle(null))
}
window.addEventListener('focus', this.focusHandler)
window.addEventListener('blur', this.blurHandler)
}
componentWillUnmount () {
document.removeEventListener('keydown', this.keyDownHandler)
window.removeEventListener('focus', this.focusHandler)
window.removeEventListener('blur', this.blurHandler)
}
handleWindowFocus (e) {
this.refs.search.focus()
}
handleWindowBlur (e) {
let { filter } = this.state
filter.type = 'ALL'
this.setState({
search: '',
filter,
index: 0
})
}
handleKeyDown (e) {
this.refs.search.focus()
if (e.keyCode === 9) {
if (e.shiftKey) {
this.refs.detail.selectPriorSnippet()
} else {
this.refs.detail.selectNextSnippet()
}
e.preventDefault()
}
if (e.keyCode === 38) {
this.selectPrevious()
e.preventDefault()
@@ -91,7 +92,8 @@ class FinderMain extends React.Component {
}
if (e.keyCode === 13) {
this.saveToClipboard()
this.refs.detail.saveToClipboard()
hideFinder()
e.preventDefault()
}
if (e.keyCode === 27) {
@@ -101,26 +103,13 @@ class FinderMain extends React.Component {
if (e.keyCode === 91 || e.metaKey) {
return
}
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
}
saveToClipboard () {
let { activeArticle } = this.props
clipboard.writeText(activeArticle.content)
ipcRenderer.send('copy-finder')
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
hideFinder()
}
handleSearchChange (e) {
let { dispatch } = this.props
dispatch(searchArticle(e.target.value))
this.setState({
search: e.target.value,
index: 0
})
}
selectArticle (article) {
@@ -128,41 +117,204 @@ class FinderMain extends React.Component {
}
selectPrevious () {
let { activeArticle, dispatch } = this.props
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
let previousArticle = this.refs.finderList.props.articles[index - 1]
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
if (this.state.index > 0) {
this.setState({
index: this.state.index - 1
})
}
}
selectNext () {
let { activeArticle, dispatch } = this.props
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
let previousArticle = this.refs.finderList.props.articles[index + 1]
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
if (this.state.index < this.noteCount - 1) {
this.setState({
index: this.state.index + 1
})
}
}
handleOnlySnippetCheckboxChange (e) {
let { filter } = this.state
filter.includeSnippet = e.target.checked
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleOnlyMarkdownCheckboxChange (e) {
let { filter } = this.state
filter.includeMarkdown = e.target.checked
this.refs.list.resetScroll()
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleAllNotesButtonClick (e) {
let { filter } = this.state
filter.type = 'ALL'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStarredButtonClick (e) {
let { filter } = this.state
filter.type = 'STARRED'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStorageButtonClick (e, storage) {
let { filter } = this.state
filter.type = 'STORAGE'
filter.storage = storage
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleFolderButtonClick (e, storage, folder) {
let { filter } = this.state
filter.type = 'FOLDER'
filter.storage = storage
filter.folder = folder
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleNoteClick (e, index) {
this.setState({
index
}, () => {
this.refs.search.focus()
})
}
render () {
let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard()
let { storages, notes, config } = this.props
let { filter, search } = this.state
let storageList = storages
.map((storage) => <StorageSection
filter={filter}
storage={storage}
key={storage.key}
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
/>)
if (!filter.includeSnippet && filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
} else if (filter.includeSnippet && !filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
}
switch (filter.type) {
case 'STORAGE':
notes = notes.filter((note) => note.storage === filter.storage)
break
case 'FOLDER':
notes = notes.filter((note) => note.storage === filter.storage && note.folder === filter.folder)
break
case 'STARRED':
notes = notes.filter((note) => note.isStarred)
}
if (search.trim().length > 0) {
let needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
notes = notes.filter((note) => note.title.match(needle))
}
let activeNote = notes[this.state.index]
this.noteCount = notes.length
return (
<div className='Finder'>
<FinderInput
handleSearchChange={e => this.handleSearchChange(e)}
ref='finderInput'
onChange={this.handleChange}
value={status.search}
/>
<FinderList
ref='finderList'
activeArticle={activeArticle}
articles={articles}
dispatch={dispatch}
selectArticle={article => this.selectArticle(article)}
/>
<FinderDetail
activeArticle={activeArticle}
saveToClipboard={saveToClipboard}
/>
<div className='Finder'
styleName='root'
ref='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={search}
placeholder='Search...'
onChange={(e) => this.handleSearchChange(e)}
/>
</div>
<div styleName='result'>
<div styleName='result-nav'>
<div styleName='result-nav-filter'>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeSnippet}
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
/> Only Snippets</label>
</div>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeMarkdown}
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
/> Only Markdown</label>
</div>
</div>
<button styleName={filter.type === 'ALL'
? 'result-nav-menu--active'
: 'result-nav-menu'
}
onClick={(e) => this.handleAllNotesButtonClick(e)}
><i className='fa fa-files-o fa-fw'/> All Notes</button>
<button styleName={filter.type === 'STARRED'
? 'result-nav-menu--active'
: 'result-nav-menu'
}
onClick={(e) => this.handleStarredButtonClick(e)}
><i className='fa fa-star fa-fw'/> Starred</button>
<div styleName='result-nav-storageList'>
{storageList}
</div>
</div>
<NoteList styleName='result-list'
storages={storages}
notes={notes}
ref='list'
search={search}
index={this.state.index}
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
/>
<div styleName='result-detail'>
<NoteDetail
note={activeNote}
config={config}
ref='detail'
/>
</div>
</div>
</div>
)
}
@@ -180,99 +332,10 @@ FinderMain.propTypes = {
dispatch: PropTypes.func
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
}
// Build filter object by key
function buildFilter (key) {
if (key.match(/^\/\/.+/)) {
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
}
if (key.match(/^\/.+/)) {
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
}
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
}
function remap (state) {
let { articles, folders, status } = state
let filters = status.search.split(' ')
.map(key => key.trim())
.filter(ignoreInvalidKey)
.map(buildFilter)
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) {
let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) {
articles = articles.filter(article => {
return _.findWhere(targetFolders, {key: article.FolderKey})
})
}
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
}
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
return {
articles,
activeArticle,
status
}
}
var Finder = connect(remap)(FinderMain)
var store = createStore(reducer)
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
function refreshData () {
let data = dataStore.getData(true)
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => {
refreshData()
// let data = dataStore.getData(true)
}
ReactDOM.render((

View File

@@ -0,0 +1,97 @@
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app, Menu } = remote
const path = require('path')
const store = require('./store')
nodeIpc.config.id = 'finder'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
function killFinder () {
let finderWindow = remote.getCurrentWindow()
finderWindow.removeAllListeners()
if (global.process.platform === 'darwin') {
// Only OSX has another app process.
app.quit()
} else {
finderWindow.close()
}
}
function toggleFinder () {
let finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'darwin') {
if (finderWindow.isVisible()) {
finderWindow.hide()
Menu.sendActionToFirstResponder('hide:')
} else {
nodeIpc.of.node.emit('request-data')
finderWindow.show()
}
} else {
if (!finderWindow.isMinimized()) {
finderWindow.minimize()
} else {
nodeIpc.of.node.emit('request-data')
finderWindow.restore()
}
}
}
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
console.log(err)
})
nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully')
})
nodeIpc.of.node.on('disconnect', function () {
console.log('disconnected')
})
nodeIpc.of.node.on('open-finder', function (payload) {
toggleFinder()
})
ipcRenderer.on('open-finder-from-tray', function () {
toggleFinder()
})
ipcRenderer.on('open-main-from-tray', function () {
nodeIpc.of.node.emit('open-main-from-finder')
})
ipcRenderer.on('quit-from-tray', function () {
nodeIpc.of.node.emit('quit-from-finder')
killFinder()
})
nodeIpc.of.node.on('throttle-data', function (payload) {
console.log('Received data from Main renderer')
store.default.dispatch({
type: 'THROTTLE_DATA',
storages: payload.storages,
notes: payload.notes
})
})
nodeIpc.of.node.on('config-renew', function (payload) {
console.log('config', payload)
store.default.dispatch({
type: 'SET_CONFIG',
config: payload
})
})
nodeIpc.of.node.on('quit-finder-app', function () {
nodeIpc.of.node.emit('quit-finder-app-confirm')
killFinder()
})
}
)
const ipc = {}
module.exports = ipc

View File

@@ -1,47 +0,0 @@
import { combineReducers } from 'redux'
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
let initialArticles = []
let initialFolders = []
let initialStatus = {
articleKey: null,
search: ''
}
function status (state = initialStatus, action) {
switch (action.type) {
case SELECT_ARTICLE:
state.articleKey = action.data.key
return Object.assign({}, state)
case SEARCH_ARTICLE:
state.search = action.data.input
return Object.assign({}, state)
default:
return state
}
}
function articles (state = initialArticles, action) {
switch (action.type) {
case REFRESH_DATA:
return action.data.articles
default:
return state
}
}
function folders (state = initialFolders, action) {
switch (action.type) {
case REFRESH_DATA:
console.log(action)
return action.data.folders
default:
return state
}
}
export default combineReducers({
status,
folders,
articles
})

77
browser/finder/store.js Normal file
View File

@@ -0,0 +1,77 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
const OSX = global.process.platform === 'darwin'
const defaultConfig = {
zoom: 1,
isSideNavFolded: false,
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: 'BLUR' // Available value: RIGHTCLICK, BLUR
},
preview: {
fontSize: '14',
fontFamily: 'Lato',
codeBlockTheme: 'xcode',
lineNumber: true
}
}
function storages (state = [], action) {
switch (action.type) {
case 'THROTTLE_DATA':
state = action.storages
}
return state
}
function notes (state = [], action) {
switch (action.type) {
case 'THROTTLE_DATA':
state = action.notes
}
return state
}
function config (state = defaultConfig, action) {
switch (action.type) {
case 'INIT_CONFIG':
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
let reducer = combineReducers({
storages,
notes,
config,
routing: routerReducer
})
let store = createStore(reducer)
export default store

View File

@@ -12,6 +12,7 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
import ipc from 'browser/main/lib/ipc'
class Main extends React.Component {
constructor (props) {

View File

@@ -58,7 +58,7 @@
.folderList-item
display block
width 100%
height 3 0px
height 30px
background-color transparent
color $ui-inactive-text-color
padding 0

View File

@@ -27,7 +27,6 @@
position absolute
right 60px
height 24px
width 125px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor

View File

@@ -15,13 +15,17 @@ class StatusBar extends React.Component {
super(props)
this.state = {
updateAvailable: false
updateReady: false
}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
ipc.on('update-ready', function (message) {
this.setState({
updateReady: true
}, () => {
this.updateApp()
})
}.bind(this))
}
@@ -34,7 +38,7 @@ class StatusBar extends React.Component {
})
if (index === 0) {
ipc.send('update-app', 'Deal with it.')
remote.getCurrentWindow().webContents.send('update-app')
}
}
@@ -68,9 +72,9 @@ class StatusBar extends React.Component {
styleName='root'
>
<div styleName='pathname'>{location.pathname + location.search}</div>
{this.state.updateAvailable
{this.state.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download'/> Update is available!
<i styleName='update-icon' className='fa fa-cloud-download'/> Update is ready!
</button>
: null
}

View File

@@ -10,18 +10,9 @@ import { syncHistoryWithStore } from 'react-router-redux'
const electron = require('electron')
const ipc = electron.ipcRenderer
const path = require('path')
const remote = electron.remote
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
activityRecord.init()
ipc.send('check-update', 'check-update')
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
})
@@ -35,28 +26,6 @@ document.addEventListener('dragover', function (e) {
e.stopPropagation()
})
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
}
console.log(options)
return new window.Notification(title, options)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
ipc.on('copy-finder', function () {
activityRecord.emit('FINDER_COPY')
})
ipc.on('open-finder', function () {
activityRecord.emit('FINDER_OPEN')
})
let el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)

View File

@@ -2,7 +2,7 @@ import _ from 'lodash'
const OSX = global.process.platform === 'darwin'
const electron = require('electron')
const { ipcRenderer } = electron
const { remote } = electron
const defaultConfig = {
zoom: 1,
@@ -66,17 +66,13 @@ function set (updates) {
let newConfig = Object.assign({}, defaultConfig, currentConfig, updates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
ipcRenderer.send('CONFIG_RENEW', {
remote.getCurrentWindow().webContents.send('config-renew', {
config: get(),
silent: false
})
}
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: true
})
export default {
get,
set,

138
browser/main/lib/ipc.js Normal file
View File

@@ -0,0 +1,138 @@
import store from 'browser/main/store'
import ConfigManager from 'browser/main/lib/ConfigManager'
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app, Menu } = remote
const path = require('path')
nodeIpc.config.id = 'node'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
console.log('Initializing IPC Server')
// TODO: IPC SERVER WILL BE MOVED TO MAIN PROCESS FROM MAIN WINDOW PROCESS(RENDERER)
nodeIpc.serve(
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
console.log('IPC Server Started')
ipcRenderer.on('open-finder', function () {
console.log('Open finder')
nodeIpc.server.broadcast('open-finder')
})
/** Quit Sequence
1. `quit-app` Main process -> Main window by Electron IPC
2. `quit-finder-app` Main window -> Finder window by Node IPC(socket)
3. Finder window (and Finder main process: OSX only) killed by remote API
4. `quit-finder-app-confirm` Finder window -> Main window by NodeIPC
5. `quit-app-confirm` Main window -> Main process by Electron IPC
6. Main process discard close preventer and terminate Main window and itself.
If the platform is a linux without cinnamon, the app will skip 2.-4. because it doesn't launch finder window.
`quit-app` will fires directly `quit-app-confirm`.
*/
ipcRenderer.on('quit-app', function () {
// Finder app exists only in the linux with cinnamon.
if (global.process.env.platform === 'linux' && global.process.env.DESKTOP_SESSION !== 'cinnamon') {
ipcRenderer.send('quit-app-confirm')
return
}
let confirmHandler = function () {
ipcRenderer.send('quit-app-confirm')
}
nodeIpc.server.on('quit-finder-app-confirm', confirmHandler)
setTimeout(() => {
nodeIpc.server.removeListener('quit-finder-app-confirm', confirmHandler)
}, 1000)
nodeIpc.server.broadcast('quit-finder-app')
})
/** Update Sequence
1. `update-ready` Main process -> Main window by Electron IPC
2. `update-app` Main window -> Main window by Electron IPC
3. `quit-finder-app` Main window -> Finder window by Node IPC
4. Finder window (and Finder main process: OSX only) killed by remote API
5. `quit-finder-app-confirm` Finder window -> Main window by NodeIPC
6. `update-app-confirm` Main window -> Main process by Electron IPC
7. Main process discard close preventer and start updating.
Handlers of 1. and 2. can be found in StatusBar component.
*/
ipcRenderer.on('update-app', function () {
// Linux app doesn't support auto updater
if (global.process.env.platform === 'linux') {
return
}
let confirmHandler = function () {
ipcRenderer.send('update-app-confirm')
}
nodeIpc.server.on('quit-finder-app-confirm', confirmHandler)
setTimeout(() => {
nodeIpc.server.removeListener('quit-finder-app-confirm', confirmHandler)
}, 1000)
nodeIpc.server.broadcast('quit-finder-app')
})
ipcRenderer.on('update-found', function () {
console.log('Update found')
})
let config = ConfigManager.get()
nodeIpc.server.broadcast('config-renew', config)
ipcRenderer.send('config-renew', {
config: config,
silent: true
})
ipcRenderer.on('config-renew', function (e, data) {
nodeIpc.server.broadcast('config-renew', data.config)
ipcRenderer.send('config-renew', data)
})
nodeIpc.server.on('open-main-from-finder', function () {
let mainWindow = remote.getCurrentWindow()
console.log('open main from finder')
if (mainWindow.isFocused()) {
if (global.process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
} else {
mainWindow.minimize()
}
} else {
if (global.process.platform === 'darwin') {
mainWindow.show()
} else {
mainWindow.minimize()
mainWindow.restore()
}
}
})
nodeIpc.server.on('quit-from-finder', function () {
ipcRenderer.send('quit-app-confirm')
})
nodeIpc.server.on('connect', function (socket) {
console.log('connected')
socket.on('close', function () {
console.log('socket dead')
})
})
nodeIpc.server.on('error', function (err) {
console.error('Node IPC error', err)
})
nodeIpc.server.on('request-data', function (data, socket) {
let state = store.getState()
nodeIpc.server.broadcast('throttle-data', {
storages: state.storages,
notes: state.notes
})
})
}
)
const ipc = {
}
nodeIpc.server.start()
module.exports = ipc

View File

@@ -0,0 +1,11 @@
const path = require('path')
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
}
return new window.Notification(title, options)
}
export default notify

View File

@@ -180,7 +180,6 @@ class ConfigTab extends React.Component {
ref='toggleFinder'
value={config.hotkey.toggleFinder}
type='text'
disabled
/>
</div>
</div>

View File

@@ -9,9 +9,9 @@ app.on('ready', function () {
app.dock.hide()
}
var template = require('./finder-menu')
var menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
// var template = require('./finder-menu')
// var menu = Menu.buildFromTemplate(template)
// Menu.setApplicationMenu(menu)
finderWindow = require('./finder-window')
})

View File

@@ -2,80 +2,12 @@ const electron = require('electron')
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu
const MenuItem = electron.MenuItem
const app = electron.app
const ipcMain = electron.ipcMain
const Tray = electron.Tray
const path = require('path')
const nodeIpc = require('@rokt33r/node-ipc')
var appQuit = false
var isFinderLoaded = false
nodeIpc.config.id = 'finder'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
nodeIpc.connectTo(
'main',
path.join(app.getPath('userData'), 'boost.service'),
function () {
nodeIpc.of.main.on(
'error',
function (err) {
nodeIpc.log('<< ## err ##'.rainbow, nodeIpc.config.delay)
nodeIpc.log(err)
}
)
nodeIpc.of.main.on(
'connect',
function () {
nodeIpc.log('<< ## connected to world ##'.rainbow, nodeIpc.config.delay)
}
)
nodeIpc.of.main.on(
'disconnect',
function () {
nodeIpc.log('<< disconnected from main'.notice)
if (process.platform === 'darwin') {
appQuit = true
app.quit()
}
}
)
nodeIpc.of.main.on(
'message',
function (payload) {
if (isFinderLoaded) {
switch (payload.type) {
case 'open-finder':
if (finderWindow.isFocused()) {
hideFinder()
} else {
openFinder()
}
break
case 'config-apply': {
finderWindow.webContents.send('config-apply', payload.data)
break
}
}
}
}
)
}
)
function emit (type, data) {
var payload = {
type: type,
data: data
}
nodeIpc.of.main.emit('message', payload)
}
var config = {
width: 640,
height: 400,
width: 840,
height: 540,
show: false,
frame: false,
resizable: false,
@@ -107,11 +39,8 @@ finderWindow.on('blur', function () {
})
finderWindow.on('close', function (e) {
if (process.platform === 'darwin') {
if (appQuit) return true
e.preventDefault()
finderWindow.hide()
}
e.preventDefault()
finderWindow.hide()
})
var appIcon = new Tray(path.join(__dirname, '../resources/tray-icon.png'))
@@ -121,19 +50,21 @@ var trayMenu = new Menu()
trayMenu.append(new MenuItem({
label: 'Open Main window',
click: function () {
emit('show-main-window')
}
}))
trayMenu.append(new MenuItem({
label: 'Open Finder window',
click: function () {
openFinder()
finderWindow.webContents.send('open-main-from-tray')
}
}))
if (process.env.platform !== 'linux' || process.env.DESKTOP_SESSION === 'cinnamon') {
trayMenu.append(new MenuItem({
label: 'Open Finder window',
click: function () {
finderWindow.webContents.send('open-finder-from-tray')
}
}))
}
trayMenu.append(new MenuItem({
label: 'Quit',
click: function () {
emit('quit-app')
finderWindow.webContents.send('quit-from-tray')
}
}))
@@ -143,21 +74,6 @@ appIcon.on('click', function (e) {
appIcon.popUpContextMenu(trayMenu)
})
ipcMain.on('copy-finder', function () {
emit('copy-finder')
})
ipcMain.on('hide-finder', function () {
hideFinder()
})
finderWindow.webContents.on('did-finish-load', function () {
isFinderLoaded = true
})
function openFinder () {
if (isFinderLoaded) finderWindow.show()
}
function hideFinder () {
if (process.platform === 'win32') {
finderWindow.minimize()

View File

@@ -28,11 +28,12 @@
<body>
<div id="content"></div>
<script src="../node_modules/ace-builds/src-min/ace.js"></script>
<script src="../node_modules/ace-builds/src-min/ext-themelist.js"></script>
<script src="../resources/katex.min.js"></script>
<script src="../compiled/react.min.js"></script>
<script src="../compiled/react-dom.min.js"></script>
<script src="../compiled/redux.min.js"></script>
<script src="../compiled/react-redux.min.js"></script>
<script src="../compiled/react.js"></script>
<script src="../compiled/react-dom.js"></script>
<script src="../compiled/redux.js"></script>
<script src="../compiled/react-redux.js"></script>
<script>
const electron = require('electron')
electron.webFrame.setZoomLevelLimits(1, 1)

View File

@@ -3,19 +3,8 @@ const ipc = electron.ipcMain
const Menu = electron.Menu
const globalShortcut = electron.globalShortcut
const mainWindow = require('./main-window')
// const nodeIpc = require('@rokt33r/node-ipc')
function emitToFinder (type, data) {
var payload = {
type: type,
data: data
}
// nodeIpc.server.broadcast('message', payload)
}
function toggleFinder () {
emitToFinder('open-finder')
mainWindow.webContents.send('open-finder', {})
}
@@ -33,11 +22,10 @@ function toggleMain () {
mainWindow.minimize()
mainWindow.restore()
}
mainWindow.webContents.send('top-focus-search')
}
}
ipc.on('CONFIG_RENEW', (e, payload) => {
ipc.on('config-renew', (e, payload) => {
globalShortcut.unregisterAll()
var { config } = payload

View File

@@ -12,7 +12,6 @@ const GhReleases = require('electron-gh-releases')
var mainWindow = null
var finderProcess = null
var finderWindow = null
var update = null
const appRootPath = path.join(process.execPath, '../..')
const updateDotExePath = path.join(appRootPath, 'Update.exe')
@@ -58,20 +57,19 @@ var handleStartupEvent = function () {
switch (squirrelCommand) {
case '--squirrel-install':
spawnUpdate(['--createShortcut', exeName], function (err) {
quitApp()
app.quit()
})
return true
case '--squirrel-updated':
quitApp()
app.quit()
return true
case '--squirrel-uninstall':
spawnUpdate(['--removeShortcut', exeName], function (err) {
quitApp()
app.quit()
})
quitApp()
return true
case '--squirrel-obsolete':
quitApp()
app.quit()
return true
}
}
@@ -92,24 +90,13 @@ var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory)
})
if (shouldQuit) {
quitApp()
if (mainWindow != null) mainWindow.removeAllListeners()
app.quit()
return
}
var appQuit = false
var version = app.getVersion()
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
var versionNotified = false
function notify (title, body) {
if (mainWindow != null) {
mainWindow.webContents.send('notify', {
title: title,
body: body
})
}
}
var isUpdateReady = false
@@ -129,17 +116,15 @@ function checkUpdate () {
updater.check((err, status) => {
if (err) {
var isLatest = err.message === 'There is no newer version.'
if (!isLatest && !versionNotified) console.error('Updater error! %s', err.message)
if (!isLatest) console.error('Updater error! %s', err.message)
}
if (!err) {
if (status) {
notify('Update is available!', 'Download started.. wait for the update ready.')
// Download start
mainWindow.webContents.send('update-found', 'Update found!')
updater.download()
} else {
if (!versionNotified) {
versionNotified = true
notify('Latest Build!! ' + versionText, 'Hope you to enjoy our app :D')
}
// Latest version
}
}
})
@@ -147,167 +132,73 @@ function checkUpdate () {
updater.on('update-downloaded', (info) => {
if (mainWindow != null) {
notify('Ready to Update!', 'Click update button on Main window.')
mainWindow.webContents.send('update-available', 'Update available!')
mainWindow.webContents.send('update-ready', 'Update available!')
isUpdateReady = true
}
})
// nodeIpc.config.id = 'node'
// nodeIpc.config.retry = 1500
// nodeIpc.config.silent = true
// nodeIpc.serve(
// path.join(app.getPath('userData'), 'boost.service'),
// function () {
// nodeIpc.server.on(
// 'connect',
// function (socket) {
// socket.on('close', function () {
// console.log('socket dead')
// if (!appQuit) spawnFinder()
// })
// }
// )
// nodeIpc.server.on(
// 'message',
// function (data, socket) {
// console.log('>>', data)
// handleIpcEvent(data)
// }
// )
// nodeIpc.server.on(
// 'error',
// function (err) {
// console.log('>>', err)
// }
// )
// }
// )
function handleIpcEvent (payload) {
switch (payload.type) {
case 'show-main-window':
switch (process.platform) {
case 'darwin':
mainWindow.show()
case 'win32':
mainWindow.minimize()
mainWindow.restore()
case 'linux':
// Do nothing
// due to bug of `app.focus()` some desktop Env
}
break
case 'copy-finder':
mainWindow.webContents.send('copy-finder')
break
case 'quit-app':
quitApp()
break
}
}
function spawnFinder() {
// if (process.platform === 'darwin') {
// var finderArgv = [path.join(__dirname, 'finder-app.js'), '--finder']
// if (_.find(process.argv, a => a === '--hot')) finderArgv.push('--hot')
// finderProcess = ChildProcess
// .execFile(process.execPath, finderArgv)
// }
}
function quitApp () {
appQuit = true
if (finderProcess) finderProcess.kill()
app.quit()
if (process.platform === 'darwin') {
var finderArgv = [path.join(__dirname, 'finder-app.js'), '--finder']
if (_.find(process.argv, a => a === '--hot')) finderArgv.push('--hot')
finderProcess = ChildProcess
.execFile(process.execPath, finderArgv)
}
}
app.on('ready', function () {
app.on('before-quit', function () {
appQuit = true
if (finderProcess) finderProcess.kill()
})
var template = require('./main-menu')
if (process.platform === 'win32') {
template.unshift({
label: 'Boostnote',
submenu: [
{
label: 'Quit',
accelerator: 'Control+Q',
click: function (e) {
quitApp()
}
}
]
})
}
var menu = Menu.buildFromTemplate(template)
if (process.platform === 'darwin' || process.platform === 'linux') {
Menu.setApplicationMenu(menu)
}
// Check update every 24 hours
setInterval(function () {
checkUpdate()
}, 1000 * 60 * 60 * 24)
ipc.on('check-update', function (event, msg) {
if (update == null) checkUpdate()
checkUpdate()
})
ipc.on('update-app', function (event, msg) {
ipc.on('update-app-confirm', function (event, msg) {
if (isUpdateReady) {
appQuit = true
mainWindow.removeAllListeners()
updater.install()
}
})
ipc.on('quit-app-confirm', function () {
mainWindow.removeAllListeners()
app.quit()
})
checkUpdate()
mainWindow = require('./main-window')
if (process.platform === 'win32' || process.platform === 'linux') {
mainWindow.setMenu(menu)
}
mainWindow.on('close', function (e) {
if (appQuit || process.platform != 'darwin') {
app.quit()
} else {
mainWindow.hide()
e.preventDefault()
}
})
// switch (process.platform) {
// case 'darwin':
// spawnFinder()
// break
// case 'win32':
// finderWindow = require('./finder-window')
// finderWindow.on('close', function (e) {
// if (appQuit) return true
// e.preventDefault()
// finderWindow.hide()
// })
// break
// case 'linux':
// if (process.env.DESKTOP_SESSION === 'cinnamon') {
// finderWindow = require('./finder-window')
// finderWindow.on('close', function (e) {
// if (appQuit) return true
// e.preventDefault()
// finderWindow.hide()
// })
// }
// // Do nothing.
// }
// nodeIpc.server.start(function (err) {
// if (err.code === 'EADDRINUSE') {
// notify('Error occurs!', 'You have to kill other Boostnote processes.')
// quitApp()
// }
// })
switch (process.platform) {
case 'darwin':
spawnFinder()
break
case 'win32':
finderWindow = require('./finder-window')
finderWindow.on('close', function (e) {
e.preventDefault()
finderWindow.hide()
})
break
case 'linux':
// Finder is available on cinnamon only.
if (process.env.DESKTOP_SESSION === 'cinnamon') {
finderWindow = require('./finder-window')
}
}
require('./hotkey')
})

View File

@@ -7,46 +7,61 @@ const OSX = process.platform === 'darwin'
// const WIN = process.platform === 'win32'
const LINUX = process.platform === 'linux'
var boost = {
label: 'Boostnote',
submenu: [
{
label: 'About Boostnote',
selector: 'orderFrontStandardAboutPanel:'
},
{
type: 'separator'
},
{
label: 'Hide Boostnote',
accelerator: 'Command+H',
selector: 'hide:'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
},
{
label: 'Show All',
selector: 'unhideAllApplications:'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
selector: 'terminate:'
}
]
}
var boost = OSX
? {
label: 'Boostnote',
submenu: [
{
label: 'About Boostnote',
selector: 'orderFrontStandardAboutPanel:'
},
{
type: 'separator'
},
{
label: 'Hide Boostnote',
accelerator: 'Command+H',
selector: 'hide:'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
},
{
label: 'Show All',
selector: 'unhideAllApplications:'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: function () {
mainWindow.webContents.send('quit-app', {})
}
}
]
}
: {
label: 'Boostnote',
submenu: [
{
label: 'Quit',
accelerator: 'Command+Q',
click: function () {
mainWindow.webContents.send('quit-app', {})
}
}
]
}
var file = {
label: 'File',
submenu: [
{
label: 'New Post',
label: 'New Note',
accelerator: OSX ? 'Command + N' : 'Control + N',
click: function () {
mainWindow.webContents.send('top:new-note')
@@ -56,7 +71,7 @@ var file = {
type: 'separator'
},
{
label: 'Delete Post',
label: 'Delete Note',
accelerator: OSX ? 'Control + Backspace' : 'Control + Delete',
click: function () {
mainWindow.webContents.send('detail:delete')
@@ -188,5 +203,5 @@ var help = {
module.exports = process.platform === 'darwin'
? [boost, file, edit, view, window, help]
: process.platform === 'win32'
? [file, view, help]
? [boost, file, view, help]
: [file, view, help]

View File

@@ -32,6 +32,11 @@ mainWindow.webContents.sendInputEvent({
keyCode: '\u0008'
})
mainWindow.on('close', function (e) {
mainWindow.hide()
e.preventDefault()
})
app.on('activate', function () {
if (mainWindow == null) return null
mainWindow.show()

View File

@@ -51,6 +51,7 @@
"markdown-it-footnote": "^3.0.0",
"md5": "^2.0.0",
"moment": "^2.10.3",
"node-ipc": "^8.1.0",
"sander": "^0.5.1",
"season": "^5.3.0",
"superagent": "^1.2.0",
@@ -75,6 +76,7 @@
"nib": "^1.1.0",
"oh-my-cdn": "^0.1.1",
"react-css-modules": "^3.7.6",
"react-input-autosize": "^1.1.0",
"react-router": "^2.4.0",
"react-router-redux": "^4.0.4",
"standard": "^6.0.8",

View File

@@ -5,7 +5,7 @@ const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin')
var config = {
entry: {
main: './browser/main/index.js',
// finder: './browser/finder/index.js'
finder: './browser/finder/index.js'
},
resolve: {
extensions: ['', '.js', '.jsx', '.styl'],
@@ -27,6 +27,7 @@ var config = {
]
},
externals: [
'node-ipc',
'electron',
'md5',
'superagent',