mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Finder
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
78
browser/finder/FinderMain.styl
Normal file
78
browser/finder/FinderMain.styl
Normal 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
|
||||
198
browser/finder/NoteDetail.js
Normal file
198
browser/finder/NoteDetail.js
Normal 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)
|
||||
93
browser/finder/NoteDetail.styl
Normal file
93
browser/finder/NoteDetail.styl
Normal 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%
|
||||
86
browser/finder/NoteItem.js
Normal file
86
browser/finder/NoteItem.js
Normal 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)
|
||||
86
browser/finder/NoteItem.styl
Normal file
86
browser/finder/NoteItem.styl
Normal 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
|
||||
|
||||
89
browser/finder/NoteList.js
Normal file
89
browser/finder/NoteList.js
Normal 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
|
||||
72
browser/finder/StorageSection.js
Normal file
72
browser/finder/StorageSection.js
Normal 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)
|
||||
59
browser/finder/StorageSection.styl
Normal file
59
browser/finder/StorageSection.styl
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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((
|
||||
|
||||
97
browser/finder/ipcClient.js
Normal file
97
browser/finder/ipcClient.js
Normal 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
|
||||
@@ -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
77
browser/finder/store.js
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
138
browser/main/lib/ipc.js
Normal 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
|
||||
11
browser/main/lib/notify.js
Normal file
11
browser/main/lib/notify.js
Normal 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
|
||||
@@ -180,7 +180,6 @@ class ConfigTab extends React.Component {
|
||||
ref='toggleFinder'
|
||||
value={config.hotkey.toggleFinder}
|
||||
type='text'
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
195
lib/main-app.js
195
lib/main-app.js
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user