1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 18:26:26 +00:00

Compare commits

...

65 Commits

Author SHA1 Message Date
Rokt33r
2b384b1d15 fix updater bug 2015-12-01 02:00:18 +09:00
Rokt33r
a1d61edb9c Merge branch 'dev'
* dev:
  bump version
  Folder create modalを出したら、まっすぐName inputをFocusする
  編集警告が出ている時にCode editorがキー入力を認識する問題解決
  External link動きDebug
  add copy button
  External link用のDropdown menu追加
  コードを綺麗に
  Titleがなかったら灰色でUntitleと出す
  新規投稿 Cmd + n / Preview Cmd + P 追加
  articleのタイトルの基本タイトル追加 / 何も書かれていない時にUntitled labelをだす
  Finderのvisibile on all workspaces解除
  Searchbar tooltip changed(add exact match)
  change tray menu label(Open Finder => Open FInder window)
  Main windowの visible on all worpspace解除

Conflicts:
	package.json
2015-12-01 00:10:15 +09:00
Rokt33r
96a8687896 bump version 2015-11-30 23:11:31 +09:00
Rokt33r
0448773682 Folder create modalを出したら、まっすぐName inputをFocusする 2015-11-30 16:28:14 +09:00
Rokt33r
57998ba727 編集警告が出ている時にCode editorがキー入力を認識する問題解決 2015-11-30 16:22:10 +09:00
Rokt33r
de83447cb3 External link動きDebug 2015-11-30 12:53:46 +09:00
Rokt33r
eba19468d5 add copy button 2015-11-30 12:53:21 +09:00
Rokt33r
65c78df671 External link用のDropdown menu追加 2015-11-30 11:14:16 +09:00
Rokt33r
a7096aa89f コードを綺麗に 2015-11-30 04:28:23 +09:00
Rokt33r
15a50ef452 Titleがなかったら灰色でUntitleと出す 2015-11-30 04:17:52 +09:00
Rokt33r
04036e5c87 新規投稿 Cmd + n / Preview Cmd + P 追加 2015-11-30 03:44:58 +09:00
Rokt33r
2bbb5ef74e articleのタイトルの基本タイトル追加 / 何も書かれていない時にUntitled labelをだす 2015-11-29 18:57:49 +09:00
Rokt33r
91eb7feb3c Finderのvisibile on all workspaces解除 2015-11-29 11:08:13 +09:00
Rokt33r
978d77142c Searchbar tooltip changed(add exact match) 2015-11-29 11:05:18 +09:00
Rokt33r
e36478b9ac modify method name (api changed as electron updated) 2015-11-25 10:49:06 +09:00
Rokt33r
e1fe4dd693 change to use HTTPS for checking update 2015-11-25 09:51:33 +09:00
Rokt33r
b1ee949b1c This is a release version 2015-11-25 09:40:07 +09:00
Rokt33r
a0e5f8e97e Merge commit '80a0c59f878d899fc21b72f08eb8afeb1970f9ba'
* commit '80a0c59f878d899fc21b72f08eb8afeb1970f9ba':
  make it as prerelease
  bump up version
  MarkdownのCodeblockの行間をひろげる 
  編集中キャンセルを押しても消える情報があれば警告をだす
  データ移転バグ修正
  最初以降からはUpdaterがエラーをださない。
  Stream EPIPEエラー解決、データはこれからJSON保存
  notification デバッグ
  intercept entry point
  using ipc but not working in production
  bump up electron version 0.34 -> 0.35.1
  MarkdownでEmojiが使える
  Markdown内のコードにSyntax highlightenをいれる

Conflicts:
	main.js
2015-11-25 09:08:13 +09:00
Rokt33r
e9cfb2c4ee change tray menu label(Open Finder => Open FInder window) 2015-11-25 08:59:43 +09:00
Rokt33r
190b6edfb1 Main windowの visible on all worpspace解除 2015-11-25 08:50:56 +09:00
Rokt33r
80a0c59f87 make it as prerelease 2015-11-25 08:01:57 +09:00
Rokt33r
823fdec705 bump up version 2015-11-25 07:56:39 +09:00
Rokt33r
fe87dcced7 MarkdownのCodeblockの行間をひろげる 2015-11-25 07:42:22 +09:00
Rokt33r
137eb44516 編集中キャンセルを押しても消える情報があれば警告をだす 2015-11-25 07:42:02 +09:00
Rokt33r
f60d957102 データ移転バグ修正 2015-11-25 07:39:32 +09:00
Rokt33r
8f0b04504f 最初以降からはUpdaterがエラーをださない。 2015-11-25 07:38:46 +09:00
Rokt33r
2c39d8b1c8 Stream EPIPEエラー解決、データはこれからJSON保存 2015-11-25 07:37:33 +09:00
Rokt33r
d4d1c32288 notification デバッグ 2015-11-24 06:17:49 +09:00
Rokt33r
e4f39d2b6a intercept entry point 2015-11-24 04:16:43 +09:00
Rokt33r
e5a2bfbcbd using ipc but not working in production 2015-11-24 02:54:45 +09:00
Rokt33r
de3b76b31d bump up electron version 0.34 -> 0.35.1 2015-11-23 11:38:35 +09:00
Rokt33r
53455496bf MarkdownでEmojiが使える 2015-11-23 11:04:43 +09:00
Rokt33r
cc2a2f6dfb Markdown内のコードにSyntax highlightenをいれる 2015-11-23 10:39:21 +09:00
Rokt33r
ee4ac7371c Merge branch 'dev'
* dev:
  No node-notifier
  fix: 新しい記事を書く時に発生するバグ一体
  cleanup notification code
  Default文書修正
  開発中のものはデータを送らない
  初期記事内容修正cmd -> ctrl
  show devtool only devmode

Conflicts:
	main.js
2015-11-22 16:31:48 +09:00
Rokt33r
d5265407b9 No node-notifier 2015-11-22 15:57:44 +09:00
Rokt33r
954b3e9fc5 fix: 新しい記事を書く時に発生するバグ一体 2015-11-22 15:03:48 +09:00
Rokt33r
7d9894bef7 cleanup notification code 2015-11-21 22:07:59 +09:00
Rokt33r
3b34698e8b Default文書修正 2015-11-21 16:03:20 +09:00
Rokt33r
263cb581c4 開発中のものはデータを送らない 2015-11-21 06:37:23 +09:00
Rokt33r
1c9cb4516c 初期記事内容修正cmd -> ctrl 2015-11-21 06:36:50 +09:00
Rokt33r
ac4ceccb4f show devtool only devmode 2015-11-21 06:35:27 +09:00
Rokt33r
e731b7882d Merge branch 'dev'
* dev:
  no source map
  bump version
  hidden code
2015-11-21 06:05:17 +09:00
Rokt33r
84e0728ff3 no source map 2015-11-21 06:03:32 +09:00
Rokt33r
666bc18e91 bump version 2015-11-21 05:56:29 +09:00
Rokt33r
8f83124a0d hidden code 2015-11-21 05:54:15 +09:00
Rokt33r
ee91daad7e Merge branch 'dev'
* dev:
  hotfix: Edited alertが変な時に出る
2015-11-18 18:51:39 +09:00
Rokt33r
ee78c0d33b hotfix: Edited alertが変な時に出る 2015-11-18 18:39:27 +09:00
Dick Choi
09482ebcf3 fix wrong dependency 2015-11-17 05:36:08 +09:00
Rokt33r
67424f2d3a Merge branch 'dev'
* dev:
  bump up version 0.4.1
  記事が編集された状態で他の記事を見ようとすると警告をだす
  updateが準備できたら、再起動されるまで改めてUpdateの確認をしない
  Tag suggest
  EnterでSubmitができる - Hotkey, folder edit, folder create(preference/create new folder modal両方)
  Folderの位置を変えることができる
  Preferenceからもフォルダーの色の選択ができる。
  // filterを使うと確実にFolder名が一致するもののみを表示する
  fix style
  new folder modalにcolor select追加
  auto update確認
  FinderからCopyした時、通知を出す
  FinderにCopy to clipboard button追加
  Folder リストに articleの数をだす
  フォルダーで検索するときに in:じゃなくて /にする +バグ修正
  IntroのFinder説明変更
2015-11-16 07:03:46 +09:00
Rokt33r
51f530ffbe bump up version 0.4.1 2015-11-16 05:36:37 +09:00
Rokt33r
013f96a754 記事が編集された状態で他の記事を見ようとすると警告をだす 2015-11-16 05:34:37 +09:00
Rokt33r
df6a018fb6 updateが準備できたら、再起動されるまで改めてUpdateの確認をしない 2015-11-16 04:17:56 +09:00
Rokt33r
409eaf54c1 Tag suggest 2015-11-16 04:06:14 +09:00
Rokt33r
7e04fd342c EnterでSubmitができる - Hotkey, folder edit, folder create(preference/create new folder modal両方) 2015-11-16 02:45:46 +09:00
Rokt33r
1fe15bc6a5 Folderの位置を変えることができる 2015-11-16 02:38:36 +09:00
Rokt33r
ff1bffbb55 Preferenceからもフォルダーの色の選択ができる。 2015-11-16 01:22:22 +09:00
Rokt33r
b28b18a19a // filterを使うと確実にFolder名が一致するもののみを表示する 2015-11-15 23:20:06 +09:00
Rokt33r
bbc3c85212 fix style 2015-11-15 20:40:43 +09:00
Rokt33r
26a08fac06 new folder modalにcolor select追加 2015-11-15 20:32:02 +09:00
Rokt33r
da9d7a4336 auto update確認 2015-11-15 03:52:34 +09:00
Rokt33r
46c6555f94 FinderからCopyした時、通知を出す 2015-11-15 01:39:40 +09:00
Rokt33r
3e980fd2d4 FinderにCopy to clipboard button追加 2015-11-15 01:22:56 +09:00
Rokt33r
fb1462f669 Folder リストに articleの数をだす 2015-11-15 01:07:46 +09:00
Rokt33r
41e1630aac フォルダーで検索するときに in:じゃなくて /にする +バグ修正 2015-11-15 00:57:29 +09:00
Rokt33r
ef84c4e3da IntroのFinder説明変更 2015-11-14 23:49:49 +09:00
53 changed files with 1465 additions and 618 deletions

View File

@@ -1,5 +1,6 @@
var BrowserWindow = require('browser-window') const electron = require('electron')
var path = require('path') const BrowserWindow = electron.BrowserWindow
const path = require('path')
var finderWindow = new BrowserWindow({ var finderWindow = new BrowserWindow({
width: 640, width: 640,
@@ -18,12 +19,10 @@ var finderWindow = new BrowserWindow({
var url = path.resolve(__dirname, '../browser/finder/index.html') var url = path.resolve(__dirname, '../browser/finder/index.html')
finderWindow.loadUrl('file://' + url) finderWindow.loadURL('file://' + url)
finderWindow.on('blur', function () { finderWindow.on('blur', function () {
finderWindow.hide() finderWindow.hide()
}) })
finderWindow.setVisibleOnAllWorkspaces(true)
module.exports = finderWindow module.exports = finderWindow

View File

@@ -1,5 +1,6 @@
var BrowserWindow = require('browser-window') const electron = require('electron')
var path = require('path') const BrowserWindow = electron.BrowserWindow
const path = require('path')
var mainWindow = new BrowserWindow({ var mainWindow = new BrowserWindow({
width: 1080, width: 1080,
@@ -11,11 +12,9 @@ var mainWindow = new BrowserWindow({
'standard-window': false 'standard-window': false
}) })
var url = path.resolve(__dirname, '../browser/main/index.html') const url = path.resolve(__dirname, '../browser/main/index.html')
mainWindow.loadUrl('file://' + url) mainWindow.loadURL('file://' + url)
mainWindow.setVisibleOnAllWorkspaces(true)
mainWindow.webContents.on('new-window', function (e) { mainWindow.webContents.on('new-window', function (e) {
e.preventDefault() e.preventDefault()

View File

@@ -1,4 +1,5 @@
var BrowserWindow = require('browser-window') const electron = require('electron')
const BrowserWindow = electron.BrowserWindow
module.exports = [ module.exports = [
{ {
@@ -89,13 +90,6 @@ module.exports = [
click: function () { click: function () {
BrowserWindow.getFocusedWindow().reload() BrowserWindow.getFocusedWindow().reload()
} }
},
{
label: 'Toggle DevTools',
accelerator: 'Alt+Command+I',
click: function () {
BrowserWindow.getFocusedWindow().toggleDevTools()
}
} }
] ]
}, },

View File

@@ -1,37 +0,0 @@
var autoUpdater = require('auto-updater')
var nn = require('node-notifier')
var app = require('app')
var path = require('path')
var version = app.getVersion()
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
autoUpdater
.on('error', function (err, message) {
console.error(err)
console.error(message)
nn.notify({
title: 'Error! ' + versionText,
icon: path.join(__dirname, 'browser/main/resources/favicon-230x230.png'),
message: message
})
})
// .on('checking-for-update', function () {
// // Connecting
// })
.on('update-available', function () {
nn.notify({
title: 'Update is available!! ' + versionText,
icon: path.join(__dirname, 'browser/main/resources/favicon-230x230.png'),
message: 'Download started.. wait for the update ready.'
})
})
.on('update-not-available', function () {
nn.notify({
title: 'Latest Build!! ' + versionText,
icon: path.join(__dirname, 'browser/main/resources/favicon-230x230.png'),
message: 'Hope you to enjoy our app :D'
})
})
module.exports = autoUpdater

View File

@@ -11,7 +11,16 @@ export default class FinderDetail extends React.Component {
return ( return (
<div className='FinderDetail'> <div className='FinderDetail'>
<div className='header'> <div className='header'>
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}</div> <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'> <div className='content'>
{activeArticle.mode === 'markdown' {activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/> ? <MarkdownPreview content={activeArticle.content}/>
@@ -30,5 +39,6 @@ export default class FinderDetail extends React.Component {
} }
FinderDetail.propTypes = { FinderDetail.propTypes = {
activeArticle: PropTypes.shape() activeArticle: PropTypes.shape(),
saveToClipboard: PropTypes.func
} }

View File

@@ -16,11 +16,8 @@ export function searchArticle (input) {
} }
} }
export function refreshData () { export function refreshData (data) {
console.log('refreshing data') console.log('refreshing data')
let data = JSON.parse(localStorage.getItem('local'))
if (data == null) return null
let { folders, articles } = data let { folders, articles } = data
return { return {
@@ -31,3 +28,12 @@ export function refreshData () {
} }
} }
} }
export default {
SELECT_ARTICLE,
SEARCH_ARTICLE,
REFRESH_DATA,
selectArticle,
searchArticle,
refreshData
}

View File

@@ -2,13 +2,14 @@
<html> <html>
<head> <head>
<title>CodeXen Popup</title> <title>Boost Finder</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8"> <link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css"> <link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="stylesheet" href="../../node_modules/highlight.js/styles/xcode.css">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<style> <style>
@@ -27,7 +28,8 @@
<div id="content"></div> <div id="content"></div>
<script src="../../submodules/ace/src-min/ace.js"></script> <script src="../../submodules/ace/src-min/ace.js"></script>
<script> <script>
require('web-frame').setZoomLevelLimits(1, 1) const electron = require('electron')
electron.webFrame.setZoomLevelLimits(1, 1)
var scriptUrl = process.env.BOOST_ENV === 'development' var scriptUrl = process.env.BOOST_ENV === 'development'
? 'http://localhost:8080/assets/finder.js' ? 'http://localhost:8080/assets/finder.js'
: '../../compiled/finder.js' : '../../compiled/finder.js'

View File

@@ -6,17 +6,23 @@ import { createStore } from 'redux'
import FinderInput from './FinderInput' import FinderInput from './FinderInput'
import FinderList from './FinderList' import FinderList from './FinderList'
import FinderDetail from './FinderDetail' import FinderDetail from './FinderDetail'
import { selectArticle, searchArticle, refreshData } from './actions' import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash' import _ from 'lodash'
import activityRecord from 'boost/activityRecord' import dataStore from 'boost/dataStore'
const electron = require('electron')
const { remote, clipboard } = electron
import remote from 'remote'
var hideFinder = remote.getGlobal('hideFinder') var hideFinder = remote.getGlobal('hideFinder')
import clipboard from 'clipboard'
function notify (...args) {
return new window.Notification(...args)
}
require('../styles/finder/index.styl') require('../styles/finder/index.styl')
const FOLDER_FILTER = 'FOLDER_FILTER' const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TEXT_FILTER = 'TEXT_FILTER' const TEXT_FILTER = 'TEXT_FILTER'
const TAG_FILTER = 'TAG_FILTER' const TAG_FILTER = 'TAG_FILTER'
@@ -45,10 +51,7 @@ class FinderMain extends React.Component {
} }
if (e.keyCode === 13) { if (e.keyCode === 13) {
let { activeArticle } = this.props this.saveToClipboard()
clipboard.writeText(activeArticle.content)
activityRecord.emit('FINDER_COPY')
hideFinder()
e.preventDefault() e.preventDefault()
} }
if (e.keyCode === 27) { if (e.keyCode === 27) {
@@ -57,6 +60,16 @@ class FinderMain extends React.Component {
} }
} }
saveToClipboard () {
let { activeArticle } = this.props
clipboard.writeText(activeArticle.content)
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!'
})
hideFinder()
}
handleSearchChange (e) { handleSearchChange (e) {
let { dispatch } = this.props let { dispatch } = this.props
@@ -83,6 +96,7 @@ class FinderMain extends React.Component {
render () { render () {
let { articles, activeArticle, status, dispatch } = this.props let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard()
return ( return (
<div onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'> <div onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'>
<FinderInput <FinderInput
@@ -98,7 +112,10 @@ class FinderMain extends React.Component {
dispatch={dispatch} dispatch={dispatch}
selectArticle={article => this.selectArticle(article)} selectArticle={article => this.selectArticle(article)}
/> />
<FinderDetail activeArticle={activeArticle}/> <FinderDetail
activeArticle={activeArticle}
saveToClipboard={saveToClipboard}
/>
</div> </div>
) )
} }
@@ -116,27 +133,47 @@ FinderMain.propTypes = {
dispatch: PropTypes.func 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 remap (state) { function remap (state) {
let { articles, folders, status } = state let { articles, folders, status } = state
let filters = status.search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => { let filters = status.search.split(' ')
if (key.match(/^in:.+$/)) { .map(key => key.trim())
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]} .filter(ignoreInvalidKey)
} .map(buildFilter)
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]} let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
}
return {type: TEXT_FILTER, value: key}
})
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER) let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER) let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER) let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) { if (folders != null) {
let targetFolders = folders.filter(folder => { let exactTargetFolders = folders.filter(folder => {
return _.findWhere(folderFilters, {value: folder.name}) return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`)))
}) })
status.targetFolders = targetFolders let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`)))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) { if (targetFolders.length > 0) {
articles = articles.filter(article => { articles = articles.filter(article => {
@@ -164,6 +201,7 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey}) let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0] if (activeArticle == null) activeArticle = articles[0]
console.log(status.search)
return { return {
articles, articles,
activeArticle, activeArticle,
@@ -174,13 +212,19 @@ function remap (state) {
var Finder = connect(remap)(FinderMain) var Finder = connect(remap)(FinderMain)
var store = createStore(reducer) var store = createStore(reducer)
function refreshData () {
let data = dataStore.getData()
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => { window.onfocus = e => {
store.dispatch(refreshData()) refreshData()
activityRecord.emit('FINDER_OPEN')
} }
ReactDOM.render(( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
<Finder/> <Finder/>
</Provider> </Provider>
), document.getElementById('content')) ), document.getElementById('content'), function () {
refreshData()
})

View File

@@ -1,10 +1,8 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions' import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
let data = JSON.parse(localStorage.getItem('local')) let initialArticles = []
let initialFolders = []
let initialArticles = data != null ? data.articles : []
let initialFolders = data != null ? data.folders : []
let initialStatus = { let initialStatus = {
articleKey: null, articleKey: null,
search: '' search: ''
@@ -14,10 +12,10 @@ function status (state = initialStatus, action) {
switch (action.type) { switch (action.type) {
case SELECT_ARTICLE: case SELECT_ARTICLE:
state.articleKey = action.data.key state.articleKey = action.data.key
return state return Object.assign({}, state)
case SEARCH_ARTICLE: case SEARCH_ARTICLE:
state.search = action.data.input state.search = action.data.input
return state return Object.assign({}, state)
default: default:
return state return state
} }

View File

@@ -1,17 +1,19 @@
import React, { PropTypes} from 'react' import React, { PropTypes} from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { CREATE_MODE, EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions' import { EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions'
// import UserNavigator from './HomePage/UserNavigator' // import UserNavigator from './HomePage/UserNavigator'
import ArticleNavigator from './HomePage/ArticleNavigator' import ArticleNavigator from './HomePage/ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar' import ArticleTopBar from './HomePage/ArticleTopBar'
import ArticleList from './HomePage/ArticleList' import ArticleList from './HomePage/ArticleList'
import ArticleDetail from './HomePage/ArticleDetail' import ArticleDetail from './HomePage/ArticleDetail'
import _ from 'lodash' import _ from 'lodash'
import keygen from 'boost/keygen'
import { isModalOpen, closeModal } from 'boost/modal' import { isModalOpen, closeModal } from 'boost/modal'
const electron = require('electron')
const BrowserWindow = electron.remote.BrowserWindow
const TEXT_FILTER = 'TEXT_FILTER' const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER' const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER' const TAG_FILTER = 'TAG_FILTER'
class HomePage extends React.Component { class HomePage extends React.Component {
@@ -27,6 +29,13 @@ class HomePage extends React.Component {
} }
handleKeyDown (e) { handleKeyDown (e) {
if (process.env.BOOST_ENV === 'development' && e.keyCode === 73 && e.metaKey && e.altKey) {
e.preventDefault()
e.stopPropagation()
BrowserWindow.getFocusedWindow().toggleDevTools()
return
}
if (isModalOpen()) { if (isModalOpen()) {
if (e.keyCode === 27) closeModal() if (e.keyCode === 27) closeModal()
return return
@@ -48,7 +57,7 @@ class HomePage extends React.Component {
} }
switch (status.mode) { switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE: case EDIT_MODE:
if (e.keyCode === 27) { if (e.keyCode === 27) {
detail.handleCancelButtonClick() detail.handleCancelButtonClick()
@@ -56,6 +65,13 @@ class HomePage extends React.Component {
if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) { if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) {
detail.handleSaveButtonClick() detail.handleSaveButtonClick()
} }
if (e.keyCode === 80 && e.metaKey) {
detail.handleTogglePreviewButtonClick()
}
if (e.keyCode === 78 && e.metaKey) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
break break
case IDLE_MODE: case IDLE_MODE:
if (e.keyCode === 69) { if (e.keyCode === 69) {
@@ -90,7 +106,7 @@ class HomePage extends React.Component {
list.selectNextArticle() list.selectNextArticle()
} }
if (e.keyCode === 65 || e.keyCode === 13 && e.metaKey) { if (e.keyCode === 65 || (e.keyCode === 13 && e.metaKey) || (e.keyCode === 78 && e.metaKey)) {
nav.handleNewPostButtonClick() nav.handleNewPostButtonClick()
e.preventDefault() e.preventDefault()
} }
@@ -98,7 +114,7 @@ class HomePage extends React.Component {
} }
render () { render () {
let { dispatch, status, articles, activeArticle, folders, filters } = this.props let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
return ( return (
<div className='HomePage'> <div className='HomePage'>
@@ -107,6 +123,7 @@ class HomePage extends React.Component {
dispatch={dispatch} dispatch={dispatch}
folders={folders} folders={folders}
status={status} status={status}
allArticles={allArticles}
/> />
<ArticleTopBar <ArticleTopBar
ref='top' ref='top'
@@ -127,6 +144,7 @@ class HomePage extends React.Component {
activeArticle={activeArticle} activeArticle={activeArticle}
folders={folders} folders={folders}
status={status} status={status}
tags={tags}
filters={filters} filters={filters}
/> />
</div> </div>
@@ -134,6 +152,25 @@ class HomePage extends React.Component {
} }
} }
// 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 remap (state) { function remap (state) {
let { folders, articles, status } = state let { folders, articles, status } = state
@@ -141,26 +178,33 @@ function remap (state) {
articles.sort((a, b) => { articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt) return new Date(b.updatedAt) - new Date(a.updatedAt)
}) })
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
// Filter articles // Filter articles
let filters = status.search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => { let filters = status.search.split(' ')
if (key.match(/^in:.+$/)) { .map(key => key.trim())
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]} .filter(ignoreInvalidKey)
} .map(buildFilter)
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]} let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
}
return {type: TEXT_FILTER, value: key}
})
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER) let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER) let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER) let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) { if (folders != null) {
let targetFolders = folders.filter(folder => { let exactTargetFolders = folders.filter(folder => {
return _.findWhere(folderFilters, {value: folder.name}) return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`)))
}) })
status.targetFolders = targetFolders let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`)))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) { if (targetFolders.length > 0) {
articles = articles.filter(article => { articles = articles.filter(article => {
@@ -189,48 +233,13 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey}) let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0] if (activeArticle == null) activeArticle = articles[0]
// remove Unsaved new article if user is not CREATE_MODE
if (status.mode !== CREATE_MODE) {
let targetIndex = _.findIndex(articles, article => article.status === NEW)
if (targetIndex >= 0) articles.splice(targetIndex, 1)
}
// switching CREATE_MODE
// restrict
// 1. team have one folder at least
// or Change IDLE MODE
if (status.mode === CREATE_MODE) {
let newArticle = _.findWhere(articles, {status: 'NEW'})
let FolderKey = folders[0].key
if (folderFilters.length > 0) {
let targetFolder = _.findWhere(folders, {name: folderFilters[0].value})
if (targetFolder != null) FolderKey = targetFolder.key
}
if (newArticle == null) {
newArticle = {
id: null,
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
status: NEW
}
articles.unshift(newArticle)
}
activeArticle = newArticle
} else if (status.mode === CREATE_MODE) {
status.mode = IDLE_MODE
}
return { return {
folders, folders,
status, status,
allArticles,
articles, articles,
activeArticle, activeArticle,
tags,
filters: { filters: {
folder: folderFilters, folder: folderFilters,
tag: tagFilters, tag: tagFilters,
@@ -247,6 +256,7 @@ HomePage.propTypes = {
userId: PropTypes.string userId: PropTypes.string
}), }),
articles: PropTypes.array, articles: PropTypes.array,
allArticles: PropTypes.array,
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
dispatch: PropTypes.func, dispatch: PropTypes.func,
folders: PropTypes.array, folders: PropTypes.array,
@@ -254,7 +264,8 @@ HomePage.propTypes = {
folder: PropTypes.array, folder: PropTypes.array,
tag: PropTypes.array, tag: PropTypes.array,
text: PropTypes.array text: PropTypes.array
}) }),
tags: PropTypes.array
} }
export default connect(remap)(HomePage) export default connect(remap)(HomePage)

View File

@@ -5,7 +5,19 @@ import _ from 'lodash'
import ModeIcon from 'boost/components/ModeIcon' import ModeIcon from 'boost/components/ModeIcon'
import MarkdownPreview from 'boost/components/MarkdownPreview' import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor' import CodeEditor from 'boost/components/CodeEditor'
import { IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode, switchArticle, switchFolder, clearSearch, updateArticle, destroyArticle, NEW } from 'boost/actions' import {
IDLE_MODE,
EDIT_MODE,
switchMode,
switchArticle,
switchFolder,
clearSearch,
lockStatus,
unlockStatus,
updateArticle,
destroyArticle,
NEW
} from 'boost/actions'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'boost/components/FolderMark'
import TagLink from 'boost/components/TagLink' import TagLink from 'boost/components/TagLink'
@@ -13,6 +25,9 @@ import TagSelect from 'boost/components/TagSelect'
import ModeSelect from 'boost/components/ModeSelect' import ModeSelect from 'boost/components/ModeSelect'
import activityRecord from 'boost/activityRecord' import activityRecord from 'boost/activityRecord'
const electron = require('electron')
const clipboard = electron.clipboard
const BRAND_COLOR = '#18AF90' const BRAND_COLOR = '#18AF90'
const editDeleteTutorialElement = ( const editDeleteTutorialElement = (
@@ -72,6 +87,10 @@ const modeSelectTutorialElement = (
</svg> </svg>
) )
function notify (...args) {
return new window.Notification(...args)
}
function makeInstantArticle (article) { function makeInstantArticle (article) {
return Object.assign({}, article) return Object.assign({}, article)
} }
@@ -82,7 +101,12 @@ export default class ArticleDetail extends React.Component {
this.state = { this.state = {
article: makeInstantArticle(props.activeArticle), article: makeInstantArticle(props.activeArticle),
previewMode: false previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false,
isModeChanged: false
} }
} }
@@ -96,7 +120,7 @@ export default class ArticleDetail extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
let isModeChanged = prevProps.status.mode !== this.props.status.mode let isModeChanged = prevProps.status.mode !== this.props.status.mode
if (isModeChanged && this.props.status.mode !== IDLE_MODE) { if (isModeChanged && this.props.status.mode === EDIT_MODE) {
ReactDOM.findDOMNode(this.refs.title).focus() ReactDOM.findDOMNode(this.refs.title).focus()
} }
} }
@@ -106,6 +130,7 @@ export default class ArticleDetail extends React.Component {
let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key) let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key)
let isModeChanged = nextProps.status.mode !== this.props.status.mode let isModeChanged = nextProps.status.mode !== this.props.status.mode
// Reset article input // Reset article input
if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) { if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) {
Object.assign(nextState, { Object.assign(nextState, {
@@ -117,7 +142,11 @@ export default class ArticleDetail extends React.Component {
if (isModeChanged) { if (isModeChanged) {
Object.assign(nextState, { Object.assign(nextState, {
openDeleteConfirmMenu: false, openDeleteConfirmMenu: false,
previewMode: false previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false
}) })
} }
@@ -132,6 +161,13 @@ export default class ArticleDetail extends React.Component {
) )
} }
handleClipboardButtonClick (e) {
clipboard.writeText(this.props.activeArticle.content)
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!'
})
}
handleEditButtonClick (e) { handleEditButtonClick (e) {
let { dispatch } = this.props let { dispatch } = this.props
dispatch(switchMode(EDIT_MODE)) dispatch(switchMode(EDIT_MODE))
@@ -163,8 +199,13 @@ export default class ArticleDetail extends React.Component {
: ( : (
<span className='noTags'>Not tagged yet</span> <span className='noTags'>Not tagged yet</span>
) : null ) : null
let folder = _.findWhere(folders, {key: activeArticle.FolderKey}) let folder = _.findWhere(folders, {key: activeArticle.FolderKey})
let title = activeArticle.title.trim().length === 0
? <small>(Untitled)</small>
: activeArticle.title
return ( return (
<div className='ArticleDetail idle'> <div className='ArticleDetail idle'>
{this.state.openDeleteConfirmMenu {this.state.openDeleteConfirmMenu
@@ -192,6 +233,9 @@ export default class ArticleDetail extends React.Component {
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div> <div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div> </div>
<div className='right'> <div className='right'>
<button onClick={e => this.handleClipboardButtonClick(e)} className='editBtn'>
<i className='fa fa-fw fa-clipboard'/><span className='tooltip'>Copy to clipboard</span>
</button>
<button onClick={e => this.handleEditButtonClick(e)} className='editBtn'> <button onClick={e => this.handleEditButtonClick(e)} className='editBtn'>
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Edit (e)</span> <i className='fa fa-fw fa-edit'/><span className='tooltip'>Edit (e)</span>
</button> </button>
@@ -210,7 +254,7 @@ export default class ArticleDetail extends React.Component {
<div className='detailPanel'> <div className='detailPanel'>
<div className='header'> <div className='header'>
<ModeIcon className='mode' mode={activeArticle.mode}/> <ModeIcon className='mode' mode={activeArticle.mode}/>
<div className='title'>{activeArticle.title}</div> <div className='title'>{title}</div>
</div> </div>
{activeArticle.mode === 'markdown' {activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/> ? <MarkdownPreview content={activeArticle.content}/>
@@ -224,22 +268,31 @@ export default class ArticleDetail extends React.Component {
handleCancelButtonClick (e) { handleCancelButtonClick (e) {
let { activeArticle, dispatch } = this.props let { activeArticle, dispatch } = this.props
if (activeArticle.status === NEW) dispatch(switchArticle(null))
if (activeArticle.status === NEW) {
dispatch(switchArticle(null))
}
dispatch(switchMode(IDLE_MODE)) dispatch(switchMode(IDLE_MODE))
} }
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
let { dispatch, folders, filters } = this.props let { dispatch, folders, status } = this.props
let article = this.state.article let article = this.state.article
let newArticle = Object.assign({}, article) let newArticle = Object.assign({}, article)
let folder = _.findWhere(folders, {key: article.FolderKey}) let folder = _.findWhere(folders, {key: article.FolderKey})
if (folder == null) return false if (folder == null) return false
dispatch(unlockStatus())
delete newArticle.status delete newArticle.status
newArticle.updatedAt = new Date() newArticle.updatedAt = new Date()
newArticle.title = newArticle.title.trim()
if (newArticle.createdAt == null) { if (newArticle.createdAt == null) {
newArticle.createdAt = new Date() newArticle.createdAt = new Date()
if (newArticle.title.length === 0) {
newArticle.title = `Created at ${moment(newArticle.createdAt).format('YYYY/MM/DD HH:mm')}`
}
activityRecord.emit('ARTICLE_CREATE') activityRecord.emit('ARTICLE_CREATE')
} else { } else {
activityRecord.emit('ARTICLE_UPDATE') activityRecord.emit('ARTICLE_UPDATE')
@@ -251,7 +304,7 @@ export default class ArticleDetail extends React.Component {
// Searchを初期化し、更新先のFolder filterをかける // Searchを初期化し、更新先のFolder filterをかける
// かかれていない時に // かかれていない時に
// Searchを初期化する // Searchを初期化する
if (filters.folder.length !== 0) dispatch(switchFolder(folder.name)) if (status.targetFolders.length > 0) dispatch(switchFolder(folder.name))
else dispatch(clearSearch()) else dispatch(clearSearch())
dispatch(switchArticle(newArticle.key)) dispatch(switchArticle(newArticle.key))
} }
@@ -263,19 +316,83 @@ export default class ArticleDetail extends React.Component {
this.setState({article: article}) this.setState({article: article})
} }
handleTitleChange (e) {
let { article } = this.state
article.title = e.target.value
let _isTitleChanged = article.title !== this.props.activeArticle.title
let { isTagChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTitleChanged || isTagChanged || isContentChanged || isModeChanged
this.setState({
article,
isTitleChanged: _isTitleChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleTagsChange (newTag, tags) { handleTagsChange (newTag, tags) {
let article = this.state.article let article = this.state.article
article.tags = tags article.tags = tags
this.setState({article: article}) let _isTagChanged = _.difference(article.tags, this.props.activeArticle.tags).length > 0 || _.difference(this.props.activeArticle.tags, article.tags).length > 0
let { isTitleChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTagChanged || isTitleChanged || isContentChanged || isModeChanged
this.setState({
article,
isTagChanged: _isTagChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleModeChange (value) { handleModeChange (value) {
let article = this.state.article let { article } = this.state
article.mode = value article.mode = value
let _isModeChanged = article.mode !== this.props.activeArticle.mode
let { isTagChanged, isContentChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isModeChanged || isTagChanged || isContentChanged || isTitleChanged
this.setState({ this.setState({
article: article, article,
previewMode: false previewMode: false,
isModeChanged: _isModeChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
}) })
} }
@@ -286,13 +403,40 @@ export default class ArticleDetail extends React.Component {
} }
handleContentChange (e, value) { handleContentChange (e, value) {
let article = this.state.article let { status } = this.props
if (status.mode === IDLE_MODE) {
return
}
let { article } = this.state
article.content = value article.content = value
this.setState({article: article}) let _isContentChanged = article.content !== this.props.activeArticle.content
let { isTagChanged, isModeChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isContentChanged || isTagChanged || isModeChanged || isTitleChanged
this.setState({
article,
isContentChanged: _isContentChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleTogglePreviewButtonClick (e) { handleTogglePreviewButtonClick (e) {
this.setState({previewMode: !this.state.previewMode}) if (this.state.article.mode === 'markdown') {
this.setState({previewMode: !this.state.previewMode})
}
} }
handleTitleKeyDown (e) { handleTitleKeyDown (e) {
@@ -303,7 +447,7 @@ export default class ArticleDetail extends React.Component {
} }
renderEdit () { renderEdit () {
let { folders, status } = this.props let { folders, status, tags } = this.props
let folderOptions = folders.map(folder => { let folderOptions = folders.map(folder => {
return ( return (
@@ -322,10 +466,12 @@ export default class ArticleDetail extends React.Component {
> >
{folderOptions} {folderOptions}
</select> </select>
{this.state.isArticleEdited ? ' (edited)' : ''}
<TagSelect <TagSelect
tags={this.state.article.tags} tags={this.state.article.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)} onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/> />
{status.isTutorialOpen ? tagSelectTutorialElement : null} {status.isTutorialOpen ? tagSelectTutorialElement : null}
@@ -335,18 +481,36 @@ export default class ArticleDetail extends React.Component {
<div className='right'> <div className='right'>
{ {
this.state.article.mode === 'markdown' this.state.article.mode === 'markdown'
? (<button className='preview' onClick={e => this.handleTogglePreviewButtonClick(e)}>{!this.state.previewMode ? 'Preview' : 'Edit'}</button>) ? (<button className='preview' onClick={e => this.handleTogglePreviewButtonClick(e)}>
{
!this.state.previewMode
? 'Preview'
: 'Edit'
}
</button>)
: null : null
} }
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button> <button onClick={e => this.handleCancelButtonClick(e)}>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button> Cancel
</button>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>
Save
</button>
</div> </div>
</div> </div>
<div className='detailBody'> <div className='detailBody'>
<div className='detailPanel'> <div className='detailPanel'>
<div className='header'> <div className='header'>
<div className='title'> <div className='title'>
<input onKeyDown={e => this.handleTitleKeyDown(e)} placeholder='Title' ref='title' valueLink={this.linkState('article.title')}/> <input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder={this.state.article.createdAt == null
? `Created at ${moment().format('YYYY/MM/DD HH:mm')}`
: 'Title'}
ref='title'
value={this.state.article.title}
onChange={e => this.handleTitleChange(e)}
/>
</div> </div>
<ModeSelect <ModeSelect
ref='mode' ref='mode'
@@ -381,7 +545,6 @@ export default class ArticleDetail extends React.Component {
if (activeArticle == null) return this.renderEmpty() if (activeArticle == null) return this.renderEmpty()
switch (status.mode) { switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE: case EDIT_MODE:
return this.renderEdit() return this.renderEdit()
case IDLE_MODE: case IDLE_MODE:
@@ -395,6 +558,7 @@ export default class ArticleDetail extends React.Component {
ArticleDetail.propTypes = { ArticleDetail.propTypes = {
status: PropTypes.shape(), status: PropTypes.shape(),
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
activeUser: PropTypes.shape() activeUser: PropTypes.shape(),
dispatch: PropTypes.func
} }
ArticleDetail.prototype.linkState = linkState ArticleDetail.prototype.linkState = linkState

View File

@@ -80,6 +80,12 @@ export default class ArticleList extends React.Component {
: (<span>Not tagged yet</span>) : (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey}) let folder = _.findWhere(folders, {key: article.FolderKey})
let title = article.status !== NEW
? article.title.trim().length === 0
? <small>(Untitled)</small>
: article.title
: '(New article)'
return ( return (
<div key={'article-' + article.key}> <div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}> <div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
@@ -91,7 +97,7 @@ export default class ArticleList extends React.Component {
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span> <span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
</div> </div>
<div className='middle'> <div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== NEW ? article.title : '(New article)'}</div> <ModeIcon className='mode' mode={article.mode}/> <div className='title'>{title}</div>
</div> </div>
<div className='bottom'> <div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div> <div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>

View File

@@ -1,12 +1,14 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { findWhere } from 'lodash' import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions' import { setSearchFilter, switchFolder, switchMode, switchArticle, updateArticle, clearNewArticle, EDIT_MODE } from 'boost/actions'
import { openModal } from 'boost/modal' import { openModal } from 'boost/modal'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'boost/components/FolderMark'
import Preferences from 'boost/components/modal/Preferences' import Preferences from 'boost/components/modal/Preferences'
import CreateNewFolder from 'boost/components/modal/CreateNewFolder' import CreateNewFolder from 'boost/components/modal/CreateNewFolder'
import keygen from 'boost/keygen'
import remote from 'remote' const electron = require('electron')
const remote = electron.remote
let userName = remote.getGlobal('process').env.USER let userName = remote.getGlobal('process').env.USER
const BRAND_COLOR = '#18AF90' const BRAND_COLOR = '#18AF90'
@@ -65,9 +67,28 @@ export default class ArticleNavigator extends React.Component {
} }
handleNewPostButtonClick (e) { handleNewPostButtonClick (e) {
let { dispatch } = this.props let { dispatch, folders, status } = this.props
let { targetFolders } = status
dispatch(switchMode(CREATE_MODE)) let FolderKey = targetFolders.length > 0
? targetFolders[0].key
: folders[0].key
let newArticle = {
id: null,
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
status: 'NEW'
}
dispatch(clearNewArticle())
dispatch(updateArticle(newArticle))
dispatch(switchArticle(newArticle.key, true))
dispatch(switchMode(EDIT_MODE))
} }
handleNewFolderButton (e) { handleNewFolderButton (e) {
@@ -88,16 +109,17 @@ export default class ArticleNavigator extends React.Component {
} }
render () { render () {
let { status, folders } = this.props let { status, folders, allArticles } = this.props
let { targetFolders } = status let { targetFolders } = status
if (targetFolders == null) targetFolders = [] if (targetFolders == null) targetFolders = []
let folderElememts = folders.map((folder, index) => { let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key}) let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
return ( return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}> <button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
<FolderMark color={folder.color}/> {folder.name} <FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
</button> </button>
) )
}) })
@@ -150,6 +172,7 @@ export default class ArticleNavigator extends React.Component {
ArticleNavigator.propTypes = { ArticleNavigator.propTypes = {
activeUser: PropTypes.object, activeUser: PropTypes.object,
folders: PropTypes.array, folders: PropTypes.array,
allArticles: PropTypes.array,
status: PropTypes.shape({ status: PropTypes.shape({
folderId: PropTypes.number folderId: PropTypes.number
}), }),

View File

@@ -10,7 +10,9 @@ const searchTutorialElement = (
<text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text> <text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
<text x='450' y='60' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text> <text x='450' y='60' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='450' y='85' fill={BRAND_COLOR} fontSize='18'> <text x='450' y='85' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : in:{folder_name}\n'}</text> {'- Search by folder : /{folder_name}\n'}</text>
<text x='465' y='105' fill={BRAND_COLOR} fontSize='14'>
{'exact match : //{folder_name}'}</text>
<svg width='500' height='300'> <svg width='500' height='300'>
<path fill='white' d='M54.5,51.5c-12.4,3.3-27.3-1.4-38.4-7C11.2,42,5,38.1,5.6,31.8c0.7-6.9,8.1-11.2,13.8-13.7 <path fill='white' d='M54.5,51.5c-12.4,3.3-27.3-1.4-38.4-7C11.2,42,5,38.1,5.6,31.8c0.7-6.9,8.1-11.2,13.8-13.7
@@ -33,18 +35,33 @@ export default class ArticleTopBar extends React.Component {
super(props) super(props)
this.state = { this.state = {
isTooltipHidden: true isTooltipHidden: true,
isLinksDropdownOpen: false
} }
} }
componentDidMount () { componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput) this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
this.linksButton = ReactDOM.findDOMNode(this.refs.links)
this.showLinksDropdown = e => {
e.preventDefault()
e.stopPropagation()
if (!this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: true})
}
}
this.linksButton.addEventListener('click', this.showLinksDropdown)
this.hideLinksDropdown = e => {
if (this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: false})
}
}
document.addEventListener('click', this.hideLinksDropdown)
} }
componentWillUnmount () { componentWillUnmount () {
this.searchInput.removeEventListener('keydown', this.showTooltip) document.removeEventListener('click', this.hideLinksDropdown)
this.searchInput.removeEventListener('focus', this.showTooltip) this.linksButton.removeEventListener('click', this.showLinksDropdown())
this.searchInput.removeEventListener('blur', this.showTooltip)
} }
handleTooltipRequest (e) { handleTooltipRequest (e) {
@@ -116,8 +133,11 @@ export default class ArticleTopBar extends React.Component {
: null : null
} }
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}> <div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
- Search by tag : #{'{string}'}<br/> <ul>
- Search by folder : in:{'{folder_name}'} <li>- Search by tag : #{'{string}'}</li>
<li>- Search by folder : /{'{folder_name}'}</li>
<li><small>exact match : //{'{folder_name}'}</small></li>
</ul>
</div> </div>
</div> </div>
@@ -127,10 +147,23 @@ export default class ArticleTopBar extends React.Component {
<div className='right'> <div className='right'>
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span> <button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
</button> </button>
<ExternalLink className='logo' href='http://b00st.io'> <a ref='links' className='linksBtn' href>
<img src='../../resources/favicon-230x230.png' width='44' height='44'/> <img src='../../resources/favicon-230x230.png' width='44' height='44'/>
<span className='tooltip'>Boost official page</span> </a>
</ExternalLink> {
this.state.isLinksDropdownOpen
? (
<div className='links-dropdown'>
<ExternalLink className='links-item' href='https://b00st.io'>
<i className='fa fa-fw fa-home'/>Boost official page
</ExternalLink>
<ExternalLink className='links-item' href='https://github.com/BoostIO/boost-app-discussions/issues'>
<i className='fa fa-fw fa-bullhorn'/> Discuss
</ExternalLink>
</div>
)
: null
}
</div> </div>
{status.isTutorialOpen ? ( {status.isTutorialOpen ? (

View File

@@ -1,4 +1,5 @@
import ipc from 'ipc' const electron = require('electron')
const ipc = electron.ipcRenderer
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
var ContactModal = require('boost/components/modal/ContactModal') var ContactModal = require('boost/components/modal/ContactModal')

View File

@@ -6,6 +6,7 @@
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8"> <link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css"> <link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="stylesheet" href="../../node_modules/highlight.js/styles/xcode.css">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<style> <style>
@@ -53,8 +54,9 @@
<script src="../../submodules/ace/src-min/ace.js"></script> <script src="../../submodules/ace/src-min/ace.js"></script>
<script type='text/javascript'> <script type='text/javascript'>
require('web-frame').setZoomLevelLimits(1, 1) const electron = require('electron')
var version = require('remote').require('app').getVersion() electron.webFrame.setZoomLevelLimits(1, 1)
var version = electron.remote.app.getVersion()
document.title = 'Boost' + ((version == null || version.length === 0) ? ' DEV' : '') document.title = 'Boost' + ((version == null || version.length === 0) ? ' DEV' : '')
var scriptUrl = process.env.BOOST_ENV === 'development' var scriptUrl = process.env.BOOST_ENV === 'development'
? 'http://localhost:8080/assets/main.js' ? 'http://localhost:8080/assets/main.js'

View File

@@ -11,8 +11,23 @@ require('../styles/main/index.styl')
import { openModal } from 'boost/modal' import { openModal } from 'boost/modal'
import Tutorial from 'boost/components/modal/Tutorial' import Tutorial from 'boost/components/modal/Tutorial'
import activityRecord from 'boost/activityRecord' import activityRecord from 'boost/activityRecord'
const electron = require('electron')
const ipc = electron.ipcRenderer
activityRecord.init() activityRecord.init()
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
})
function notify (...args) {
return new window.Notification(...args)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
let routes = ( let routes = (
<Route path='/' component={MainPage}> <Route path='/' component={MainPage}>

View File

@@ -79,6 +79,31 @@ body
white-space nowrap white-space nowrap
text-overflow ellipsis text-overflow ellipsis
overflow-x hidden overflow-x hidden
clearfix()
.left
float left
.right
float right
button
border-radius 16.5px
cursor pointer
height 33px
width 33px
border none
margin-right 5px
font-size 18px
color inactiveTextColor
background-color transparent
padding 0
.tooltip
tooltip()
&.clipboardBtn .tooltip
margin-left -160px
margin-top 25px
&:hover
color textColor
.tooltip
opacity 1
.content .content
position absolute position absolute
top 55px top 55px

View File

@@ -98,44 +98,66 @@ iptFocusBorderColor = #369DCD
&:hover &:hover
background-color white background-color white
.TagSelect .TagSelect
white-space nowrap .tags
overflow-x auto white-space nowrap
position relative overflow-x auto
margin-top 5px position relative
noSelect() max-width 350px
z-index 30 margin-top 5px
background-color #E6E6E6 noSelect()
.tagItem z-index 30
background-color brandColor background-color #E6E6E6
border-radius 2px .tagItem
color white background-color brandColor
margin 0 2px border-radius 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
color white color white
margin 0 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
color white
border-radius 2px
border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s
&:hover
background-color lighten(brandColor, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px border-radius 2px
border none border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s transition 0.1s
height 18px
.suggestTags
position fixed
width 150px
max-height 150px
background-color white
z-index 5
border 1px solid borderColor
border-radius 5px
button
width 100%
display block
padding 0 15px
height 33px
line-height 33px
background-color transparent
border none
text-align left
font-size 14px
&:hover &:hover
background-color lighten(brandColor, 10%) background-color darken(white, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px
border none
transition 0.1s
height 18px
.right .right
button button
cursor pointer cursor pointer
@@ -222,9 +244,6 @@ iptFocusBorderColor = #369DCD
display inline-block display inline-block
&:hover &:hover
background-color darken(white, 10%) background-color darken(white, 10%)
.title .title
absolute left top bottom absolute left top bottom
right 150px right 150px
@@ -304,7 +323,8 @@ iptFocusBorderColor = #369DCD
right 15px right 15px
font-size 24px font-size 24px
line-height 60px line-height 60px
white-space nowrap white-space nowrap
overflow-x auto overflow-x auto
overflow-y hidden overflow-y hidden
small
color #AAA

View File

@@ -48,6 +48,8 @@ articleItemColor = #777
left 19px left 19px
right 0 right 0
overflow ellipsis overflow ellipsis
small
color #AAA
.bottom .bottom
padding 5px 0 padding 5px 0
overflow-x auto overflow-x auto

View File

@@ -1,4 +1,5 @@
articleNavBgColor = #353535 articleNavBgColor = #353535
articleCount = #999
.ArticleNavigator .ArticleNavigator
background-color articleNavBgColor background-color articleNavBgColor
@@ -13,7 +14,7 @@ articleNavBgColor = #353535
.userProfileName .userProfileName
color brandColor color brandColor
font-size 28px font-size 28px
padding 6px 0 0 10px padding 6px 37px 0 10px
white-space nowrap white-space nowrap
text-overflow ellipsis text-overflow ellipsis
overflow hidden overflow hidden
@@ -149,7 +150,10 @@ articleNavBgColor = #353535
&:hover &:hover
background-color transparentify(white, 5%) background-color transparentify(white, 5%)
&.active, &:active &.active, &:active
background-color brandColor background-color transparentify(lighten(brandColor, 25%), 70%)
.articleCount
color articleCount
font-size 12px
.members .members
.memberList>div .memberList>div
height 33px height 33px

View File

@@ -62,6 +62,13 @@ infoBtnActiveBgColor = #3A3A3A
opacity 1 opacity 1
&.hide &.hide
opacity 0 opacity 0
ul
li:last-child
line-height 10px
margin-bottom 3px
small
font-size 10px
margin-left 15px
input input
absolute top left absolute top left
width 350px width 350px
@@ -140,17 +147,33 @@ infoBtnActiveBgColor = #3A3A3A
.tooltip .tooltip
opacity 1 opacity 1
&>.logo &>.linksBtn
display block display block
position absolute position absolute
top 8px top 8px
right 15px right 15px
opacity 0.7 opacity 0.7
.tooltip
tooltip()
margin-top 44px
margin-left -120px
&:hover &:hover
opacity 1 opacity 1
.tooltip .tooltip
opacity 1 opacity 1
&>.links-dropdown
position fixed
z-index 50
right 10px
top 40px
background-color transparentify(invBackgroundColor, 80%)
padding 5px 0
.links-item
padding 0 10px
height 33px
width 100%
display block
line-height 33px
text-decoration none
color white
&:hover
background-color transparentify(lighten(invBackgroundColor, 30%), 80%)

View File

@@ -9,3 +9,4 @@
@require './lib/CreateNewFolder' @require './lib/CreateNewFolder'
@require './lib/Preferences' @require './lib/Preferences'
@require './lib/Tutorial' @require './lib/Tutorial'
@require './lib/EditedAlert'

View File

@@ -34,9 +34,30 @@ iptFocusBorderColor = #369DCD
border-radius 5px border-radius 5px
border solid 1px borderColor border solid 1px borderColor
outline none outline none
margin 100px auto 25px margin 75px auto 20px
&:focus &:focus
border-color iptFocusBorderColor border-color iptFocusBorderColor
.colorSelect
text-align center
.option
cursor pointer
font-size 22px
height 48px
width 48px
margin 0 2px
border 1px solid transparent
border-radius 5px
overflow hidden
line-height 45px
text-align center
transition 0.1s
display inline-block
&:hover
border-color borderColor
font-size 28px
&.active
font-size 28px
border-color iptFocusBorderColor
.alert .alert
color infoTextColor color infoTextColor
background-color infoBackgroundColor background-color infoBackgroundColor
@@ -44,7 +65,7 @@ iptFocusBorderColor = #369DCD
padding 15px 15px padding 15px 15px
width 330px width 330px
border-radius 5px border-radius 5px
margin 0 auto margin 15px auto 0
&.error &.error
color errorTextColor color errorTextColor
background-color errorBackgroundColor background-color errorBackgroundColor

View File

@@ -0,0 +1,28 @@
.EditedAlert.modal
width 350px
top 100px
.title
font-size 24px
margin-bottom 15px
.message
font-size 14px
margin-bottom 15px
.control
text-align right
button
border-radius 5px
height 33px
padding 0 15px
font-size 14px
background-color white
border 1px solid borderColor
border-radius 5px
margin-left 5px
&:hover
background-color darken(white, 10%)
&.primary
border-color brandColor
background-color brandColor
color white
&:hover
background-color lighten(brandColor, 10%)

View File

@@ -440,19 +440,22 @@ iptFocusBorderColor = #369DCD
padding 5px 0 padding 5px 0
&:last-child &:last-child
border-color transparent border-color transparent
.folderColor
float left
margin-left 10px
text-align center
width 44px
.folderName .folderName
float left float left
width 175px width 175px
overflow ellipsis overflow ellipsis
padding-left 15px
.folderPublic
float left
text-align center
width 100px
.folderControl .folderControl
float right float right
width 145px width 125px
text-align center text-align center
&.folderHeader
.folderName
padding-left 25px
&.newFolder &.newFolder
.alert .alert
display block display block
@@ -502,6 +505,30 @@ iptFocusBorderColor = #369DCD
&:hover &:hover
color lighten(brandColor, 10%) color lighten(brandColor, 10%)
&.FolderRow &.FolderRow
.sortBtns
float left
display block
height 30px
width 30px
margin-top 1.5px
position absolute
button
absolute left
background-color transparent
border none
height 15px
padding 0
margin 0
color stripBtnColor
&:first-child
top 0
&:last-child
top 15px
&:hover
color stripHoverBtnColor
&:disabled
color lighten(stripBtnColor, 10%)
cursor not-allowed
.folderName input .folderName input
height 33px height 33px
border 1px solid borderColor border 1px solid borderColor
@@ -512,16 +539,52 @@ iptFocusBorderColor = #369DCD
width 150px width 150px
&:focus &:focus
border-color iptFocusBorderColor border-color iptFocusBorderColor
.folderPublic select .folderColor
height 33px .select
border 1px solid borderColor height 33px
background-color white width 33px
outline none border 1px solid borderColor
display block background-color white
margin 0 auto outline none
font-size 14px display block
&:focus margin 0 auto
border-color iptFocusBorderColor font-size 14px
border-radius 5px
&:focus
border-color iptFocusBorderColor
.options
position absolute
background-color white
text-align left
border 1px solid borderColor
border-radius 5px
padding 0 5px 5px
margin-left 5px
margin-top -34px
clearfix()
.label
margin-left 5px
line-height 22px
font-size 12px
button
float left
border none
width 33px
height 33px
margin-right 5px
border 1px solid transparent
line-height 29px
overflow hidden
border-radius 5px
background-color transparent
outline none
transition 0.1s
&:hover
border-color borderColor
&.active
border-color iptFocusBorderColor
.FolderMark
transform scale(1.4)
.folderControl .folderControl
button button
border none border none

View File

@@ -108,6 +108,8 @@ slideBgColor4 = #00B493
height 140px height 140px
.slide3 .slide3
background-color slideBgColor3 background-color slideBgColor3
.title
margin-bottom 15px
.content .content
font-size 18px font-size 18px
&>img &>img

View File

@@ -62,7 +62,7 @@ marked()
display list-item display list-item
line-height 1.8em line-height 1.8em
code code
font-family monospace font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace;
padding 2px 4px padding 2px 4px
border solid 1px borderColor border solid 1px borderColor
border-radius 4px border-radius 4px
@@ -77,6 +77,7 @@ marked()
overflow-x auto overflow-x auto
margin 15px 0 25px margin 15px 0 25px
background-color #F6F6F6 background-color #F6F6F6
line-height 1.35em
&>code &>code
padding 0 padding 0
border none border none

72
finder.js Executable file
View File

@@ -0,0 +1,72 @@
const electron = require('electron')
const app = electron.app
const Tray = electron.Tray
const Menu = electron.Menu
const MenuItem = electron.MenuItem
process.stdin.setEncoding('utf8')
console.log = function () {
process.stdout.write(JSON.stringify({
type: 'log',
data: JSON.stringify(Array.prototype.slice.call(arguments).join(' '))
}), 'utf-8')
}
function emit (type, data) {
process.stdout.write(JSON.stringify({
type: type,
data: JSON.stringify(data)
}), 'utf-8')
}
var finderWindow
app.on('ready', function () {
app.dock.hide()
var appIcon = new Tray(__dirname + '/resources/tray-icon.png')
appIcon.setToolTip('Boost')
finderWindow = require('./atom-lib/finder-window')
finderWindow.webContents.on('did-finish-load', function () {
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 () {
finderWindow.show()
}
}))
trayMenu.append(new MenuItem({
label: 'Quit',
click: function () {
emit('quit-app')
}
}))
appIcon.setContextMenu(trayMenu)
process.stdin.on('data', function (payload) {
try {
payload = JSON.parse(payload)
} catch (e) {
console.log('Not parsable payload : ', payload)
return
}
console.log('from main >> ', payload.type)
switch (payload.type) {
case 'open-finder':
finderWindow.show()
break
}
})
})
global.hideFinder = function () {
Menu.sendActionToFirstResponder('hide:')
}
})

10
index.js Normal file
View File

@@ -0,0 +1,10 @@
function isFinderCalled () {
var argv = process.argv.slice(1)
return argv.some(arg => arg.match(/--finder/))
}
if (isFinderCalled()) {
require('./finder.js')
} else {
require('./main.js')
}

View File

@@ -1,9 +1,11 @@
// Action types // Action types
export const CLEAR_NEW_ARTICLE = 'CLEAR_NEW_ARTICLE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE' export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY' export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const FOLDER_CREATE = 'FOLDER_CREATE' export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE' export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY' export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER' export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE' export const SWITCH_MODE = 'SWITCH_MODE'
@@ -11,17 +13,24 @@ export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER' export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER' export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH' export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const LOCK_STATUS = 'LOCK_STATUS'
export const UNLOCK_STATUS = 'UNLOCK_STATUS'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL' export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Status - mode // Status - mode
export const IDLE_MODE = 'IDLE_MODE' export const IDLE_MODE = 'IDLE_MODE'
export const CREATE_MODE = 'CREATE_MODE'
export const EDIT_MODE = 'EDIT_MODE' export const EDIT_MODE = 'EDIT_MODE'
// Article status // Article status
export const NEW = 'NEW' export const NEW = 'NEW'
// DB // DB
export function clearNewArticle () {
return {
type: CLEAR_NEW_ARTICLE
}
}
export function updateArticle (article) { export function updateArticle (article) {
return { return {
type: ARTICLE_UPDATE, type: ARTICLE_UPDATE,
@@ -57,6 +66,16 @@ export function destroyFolder (key) {
} }
} }
export function replaceFolder (a, b) {
return {
type: FOLDER_REPLACE,
data: {
a,
b
}
}
}
export function switchFolder (folderName) { export function switchFolder (folderName) {
return { return {
type: SWITCH_FOLDER, type: SWITCH_FOLDER,
@@ -71,10 +90,13 @@ export function switchMode (mode) {
} }
} }
export function switchArticle (articleKey) { export function switchArticle (articleKey, isNew) {
return { return {
type: SWITCH_ARTICLE, type: SWITCH_ARTICLE,
data: articleKey data: {
key: articleKey,
isNew: isNew
}
} }
} }
@@ -98,7 +120,19 @@ export function clearSearch () {
} }
} }
export function toggleTutorial() { export function lockStatus () {
return {
type: LOCK_STATUS
}
}
export function unlockStatus () {
return {
type: UNLOCK_STATUS
}
}
export function toggleTutorial () {
return { return {
type: TOGGLE_TUTORIAL type: TOGGLE_TUTORIAL
} }

View File

@@ -47,6 +47,10 @@ Post all records(except today)
and remove all posted records and remove all posted records
*/ */
export function postRecords (data) { export function postRecords (data) {
if (process.env.BOOST_ENV === 'development') {
console.log('post failed - on development')
return
}
let records = getAllRecords() let records = getAllRecords()
records = records.filter(record => { records = records.filter(record => {
return !isSameDate(new Date(), record.date) return !isSameDate(new Date(), record.date)

View File

@@ -1,5 +1,6 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import shell from 'shell' const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component { export default class ExternalLink extends React.Component {
handleClick (e) { handleClick (e) {

View File

@@ -3,45 +3,50 @@ import React, { PropTypes } from 'react'
const BLUE = '#3460C7' const BLUE = '#3460C7'
const LIGHTBLUE = '#2BA5F7' const LIGHTBLUE = '#2BA5F7'
const ORANGE = '#FF8E00' const ORANGE = '#FF8E00'
const YELLOW = '#EAEF31' const YELLOW = '#E8D252'
const GREEN = '#02FF26' const GREEN = '#3FD941'
const DARKGREEN = '#008A59' const DARKGREEN = '#1FAD85'
const RED = '#E10051' const RED = '#E10051'
const PURPLE = '#B013A4' const PURPLE = '#B013A4'
const BRAND_COLOR = '#2BAC8F'
function getColorByIndex (index) { function getColorByIndex (index) {
switch (index % 8) { switch (index % 8) {
case 0: case 0:
return LIGHTBLUE return RED
case 1: case 1:
return ORANGE return ORANGE
case 2: case 2:
return RED return YELLOW
case 3: case 3:
return GREEN return GREEN
case 4: case 4:
return DARKGREEN return DARKGREEN
case 5: case 5:
return YELLOW return LIGHTBLUE
case 6: case 6:
return BLUE return BLUE
case 7: case 7:
return PURPLE return PURPLE
default: default:
return BRAND_COLOR return DARKGREEN
} }
} }
export default class FolderMark extends React.Component { export default class FolderMark extends React.Component {
render () { render () {
let color = getColorByIndex(this.props.color) let color = getColorByIndex(this.props.color)
let className = 'FolderMark fa fa-square fa-fw'
if (this.props.className != null) {
className += ' active'
}
return ( return (
<i className='fa fa-square fa-fw' style={{color: color}}/> <i className={className} style={{color: color}}/>
) )
} }
} }
FolderMark.propTypes = { FolderMark.propTypes = {
color: PropTypes.number color: PropTypes.number,
className: PropTypes.string
} }

View File

@@ -1,9 +1,11 @@
import shell from 'shell'
var React = require('react') var React = require('react')
var { PropTypes } = React var { PropTypes } = React
import markdown from 'boost/markdown' import markdown from 'boost/markdown'
var ReactDOM = require('react-dom') var ReactDOM = require('react-dom')
const electron = require('electron')
const shell = electron.shell
function handleAnchorClick (e) { function handleAnchorClick (e) {
shell.openExternal(e.target.href) shell.openExternal(e.target.href)
e.preventDefault() e.preventDefault()

View File

@@ -18,7 +18,7 @@ export default class ModeSelect extends React.Component {
} }
} }
componentDidMount (e) { componentDidMount () {
this.blurHandler = e => { this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search) let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) { if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
@@ -28,7 +28,7 @@ export default class ModeSelect extends React.Component {
window.addEventListener('click', this.blurHandler) window.addEventListener('click', this.blurHandler)
} }
componentWillUnmount (e) { componentWillUnmount () {
window.removeEventListener('click', this.blurHandler) window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search) let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) { if (searchElement != null && this.searchKeyDownListener != null) {

View File

@@ -3,23 +3,54 @@ import ReactDOM from 'react-dom'
import _ from 'lodash' import _ from 'lodash'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component { export default class TagSelect extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
input: '' input: '',
isInputFocused: false
} }
} }
handleKeyDown (e) { componentDidMount () {
if (e.keyCode !== 13) return false this.blurInputBlurHandler = e => {
e.preventDefault() if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0) let tags = this.props.tags.slice(0)
let newTag = this.state.input.trim() let newTag = tag.trim()
if (newTag.length === 0) { if (newTag.length === 0 && clearInput) {
this.setState({input: ''}) this.setState({input: ''})
return return
} }
@@ -30,13 +61,38 @@ export default class TagSelect extends React.Component {
if (_.isFunction(this.props.onChange)) { if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags) this.props.onChange(newTag, tags)
} }
this.setState({input: ''}) if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
} }
handleThisClick (e) { handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus() ReactDOM.findDOMNode(this.refs.tagInput).focus()
} }
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) { handleItemRemoveButton (tag) {
return e => { return e => {
e.stopPropagation() e.stopPropagation()
@@ -50,8 +106,16 @@ export default class TagSelect extends React.Component {
} }
} }
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () { render () {
var tagElements = _.isArray(this.props.tags) let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => ( ? this.props.tags.map(tag => (
<span key={tag} className='tagItem'> <span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button> <button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
@@ -59,16 +123,37 @@ export default class TagSelect extends React.Component {
</span>)) </span>))
: null : null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return ( return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}> <div className='TagSelect' onClick={e => this.handleThisClick(e)}>
{tagElements} <div className='tags'>
<input {tagElements}
type='text' <input
onKeyDown={e => this.handleKeyDown(e)} type='text'
ref='tagInput' onKeyDown={e => this.handleKeyDown(e)}
valueLink={this.linkState('input')} ref='tagInput'
placeholder='Click here to add tags' valueLink={this.linkState('input')}
className='tagInput'/> placeholder='Click here to add tags'
className='tagInput'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
{suggestElements}
</div>
)
: null
}
</div> </div>
) )
} }
@@ -76,7 +161,8 @@ export default class TagSelect extends React.Component {
TagSelect.propTypes = { TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string), tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func onChange: PropTypes.func,
suggestTags: PropTypes.array
} }
TagSelect.prototype.linkState = linkState TagSelect.prototype.linkState = linkState

View File

@@ -1,7 +1,9 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import { createFolder } from 'boost/actions' import { createFolder } from 'boost/actions'
import store from 'boost/store' import store from 'boost/store'
import FolderMark from 'boost/components/FolderMark'
export default class CreateNewFolder extends React.Component { export default class CreateNewFolder extends React.Component {
constructor (props) { constructor (props) {
@@ -9,10 +11,15 @@ export default class CreateNewFolder extends React.Component {
this.state = { this.state = {
name: '', name: '',
color: Math.round(Math.random() * 7),
alert: null alert: null
} }
} }
componentDidMount () {
ReactDOM.findDOMNode(this.refs.folderName).focus()
}
handleCloseButton (e) { handleCloseButton (e) {
this.props.close() this.props.close()
} }
@@ -20,9 +27,11 @@ export default class CreateNewFolder extends React.Component {
handleConfirmButton (e) { handleConfirmButton (e) {
this.setState({alert: null}, () => { this.setState({alert: null}, () => {
let { close } = this.props let { close } = this.props
let name = this.state.name let { name, color } = this.state
let input = { let input = {
name name,
color
} }
try { try {
@@ -38,6 +47,20 @@ export default class CreateNewFolder extends React.Component {
}) })
} }
handleColorClick (colorIndex) {
return e => {
this.setState({
color: colorIndex
})
}
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.handleConfirmButton()
}
}
render () { render () {
let alert = this.state.alert let alert = this.state.alert
let alertElement = alert != null ? ( let alertElement = alert != null ? (
@@ -45,6 +68,20 @@ export default class CreateNewFolder extends React.Component {
{alert.message} {alert.message}
</p> </p>
) : null ) : null
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorElements = colorIndexes.map(index => {
let className = 'option'
if (index === this.state.color) className += ' active'
return (
<span className={className} key={index} onClick={e => this.handleColorClick(index)(e)}>
<FolderMark color={index}/>
</span>
)
})
return ( return (
<div className='CreateNewFolder modal'> <div className='CreateNewFolder modal'>
@@ -52,7 +89,10 @@ export default class CreateNewFolder extends React.Component {
<div className='title'>Create new folder</div> <div className='title'>Create new folder</div>
<input className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/> <input ref='folderName' onKeyDown={e => this.handleKeyDown(e)} className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/>
<div className='colorSelect'>
{colorElements}
</div>
{alertElement} {alertElement}
<button onClick={e => this.handleConfirmButton(e)} className='confirmBtn'>Create</button> <button onClick={e => this.handleConfirmButton(e)} className='confirmBtn'>Create</button>

View File

@@ -0,0 +1,41 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import store from 'boost/store'
import { unlockStatus, clearNewArticle } from 'boost/actions'
export default class EditedAlert extends React.Component {
componentDidMount () {
ReactDOM.findDOMNode(this.refs.no).focus()
}
handleNoButtonClick (e) {
this.props.close()
}
handleYesButtonClick (e) {
store.dispatch(unlockStatus())
store.dispatch(this.props.action)
store.dispatch(clearNewArticle())
this.props.close()
}
render () {
return (
<div className='EditedAlert modal'>
<div className='title'>Your article is still editing!</div>
<div className='message'>Do you really want to leave without finishing?</div>
<div className='control'>
<button ref='no' onClick={e => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close'/> No</button>
<button ref='yes' onClick={e => this.handleYesButtonClick(e)} className='primary'><i className='fa fa-fw fa-check'/> Yes</button>
</div>
</div>
)
}
}
EditedAlert.propTypes = {
action: PropTypes.object,
close: PropTypes.func
}

View File

@@ -1,7 +1,9 @@
import React from 'react' import React from 'react'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import remote from 'remote'
import ipc from 'ipc' const electron = require('electron')
const ipc = electron.ipcRenderer
const remote = electron.remote
export default class AppSettingTab extends React.Component { export default class AppSettingTab extends React.Component {
constructor (props) { constructor (props) {
@@ -36,12 +38,22 @@ export default class AppSettingTab extends React.Component {
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
} }
handleSaveButtonClick (e) { submitHotKey () {
ipc.send('hotkeyUpdated', { ipc.send('hotkeyUpdated', {
toggleFinder: this.state.toggleFinder toggleFinder: this.state.toggleFinder
}) })
} }
handleSaveButtonClick (e) {
this.submitHotKey()
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
render () { render () {
let alert = this.state.alert let alert = this.state.alert
let alertElement = alert != null ? ( let alertElement = alert != null ? (
@@ -56,7 +68,7 @@ export default class AppSettingTab extends React.Component {
<div className='sectionTitle'>Hotkey</div> <div className='sectionTitle'>Hotkey</div>
<div className='sectionInput'> <div className='sectionInput'>
<label>Toggle Finder(popup)</label> <label>Toggle Finder(popup)</label>
<input valueLink={this.linkState('toggleFinder')} type='text'/> <input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('toggleFinder')} type='text'/>
</div> </div>
<div className='sectionConfirm'> <div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button> <button onClick={e => this.handleSaveButtonClick(e)}>Save</button>

View File

@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'
import linkState from 'boost/linkState' import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'boost/components/FolderMark'
import store from 'boost/store' import store from 'boost/store'
import { updateFolder, destroyFolder } from 'boost/actions' import { updateFolder, destroyFolder, replaceFolder } from 'boost/actions'
const IDLE = 'IDLE' const IDLE = 'IDLE'
const EDIT = 'EDIT' const EDIT = 'EDIT'
@@ -17,6 +17,20 @@ export default class FolderRow extends React.Component {
} }
} }
handleUpClick (e) {
let { index } = this.props
if (index > 0) {
store.dispatch(replaceFolder(index, index - 1))
}
}
handleDownClick (e) {
let { index, count } = this.props
if (index < count - 1) {
store.dispatch(replaceFolder(index, index + 1))
}
}
handleCancelButtonClick (e) { handleCancelButtonClick (e) {
this.setState({ this.setState({
mode: IDLE mode: IDLE
@@ -26,7 +40,9 @@ export default class FolderRow extends React.Component {
handleEditButtonClick (e) { handleEditButtonClick (e) {
this.setState({ this.setState({
mode: EDIT, mode: EDIT,
name: this.props.folder.name name: this.props.folder.name,
color: this.props.folder.color,
isColorEditing: false
}) })
} }
@@ -34,12 +50,34 @@ export default class FolderRow extends React.Component {
this.setState({mode: DELETE}) this.setState({mode: DELETE})
} }
handleNameInputKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleColorSelectClick (e) {
this.setState({
isColorEditing: true
})
}
handleColorButtonClick (index) {
return e => {
this.setState({
color: index,
isColorEditing: false
})
}
}
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
let { folder, setAlert } = this.props let { folder, setAlert } = this.props
setAlert(null, () => { setAlert(null, () => {
let input = { let input = {
name: this.state.name name: this.state.name,
color: this.state.color
} }
folder = Object.assign({}, folder, input) folder = Object.assign({}, folder, input)
@@ -68,10 +106,40 @@ export default class FolderRow extends React.Component {
switch (this.state.mode) { switch (this.state.mode) {
case EDIT: case EDIT:
let colorIndexes = []
for (let i = 0; i < 8; i++) {
colorIndexes.push(i)
}
let colorOptions = colorIndexes.map(index => {
let className = this.state.color === index
? 'active'
: null
return (
<button onClick={e => this.handleColorButtonClick(index)(e)} className={className} key={index}>
<FolderMark color={index}/>
</button>
)
})
return ( return (
<div className='FolderRow edit'> <div className='FolderRow edit'>
<div className='folderColor'>
<button onClick={e => this.handleColorSelectClick(e)} className='select'>
<FolderMark color={this.state.color}/>
</button>
{this.state.isColorEditing
? (
<div className='options'>
<div className='label'>Color select</div>
{colorOptions}
</div>
)
: null
}
</div>
<div className='folderName'> <div className='folderName'>
<input valueLink={this.linkState('name')} type='text'/> <input onKeyDown={e => this.handleNameInputKeyDown(e)} valueLink={this.linkState('name')} type='text'/>
</div> </div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button> <button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
@@ -93,7 +161,12 @@ export default class FolderRow extends React.Component {
default: default:
return ( return (
<div className='FolderRow'> <div className='FolderRow'>
<div className='folderName'><FolderMark color={folder.color}/> {folder.name}</div> <div className='sortBtns'>
<button onClick={e => this.handleUpClick(e)}><i className='fa fa-sort-up fa-fw'/></button>
<button onClick={e => this.handleDownClick(e)}><i className='fa fa-sort-down fa-fw'/></button>
</div>
<div className='folderColor'><FolderMark color={folder.color}/></div>
<div className='folderName'>{folder.name}</div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button> <button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button>
<button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-close'/></button> <button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-close'/></button>
@@ -106,6 +179,8 @@ export default class FolderRow extends React.Component {
FolderRow.propTypes = { FolderRow.propTypes = {
folder: PropTypes.shape(), folder: PropTypes.shape(),
index: PropTypes.number,
count: PropTypes.number,
setAlert: PropTypes.func setAlert: PropTypes.func
} }

View File

@@ -12,6 +12,12 @@ export default class FolderSettingTab extends React.Component {
} }
} }
handleNewFolderNameKeyDown (e) {
if (e.keyCode === 13) {
this.handleSaveButtonClick()
}
}
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
this.setState({alert: null}, () => { this.setState({alert: null}, () => {
if (this.state.name.trim().length === 0) return false if (this.state.name.trim().length === 0) return false
@@ -40,10 +46,16 @@ export default class FolderSettingTab extends React.Component {
render () { render () {
let { folders } = this.props let { folders } = this.props
let folderElements = folders.map(folder => { let folderElements = folders.map((folder, index) => {
return ( return (
<FolderRow key={'folder-' + folder.key} folder={folder} setAlert={(alert, cb) => this.setAlert(alert, cb)}/> <FolderRow
) key={'folder-' + folder.key}
folder={folder}
index={index}
count={folders.length}
setAlert={(alert, cb) => this.setAlert(alert, cb)}
/>
)
}) })
let alert = this.state.alert let alert = this.state.alert
@@ -59,13 +71,13 @@ export default class FolderSettingTab extends React.Component {
<div className='sectionTitle'>Manage folder</div> <div className='sectionTitle'>Manage folder</div>
<div className='folderTable'> <div className='folderTable'>
<div className='folderHeader'> <div className='folderHeader'>
<div className='folderName'>Folder name</div> <div className='folderName'>Folder</div>
<div className='folderControl'>Edit/Delete</div> <div className='folderControl'>Edit/Delete</div>
</div> </div>
{folderElements} {folderElements}
<div className='newFolder'> <div className='newFolder'>
<div className='folderName'> <div className='folderName'>
<input valueLink={this.linkState('name')} type='text' placeholder='New Folder'/> <input onKeyDown={e => this.handleNewFolderNameKeyDown(e)} valueLink={this.linkState('name')} type='text' placeholder='New Folder'/>
</div> </div>
<div className='folderControl'> <div className='folderControl'>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button> <button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button>

View File

@@ -83,158 +83,6 @@ class Preferences extends React.Component {
return (<AppSettingTab/>) return (<AppSettingTab/>)
} }
} }
// handleProfileSaveButtonClick (e) {
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'info',
// message: 'Sending...'
// }
// this.setState({profile: profileState}, () => {
// let input = {
// profileName: profileState.userInfo.profileName,
// email: profileState.userInfo.email
// }
// api.updateUserInfo(input)
// .then(res => {
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'success',
// message: 'Successfully done!'
// }
// this.setState({profile: profileState})
// })
// .catch(err => {
// var message
// if (err.status != null) {
// message = err.response.body.message
// } else if (err.code === 'ECONNREFUSED') {
// message = 'Can\'t connect to API server.'
// } else throw err
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'error',
// message: message
// }
// this.setState({profile: profileState})
// })
// })
// }
// handlePasswordSaveButton (e) {
// let profileState = this.state.profile
// if (profileState.password.newPassword !== profileState.password.confirmation) {
// profileState.password.alert = {
// type: 'error',
// message: 'Confirmation doesn\'t match'
// }
// this.setState({profile: profileState})
// return
// }
// profileState.password.alert = {
// type: 'info',
// message: 'Sending...'
// }
// this.setState({profile: profileState}, () => {
// let input = {
// password: profileState.password.currentPassword,
// newPassword: profileState.password.newPassword
// }
// api.updatePassword(input)
// .then(res => {
// let profileState = this.state.profile
// profileState.password.alert = {
// type: 'success',
// message: 'Successfully done!'
// }
// profileState.password.currentPassword = ''
// profileState.password.newPassword = ''
// profileState.password.confirmation = ''
// this.setState({profile: profileState})
// })
// .catch(err => {
// var message
// if (err.status != null) {
// message = err.response.body.message
// } else if (err.code === 'ECONNREFUSED') {
// message = 'Can\'t connect to API server.'
// } else throw err
// let profileState = this.state.profile
// profileState.password.alert = {
// type: 'error',
// message: message
// }
// profileState.password.currentPassword = ''
// profileState.password.newPassword = ''
// profileState.password.confirmation = ''
// this.setState({profile: profileState}, () => {
// if (this.refs.currentPassword != null) findDOMNode(this.refs.currentPassword).focus()
// })
// })
// })
// }
// renderProfile () {
// let profileState = this.state.profile
// return (
// <div className='content profile'>
// <div className='section userSection'>
// <div className='sectionTitle'>User Info</div>
// <div className='sectionInput'>
// <label>Profile Name</label>
// <input valueLink={this.linkState('profile.userInfo.profileName')} type='text'/>
// </div>
// <div className='sectionInput'>
// <label>E-mail</label>
// <input valueLink={this.linkState('profile.userInfo.email')} type='text'/>
// </div>
// <div className='sectionConfirm'>
// <button onClick={e => this.handleProfileSaveButtonClick(e)}>Save</button>
// {this.state.profile.userInfo.alert != null
// ? (
// <div className={'alert ' + profileState.userInfo.alert.type}>{profileState.userInfo.alert.message}</div>
// )
// : null}
// </div>
// </div>
// <div className='section passwordSection'>
// <div className='sectionTitle'>Password</div>
// <div className='sectionInput'>
// <label>Current Password</label>
// <input ref='currentPassword' valueLink={this.linkState('profile.password.currentPassword')} type='password' placeholder='Current Password'/>
// </div>
// <div className='sectionInput'>
// <label>New Password</label>
// <input valueLink={this.linkState('profile.password.newPassword')} type='password' placeholder='New Password'/>
// </div>
// <div className='sectionInput'>
// <label>Confirmation</label>
// <input valueLink={this.linkState('profile.password.confirmation')} type='password' placeholder='Confirmation'/>
// </div>
// <div className='sectionConfirm'>
// <button onClick={e => this.handlePasswordSaveButton(e)}>Save</button>
// {profileState.password.alert != null
// ? (
// <div className={'alert ' + profileState.password.alert.type}>{profileState.password.alert.message}</div>
// )
// : null}
// </div>
// </div>
// </div>
// )
// }
} }
Preferences.propTypes = { Preferences.propTypes = {

View File

@@ -88,10 +88,11 @@ export default class Tutorial extends React.Component {
return (<div className='slide slide3'> return (<div className='slide slide3'>
<div className='title'>Easy to access with Finder</div> <div className='title'>Easy to access with Finder</div>
<div className='content'> <div className='content'>
With Finder, You can search your articles faster.<br/> The Finder helps you organize all of the files and documents.<br/>
You can open Finder by pressing Control + shift + tab<br/> There is a short-cut key [control + shift + tab] to open the Finder.<br/>
To put the content of an article in the clipboard, press Enter.<br/> It is available to save your articles on the Clipboard<br/>
So you can paste it with Cmd() + V by selecting your file with pressing Enter key,<br/>
and to paste the contents of the Clipboard with [Command-V]
<img width='480' src='../../resources/finder.png'/> <img width='480' src='../../resources/finder.png'/>
</div> </div>

View File

@@ -1,11 +1,27 @@
import keygen from 'boost/keygen' import keygen from 'boost/keygen'
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const path = require('path')
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +cmd+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n4. チーム機能(リアルタイム搭載)\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+cmd+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory' let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory'
function getLocalPath () {
return path.join(remote.app.getPath('userData'), 'local.json')
}
export function init () { export function init () {
console.log('initialize data store') console.log('initialize data store')
let data = JSON.parse(localStorage.getItem('local')) let data = jetpack.read(getLocalPath(), 'json')
if (data == null) { if (data == null) {
if (localStorage.getItem('local') != null) {
data = JSON.parse(localStorage.getItem('local'))
jetpack.write(getLocalPath(), data)
localStorage.removeItem('local')
return
}
let defaultFolder = { let defaultFolder = {
name: 'default', name: 'default',
key: keygen() key: keygen()
@@ -24,37 +40,35 @@ export function init () {
folders: [defaultFolder], folders: [defaultFolder],
version: '0.4' version: '0.4'
} }
localStorage.setItem('local', JSON.stringify(data)) jetpack.write(getLocalPath(), data)
} }
} }
function getKey (teamId) { export function getData () {
return teamId == null return jetpack.read(getLocalPath(), 'json')
? 'local'
: `team-${teamId}`
} }
export function getData (teamId) { export function setArticles (articles) {
let key = getKey(teamId) let data = getData()
return JSON.parse(localStorage.getItem(key))
}
export function setArticles (teamId, articles) {
let key = getKey(teamId)
let data = JSON.parse(localStorage.getItem(key))
data.articles = articles data.articles = articles
localStorage.setItem(key, JSON.stringify(data)) jetpack.write(getLocalPath(), data)
} }
export function setFolders (teamId, folders) { export function setFolders (folders) {
let key = getKey(teamId) let data = getData()
let data = JSON.parse(localStorage.getItem(key))
data.folders = folders data.folders = folders
localStorage.setItem(key, JSON.stringify(data)) jetpack.write(getLocalPath(), data)
}
function isFinderCalled () {
var argv = process.argv.slice(1)
return argv.some(arg => arg.match(/--finder/))
} }
export default (function () { export default (function () {
init() if (!isFinderCalled()) {
init()
}
return { return {
init, init,
getData, getData,

View File

@@ -1,9 +1,25 @@
import markdownit from 'markdown-it' import markdownit from 'markdown-it'
import hljs from 'highlight.js'
import emoji from 'markdown-it-emoji'
var md = markdownit({ var md = markdownit({
typographer: true, typographer: true,
linkify: true linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (__) {}
}
try {
return hljs.highlightAuto(str).value;
} catch (__) {}
return ''; // use external default escaping
}
}) })
md.use(emoji)
export default function markdown (content) { export default function markdown (content) {
if (content == null) content = '' if (content == null) content = ''

View File

@@ -1,4 +1,5 @@
var shell = require('shell') const electron = require('electron')
const shell = electron.shell
export default function (e) { export default function (e) {
shell.openExternal(e.currentTarget.href) shell.openExternal(e.currentTarget.href)

View File

@@ -1,20 +1,51 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import _ from 'lodash' import _ from 'lodash'
import { SWITCH_FOLDER, SWITCH_MODE, SWITCH_ARTICLE, SET_SEARCH_FILTER, SET_TAG_FILTER, CLEAR_SEARCH, TOGGLE_TUTORIAL, ARTICLE_UPDATE, ARTICLE_DESTROY, FOLDER_CREATE, FOLDER_UPDATE, FOLDER_DESTROY, IDLE_MODE, CREATE_MODE } from './actions' import {
// Status action type
SWITCH_FOLDER,
SWITCH_MODE,
SWITCH_ARTICLE,
SET_SEARCH_FILTER,
SET_TAG_FILTER,
CLEAR_SEARCH,
LOCK_STATUS,
UNLOCK_STATUS,
TOGGLE_TUTORIAL,
// Article action type
ARTICLE_UPDATE,
ARTICLE_DESTROY,
CLEAR_NEW_ARTICLE,
// Folder action type
FOLDER_CREATE,
FOLDER_UPDATE,
FOLDER_DESTROY,
FOLDER_REPLACE,
// view mode
IDLE_MODE
} from './actions'
import dataStore from 'boost/dataStore' import dataStore from 'boost/dataStore'
import keygen from 'boost/keygen' import keygen from 'boost/keygen'
import activityRecord from 'boost/activityRecord' import activityRecord from 'boost/activityRecord'
import { openModal } from 'boost/modal'
import EditedAlert from 'boost/components/modal/EditedAlert'
const initialStatus = { const initialStatus = {
mode: IDLE_MODE, mode: IDLE_MODE,
search: '', search: '',
isTutorialOpen: false isTutorialOpen: false,
isStatusLocked: false
} }
let data = dataStore.getData() let data = dataStore.getData()
let initialArticles = data.articles let initialArticles = data.articles
let initialFolders = data.folders let initialFolders = data.folders
let isStatusLocked = false
let isCreatingNew = false
function folders (state = initialFolders, action) { function folders (state = initialFolders, action) {
state = state.slice() state = state.slice()
switch (action.type) { switch (action.type) {
@@ -26,18 +57,17 @@ function folders (state = initialFolders, action) {
Object.assign(newFolder, { Object.assign(newFolder, {
key: keygen(), key: keygen(),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
// random number (0-7)
color: Math.round(Math.random() * 7)
}) })
if (newFolder.length === 0) throw new Error('Folder name is required') if (newFolder.name == null && newFolder.name.length === 0) throw new Error('Folder name is required')
if (newFolder.name.match(/\//)) throw new Error('`/` is not available for folder name')
let conflictFolder = _.findWhere(state, {name: newFolder.name}) let conflictFolder = _.findWhere(state, {name: newFolder.name})
if (conflictFolder != null) throw new Error(`${newFolder.name} already exists!`) if (conflictFolder != null) throw new Error(`${newFolder.name} already exists!`)
state.push(newFolder) state.push(newFolder)
dataStore.setFolders(null, state) dataStore.setFolders(state)
activityRecord.emit('FOLDER_CREATE') activityRecord.emit('FOLDER_CREATE')
return state return state
} }
@@ -48,7 +78,8 @@ function folders (state = initialFolders, action) {
if (!_.isString(folder.name)) throw new Error('Folder name must be a string') if (!_.isString(folder.name)) throw new Error('Folder name must be a string')
folder.name = folder.name.trim().replace(/\s/, '_') folder.name = folder.name.trim().replace(/\s/, '_')
if (folder.length === 0) throw new Error('Folder name is required') if (folder.name.length === 0) throw new Error('Folder name is required')
if (folder.name.match(/\//)) throw new Error('`/` is not available for folder name')
// Folder existence check // Folder existence check
if (targetFolder == null) throw new Error('Folder doesnt exist') if (targetFolder == null) throw new Error('Folder doesnt exist')
@@ -63,7 +94,7 @@ function folders (state = initialFolders, action) {
updatedAt: new Date() updatedAt: new Date()
}) })
dataStore.setFolders(null, state) dataStore.setFolders(state)
activityRecord.emit('FOLDER_UPDATE') activityRecord.emit('FOLDER_UPDATE')
return state return state
} }
@@ -76,18 +107,58 @@ function folders (state = initialFolders, action) {
if (targetIndex >= 0) { if (targetIndex >= 0) {
state.splice(targetIndex, 1) state.splice(targetIndex, 1)
} }
dataStore.setFolders(null, state) dataStore.setFolders(state)
activityRecord.emit('FOLDER_DESTROY') activityRecord.emit('FOLDER_DESTROY')
return state return state
} }
case FOLDER_REPLACE:
{
let { a, b } = action.data
let folderA = state[a]
let folderB = state[b]
state.splice(a, 1, folderB)
state.splice(b, 1, folderA)
}
return state
default: default:
return state return state
} }
} }
let isCleaned = true
function articles (state = initialArticles, action) { function articles (state = initialArticles, action) {
state = state.slice() state = state.slice()
if (!isCreatingNew && !isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
switch (action.type) { switch (action.type) {
case SWITCH_ARTICLE:
if (action.data.isNew) {
isCleaned = false
}
if (!isStatusLocked && !action.data.isNew) {
isCreatingNew = false
if (!isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
}
return state
case SWITCH_FOLDER:
case SET_SEARCH_FILTER:
case SET_TAG_FILTER:
case CLEAR_SEARCH:
if (!isStatusLocked) {
isCreatingNew = false
if (!isCleaned) {
state = state.filter(article => article.status !== 'NEW')
isCleaned = true
}
}
return state
case CLEAR_NEW_ARTICLE:
return state.filter(article => article.status !== 'NEW')
case ARTICLE_UPDATE: case ARTICLE_UPDATE:
{ {
let article = action.data.article let article = action.data.article
@@ -96,7 +167,8 @@ function articles (state = initialArticles, action) {
if (targetIndex < 0) state.unshift(article) if (targetIndex < 0) state.unshift(article)
else state.splice(targetIndex, 1, article) else state.splice(targetIndex, 1, article)
dataStore.setArticles(null, state) if (article.status !== 'NEW') dataStore.setArticles(state)
else isCreatingNew = true
return state return state
} }
case ARTICLE_DESTROY: case ARTICLE_DESTROY:
@@ -106,7 +178,7 @@ function articles (state = initialArticles, action) {
let targetIndex = _.findIndex(state, _article => articleKey === _article.key) let targetIndex = _.findIndex(state, _article => articleKey === _article.key)
if (targetIndex >= 0) state.splice(targetIndex, 1) if (targetIndex >= 0) state.splice(targetIndex, 1)
dataStore.setArticles(null, state) dataStore.setArticles(state)
return state return state
} }
case FOLDER_DESTROY: case FOLDER_DESTROY:
@@ -115,7 +187,7 @@ function articles (state = initialArticles, action) {
state = state.filter(article => article.FolderKey !== folderKey) state = state.filter(article => article.FolderKey !== folderKey)
dataStore.setArticles(null, state) dataStore.setArticles(state)
return state return state
} }
default: default:
@@ -129,18 +201,31 @@ function status (state = initialStatus, action) {
case TOGGLE_TUTORIAL: case TOGGLE_TUTORIAL:
state.isTutorialOpen = !state.isTutorialOpen state.isTutorialOpen = !state.isTutorialOpen
return state return state
case LOCK_STATUS:
isStatusLocked = state.isStatusLocked = true
return state
case UNLOCK_STATUS:
isStatusLocked = state.isStatusLocked = false
return state
}
// if status locked, status become unmutable
if (state.isStatusLocked) {
openModal(EditedAlert, {action})
return state
}
switch (action.type) {
case SWITCH_FOLDER: case SWITCH_FOLDER:
state.mode = IDLE_MODE state.mode = IDLE_MODE
state.search = `in:${action.data} ` state.search = `//${action.data} `
return state return state
case SWITCH_MODE: case SWITCH_MODE:
state.mode = action.data state.mode = action.data
if (state.mode === CREATE_MODE) state.articleKey = null
return state return state
case SWITCH_ARTICLE: case SWITCH_ARTICLE:
state.articleKey = action.data state.articleKey = action.data.key
state.mode = IDLE_MODE state.mode = IDLE_MODE
return state return state

194
main.js
View File

@@ -1,54 +1,84 @@
var app = require('app') const electron = require('electron')
var Menu = require('menu') const app = electron.app
var MenuItem = require('menu-item') const Menu = electron.Menu
var Tray = require('tray') const ipc = electron.ipcMain
var ipc = require('ipc') const globalShortcut = electron.globalShortcut
var jetpack = require('fs-jetpack') const autoUpdater = electron.autoUpdater
const jetpack = require('fs-jetpack')
require('crash-reporter').start() const path = require('path')
const ChildProcess = require('child_process')
electron.crashReporter.start()
var mainWindow = null var mainWindow = null
var appIcon = null var finderProcess
var menu = null
var finderWindow = null
var update = null var update = null
// app.on('window-all-closed', function () { // app.on('window-all-closed', function () {
// if (process.platform !== 'darwin') app.quit() // if (process.platform !== 'darwin') app.quit()
// }) // })
var version = app.getVersion()
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
var nn = require('node-notifier')
var updater = require('./atom-lib/updater')
var path = require('path')
var appQuit = false var appQuit = false
updater var version = app.getVersion()
.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) { var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
nn.notify({ var versionNotified = false
title: 'Ready to Update!! ' + versionText,
icon: path.join(__dirname, 'browser/main/resources/favicon-230x230.png'), function notify (title, body) {
message: 'Click tray icon to update app: ' + releaseName if (mainWindow != null) {
mainWindow.webContents.send('notify', {
title: title,
body: body
}) })
}
}
autoUpdater
.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
update = quitAndUpdate update = quitAndUpdate
if (mainWindow != null && !mainWindow.webContents.isLoading()) { if (mainWindow != null) {
notify('Ready to Update! ' + releaseName, 'Click update button on Main window.')
mainWindow.webContents.send('update-available', 'Update available!') mainWindow.webContents.send('update-available', 'Update available!')
} }
}) })
.on('error', function (err, message) {
console.error(err)
if (!versionNotified) {
notify('Updater error!', message)
}
})
// .on('checking-for-update', function () {
// // Connecting
// console.log('checking...')
// })
.on('update-available', function () {
notify('Update is available!', 'Download started.. wait for the update ready.')
})
.on('update-not-available', function () {
if (mainWindow != null && !versionNotified) {
versionNotified = true
notify('Latest Build!! ' + versionText, 'Hope you to enjoy our app :D')
}
})
app.on('ready', function () { app.on('ready', function () {
app.on('before-quit', function () { app.on('before-quit', function () {
if (finderProcess) finderProcess.kill()
appQuit = true appQuit = true
}) })
console.log('Version ' + version) autoUpdater.setFeedURL('https://orbital.b00st.io/rokt33r/boost-app/latest?version=' + version)
updater.setFeedUrl('http://orbital.b00st.io/rokt33r/boost-app/latest?version=' + version)
updater.checkForUpdates()
// menu start
var template = require('./atom-lib/menu-template') var template = require('./atom-lib/menu-template')
var menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
setInterval(function () {
if (update == null) autoUpdater.checkForUpdates()
}, 1000 * 60 * 60 * 24)
ipc.on('check-update', function (event, msg) {
if (update == null) autoUpdater.checkForUpdates()
})
ipc.on('update-app', function (event, msg) { ipc.on('update-app', function (event, msg) {
if (update != null) { if (update != null) {
@@ -57,48 +87,76 @@ app.on('ready', function () {
} }
}) })
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
// menu end
appIcon = new Tray(__dirname + '/resources/tray-icon.png')
appIcon.setToolTip('Boost')
var trayMenu = new Menu()
trayMenu.append(new MenuItem({
label: 'Open main window',
click: function () {
if (mainWindow != null) mainWindow.show()
}
}))
trayMenu.append(new MenuItem({
label: 'Quit',
click: function () {
app.quit()
}
}))
appIcon.setContextMenu(trayMenu)
mainWindow = require('./atom-lib/main-window') mainWindow = require('./atom-lib/main-window')
mainWindow.on('close', function (e) { mainWindow.on('close', function (e) {
if (appQuit) return true if (appQuit) return true
e.preventDefault() e.preventDefault()
mainWindow.hide() mainWindow.hide()
}) })
if (update != null) { mainWindow.webContents.on('did-finish-load', function () {
mainWindow.webContents.on('did-finish-load', function () { if (finderProcess == null) {
mainWindow.webContents.send('update-available', 'whoooooooh!') finderProcess = ChildProcess
}) .execFile(process.execPath, [path.resolve(__dirname, 'finder.js'), '--finder'])
} finderProcess.stdout.setEncoding('utf8')
finderProcess.stderr.setEncoding('utf8')
finderProcess.stdout.on('data', format)
finderProcess.stderr.on('data', errorFormat)
}
app.on('activate-with-no-open-windows', function () { if (update != null) {
mainWindow.webContents.send('update-available', 'whoooooooh!')
} else {
autoUpdater.checkForUpdates()
}
})
app.on('activate', function () {
if (mainWindow == null) return null if (mainWindow == null) return null
mainWindow.show() mainWindow.show()
}) })
finderWindow = require('./atom-lib/finder-window') function format (payload) {
// console.log('from finder >> ', payload)
try {
payload = JSON.parse(payload)
} catch (e) {
console.log('Not parsable payload : ', payload)
return
}
switch (payload.type) {
case 'log':
console.log('FINDER(stdout): ' + payload.data)
break
case 'show-main-window':
if (mainWindow != null) {
mainWindow.show()
}
break
case 'request-data':
mainWindow.webContents.send('request-data')
break
case 'quit-app':
appQuit = true
app.quit()
break
}
}
function errorFormat (output) {
console.error('FINDER(stderr):' + output)
}
function emitToFinder (type, data) {
if (!finderProcess) {
console.log('finder process is not ready')
return
}
var payload = {
type: type,
data: data
}
finderProcess.stdin.write(JSON.stringify(payload), 'utf-8')
}
var globalShortcut = require('global-shortcut')
var userDataPath = app.getPath('userData') var userDataPath = app.getPath('userData')
if (!jetpack.cwd(userDataPath).exists('keymap.json')) { if (!jetpack.cwd(userDataPath).exists('keymap.json')) {
jetpack.cwd(userDataPath).file('keymap.json', {content: '{}'}) jetpack.cwd(userDataPath).file('keymap.json', {content: '{}'})
@@ -114,10 +172,7 @@ app.on('ready', function () {
try { try {
globalShortcut.register(toggleFinderKey, function () { globalShortcut.register(toggleFinderKey, function () {
if (mainWindow != null && !mainWindow.isFocused()) { emitToFinder('open-finder')
mainWindow.hide()
}
finderWindow.show()
}) })
} catch (err) { } catch (err) {
console.log(err.name) console.log(err.name)
@@ -133,10 +188,7 @@ app.on('ready', function () {
var toggleFinderKey = global.keymap.toggleFinder != null ? global.keymap.toggleFinder : 'ctrl+tab+shift' var toggleFinderKey = global.keymap.toggleFinder != null ? global.keymap.toggleFinder : 'ctrl+tab+shift'
try { try {
globalShortcut.register(toggleFinderKey, function () { globalShortcut.register(toggleFinderKey, function () {
if (mainWindow != null && !mainWindow.isFocused()) { emitToFinder('open-finder')
mainWindow.hide()
}
finderWindow.show()
}) })
mainWindow.webContents.send('APP_SETTING_DONE', {}) mainWindow.webContents.send('APP_SETTING_DONE', {})
} catch (err) { } catch (err) {
@@ -146,12 +198,4 @@ app.on('ready', function () {
}) })
} }
}) })
global.hideFinder = function () {
if (!mainWindow.isVisible()) {
Menu.sendActionToFirstResponder('hide:')
} else {
mainWindow.focus()
}
}
}) })

View File

@@ -1,17 +1,17 @@
{ {
"name": "boost", "name": "boost",
"version": "0.4.0-beta.2", "version": "0.4.3",
"description": "Boost App", "description": "Boost App",
"main": "main.js", "main": "index.js",
"scripts": { "scripts": {
"start": "BOOST_ENV=development electron ./main.js", "start": "BOOST_ENV=development electron ./main.js",
"webpack": "webpack-dev-server --hot --inline --config webpack.config.js", "webpack": "webpack-dev-server --hot --inline --config webpack.config.js",
"compile": "NODE_ENV=production webpack --config webpack.config.production.js", "compile": "NODE_ENV=production webpack --config webpack.config.production.js",
"build": "electron-packager ./ Boost --app-version=$npm_package_version $npm_package_config_platform $npm_package_config_version $npm_package_config_ignore --overwrite", "build": "electron-packager ./ Boost --app-version=$npm_package_version $npm_package_config_platform $npm_package_config_version $npm_package_config_ignore --overwrite --asar",
"codesign": "codesign --verbose --deep --force --sign \"MAISIN solutions Inc.\" Boost-darwin-x64/Boost.app" "codesign": "codesign --verbose --deep --force --sign \"MAISIN solutions Inc.\" Boost-darwin-x64/Boost.app"
}, },
"config": { "config": {
"version": "--version=0.34.0 --app-bundle-id=com.maisin.boost", "version": "--version=0.35.1 --app-bundle-id=com.maisin.boost",
"platform": "--platform=darwin --arch=x64 --prune --icon=resources/app.icns", "platform": "--platform=darwin --arch=x64 --prune --icon=resources/app.icns",
"ignore": "--ignore=Boost-darwin-x64 --ignore=node_modules/devicon/icons --ignore=submodules/ace/(?!src-min)|submodules/ace/(?=src-min-noconflict)" "ignore": "--ignore=Boost-darwin-x64 --ignore=node_modules/devicon/icons --ignore=submodules/ace/(?!src-min)|submodules/ace/(?=src-min-noconflict)"
}, },
@@ -38,14 +38,14 @@
"homepage": "https://github.com/Rokt33r/codexen-app#readme", "homepage": "https://github.com/Rokt33r/codexen-app#readme",
"dependencies": { "dependencies": {
"devicon": "^2.0.0", "devicon": "^2.0.0",
"electron-packager": "^5.1.1",
"font-awesome": "^4.3.0", "font-awesome": "^4.3.0",
"fs-jetpack": "^0.7.0", "fs-jetpack": "^0.7.0",
"highlight.js": "^8.9.1",
"lodash": "^3.10.1", "lodash": "^3.10.1",
"markdown-it": "^4.3.1", "markdown-it": "^4.3.1",
"markdown-it-emoji": "^1.1.0",
"md5": "^2.0.0", "md5": "^2.0.0",
"moment": "^2.10.3", "moment": "^2.10.3",
"node-notifier": "^4.2.3",
"socket.io-client": "^1.3.6", "socket.io-client": "^1.3.6",
"superagent": "^1.2.0", "superagent": "^1.2.0",
"superagent-promise": "^1.0.3" "superagent-promise": "^1.0.3"
@@ -55,16 +55,15 @@
"babel-plugin-react-transform": "^1.1.1", "babel-plugin-react-transform": "^1.1.1",
"css-loader": "^0.19.0", "css-loader": "^0.19.0",
"electron-packager": "^5.1.0", "electron-packager": "^5.1.0",
"electron-prebuilt": "^0.33.6", "electron-prebuilt": "^0.35.1",
"nib": "^1.1.0", "nib": "^1.1.0",
"react": "^0.14.0", "react": "^0.14.0",
"react-dom": "^0.14.0", "react-dom": "^0.14.0",
"react-redux": "^4.0.0", "react-redux": "^4.0.0",
"react-router": "^1.0.0-rc1", "react-router": "^1.0.0-rc1",
"react-select": "^0.8.1",
"react-transform-catch-errors": "^1.0.0", "react-transform-catch-errors": "^1.0.0",
"react-transform-hmr": "^1.0.1", "react-transform-hmr": "^1.0.1",
"redbox-react": "^1.1.1", "redbox-react": "^1.2.0",
"redux": "^3.0.2", "redux": "^3.0.2",
"standard": "^5.3.1", "standard": "^5.3.1",
"style-loader": "^0.12.4", "style-loader": "^0.12.4",

View File

@@ -3,7 +3,6 @@ var path = require('path')
var JsonpTemplatePlugin = webpack.JsonpTemplatePlugin var JsonpTemplatePlugin = webpack.JsonpTemplatePlugin
var FunctionModulePlugin = require('webpack/lib/FunctionModulePlugin') var FunctionModulePlugin = require('webpack/lib/FunctionModulePlugin')
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin') var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin')
var ExternalsPlugin = webpack.ExternalsPlugin
var opt = { var opt = {
path: path.join(__dirname, 'compiled'), path: path.join(__dirname, 'compiled'),
filename: '[name].js', filename: '[name].js',
@@ -39,36 +38,20 @@ var config = {
}, },
plugins: [ plugins: [
new webpack.NoErrorsPlugin(), new webpack.NoErrorsPlugin(),
new ExternalsPlugin('commonjs', [
'app',
'auto-updater',
'browser-window',
'content-tracing',
'dialog',
'global-shortcut',
'ipc',
'menu',
'menu-item',
'power-monitor',
'protocol',
'tray',
'remote',
'web-frame',
'clipboard',
'crash-reporter',
'screen',
'shell'
]),
new NodeTargetPlugin() new NodeTargetPlugin()
], ],
externals: [ externals: [
'electron',
'socket.io-client', 'socket.io-client',
'md5', 'md5',
'superagent', 'superagent',
'superagent-promise', 'superagent-promise',
'lodash', 'lodash',
'markdown-it', 'markdown-it',
'moment' 'moment',
'highlight.js',
'markdown-it-emoji',
'fs-jetpack'
] ]
} }

View File

@@ -1,6 +1,5 @@
var webpack = require('webpack') var webpack = require('webpack')
module.exports = { module.exports = {
devtool: 'source-map',
entry: { entry: {
main: './browser/main/index.js', main: './browser/main/index.js',
finder: './browser/finder/index.js' finder: './browser/finder/index.js'
@@ -8,7 +7,7 @@ module.exports = {
output: { output: {
path: 'compiled', path: 'compiled',
filename: '[name].js', filename: '[name].js',
sourceMapFilename: '[name].map', // sourceMapFilename: '[name].map',
libraryTarget: 'commonjs2' libraryTarget: 'commonjs2'
}, },
module: { module: {
@@ -31,24 +30,27 @@ module.exports = {
'process.env': { 'process.env': {
'NODE_ENV': JSON.stringify('production') 'NODE_ENV': JSON.stringify('production')
} }
}),
new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false
}
}) })
// new webpack.optimize.UglifyJsPlugin({
// compressor: {
// warnings: false
// }
// })
], ],
externals: [ externals: [
'electron',
'socket.io-client', 'socket.io-client',
'md5', 'md5',
'superagent', 'superagent',
'superagent-promise', 'superagent-promise',
'lodash', 'lodash',
'markdown-it', 'markdown-it',
'moment' 'moment',
'highlight.js',
'markdown-it-emoji',
'fs-jetpack'
], ],
resolve: { resolve: {
extensions: ['', '.js', '.jsx', 'styl'] extensions: ['', '.js', '.jsx', 'styl']
}, }
target: 'atom'
} }