mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 18:26:26 +00:00
Compare commits
277 Commits
0.4.0-beta
...
v0.5.5-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75e872415 | ||
|
|
aef0712165 | ||
|
|
bed4b7fd27 | ||
|
|
b53ff5daf3 | ||
|
|
bb0872b4fc | ||
|
|
b65101f4be | ||
|
|
593d242a4c | ||
|
|
db7f339c34 | ||
|
|
9f3575a874 | ||
|
|
1c9c59c512 | ||
|
|
127202b831 | ||
|
|
4f8a04ed21 | ||
|
|
63b2e0560b | ||
|
|
6907cf9972 | ||
|
|
d4f8d1498d | ||
|
|
0952e4a664 | ||
|
|
983bfb7adf | ||
|
|
d7aaf5e210 | ||
|
|
50281132ad | ||
|
|
6a2b22015e | ||
|
|
0fe83a0583 | ||
|
|
ce74e69480 | ||
|
|
ddea2aeb22 | ||
|
|
7bbe69cce9 | ||
|
|
e921e30d64 | ||
|
|
cd4f9d8bb4 | ||
|
|
a0553788b6 | ||
|
|
1a183d78af | ||
|
|
cabcaa892c | ||
|
|
01c9d62a2b | ||
|
|
ba76df863c | ||
|
|
81441a0895 | ||
|
|
da0222f213 | ||
|
|
fb8a2eb2e0 | ||
|
|
cde2e27e04 | ||
|
|
3758ea2cf4 | ||
|
|
e62fc11328 | ||
|
|
3cbfae83c1 | ||
|
|
57667654ef | ||
|
|
eadd66fa91 | ||
|
|
75cd94a39a | ||
|
|
7872bfe19d | ||
|
|
af008e69c2 | ||
|
|
a549abc20f | ||
|
|
116344737a | ||
|
|
93c03f4e88 | ||
|
|
445332c27c | ||
|
|
c42e1892d0 | ||
|
|
b6b526dd57 | ||
|
|
3ef7f19ffc | ||
|
|
9d0d851c2e | ||
|
|
adb35b5bef | ||
|
|
acead09377 | ||
|
|
714cf43f6a | ||
|
|
5df0755252 | ||
|
|
c14827b234 | ||
|
|
ff9ef2af41 | ||
|
|
91ef5edcc3 | ||
|
|
27302c6fcc | ||
|
|
4d975da176 | ||
|
|
5b58d8a1e8 | ||
|
|
3105958afb | ||
|
|
a505227d01 | ||
|
|
673503b76f | ||
|
|
384682421d | ||
|
|
2ddd6e6321 | ||
|
|
86739aa1ac | ||
|
|
45a46cbc7a | ||
|
|
567f453232 | ||
|
|
890f654971 | ||
|
|
572a0ac266 | ||
|
|
d26ffdbe1a | ||
|
|
0bfc9236ed | ||
|
|
32e6394b3f | ||
|
|
09735b7f47 | ||
|
|
ee280d5c7b | ||
|
|
c1b56e4cb6 | ||
|
|
6698d15f20 | ||
|
|
ef35fd02e5 | ||
|
|
8e70e20f9e | ||
|
|
9632bf5b93 | ||
|
|
dde0cab04b | ||
|
|
c8337c7287 | ||
|
|
15560a3bce | ||
|
|
2e3a60cf6e | ||
|
|
08b0c43382 | ||
|
|
4e0e11a611 | ||
|
|
ef41dfca4c | ||
|
|
cfbca4b0fd | ||
|
|
fdea9a68a1 | ||
|
|
47169e19aa | ||
|
|
0b03c8360b | ||
|
|
62f8af1455 | ||
|
|
0934d452bb | ||
|
|
f2f31790b4 | ||
|
|
cf6ecc17cc | ||
|
|
931f9bdce0 | ||
|
|
bec0528a3a | ||
|
|
670f2b1fc3 | ||
|
|
f2f6de717b | ||
|
|
f8ad2eddf3 | ||
|
|
c36a46cad6 | ||
|
|
00360c77d2 | ||
|
|
8a62cd386e | ||
|
|
450327f093 | ||
|
|
e87ec04058 | ||
|
|
f9d41de8f1 | ||
|
|
f80a1a5f6b | ||
|
|
f81caf962d | ||
|
|
d18fcf0a18 | ||
|
|
0187217c86 | ||
|
|
b820bdec09 | ||
|
|
adace2954e | ||
|
|
6eeb8eeba6 | ||
|
|
dd2a8202ef | ||
|
|
d1cfd627bc | ||
|
|
fb97b7443d | ||
|
|
48fcd45d7d | ||
|
|
5cfc418d77 | ||
|
|
f3fbe38247 | ||
|
|
a0a1c84db1 | ||
|
|
54d563f49e | ||
|
|
e8ee8b8a16 | ||
|
|
c6ac44ba14 | ||
|
|
e4d8438801 | ||
|
|
f9539ab50a | ||
|
|
59f83c2432 | ||
|
|
cd789136c0 | ||
|
|
54b5bc441e | ||
|
|
2537b6ba09 | ||
|
|
013a1b4f51 | ||
|
|
d2377bd7c3 | ||
|
|
c17314125e | ||
|
|
09a59480f3 | ||
|
|
63cc2ce70a | ||
|
|
4642e050ba | ||
|
|
27a442ed2e | ||
|
|
325ae00eeb | ||
|
|
152e4129b2 | ||
|
|
2ddcf84625 | ||
|
|
13314700cd | ||
|
|
a7a499a2b1 | ||
|
|
b646313b58 | ||
|
|
f3ce4ca803 | ||
|
|
93d99c0c47 | ||
|
|
ae1fc7572a | ||
|
|
1a527cca10 | ||
|
|
c625513924 | ||
|
|
3f58302a14 | ||
|
|
63b199c9c2 | ||
|
|
fc64c565db | ||
|
|
91e60fa82b | ||
|
|
0cc52c2206 | ||
|
|
2ffe4ba70b | ||
|
|
2afd7e3687 | ||
|
|
a0f8d13c4f | ||
|
|
2571ea021a | ||
|
|
6950e05b6a | ||
|
|
7eb767a268 | ||
|
|
8e64abc4bc | ||
|
|
52df793a74 | ||
|
|
8e44a421a2 | ||
|
|
7f4ccdcac8 | ||
|
|
03e8de2f62 | ||
|
|
8b04eecc90 | ||
|
|
16bcd86792 | ||
|
|
be3c519a57 | ||
|
|
8776cb1cea | ||
|
|
4c94503f9a | ||
|
|
48f57376d3 | ||
|
|
958469f526 | ||
|
|
2a774a7bb6 | ||
|
|
a872ad9d8b | ||
|
|
2499a05473 | ||
|
|
6b66893ea4 | ||
|
|
529c27aed5 | ||
|
|
70fc0afbc4 | ||
|
|
09f81fd0d6 | ||
|
|
af7f2d4d5e | ||
|
|
3bd5d6b9f6 | ||
|
|
57912b5a5a | ||
|
|
a05f5b9737 | ||
|
|
1963b586ac | ||
|
|
3b9ad59849 | ||
|
|
79e0e5668d | ||
|
|
0e8edf0c72 | ||
|
|
24e2544544 | ||
|
|
f3732c76ea | ||
|
|
a4c72a9a86 | ||
|
|
455610e586 | ||
|
|
634d58b3ca | ||
|
|
27bbd77e8c | ||
|
|
d8ae77ded7 | ||
|
|
0648c04728 | ||
|
|
57c26e3b4a | ||
|
|
b03afff994 | ||
|
|
77f9e60177 | ||
|
|
35bb792496 | ||
|
|
8a87304800 | ||
|
|
64bbe053f8 | ||
|
|
d3f420bf6d | ||
|
|
7fcaaa297a | ||
|
|
7c2d2044a9 | ||
|
|
aa32f59dc6 | ||
|
|
182af99e7c | ||
|
|
5b520a7a81 | ||
|
|
364917c910 | ||
|
|
ca7b9c786a | ||
|
|
15c2363098 | ||
|
|
1a11095121 | ||
|
|
2b384b1d15 | ||
|
|
a1d61edb9c | ||
|
|
96a8687896 | ||
|
|
0448773682 | ||
|
|
57998ba727 | ||
|
|
de83447cb3 | ||
|
|
eba19468d5 | ||
|
|
65c78df671 | ||
|
|
a7096aa89f | ||
|
|
15a50ef452 | ||
|
|
04036e5c87 | ||
|
|
2bbb5ef74e | ||
|
|
91eb7feb3c | ||
|
|
978d77142c | ||
|
|
e36478b9ac | ||
|
|
e1fe4dd693 | ||
|
|
b1ee949b1c | ||
|
|
a0e5f8e97e | ||
|
|
e9cfb2c4ee | ||
|
|
190b6edfb1 | ||
|
|
80a0c59f87 | ||
|
|
823fdec705 | ||
|
|
fe87dcced7 | ||
|
|
137eb44516 | ||
|
|
f60d957102 | ||
|
|
8f0b04504f | ||
|
|
2c39d8b1c8 | ||
|
|
d4d1c32288 | ||
|
|
e4f39d2b6a | ||
|
|
e5a2bfbcbd | ||
|
|
de3b76b31d | ||
|
|
53455496bf | ||
|
|
cc2a2f6dfb | ||
|
|
ee4ac7371c | ||
|
|
d5265407b9 | ||
|
|
954b3e9fc5 | ||
|
|
7d9894bef7 | ||
|
|
3b34698e8b | ||
|
|
263cb581c4 | ||
|
|
1c9cb4516c | ||
|
|
ac4ceccb4f | ||
|
|
e731b7882d | ||
|
|
84e0728ff3 | ||
|
|
666bc18e91 | ||
|
|
8f83124a0d | ||
|
|
ee91daad7e | ||
|
|
ee78c0d33b | ||
|
|
1318abd37e | ||
|
|
76a031a8c9 | ||
|
|
09482ebcf3 | ||
|
|
67424f2d3a | ||
|
|
51f530ffbe | ||
|
|
013f96a754 | ||
|
|
df6a018fb6 | ||
|
|
409eaf54c1 | ||
|
|
7e04fd342c | ||
|
|
1fe15bc6a5 | ||
|
|
ff1bffbb55 | ||
|
|
b28b18a19a | ||
|
|
bbc3c85212 | ||
|
|
26a08fac06 | ||
|
|
da9d7a4336 | ||
|
|
46c6555f94 | ||
|
|
3e980fd2d4 | ||
|
|
fb1462f669 | ||
|
|
41e1630aac | ||
|
|
ef84c4e3da |
16
.babelrc
16
.babelrc
@@ -1,20 +1,8 @@
|
|||||||
{
|
{
|
||||||
"stage": 0,
|
"presets": ["react", "es2015"],
|
||||||
"env": {
|
"env": {
|
||||||
"development": {
|
"development": {
|
||||||
"plugins": ["react-transform"],
|
"presets": ["react-hmre"]
|
||||||
"extra": {
|
|
||||||
"react-transform": {
|
|
||||||
"transforms": [{
|
|
||||||
"transform": "react-transform-hmr",
|
|
||||||
"imports": ["react"],
|
|
||||||
"locals": ["module"]
|
|
||||||
}, {
|
|
||||||
"transform": "react-transform-catch-errors",
|
|
||||||
"imports": ["react", "redbox-react"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
node_modules/*
|
node_modules/*
|
||||||
!node_modules/boost
|
!node_modules/boost
|
||||||
Boost-darwin-x64/
|
/dist
|
||||||
backup/
|
/compiled
|
||||||
compiled
|
/secret
|
||||||
|
|||||||
16
LICENSE
Normal file
16
LICENSE
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Boostnote - the simplest note app
|
||||||
|
|
||||||
|
Copyright (C) 2016 MAISIN&CO.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
11
appdmg.json
Normal file
11
appdmg.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"title": "Boostnote",
|
||||||
|
"icon": "resources/dmg.icns",
|
||||||
|
"background": "resources/boostnote-install.png",
|
||||||
|
"icon-size": 80,
|
||||||
|
"contents": [
|
||||||
|
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
|
||||||
|
{ "x": 192, "y": 344, "type": "file", "path": "dist/Boostnote-darwin-x64/Boostnote.app" }
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
var BrowserWindow = require('browser-window')
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var finderWindow = new BrowserWindow({
|
|
||||||
width: 640,
|
|
||||||
height: 400,
|
|
||||||
show: false,
|
|
||||||
frame: false,
|
|
||||||
resizable: false,
|
|
||||||
'zoom-factor': 1.0,
|
|
||||||
'always-on-top': true,
|
|
||||||
'web-preferences': {
|
|
||||||
'overlay-scrollbars': true,
|
|
||||||
'skip-taskbar': true
|
|
||||||
},
|
|
||||||
'standard-window': false
|
|
||||||
})
|
|
||||||
|
|
||||||
var url = path.resolve(__dirname, '../browser/finder/index.html')
|
|
||||||
|
|
||||||
finderWindow.loadUrl('file://' + url)
|
|
||||||
|
|
||||||
finderWindow.on('blur', function () {
|
|
||||||
finderWindow.hide()
|
|
||||||
})
|
|
||||||
|
|
||||||
finderWindow.setVisibleOnAllWorkspaces(true)
|
|
||||||
|
|
||||||
module.exports = finderWindow
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
var BrowserWindow = require('browser-window')
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var mainWindow = new BrowserWindow({
|
|
||||||
width: 1080,
|
|
||||||
height: 720,
|
|
||||||
'zoom-factor': 1.0,
|
|
||||||
'web-preferences': {
|
|
||||||
'overlay-scrollbars': true
|
|
||||||
},
|
|
||||||
'standard-window': false
|
|
||||||
})
|
|
||||||
|
|
||||||
var url = path.resolve(__dirname, '../browser/main/index.html')
|
|
||||||
|
|
||||||
mainWindow.loadUrl('file://' + url)
|
|
||||||
|
|
||||||
mainWindow.setVisibleOnAllWorkspaces(true)
|
|
||||||
|
|
||||||
mainWindow.webContents.on('new-window', function (e) {
|
|
||||||
e.preventDefault()
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = mainWindow
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
var BrowserWindow = require('browser-window')
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
label: 'Electron',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'About Boost',
|
|
||||||
selector: 'orderFrontStandardAboutPanel:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Services',
|
|
||||||
submenu: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hide Boost',
|
|
||||||
accelerator: 'Command+H',
|
|
||||||
selector: 'hide:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hide Others',
|
|
||||||
accelerator: 'Command+Shift+H',
|
|
||||||
selector: 'hideOtherApplications:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Show All',
|
|
||||||
selector: 'unhideAllApplications:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
accelerator: 'Command+Q',
|
|
||||||
selector: 'terminate:'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Undo',
|
|
||||||
accelerator: 'Command+Z',
|
|
||||||
selector: 'undo:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Redo',
|
|
||||||
accelerator: 'Shift+Command+Z',
|
|
||||||
selector: 'redo:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cut',
|
|
||||||
accelerator: 'Command+X',
|
|
||||||
selector: 'cut:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Copy',
|
|
||||||
accelerator: 'Command+C',
|
|
||||||
selector: 'copy:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Paste',
|
|
||||||
accelerator: 'Command+V',
|
|
||||||
selector: 'paste:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Select All',
|
|
||||||
accelerator: 'Command+A',
|
|
||||||
selector: 'selectAll:'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Reload',
|
|
||||||
accelerator: 'Command+R',
|
|
||||||
click: function () {
|
|
||||||
BrowserWindow.getFocusedWindow().reload()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Toggle DevTools',
|
|
||||||
accelerator: 'Alt+Command+I',
|
|
||||||
click: function () {
|
|
||||||
BrowserWindow.getFocusedWindow().toggleDevTools()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Window',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Minimize',
|
|
||||||
accelerator: 'Command+M',
|
|
||||||
selector: 'performMiniaturize:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close',
|
|
||||||
accelerator: 'Command+W',
|
|
||||||
selector: 'performClose:'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Bring All to Front',
|
|
||||||
selector: 'arrangeInFront:'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Help',
|
|
||||||
submenu: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
264
browser/components/CodeEditor.js
Normal file
264
browser/components/CodeEditor.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import modes from '../lib/modes'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import fetchConfig from '../lib/fetchConfig'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
const ace = window.ace
|
||||||
|
|
||||||
|
let config = fetchConfig()
|
||||||
|
ipc.on('config-apply', function (e, newConfig) {
|
||||||
|
config = newConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class CodeEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||||
|
this.changeHandler = e => this.handleChange(e)
|
||||||
|
this.blurHandler = (e) => {
|
||||||
|
if (e.relatedTarget === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
|
||||||
|
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
|
||||||
|
})
|
||||||
|
if (isFocusingToSearch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onBlur) this.props.onBlur(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.killedBuffer = ''
|
||||||
|
this.execHandler = (e) => {
|
||||||
|
console.log(e.command.name)
|
||||||
|
switch (e.command.name) {
|
||||||
|
case 'gotolinestart':
|
||||||
|
e.preventDefault()
|
||||||
|
{
|
||||||
|
let position = this.editor.getCursorPosition()
|
||||||
|
this.editor.navigateTo(position.row, 0)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'gotolineend':
|
||||||
|
e.preventDefault()
|
||||||
|
let position = this.editor.getCursorPosition()
|
||||||
|
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
|
||||||
|
break
|
||||||
|
case 'jumptomatching':
|
||||||
|
e.preventDefault()
|
||||||
|
this.editor.navigateUp()
|
||||||
|
break
|
||||||
|
case 'removetolineend':
|
||||||
|
e.preventDefault()
|
||||||
|
let range = this.editor.getSelectionRange()
|
||||||
|
let session = this.editor.getSession()
|
||||||
|
if (range.isEmpty()) {
|
||||||
|
range.setEnd(range.start.row, session.getLine(range.start.row).length)
|
||||||
|
this.killedBuffer = session.getTextRange(range)
|
||||||
|
if (this.killedBuffer.length > 0) {
|
||||||
|
console.log('remove to lineend')
|
||||||
|
session.remove(range)
|
||||||
|
} else {
|
||||||
|
if (session.getLength() === range.start.row) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
range.setStart(range.start.row, range.end.col)
|
||||||
|
range.setEnd(range.start.row + 1, 0)
|
||||||
|
this.killedBuffer = '\n'
|
||||||
|
session.remove(range)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.killedBuffer = session.getTextRange(range)
|
||||||
|
session.remove(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.afterExecHandler = (e) => {
|
||||||
|
switch (e.command.name) {
|
||||||
|
case 'find':
|
||||||
|
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => {
|
||||||
|
el.removeEventListener('blur', this.blurHandler)
|
||||||
|
el.addEventListener('blur', this.blurHandler)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fontSize: config['editor-font-size'],
|
||||||
|
fontFamily: config['editor-font-family'],
|
||||||
|
indentType: config['editor-indent-type'],
|
||||||
|
indentSize: config['editor-indent-size']
|
||||||
|
}
|
||||||
|
|
||||||
|
this.silentChange = false
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.readOnly !== this.props.readOnly) {
|
||||||
|
this.editor.setReadOnly(!!nextProps.readOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
let { article } = this.props
|
||||||
|
var el = ReactDOM.findDOMNode(this)
|
||||||
|
var editor = this.editor = ace.edit(el)
|
||||||
|
editor.$blockScrolling = Infinity
|
||||||
|
editor.renderer.setShowGutter(true)
|
||||||
|
editor.setTheme('ace/theme/xcode')
|
||||||
|
editor.moveCursorTo(0, 0)
|
||||||
|
editor.setReadOnly(!!this.props.readOnly)
|
||||||
|
editor.setFontSize(this.state.fontSize)
|
||||||
|
|
||||||
|
editor.on('blur', this.blurHandler)
|
||||||
|
|
||||||
|
editor.commands.addCommand({
|
||||||
|
name: 'Emacs cursor up',
|
||||||
|
bindKey: {mac: 'Ctrl-P'},
|
||||||
|
exec: function (editor) {
|
||||||
|
editor.navigateUp(1)
|
||||||
|
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
|
||||||
|
},
|
||||||
|
readOnly: true
|
||||||
|
})
|
||||||
|
editor.commands.addCommand({
|
||||||
|
name: 'Emacs cursor up',
|
||||||
|
bindKey: {mac: 'Ctrl-Y'},
|
||||||
|
exec: function (editor) {
|
||||||
|
editor.insert(this.killedBuffer)
|
||||||
|
}.bind(this),
|
||||||
|
readOnly: true
|
||||||
|
})
|
||||||
|
editor.commands.addCommand({
|
||||||
|
name: 'Focus title',
|
||||||
|
bindKey: {win: 'Esc', mac: 'Esc'},
|
||||||
|
exec: function (editor, e) {
|
||||||
|
let currentWindow = remote.getCurrentWebContents()
|
||||||
|
if (config['switch-preview'] === 'rightclick') {
|
||||||
|
currentWindow.send('detail-preview')
|
||||||
|
}
|
||||||
|
currentWindow.send('list-focus')
|
||||||
|
},
|
||||||
|
readOnly: true
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.commands.on('exec', this.execHandler)
|
||||||
|
editor.commands.on('afterExec', this.afterExecHandler)
|
||||||
|
|
||||||
|
var session = editor.getSession()
|
||||||
|
let mode = _.findWhere(modes, {name: article.mode})
|
||||||
|
let syntaxMode = mode != null
|
||||||
|
? mode.mode
|
||||||
|
: 'text'
|
||||||
|
session.setMode('ace/mode/' + syntaxMode)
|
||||||
|
|
||||||
|
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||||
|
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||||
|
session.setOption('useWorker', false)
|
||||||
|
session.setUseWrapMode(true)
|
||||||
|
session.setValue(this.props.article.content)
|
||||||
|
|
||||||
|
session.on('change', this.changeHandler)
|
||||||
|
|
||||||
|
ipc.on('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||||
|
this.editor.getSession().removeListener('change', this.changeHandler)
|
||||||
|
this.editor.removeListener('blur', this.blurHandler)
|
||||||
|
this.editor.commands.removeListener('exec', this.execHandler)
|
||||||
|
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps, prevState) {
|
||||||
|
var session = this.editor.getSession()
|
||||||
|
if (this.props.article.key !== prevProps.article.key) {
|
||||||
|
session.removeListener('change', this.changeHandler)
|
||||||
|
session.setValue(this.props.article.content)
|
||||||
|
session.getUndoManager().reset()
|
||||||
|
session.on('change', this.changeHandler)
|
||||||
|
}
|
||||||
|
if (prevProps.article.mode !== this.props.article.mode) {
|
||||||
|
let mode = _.findWhere(modes, {name: this.props.article.mode})
|
||||||
|
let syntaxMode = mode != null
|
||||||
|
? mode.mode
|
||||||
|
: 'text'
|
||||||
|
session.setMode('ace/mode/' + syntaxMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigApply (e, config) {
|
||||||
|
this.setState({
|
||||||
|
fontSize: config['editor-font-size'],
|
||||||
|
fontFamily: config['editor-font-family'],
|
||||||
|
indentType: config['editor-indent-type'],
|
||||||
|
indentSize: config['editor-indent-size']
|
||||||
|
}, function () {
|
||||||
|
var session = this.editor.getSession()
|
||||||
|
session.setUseSoftTabs(this.state.indentType === 'space')
|
||||||
|
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleChange (e) {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
var value = this.editor.getValue()
|
||||||
|
this.props.onChange(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstVisibleRow () {
|
||||||
|
return this.editor.getFirstVisibleRow()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCursorPosition () {
|
||||||
|
return this.editor.getCursorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
moveCursorTo (row, col) {
|
||||||
|
this.editor.moveCursorTo(row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToLine (num) {
|
||||||
|
this.editor.scrollToLine(num, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
|
||||||
|
style={{
|
||||||
|
fontSize: this.state.fontSize,
|
||||||
|
fontFamily: this.state.fontFamily.trim() + ', monospace'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeEditor.propTypes = {
|
||||||
|
article: PropTypes.shape({
|
||||||
|
content: PropTypes.string,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
key: PropTypes.string
|
||||||
|
}),
|
||||||
|
className: PropTypes.string,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
readOnly: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeEditor.defaultProps = {
|
||||||
|
readOnly: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeEditor
|
||||||
@@ -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) {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
222
browser/components/MarkdownPreview.js
Normal file
222
browser/components/MarkdownPreview.js
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import markdown from '../lib/markdown'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import sanitizeHtml from '@rokt33r/sanitize-html'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import fetchConfig from '../lib/fetchConfig'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const shell = electron.shell
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
const katex = window.katex
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const sanitizeOpts = {
|
||||||
|
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
|
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||||
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
|
||||||
|
allowedClasses: {
|
||||||
|
'a': ['lineAnchor'],
|
||||||
|
'div': ['math'],
|
||||||
|
'span': ['math', 'hljs-*'],
|
||||||
|
'code': ['language-*']
|
||||||
|
},
|
||||||
|
allowedAttributes: {
|
||||||
|
a: ['href', 'data-key'],
|
||||||
|
img: [ 'src' ],
|
||||||
|
label: ['for'],
|
||||||
|
input: ['checked', 'type'],
|
||||||
|
'*': ['id', 'name']
|
||||||
|
},
|
||||||
|
transformTags: {
|
||||||
|
'*': function (tagName, attribs) {
|
||||||
|
let href = attribs.href
|
||||||
|
if (tagName === 'input' && attribs.type !== 'checkbox') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
|
||||||
|
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
|
||||||
|
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
|
||||||
|
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
|
||||||
|
return {
|
||||||
|
tagName: tagName,
|
||||||
|
attribs: attribs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAnchorClick (e) {
|
||||||
|
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let href = this.href
|
||||||
|
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
|
||||||
|
shell.openExternal(href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPropagation (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function math2Katex (display) {
|
||||||
|
return function (el) {
|
||||||
|
try {
|
||||||
|
katex.render(el.innerHTML.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&'), el, {display: display})
|
||||||
|
el.className = 'math-rendered'
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = e.message
|
||||||
|
el.className = 'math-failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = fetchConfig()
|
||||||
|
ipc.on('config-apply', function (e, newConfig) {
|
||||||
|
config = newConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class MarkdownPreview extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fontSize: config['preview-font-size'],
|
||||||
|
fontFamily: config['preview-font-family']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentDidMount () {
|
||||||
|
this.addListener()
|
||||||
|
this.renderMath()
|
||||||
|
ipc.on('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
this.addListener()
|
||||||
|
this.renderMath()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.removeListener()
|
||||||
|
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate () {
|
||||||
|
this.removeListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMath () {
|
||||||
|
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
|
||||||
|
Array.prototype.forEach.call(inline, math2Katex(false))
|
||||||
|
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
|
||||||
|
Array.prototype.forEach.call(block, math2Katex(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener () {
|
||||||
|
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
|
||||||
|
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(anchors, anchor => {
|
||||||
|
anchor.addEventListener('click', handleAnchorClick)
|
||||||
|
anchor.addEventListener('mousedown', stopPropagation)
|
||||||
|
anchor.addEventListener('mouseup', stopPropagation)
|
||||||
|
})
|
||||||
|
Array.prototype.forEach.call(inputs, input => {
|
||||||
|
input.addEventListener('click', stopPropagation)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener () {
|
||||||
|
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
|
||||||
|
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
|
||||||
|
|
||||||
|
Array.prototype.forEach.call(anchors, anchor => {
|
||||||
|
anchor.removeEventListener('click', handleAnchorClick)
|
||||||
|
anchor.removeEventListener('mousedown', stopPropagation)
|
||||||
|
anchor.removeEventListener('mouseup', stopPropagation)
|
||||||
|
})
|
||||||
|
Array.prototype.forEach.call(inputs, input => {
|
||||||
|
input.removeEventListener('click', stopPropagation)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
if (this.props.onClick) {
|
||||||
|
this.props.onClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoubleClick (e) {
|
||||||
|
if (this.props.onDoubleClick) {
|
||||||
|
this.props.onDoubleClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown (e) {
|
||||||
|
if (this.props.onMouseDown) {
|
||||||
|
this.props.onMouseDown(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
if (this.props.onMouseUp) {
|
||||||
|
this.props.onMouseUp(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove (e) {
|
||||||
|
if (this.props.onMouseMove) {
|
||||||
|
this.props.onMouseMove(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigApply (e, config) {
|
||||||
|
this.setState({
|
||||||
|
fontSize: config['preview-font-size'],
|
||||||
|
fontFamily: config['preview-font-family']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let isEmpty = this.props.content.trim().length === 0
|
||||||
|
let content = isEmpty
|
||||||
|
? '(Empty content)'
|
||||||
|
: this.props.content
|
||||||
|
content = markdown(content)
|
||||||
|
content = sanitizeHtml(content, sanitizeOpts)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
|
||||||
|
onClick={e => this.handleClick(e)}
|
||||||
|
onDoubleClick={e => this.handleDoubleClick(e)}
|
||||||
|
onMouseDown={e => this.handleMouseDown(e)}
|
||||||
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
|
dangerouslySetInnerHTML={{__html: ' ' + content}}
|
||||||
|
style={{
|
||||||
|
fontSize: this.state.fontSize,
|
||||||
|
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MarkdownPreview.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onDoubleClick: PropTypes.func,
|
||||||
|
onMouseUp: PropTypes.func,
|
||||||
|
onMouseDown: PropTypes.func,
|
||||||
|
onMouseMove: PropTypes.func,
|
||||||
|
className: PropTypes.string,
|
||||||
|
content: PropTypes.string
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ModeIcon from 'boost/components/ModeIcon'
|
import ModeIcon from './ModeIcon'
|
||||||
import modes from 'boost/vars/modes'
|
import modes from '../lib/modes'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
const IDLE_MODE = 'IDLE_MODE'
|
const IDLE_MODE = 'IDLE_MODE'
|
||||||
@@ -18,17 +18,17 @@ 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) {
|
||||||
this.handleBlur()
|
this.handleBlur(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@@ -37,27 +37,9 @@ export default class ModeSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleIdleSelectClick (e) {
|
handleIdleSelectClick (e) {
|
||||||
this.setState({mode: EDIT_MODE})
|
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
|
||||||
}
|
ReactDOM.findDOMNode(this.refs.search).select()
|
||||||
|
})
|
||||||
componentDidUpdate (prevProps, prevState) {
|
|
||||||
if (prevState.mode !== this.state.mode && this.state.mode === EDIT_MODE) {
|
|
||||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
|
||||||
searchElement.focus()
|
|
||||||
if (this.searchKeyDownListener == null) {
|
|
||||||
this.searchKeyDownListener = e => this.handleSearchKeyDown
|
|
||||||
}
|
|
||||||
searchElement.addEventListener('keydown', this.searchKeyDownListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUpdate (nextProps, nextState) {
|
|
||||||
if (nextProps.mode !== this.state.mode && nextState.mode === IDLE_MODE) {
|
|
||||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
|
||||||
if (searchElement != null && this.searchKeyDownListener != null) {
|
|
||||||
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModeOptionClick (modeName) {
|
handleModeOptionClick (modeName) {
|
||||||
@@ -82,9 +64,9 @@ export default class ModeSelect extends React.Component {
|
|||||||
case 40:
|
case 40:
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
{
|
{
|
||||||
|
let search = _.escapeRegExp(this.state.search)
|
||||||
let filteredModes = modes
|
let filteredModes = modes
|
||||||
.filter(mode => {
|
.filter(mode => {
|
||||||
let search = this.state.search
|
|
||||||
let nameMatched = mode.name.match(search)
|
let nameMatched = mode.name.match(search)
|
||||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||||
return nameMatched || aliasMatched
|
return nameMatched || aliasMatched
|
||||||
@@ -97,9 +79,9 @@ export default class ModeSelect extends React.Component {
|
|||||||
case 13:
|
case 13:
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
{
|
{
|
||||||
|
let search = _.escapeRegExp(this.state.search)
|
||||||
let filteredModes = modes
|
let filteredModes = modes
|
||||||
.filter(mode => {
|
.filter(mode => {
|
||||||
let search = this.state.search
|
|
||||||
let nameMatched = mode.name.match(search)
|
let nameMatched = mode.name.match(search)
|
||||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||||
return nameMatched || aliasMatched
|
return nameMatched || aliasMatched
|
||||||
@@ -107,19 +89,17 @@ export default class ModeSelect extends React.Component {
|
|||||||
let targetMode = filteredModes[this.state.focusIndex]
|
let targetMode = filteredModes[this.state.focusIndex]
|
||||||
if (targetMode != null) {
|
if (targetMode != null) {
|
||||||
this.props.onChange(targetMode.name)
|
this.props.onChange(targetMode.name)
|
||||||
this.handleBlur()
|
this.setIdle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
// esc
|
// esc
|
||||||
case 27:
|
case 27:
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
this.handleBlur()
|
|
||||||
break
|
|
||||||
case 9:
|
case 9:
|
||||||
this.handleBlur()
|
e.stopPropagation()
|
||||||
|
this.setIdle()
|
||||||
}
|
}
|
||||||
|
if (this.props.onKeyDown) this.props.onKeyDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
@@ -129,15 +109,18 @@ export default class ModeSelect extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlur () {
|
handleBlur (e) {
|
||||||
if (this.state.mode === EDIT_MODE) {
|
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
|
||||||
this.setState({
|
this.setIdle()
|
||||||
mode: IDLE_MODE,
|
|
||||||
search: '',
|
|
||||||
focusIndex: 0
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (this.props.onBlur != null) this.props.onBlur()
|
}
|
||||||
|
|
||||||
|
setIdle () {
|
||||||
|
this.setState({
|
||||||
|
mode: IDLE_MODE,
|
||||||
|
search: '',
|
||||||
|
focusIndex: 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -148,33 +131,32 @@ export default class ModeSelect extends React.Component {
|
|||||||
if (this.state.mode === IDLE_MODE) {
|
if (this.state.mode === IDLE_MODE) {
|
||||||
let mode = _.findWhere(modes, {name: this.props.value})
|
let mode = _.findWhere(modes, {name: this.props.value})
|
||||||
let modeName = mode != null ? mode.name : 'text'
|
let modeName = mode != null ? mode.name : 'text'
|
||||||
let modeLabel = mode != null ? mode.label : 'Plain text'
|
let modeLabel = mode != null ? mode.label : this.props.value
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
||||||
<ModeIcon mode={modeName}/>
|
<ModeIcon mode={modeName}/>{modeLabel}
|
||||||
<span className='modeLabel'>{modeLabel}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let search = _.escapeRegExp(this.state.search)
|
||||||
let filteredOptions = modes
|
let filteredOptions = modes
|
||||||
.filter(mode => {
|
.filter(mode => {
|
||||||
let search = this.state.search
|
|
||||||
let nameMatched = mode.name.match(search)
|
let nameMatched = mode.name.match(search)
|
||||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||||
return nameMatched || aliasMatched
|
return nameMatched || aliasMatched
|
||||||
})
|
})
|
||||||
.map((mode, index) => {
|
.map((mode, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={mode.name} className={index === this.state.focusIndex ? 'option active' : 'option'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
|
<div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className + ' edit'}>
|
<div className={className + ' edit'}>
|
||||||
<input onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
|
<input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
|
||||||
<div ref='options' className='modeOptions hide'>
|
<div ref='options' className='ModeSelect-options hide'>
|
||||||
{filteredOptions}
|
{filteredOptions}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,5 +168,5 @@ ModeSelect.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onBlur: PropTypes.func
|
onKeyDown: PropTypes.func
|
||||||
}
|
}
|
||||||
168
browser/components/TagSelect.js
Normal file
168
browser/components/TagSelect.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import linkState from '../lib/linkState'
|
||||||
|
|
||||||
|
function isNotEmptyString (str) {
|
||||||
|
return _.isString(str) && str.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TagSelect extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
input: '',
|
||||||
|
isInputFocused: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.blurInputBlurHandler = e => {
|
||||||
|
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 newTag = tag.trim()
|
||||||
|
|
||||||
|
if (newTag.length === 0 && clearInput) {
|
||||||
|
this.setState({input: ''})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.push(newTag)
|
||||||
|
tags = _.uniq(tags)
|
||||||
|
|
||||||
|
if (_.isFunction(this.props.onChange)) {
|
||||||
|
this.props.onChange(newTag, tags)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
ReactDOM.findDOMNode(this.refs.tagInput).focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputFocus (e) {
|
||||||
|
this.setState({isInputFocused: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemRemoveButton (tag) {
|
||||||
|
return e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
let tags = this.props.tags.slice(0)
|
||||||
|
tags.splice(tags.indexOf(tag), 1)
|
||||||
|
|
||||||
|
if (_.isFunction(this.props.onChange)) {
|
||||||
|
this.props.onChange(null, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSuggestClick (tag) {
|
||||||
|
return e => {
|
||||||
|
this.addTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { tags, suggestTags } = this.props
|
||||||
|
|
||||||
|
let tagElements = _.isArray(tags)
|
||||||
|
? this.props.tags.map(tag => (
|
||||||
|
<div key={tag} className='TagSelect-tags-item'>
|
||||||
|
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='TagSelect-tags-item-remove'><i className='fa fa-fw fa-times'/></button>
|
||||||
|
<div className='TagSelect-tags-item-label'>{tag}</div>
|
||||||
|
</div>))
|
||||||
|
: 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 (
|
||||||
|
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
|
||||||
|
<div className='TagSelect-tags'>
|
||||||
|
{tagElements}
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
onKeyDown={e => this.handleKeyDown(e)}
|
||||||
|
ref='tagInput'
|
||||||
|
valueLink={this.linkState('input')}
|
||||||
|
placeholder='Click here to add tags'
|
||||||
|
className='TagSelect-input'
|
||||||
|
onFocus={e => this.handleInputFocus(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{suggestElements != null && suggestElements.length > 0
|
||||||
|
? (
|
||||||
|
<div ref='suggestTags' className='TagSelect-suggest'>
|
||||||
|
{suggestElements}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagSelect.propTypes = {
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
suggestTags: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
|
TagSelect.prototype.linkState = linkState
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import CodeEditor from 'boost/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
import MarkdownPreview from 'boost/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import ModeIcon from 'boost/components/ModeIcon'
|
import ModeIcon from 'browser/components/ModeIcon'
|
||||||
|
|
||||||
export default class FinderDetail extends React.Component {
|
export default class FinderDetail extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
@@ -11,11 +11,20 @@ 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}/>
|
||||||
: <CodeEditor readOnly mode={activeArticle.mode} code={activeArticle.content}/>
|
: <CodeEditor readOnly article={activeArticle}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,5 +39,6 @@ export default class FinderDetail extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FinderDetail.propTypes = {
|
FinderDetail.propTypes = {
|
||||||
activeArticle: PropTypes.shape()
|
activeArticle: PropTypes.shape(),
|
||||||
|
saveToClipboard: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ModeIcon from 'boost/components/ModeIcon'
|
import ModeIcon from 'browser/components/ModeIcon'
|
||||||
import { selectArticle } from './actions'
|
import { selectArticle } from './actions'
|
||||||
|
|
||||||
export default class FinderList extends React.Component {
|
export default class FinderList extends React.Component {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<title>CodeXen Popup</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<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/devicon/devicon.min.css">
|
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Lato';
|
|
||||||
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
|
||||||
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
|
||||||
url('../../resources/Lato-Regular.ttf') format('truetype');
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="content"></div>
|
|
||||||
<script src="../../submodules/ace/src-min/ace.js"></script>
|
|
||||||
<script>
|
|
||||||
require('web-frame').setZoomLevelLimits(1, 1)
|
|
||||||
var scriptUrl = process.env.BOOST_ENV === 'development'
|
|
||||||
? 'http://localhost:8080/assets/finder.js'
|
|
||||||
: '../../compiled/finder.js'
|
|
||||||
var scriptEl=document.createElement('script')
|
|
||||||
scriptEl.setAttribute("type","text/javascript")
|
|
||||||
scriptEl.setAttribute("src", scriptUrl)
|
|
||||||
document.getElementsByTagName("head")[0].appendChild(scriptEl)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -6,17 +6,37 @@ 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 'browser/lib/dataStore'
|
||||||
|
|
||||||
import remote from 'remote'
|
const electron = require('electron')
|
||||||
var hideFinder = remote.getGlobal('hideFinder')
|
const { clipboard, ipcRenderer, remote } = electron
|
||||||
import clipboard from 'clipboard'
|
const path = require('path')
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
window.addEventListener('keydown', function (e) {
|
||||||
|
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||||
|
remote.getCurrentWindow().toggleDevTools()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFinder () {
|
||||||
|
ipcRenderer.send('hide-finder')
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify (title, options) {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||||
|
}
|
||||||
|
return new window.Notification(title, options)
|
||||||
|
}
|
||||||
|
|
||||||
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'
|
||||||
|
|
||||||
@@ -26,11 +46,21 @@ class FinderMain extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
this.keyDownHandler = e => this.handleKeyDown(e)
|
||||||
|
document.addEventListener('keydown', this.keyDownHandler)
|
||||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||||
|
this.focusHandler = e => {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
|
||||||
|
dispatch(searchArticle(''))
|
||||||
|
dispatch(selectArticle(null))
|
||||||
|
}
|
||||||
|
window.addEventListener('focus', this.focusHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick (e) {
|
componentWillUnmount () {
|
||||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
document.removeEventListener('keydown', this.keyDownHandler)
|
||||||
|
window.removeEventListener('focus', this.focusHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
@@ -45,16 +75,30 @@ 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) {
|
||||||
hideFinder()
|
hideFinder()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
if (e.keyCode === 91 || e.metaKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToClipboard () {
|
||||||
|
let { activeArticle } = this.props
|
||||||
|
clipboard.writeText(activeArticle.content)
|
||||||
|
|
||||||
|
ipcRenderer.send('copy-finder')
|
||||||
|
notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!',
|
||||||
|
silent: true
|
||||||
|
})
|
||||||
|
hideFinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchChange (e) {
|
handleSearchChange (e) {
|
||||||
@@ -83,8 +127,9 @@ 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 className='Finder'>
|
||||||
<FinderInput
|
<FinderInput
|
||||||
handleSearchChange={e => this.handleSearchChange(e)}
|
handleSearchChange={e => this.handleSearchChange(e)}
|
||||||
ref='finderInput'
|
ref='finderInput'
|
||||||
@@ -98,7 +143,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 +164,55 @@ 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 isContaining (target, needle) {
|
||||||
|
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWith (target, needle) {
|
||||||
|
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||||
|
}
|
||||||
|
|
||||||
function remap (state) {
|
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 => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||||
})
|
})
|
||||||
status.targetFolders = targetFolders
|
let fuzzyTargetFolders = folders.filter(folder => {
|
||||||
|
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||||
|
})
|
||||||
|
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||||
|
|
||||||
if (targetFolders.length > 0) {
|
if (targetFolders.length > 0) {
|
||||||
articles = articles.filter(article => {
|
articles = articles.filter(article => {
|
||||||
@@ -147,7 +223,7 @@ function remap (state) {
|
|||||||
if (textFilters.length > 0) {
|
if (textFilters.length > 0) {
|
||||||
articles = textFilters.reduce((articles, textFilter) => {
|
articles = textFilters.reduce((articles, textFilter) => {
|
||||||
return articles.filter(article => {
|
return articles.filter(article => {
|
||||||
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
|
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||||
})
|
})
|
||||||
}, articles)
|
}, articles)
|
||||||
}
|
}
|
||||||
@@ -155,7 +231,7 @@ function remap (state) {
|
|||||||
if (tagFilters.length > 0) {
|
if (tagFilters.length > 0) {
|
||||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||||
return articles.filter(article => {
|
return articles.filter(article => {
|
||||||
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i')))
|
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||||
})
|
})
|
||||||
}, articles)
|
}, articles)
|
||||||
}
|
}
|
||||||
@@ -174,13 +250,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(true)
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import keygen from 'boost/keygen'
|
import dataStore from './dataStore'
|
||||||
import dataStore from 'boost/dataStore'
|
import { request, SERVER_URL } from './api'
|
||||||
import { request, WEB_URL } from 'boost/api'
|
import clientKey from './clientKey'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const version = electron.remote.app.getVersion()
|
||||||
|
|
||||||
function isSameDate (a, b) {
|
function isSameDate (a, b) {
|
||||||
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
||||||
@@ -16,6 +19,7 @@ export function init () {
|
|||||||
if (records == null) {
|
if (records == null) {
|
||||||
saveAllRecords([])
|
saveAllRecords([])
|
||||||
}
|
}
|
||||||
|
emit(null)
|
||||||
|
|
||||||
postRecords()
|
postRecords()
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
@@ -24,16 +28,6 @@ export function init () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClientKey () {
|
|
||||||
let clientKey = localStorage.getItem('clientKey')
|
|
||||||
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
|
||||||
clientKey = keygen()
|
|
||||||
localStorage.setItem('clientKey', clientKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientKey
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllRecords () {
|
export function getAllRecords () {
|
||||||
return JSON.parse(localStorage.getItem('activityRecords'))
|
return JSON.parse(localStorage.getItem('activityRecords'))
|
||||||
}
|
}
|
||||||
@@ -47,6 +41,11 @@ 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.NODE_ENV !== 'production') {
|
||||||
|
console.log('post failed - NOT PRODUCTION ')
|
||||||
|
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)
|
||||||
@@ -59,10 +58,10 @@ export function postRecords (data) {
|
|||||||
|
|
||||||
console.log('posting...', records)
|
console.log('posting...', records)
|
||||||
let input = {
|
let input = {
|
||||||
clientKey: getClientKey(),
|
clientKey: clientKey.get(),
|
||||||
records
|
records
|
||||||
}
|
}
|
||||||
return request.post(WEB_URL + 'apis/activity')
|
return request.post(SERVER_URL + 'apis/activity')
|
||||||
.send(input)
|
.send(input)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let records = getAllRecords()
|
let records = getAllRecords()
|
||||||
@@ -77,7 +76,7 @@ export function postRecords (data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emit (type, data) {
|
export function emit (type, data = {}) {
|
||||||
let records = getAllRecords()
|
let records = getAllRecords()
|
||||||
|
|
||||||
let index = _.findIndex(records, record => {
|
let index = _.findIndex(records, record => {
|
||||||
@@ -90,7 +89,6 @@ export function emit (type, data) {
|
|||||||
records.push(todayRecord)
|
records.push(todayRecord)
|
||||||
}
|
}
|
||||||
else todayRecord = records[index]
|
else todayRecord = records[index]
|
||||||
console.log(type)
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'ARTICLE_CREATE':
|
case 'ARTICLE_CREATE':
|
||||||
case 'ARTICLE_UPDATE':
|
case 'ARTICLE_UPDATE':
|
||||||
@@ -100,6 +98,8 @@ export function emit (type, data) {
|
|||||||
case 'FOLDER_DESTROY':
|
case 'FOLDER_DESTROY':
|
||||||
case 'FINDER_OPEN':
|
case 'FINDER_OPEN':
|
||||||
case 'FINDER_COPY':
|
case 'FINDER_COPY':
|
||||||
|
case 'MAIN_DETAIL_COPY':
|
||||||
|
case 'ARTICLE_SHARE':
|
||||||
todayRecord[type] = todayRecord[type] == null
|
todayRecord[type] = todayRecord[type] == null
|
||||||
? 1
|
? 1
|
||||||
: todayRecord[type] + 1
|
: todayRecord[type] + 1
|
||||||
@@ -107,9 +107,26 @@ export function emit (type, data) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
|
||||||
|
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
|
||||||
|
let recordKey = type + '_BY_SYNTAX'
|
||||||
|
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
|
||||||
|
|
||||||
|
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
|
||||||
|
? 1
|
||||||
|
: todayRecord[recordKey][data.mode] + 1
|
||||||
|
}
|
||||||
|
|
||||||
let storeData = dataStore.getData()
|
let storeData = dataStore.getData()
|
||||||
todayRecord.FOLDER_COUNT = _.isArray(storeData.folders) ? storeData.folders.length : 0
|
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
|
||||||
todayRecord.ARTICLE_COUNT = _.isArray(storeData.articles) ? storeData.articles.length : 0
|
todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
|
||||||
|
todayRecord.CLIENT_VERSION = version
|
||||||
|
|
||||||
|
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
|
||||||
|
if (sum[article.mode] == null) sum[article.mode] = 1
|
||||||
|
else sum[article.mode]++
|
||||||
|
return sum
|
||||||
|
}, {}) : 0
|
||||||
|
|
||||||
saveAllRecords(records)
|
saveAllRecords(records)
|
||||||
}
|
}
|
||||||
@@ -117,6 +134,5 @@ export function emit (type, data) {
|
|||||||
export default {
|
export default {
|
||||||
init,
|
init,
|
||||||
emit,
|
emit,
|
||||||
getClientKey,
|
|
||||||
postRecords
|
postRecords
|
||||||
}
|
}
|
||||||
21
browser/lib/api.js
Normal file
21
browser/lib/api.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import superagent from 'superagent'
|
||||||
|
import superagentPromise from 'superagent-promise'
|
||||||
|
|
||||||
|
export const SERVER_URL = 'https://b00st.io/'
|
||||||
|
// export const SERVER_URL = 'http://localhost:3333/'
|
||||||
|
|
||||||
|
export const request = superagentPromise(superagent, Promise)
|
||||||
|
|
||||||
|
export function shareViaPublicURL (input) {
|
||||||
|
return request
|
||||||
|
.post(SERVER_URL + 'apis/share')
|
||||||
|
// .set({
|
||||||
|
// Authorization: 'Bearer ' + auth.token()
|
||||||
|
// })
|
||||||
|
.send(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SERVER_URL,
|
||||||
|
shareViaPublicURL
|
||||||
|
}
|
||||||
23
browser/lib/clientKey.js
Normal file
23
browser/lib/clientKey.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
import keygen from './keygen'
|
||||||
|
|
||||||
|
function getClientKey () {
|
||||||
|
let clientKey = localStorage.getItem('clientKey')
|
||||||
|
if (!_.isString(clientKey) || clientKey.length !== 40) {
|
||||||
|
clientKey = keygen()
|
||||||
|
setClientKey(clientKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function setClientKey (newKey) {
|
||||||
|
localStorage.setItem('clientKey', newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientKey()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get: getClientKey,
|
||||||
|
set: setClientKey
|
||||||
|
}
|
||||||
156
browser/lib/dataStore.js
Normal file
156
browser/lib/dataStore.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import keygen from './keygen'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
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 +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**'
|
||||||
|
|
||||||
|
let data = null
|
||||||
|
|
||||||
|
function getLocalPath () {
|
||||||
|
return path.join(remote.app.getPath('userData'), 'local.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgeInitialRepositories () {
|
||||||
|
let defaultRepo = {
|
||||||
|
key: keygen(),
|
||||||
|
name: 'local',
|
||||||
|
type: 'userData',
|
||||||
|
user: {
|
||||||
|
name: 'New user'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
defaultRepo.user.name = remote.process.env.USER
|
||||||
|
} else if (process.platform === 'win32') {
|
||||||
|
defaultRepo.user.name = remote.process.env.USERNAME
|
||||||
|
}
|
||||||
|
|
||||||
|
return [defaultRepo]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepositories () {
|
||||||
|
let raw = localStorage.getItem('repositories')
|
||||||
|
try {
|
||||||
|
let parsed = JSON.parse(raw)
|
||||||
|
if (!_.isArray(parsed)) {
|
||||||
|
throw new Error('repositories data is corrupted. re-init data.')
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
let newRepos = forgeInitialRepositories()
|
||||||
|
saveRepositories(newRepos)
|
||||||
|
return newRepos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRepositories (repos) {
|
||||||
|
localStorage.setItem('repositories', JSON.stringify(repos))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser (repoName) {
|
||||||
|
if (repoName == null) {
|
||||||
|
return getRepositories()[0]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveUser (repoName, user) {
|
||||||
|
let repos = getRepositories()
|
||||||
|
if (repoName == null) {
|
||||||
|
Object.assign(repos[0].user, user)
|
||||||
|
}
|
||||||
|
saveRepositories(repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init () {
|
||||||
|
// set repositories info
|
||||||
|
getRepositories()
|
||||||
|
data = jetpack.read(getLocalPath(), 'json')
|
||||||
|
if (data == null) {
|
||||||
|
let defaultFolder = {
|
||||||
|
name: 'default',
|
||||||
|
key: keygen()
|
||||||
|
}
|
||||||
|
let defaultArticle = {
|
||||||
|
title: 'About Boost',
|
||||||
|
tags: ['boost', 'intro'],
|
||||||
|
content: defaultContent,
|
||||||
|
mode: 'markdown',
|
||||||
|
key: keygen(),
|
||||||
|
FolderKey: defaultFolder.key,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
articles: [defaultArticle],
|
||||||
|
folders: [defaultFolder],
|
||||||
|
version: '0.4'
|
||||||
|
}
|
||||||
|
saveData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getData (forceRead) {
|
||||||
|
if (forceRead) {
|
||||||
|
try {
|
||||||
|
data = jetpack.read(getLocalPath(), 'json')
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer = null
|
||||||
|
let isSaving = false
|
||||||
|
let saveAgain = false
|
||||||
|
function saveData () {
|
||||||
|
timer = null
|
||||||
|
isSaving = true
|
||||||
|
jetpack.writeAsync(getLocalPath(), data)
|
||||||
|
.then(function () {
|
||||||
|
isSaving = false
|
||||||
|
if (saveAgain) {
|
||||||
|
saveAgain = false
|
||||||
|
queueSave()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function queueSave () {
|
||||||
|
if (!isSaving) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
timer = setTimeout(saveData, 500)
|
||||||
|
} else {
|
||||||
|
saveAgain = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setArticles (articles) {
|
||||||
|
if (!_.isArray(articles)) throw new Error('Articles must be an array')
|
||||||
|
let data = getData()
|
||||||
|
data.articles = articles
|
||||||
|
queueSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setFolders (folders) {
|
||||||
|
if (!_.isArray(folders)) throw new Error('Folders must be an array')
|
||||||
|
let data = getData()
|
||||||
|
data.folders = folders
|
||||||
|
queueSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getUser,
|
||||||
|
saveUser,
|
||||||
|
init,
|
||||||
|
getData,
|
||||||
|
setArticles,
|
||||||
|
setFolders
|
||||||
|
}
|
||||||
21
browser/lib/fetchConfig.js
Normal file
21
browser/lib/fetchConfig.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
const jetpack = require('fs-jetpack')
|
||||||
|
|
||||||
|
const userDataPath = remote.app.getPath('userData')
|
||||||
|
const configFile = 'config.json'
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
'editor-font-size': '14',
|
||||||
|
'editor-font-family': 'Monaco, Consolas',
|
||||||
|
'editor-indent-type': 'space',
|
||||||
|
'editor-indent-size': '4',
|
||||||
|
'preview-font-size': '14',
|
||||||
|
'preview-font-family': 'Lato',
|
||||||
|
'switch-preview': 'blur',
|
||||||
|
'disable-direct-write': false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function fetchConfig () {
|
||||||
|
return Object.assign({}, defaultConfig, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@ var crypto = require('crypto')
|
|||||||
|
|
||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
var shasum = crypto.createHash('sha1')
|
var shasum = crypto.createHash('sha1')
|
||||||
shasum.update(((new Date()).getTime()).toString())
|
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
|
||||||
return shasum.digest('hex')
|
return shasum.digest('hex')
|
||||||
}
|
}
|
||||||
48
browser/lib/markdown.js
Normal file
48
browser/lib/markdown.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import markdownit from 'markdown-it'
|
||||||
|
import emoji from 'markdown-it-emoji'
|
||||||
|
import math from 'markdown-it-math'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
|
var md = markdownit({
|
||||||
|
typographer: true,
|
||||||
|
linkify: true,
|
||||||
|
html: true,
|
||||||
|
xhtmlOut: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
try {
|
||||||
|
return hljs.highlight(lang, str).value
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return str.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\"/g, '"')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
md.use(emoji, {
|
||||||
|
shortcuts: {}
|
||||||
|
})
|
||||||
|
md.use(math, {
|
||||||
|
inlineRenderer: function (str) {
|
||||||
|
return `<span class='math'>${str}</span>`
|
||||||
|
},
|
||||||
|
blockRenderer: function (str) {
|
||||||
|
return `<div class='math'>${str}</div>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
md.use(require('markdown-it-checkbox'))
|
||||||
|
|
||||||
|
let originalRenderToken = md.renderer.renderToken
|
||||||
|
md.renderer.renderToken = function renderToken (tokens, idx, options) {
|
||||||
|
let token = tokens[idx]
|
||||||
|
|
||||||
|
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
|
||||||
|
if (token.map != null) {
|
||||||
|
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function markdown (content) {
|
||||||
|
if (content == null) content = ''
|
||||||
|
|
||||||
|
return md.render(content.toString())
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
|
|
||||||
|
const remote = require('electron').remote
|
||||||
|
|
||||||
class ModalBase extends React.Component {
|
class ModalBase extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
@@ -13,6 +15,8 @@ class ModalBase extends React.Component {
|
|||||||
|
|
||||||
close () {
|
close () {
|
||||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
||||||
|
|
||||||
|
remote.getCurrentWebContents().send('list-focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -38,9 +42,15 @@ export function openModal (component, props) {
|
|||||||
|
|
||||||
export function closeModal () {
|
export function closeModal () {
|
||||||
if (modalBase == null) { return }
|
if (modalBase == null) { return }
|
||||||
modalBase.setState({component: null, componentProps: null, isHidden: true})
|
modalBase.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isModalOpen () {
|
export function isModalOpen () {
|
||||||
return !modalBase.state.isHidden
|
return !modalBase.state.isHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
open: openModal,
|
||||||
|
close: closeModal,
|
||||||
|
isOpen: isModalOpen
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ const modes = [
|
|||||||
{
|
{
|
||||||
name: 'csharp',
|
name: 'csharp',
|
||||||
label: 'C#',
|
label: 'C#',
|
||||||
alias: ['cs'],
|
alias: ['cs', 'c#'],
|
||||||
mode: 'csharp'
|
mode: 'csharp'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -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)
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import React, { PropTypes} from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { CREATE_MODE, EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions'
|
|
||||||
// import UserNavigator from './HomePage/UserNavigator'
|
|
||||||
import ArticleNavigator from './HomePage/ArticleNavigator'
|
|
||||||
import ArticleTopBar from './HomePage/ArticleTopBar'
|
|
||||||
import ArticleList from './HomePage/ArticleList'
|
|
||||||
import ArticleDetail from './HomePage/ArticleDetail'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import keygen from 'boost/keygen'
|
|
||||||
import { isModalOpen, closeModal } from 'boost/modal'
|
|
||||||
|
|
||||||
const TEXT_FILTER = 'TEXT_FILTER'
|
|
||||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
|
||||||
const TAG_FILTER = 'TAG_FILTER'
|
|
||||||
|
|
||||||
class HomePage extends React.Component {
|
|
||||||
componentDidMount () {
|
|
||||||
// React自体のKey入力はfocusされていないElementからは動かないため、
|
|
||||||
// `window`に直接かける
|
|
||||||
this.keyHandler = e => this.handleKeyDown(e)
|
|
||||||
window.addEventListener('keydown', this.keyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('keydown', this.keyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown (e) {
|
|
||||||
if (isModalOpen()) {
|
|
||||||
if (e.keyCode === 27) closeModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let { status, dispatch } = this.props
|
|
||||||
let { nav, top, list, detail } = this.refs
|
|
||||||
|
|
||||||
if (status.isTutorialOpen) {
|
|
||||||
dispatch(toggleTutorial())
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search inputがfocusされていたら大体のキー入力は無視される。
|
|
||||||
if (top.isInputFocused() && !e.metaKey) {
|
|
||||||
if (e.keyCode === 13 || e.keyCode === 27) top.escape()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status.mode) {
|
|
||||||
case CREATE_MODE:
|
|
||||||
case EDIT_MODE:
|
|
||||||
if (e.keyCode === 27) {
|
|
||||||
detail.handleCancelButtonClick()
|
|
||||||
}
|
|
||||||
if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) {
|
|
||||||
detail.handleSaveButtonClick()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case IDLE_MODE:
|
|
||||||
if (e.keyCode === 69) {
|
|
||||||
detail.handleEditButtonClick()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 68) {
|
|
||||||
detail.handleDeleteButtonClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
// `detail`の`openDeleteConfirmMenu`の時。
|
|
||||||
if (detail.state.openDeleteConfirmMenu) {
|
|
||||||
if (e.keyCode === 27) {
|
|
||||||
detail.handleDeleteCancelButtonClick()
|
|
||||||
}
|
|
||||||
if (e.keyCode === 13 && e.metaKey) {
|
|
||||||
detail.handleDeleteConfirmButtonClick()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
|
|
||||||
if (e.keyCode === 27 || (e.keyCode === 70 && e.metaKey)) {
|
|
||||||
top.focusInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 38) {
|
|
||||||
list.selectPriorArticle()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 40) {
|
|
||||||
list.selectNextArticle()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === 65 || e.keyCode === 13 && e.metaKey) {
|
|
||||||
nav.handleNewPostButtonClick()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let { dispatch, status, articles, activeArticle, folders, filters } = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='HomePage'>
|
|
||||||
<ArticleNavigator
|
|
||||||
ref='nav'
|
|
||||||
dispatch={dispatch}
|
|
||||||
folders={folders}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
<ArticleTopBar
|
|
||||||
ref='top'
|
|
||||||
dispatch={dispatch}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
<ArticleList
|
|
||||||
ref='list'
|
|
||||||
dispatch={dispatch}
|
|
||||||
folders={folders}
|
|
||||||
articles={articles}
|
|
||||||
status={status}
|
|
||||||
activeArticle={activeArticle}
|
|
||||||
/>
|
|
||||||
<ArticleDetail
|
|
||||||
ref='detail'
|
|
||||||
dispatch={dispatch}
|
|
||||||
activeArticle={activeArticle}
|
|
||||||
folders={folders}
|
|
||||||
status={status}
|
|
||||||
filters={filters}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function remap (state) {
|
|
||||||
let { folders, articles, status } = state
|
|
||||||
|
|
||||||
if (articles == null) articles = []
|
|
||||||
articles.sort((a, b) => {
|
|
||||||
return new Date(b.updatedAt) - new Date(a.updatedAt)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Filter articles
|
|
||||||
let filters = status.search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => {
|
|
||||||
if (key.match(/^in:.+$/)) {
|
|
||||||
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]}
|
|
||||||
}
|
|
||||||
if (key.match(/^#(.+)/)) {
|
|
||||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
|
||||||
}
|
|
||||||
return {type: TEXT_FILTER, value: key}
|
|
||||||
})
|
|
||||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
|
||||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
|
||||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
|
||||||
|
|
||||||
if (folders != null) {
|
|
||||||
let targetFolders = folders.filter(folder => {
|
|
||||||
return _.findWhere(folderFilters, {value: folder.name})
|
|
||||||
})
|
|
||||||
status.targetFolders = targetFolders
|
|
||||||
|
|
||||||
if (targetFolders.length > 0) {
|
|
||||||
articles = articles.filter(article => {
|
|
||||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textFilters.length > 0) {
|
|
||||||
articles = textFilters.reduce((articles, textFilter) => {
|
|
||||||
return articles.filter(article => {
|
|
||||||
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
|
|
||||||
})
|
|
||||||
}, articles)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagFilters.length > 0) {
|
|
||||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
|
||||||
return articles.filter(article => {
|
|
||||||
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i')))
|
|
||||||
})
|
|
||||||
}, articles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab active article
|
|
||||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
|
||||||
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 {
|
|
||||||
folders,
|
|
||||||
status,
|
|
||||||
articles,
|
|
||||||
activeArticle,
|
|
||||||
filters: {
|
|
||||||
folder: folderFilters,
|
|
||||||
tag: tagFilters,
|
|
||||||
text: textFilters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomePage.propTypes = {
|
|
||||||
params: PropTypes.shape({
|
|
||||||
userId: PropTypes.string
|
|
||||||
}),
|
|
||||||
status: PropTypes.shape({
|
|
||||||
userId: PropTypes.string
|
|
||||||
}),
|
|
||||||
articles: PropTypes.array,
|
|
||||||
activeArticle: PropTypes.shape(),
|
|
||||||
dispatch: PropTypes.func,
|
|
||||||
folders: PropTypes.array,
|
|
||||||
filters: PropTypes.shape({
|
|
||||||
folder: PropTypes.array,
|
|
||||||
tag: PropTypes.array,
|
|
||||||
text: PropTypes.array
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(remap)(HomePage)
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import moment from 'moment'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import ModeIcon from 'boost/components/ModeIcon'
|
|
||||||
import MarkdownPreview from 'boost/components/MarkdownPreview'
|
|
||||||
import CodeEditor from 'boost/components/CodeEditor'
|
|
||||||
import { IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode, switchArticle, switchFolder, clearSearch, updateArticle, destroyArticle, NEW } from 'boost/actions'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import FolderMark from 'boost/components/FolderMark'
|
|
||||||
import TagLink from 'boost/components/TagLink'
|
|
||||||
import TagSelect from 'boost/components/TagSelect'
|
|
||||||
import ModeSelect from 'boost/components/ModeSelect'
|
|
||||||
import activityRecord from 'boost/activityRecord'
|
|
||||||
|
|
||||||
const BRAND_COLOR = '#18AF90'
|
|
||||||
|
|
||||||
const editDeleteTutorialElement = (
|
|
||||||
<svg width='300' height='500' className='tutorial'>
|
|
||||||
<text x='50' y='220' fill={BRAND_COLOR} fontSize='24'>Edit / Delete a post</text>
|
|
||||||
<text x='90' y='245' fill={BRAND_COLOR} fontSize='18'>press `e`/`d`</text>
|
|
||||||
|
|
||||||
<svg x='150' y='35'>
|
|
||||||
<path fill='white' d='M87.5,93.6c-16.3-5.7-30.6-16.7-39.9-31.4c-5.5-8.7-9-19.1-3.4-28.7c4.8-8.2,13.6-12.8,22.4-15.3
|
|
||||||
c15.7-4.5,34.4-6.2,49.7,0.4c17.3,7.4,25.6,26.3,25.7,44.4c0.1,10.4-3.4,20.9-13.1,26c-8.6,4.5-19,4.1-28.4,3.7
|
|
||||||
c-1.9-0.1-1.9,2.9,0,3c9.3,0.4,19.2,0.6,27.9-3.2c8.5-3.7,13.8-11.2,15.7-20.2c3.6-17.9-2.9-40.2-17.7-51.4
|
|
||||||
C110.8,9.1,89,9.9,70.8,14c-17.9,4-37.4,16.8-31.3,37.9C45.6,73,66.7,89.5,86.7,96.5C88.6,97.1,89.4,94.2,87.5,93.6L87.5,93.6z'/>
|
|
||||||
<path fill='white' d='M11.9,89.7c14.8-3.4,29.7-6,44.8-7.9c-0.5-0.6-1-1.3-1.4-1.9c-2.6,6.3-2.8,12.7-0.7,19.2
|
|
||||||
c0.6,1.8,3.5,1,2.9-0.8c-1.9-6-1.7-11.8,0.7-17.6c0.3-0.8-0.5-2-1.4-1.9c-15.3,1.9-30.6,4.5-45.6,8C9.3,87.3,10.1,90.2,11.9,89.7
|
|
||||||
L11.9,89.7z'/>
|
|
||||||
<path fill='white' d='M48.6,81.5c-9.4,10.4-17,22.3-22.2,35.3c-5.5,13.6-9.3,28.9-6,43.4c0.4,1.9,3.3,1.1,2.9-0.8
|
|
||||||
c-3.2-14,0.7-28.8,6-41.8c5.1-12.5,12.4-24,21.5-34C52,82.2,49.9,80,48.6,81.5L48.6,81.5z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const tagSelectTutorialElement = (
|
|
||||||
<svg width='500' height='500' className='tutorial'>
|
|
||||||
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
|
|
||||||
|
|
||||||
<svg x='0' y='-15'>
|
|
||||||
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
|
|
||||||
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
|
|
||||||
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
|
|
||||||
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
|
|
||||||
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
|
|
||||||
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
|
|
||||||
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
|
|
||||||
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
|
|
||||||
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const modeSelectTutorialElement = (
|
|
||||||
<svg width='500' height='500' className='tutorial'>
|
|
||||||
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
|
|
||||||
|
|
||||||
<svg x='300' y='0'>
|
|
||||||
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
|
|
||||||
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
|
|
||||||
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
|
|
||||||
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
|
|
||||||
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
|
|
||||||
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
|
|
||||||
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
|
|
||||||
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
|
|
||||||
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
|
|
||||||
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
|
|
||||||
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
function makeInstantArticle (article) {
|
|
||||||
return Object.assign({}, article)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ArticleDetail extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
article: makeInstantArticle(props.activeArticle),
|
|
||||||
previewMode: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
clearInterval(this.refreshTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
|
||||||
let isModeChanged = prevProps.status.mode !== this.props.status.mode
|
|
||||||
if (isModeChanged && this.props.status.mode !== IDLE_MODE) {
|
|
||||||
ReactDOM.findDOMNode(this.refs.title).focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
let nextState = {}
|
|
||||||
|
|
||||||
let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key)
|
|
||||||
let isModeChanged = nextProps.status.mode !== this.props.status.mode
|
|
||||||
// Reset article input
|
|
||||||
if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) {
|
|
||||||
Object.assign(nextState, {
|
|
||||||
article: makeInstantArticle(nextProps.activeArticle)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean state
|
|
||||||
if (isModeChanged) {
|
|
||||||
Object.assign(nextState, {
|
|
||||||
openDeleteConfirmMenu: false,
|
|
||||||
previewMode: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(nextState)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEmpty () {
|
|
||||||
return (
|
|
||||||
<div className='ArticleDetail empty'>
|
|
||||||
Command(⌘) + Enter to create a new post
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEditButtonClick (e) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
dispatch(switchMode(EDIT_MODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteButtonClick (e) {
|
|
||||||
this.setState({openDeleteConfirmMenu: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteConfirmButtonClick (e) {
|
|
||||||
let { dispatch, activeArticle } = this.props
|
|
||||||
|
|
||||||
dispatch(destroyArticle(activeArticle.key))
|
|
||||||
activityRecord.emit('ARTICLE_DESTROY')
|
|
||||||
this.setState({openDeleteConfirmMenu: false})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteCancelButtonClick (e) {
|
|
||||||
this.setState({openDeleteConfirmMenu: false})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIdle () {
|
|
||||||
let { status, activeArticle, folders } = this.props
|
|
||||||
|
|
||||||
let tags = activeArticle.tags != null ? activeArticle.tags.length > 0
|
|
||||||
? activeArticle.tags.map(tag => {
|
|
||||||
return (<TagLink key={tag} tag={tag}/>)
|
|
||||||
})
|
|
||||||
: (
|
|
||||||
<span className='noTags'>Not tagged yet</span>
|
|
||||||
) : null
|
|
||||||
let folder = _.findWhere(folders, {key: activeArticle.FolderKey})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='ArticleDetail idle'>
|
|
||||||
{this.state.openDeleteConfirmMenu
|
|
||||||
? (
|
|
||||||
<div className='deleteConfirm'>
|
|
||||||
<div className='right'>
|
|
||||||
Are you sure to delete this article?
|
|
||||||
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>
|
|
||||||
<i className='fa fa-fw fa-check'/> Sure
|
|
||||||
</button>
|
|
||||||
<button onClick={e => this.handleDeleteCancelButtonClick(e)}>
|
|
||||||
<i className='fa fa-fw fa-times'/> Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className='detailInfo'>
|
|
||||||
<div className='left'>
|
|
||||||
<div className='info'>
|
|
||||||
<FolderMark color={folder.color}/> <span className='folderName'>{folder.name}</span>
|
|
||||||
Created : {moment(activeArticle.createdAt).format('YYYY/MM/DD')}
|
|
||||||
Updated : {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
|
|
||||||
</div>
|
|
||||||
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
|
|
||||||
</div>
|
|
||||||
<div className='right'>
|
|
||||||
<button onClick={e => this.handleEditButtonClick(e)} className='editBtn'>
|
|
||||||
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Edit (e)</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={e => this.handleDeleteButtonClick(e)} className='deleteBtn'>
|
|
||||||
<i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete (d)</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status.isTutorialOpen ? editDeleteTutorialElement : null}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='detailBody'>
|
|
||||||
<div className='detailPanel'>
|
|
||||||
<div className='header'>
|
|
||||||
<ModeIcon className='mode' mode={activeArticle.mode}/>
|
|
||||||
<div className='title'>{activeArticle.title}</div>
|
|
||||||
</div>
|
|
||||||
{activeArticle.mode === 'markdown'
|
|
||||||
? <MarkdownPreview content={activeArticle.content}/>
|
|
||||||
: <CodeEditor readOnly onChange={(e, value) => this.handleContentChange(e, value)} mode={activeArticle.mode} code={activeArticle.content}/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelButtonClick (e) {
|
|
||||||
let { activeArticle, dispatch } = this.props
|
|
||||||
if (activeArticle.status === NEW) dispatch(switchArticle(null))
|
|
||||||
dispatch(switchMode(IDLE_MODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveButtonClick (e) {
|
|
||||||
let { dispatch, folders, filters } = this.props
|
|
||||||
let article = this.state.article
|
|
||||||
let newArticle = Object.assign({}, article)
|
|
||||||
|
|
||||||
let folder = _.findWhere(folders, {key: article.FolderKey})
|
|
||||||
if (folder == null) return false
|
|
||||||
|
|
||||||
delete newArticle.status
|
|
||||||
newArticle.updatedAt = new Date()
|
|
||||||
if (newArticle.createdAt == null) {
|
|
||||||
newArticle.createdAt = new Date()
|
|
||||||
activityRecord.emit('ARTICLE_CREATE')
|
|
||||||
} else {
|
|
||||||
activityRecord.emit('ARTICLE_UPDATE')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(updateArticle(newArticle))
|
|
||||||
dispatch(switchMode(IDLE_MODE))
|
|
||||||
// Folder filterがかかっている時に、
|
|
||||||
// Searchを初期化し、更新先のFolder filterをかける
|
|
||||||
// かかれていない時に
|
|
||||||
// Searchを初期化する
|
|
||||||
if (filters.folder.length !== 0) dispatch(switchFolder(folder.name))
|
|
||||||
else dispatch(clearSearch())
|
|
||||||
dispatch(switchArticle(newArticle.key))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFolderKeyChange (e) {
|
|
||||||
let article = this.state.article
|
|
||||||
article.FolderKey = e.target.value
|
|
||||||
|
|
||||||
this.setState({article: article})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTagsChange (newTag, tags) {
|
|
||||||
let article = this.state.article
|
|
||||||
article.tags = tags
|
|
||||||
|
|
||||||
this.setState({article: article})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModeChange (value) {
|
|
||||||
let article = this.state.article
|
|
||||||
article.mode = value
|
|
||||||
this.setState({
|
|
||||||
article: article,
|
|
||||||
previewMode: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModeSelectBlur () {
|
|
||||||
if (this.refs.code != null) {
|
|
||||||
this.refs.code.editor.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange (e, value) {
|
|
||||||
let article = this.state.article
|
|
||||||
article.content = value
|
|
||||||
this.setState({article: article})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTogglePreviewButtonClick (e) {
|
|
||||||
this.setState({previewMode: !this.state.previewMode})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTitleKeyDown (e) {
|
|
||||||
if (e.keyCode === 9 && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.refs.mode.handleIdleSelectClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEdit () {
|
|
||||||
let { folders, status } = this.props
|
|
||||||
|
|
||||||
let folderOptions = folders.map(folder => {
|
|
||||||
return (
|
|
||||||
<option key={folder.key} value={folder.key}>{folder.name}</option>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='ArticleDetail edit'>
|
|
||||||
<div className='detailInfo'>
|
|
||||||
<div className='left'>
|
|
||||||
<select
|
|
||||||
className='folder'
|
|
||||||
value={this.state.article.FolderKey}
|
|
||||||
onChange={e => this.handleFolderKeyChange(e)}
|
|
||||||
>
|
|
||||||
{folderOptions}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<TagSelect
|
|
||||||
tags={this.state.article.tags}
|
|
||||||
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{status.isTutorialOpen ? tagSelectTutorialElement : null}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='right'>
|
|
||||||
{
|
|
||||||
this.state.article.mode === 'markdown'
|
|
||||||
? (<button className='preview' onClick={e => this.handleTogglePreviewButtonClick(e)}>{!this.state.previewMode ? 'Preview' : 'Edit'}</button>)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
|
||||||
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='detailBody'>
|
|
||||||
<div className='detailPanel'>
|
|
||||||
<div className='header'>
|
|
||||||
<div className='title'>
|
|
||||||
<input onKeyDown={e => this.handleTitleKeyDown(e)} placeholder='Title' ref='title' valueLink={this.linkState('article.title')}/>
|
|
||||||
</div>
|
|
||||||
<ModeSelect
|
|
||||||
ref='mode'
|
|
||||||
onChange={e => this.handleModeChange(e)}
|
|
||||||
value={this.state.article.mode}
|
|
||||||
className='mode'
|
|
||||||
onBlur={() => this.handleModeSelectBlur()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{status.isTutorialOpen ? modeSelectTutorialElement : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.previewMode
|
|
||||||
? <MarkdownPreview content={this.state.article.content}/>
|
|
||||||
: (<CodeEditor
|
|
||||||
ref='code'
|
|
||||||
onChange={(e, value) => this.handleContentChange(e, value)}
|
|
||||||
readOnly={false}
|
|
||||||
mode={this.state.article.mode}
|
|
||||||
code={this.state.article.content}
|
|
||||||
/>)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let { status, activeArticle } = this.props
|
|
||||||
|
|
||||||
if (activeArticle == null) return this.renderEmpty()
|
|
||||||
|
|
||||||
switch (status.mode) {
|
|
||||||
case CREATE_MODE:
|
|
||||||
case EDIT_MODE:
|
|
||||||
return this.renderEdit()
|
|
||||||
case IDLE_MODE:
|
|
||||||
default:
|
|
||||||
return this.renderIdle()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleDetail.propTypes = {
|
|
||||||
status: PropTypes.shape(),
|
|
||||||
activeArticle: PropTypes.shape(),
|
|
||||||
activeUser: PropTypes.shape()
|
|
||||||
}
|
|
||||||
ArticleDetail.prototype.linkState = linkState
|
|
||||||
225
browser/main/HomePage/ArticleDetail/ArticleEditor.js
Normal file
225
browser/main/HomePage/ArticleDetail/ArticleEditor.js
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
import fetchConfig from 'browser/lib/fetchConfig'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
export const PREVIEW_MODE = 'PREVIEW_MODE'
|
||||||
|
export const EDIT_MODE = 'EDIT_MODE'
|
||||||
|
|
||||||
|
let config = fetchConfig()
|
||||||
|
ipc.on('config-apply', function (e, newConfig) {
|
||||||
|
config = newConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class ArticleEditor extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||||
|
this.isMouseDown = false
|
||||||
|
this.state = {
|
||||||
|
status: PREVIEW_MODE,
|
||||||
|
cursorPosition: null,
|
||||||
|
firstVisibleRow: null,
|
||||||
|
switchPreview: config['switch-preview'],
|
||||||
|
isTemporary: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
ipc.on('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.article.key !== this.props.article.key) {
|
||||||
|
this.setState({
|
||||||
|
content: this.props.article.content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigApply (e, newConfig) {
|
||||||
|
this.setState({
|
||||||
|
switchPreview: newConfig['switch-preview']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCursorPosition () {
|
||||||
|
this.setState({
|
||||||
|
cursorPosition: null,
|
||||||
|
firstVisibleRow: null
|
||||||
|
}, function () {
|
||||||
|
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
|
||||||
|
if (previewEl) previewEl.scrollTop = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchPreviewMode (isTemporary = false) {
|
||||||
|
if (this.props.article.mode !== 'markdown') return true
|
||||||
|
let cursorPosition = this.refs.editor.getCursorPosition()
|
||||||
|
let firstVisibleRow = this.refs.editor.getFirstVisibleRow()
|
||||||
|
this.setState({
|
||||||
|
status: PREVIEW_MODE,
|
||||||
|
cursorPosition,
|
||||||
|
firstVisibleRow,
|
||||||
|
isTemporary: isTemporary
|
||||||
|
}, function () {
|
||||||
|
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
|
||||||
|
let anchors = previewEl.querySelectorAll('.lineAnchor')
|
||||||
|
for (let i = 0; i < anchors.length; i++) {
|
||||||
|
if (parseInt(anchors[i].dataset.key, 10) > cursorPosition.row || i === anchors.length - 1) {
|
||||||
|
var targetAnchor = anchors[i > 0 ? i - 1 : 0]
|
||||||
|
previewEl.scrollTop = targetAnchor.offsetTop - 100
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchEditMode (isTemporary = false) {
|
||||||
|
this.setState({
|
||||||
|
status: EDIT_MODE,
|
||||||
|
isTemporary: false
|
||||||
|
}, function () {
|
||||||
|
if (this.state.cursorPosition != null) {
|
||||||
|
this.refs.editor.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column)
|
||||||
|
this.refs.editor.scrollToLine(this.state.firstVisibleRow)
|
||||||
|
}
|
||||||
|
this.refs.editor.editor.focus()
|
||||||
|
|
||||||
|
if (!isTemporary) activityRecord.emit('ARTICLE_UPDATE', this.props.article)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlurCodeEditor (e) {
|
||||||
|
let isFocusingToThis = e.relatedTarget === ReactDOM.findDOMNode(this)
|
||||||
|
if (isFocusingToThis || this.state.switchPreview !== 'blur') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { article } = this.props
|
||||||
|
if (article.mode === 'markdown') {
|
||||||
|
this.switchPreviewMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCodeEditorChange (value) {
|
||||||
|
this.props.onChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRightClick (e) {
|
||||||
|
let { article } = this.props
|
||||||
|
if (this.state.switchPreview === 'rightclick' && article.mode === 'markdown') {
|
||||||
|
if (this.state.status === EDIT_MODE) this.switchPreviewMode()
|
||||||
|
else this.switchEditMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp (e) {
|
||||||
|
switch (this.state.switchPreview) {
|
||||||
|
case 'blur':
|
||||||
|
switch (e.button) {
|
||||||
|
case 0:
|
||||||
|
this.isMouseDown = false
|
||||||
|
this.moveCount = 0
|
||||||
|
if (!this.isDrag) {
|
||||||
|
this.switchEditMode()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
if (this.state.isTemporary) this.switchEditMode(true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'rightclick':
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove (e) {
|
||||||
|
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
|
||||||
|
this.moveCount++
|
||||||
|
if (this.moveCount > 5) {
|
||||||
|
this.isDrag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDowm (e) {
|
||||||
|
switch (this.state.switchPreview) {
|
||||||
|
case 'blur':
|
||||||
|
switch (e.button) {
|
||||||
|
case 0:
|
||||||
|
this.isDrag = false
|
||||||
|
this.isMouseDown = true
|
||||||
|
this.moveCount = 0
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
if (this.state.status === EDIT_MODE && this.props.article.mode === 'markdown') {
|
||||||
|
this.switchPreviewMode(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'rightclick':
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { article } = this.props
|
||||||
|
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex='5'
|
||||||
|
onContextMenu={e => this.handleRightClick(e)}
|
||||||
|
onMouseUp={e => this.handleMouseUp(e)}
|
||||||
|
onMouseMove={e => this.handleMouseMove(e)}
|
||||||
|
onMouseDown={e => this.handleMouseDowm(e)}
|
||||||
|
className='ArticleEditor'
|
||||||
|
>
|
||||||
|
{showPreview
|
||||||
|
? <MarkdownPreview
|
||||||
|
ref='preview'
|
||||||
|
content={article.content}
|
||||||
|
/>
|
||||||
|
: <CodeEditor
|
||||||
|
ref='editor'
|
||||||
|
onBlur={e => this.handleBlurCodeEditor(e)}
|
||||||
|
onChange={value => this.handleCodeEditorChange(value)}
|
||||||
|
article={article}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{article.mode === 'markdown'
|
||||||
|
? <div className='ArticleDetail-panel-content-tooltip' children={
|
||||||
|
showPreview
|
||||||
|
? this.state.switchPreview === 'blur'
|
||||||
|
? 'Click to Edit'
|
||||||
|
: 'Right Click to Edit'
|
||||||
|
: this.state.switchPreview === 'blur'
|
||||||
|
? 'Press ESC to Watch Preview'
|
||||||
|
: 'Right Click to Watch Preview'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticleEditor.propTypes = {
|
||||||
|
article: PropTypes.shape({
|
||||||
|
content: PropTypes.string,
|
||||||
|
key: PropTypes.string,
|
||||||
|
mode: PropTypes.string
|
||||||
|
}),
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
parent: PropTypes.object
|
||||||
|
}
|
||||||
168
browser/main/HomePage/ArticleDetail/ShareButton.js
Normal file
168
browser/main/HomePage/ArticleDetail/ShareButton.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import api from 'browser/lib/api'
|
||||||
|
import clientKey from 'browser/lib/clientKey'
|
||||||
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
const clipboard = require('electron').clipboard
|
||||||
|
|
||||||
|
function notify (...args) {
|
||||||
|
return new window.Notification(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefault () {
|
||||||
|
return {
|
||||||
|
openDropdown: false,
|
||||||
|
isSharing: false,
|
||||||
|
// Fetched url
|
||||||
|
url: null,
|
||||||
|
// for tooltip Copy -> Copied!
|
||||||
|
copied: false,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ShareButton extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.state = getDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
this.setState(getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.dropdownInterceptor = e => {
|
||||||
|
this.dropdownClicked = true
|
||||||
|
}
|
||||||
|
ReactDOM.findDOMNode(this.refs.dropdown).addEventListener('click', this.dropdownInterceptor)
|
||||||
|
this.shareViaPublicURLHandler = e => {
|
||||||
|
this.handleShareViaPublicURLClick(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
ReactDOM.findDOMNode(this.refs.dropdown).removeEventListener('click', this.dropdownInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenButtonClick (e) {
|
||||||
|
this.openDropdown()
|
||||||
|
if (this.dropdownHandler == null) {
|
||||||
|
this.dropdownHandler = e => {
|
||||||
|
if (!this.dropdownClicked) {
|
||||||
|
this.closeDropdown()
|
||||||
|
} else {
|
||||||
|
this.dropdownClicked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
document.addEventListener('click', this.dropdownHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
openDropdown () {
|
||||||
|
this.setState({openDropdown: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown () {
|
||||||
|
document.removeEventListener('click', this.dropdownHandler)
|
||||||
|
this.setState({openDropdown: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClipboardButtonClick (e) {
|
||||||
|
activityRecord.emit('MAIN_DETAIL_COPY')
|
||||||
|
clipboard.writeText(this.props.article.content)
|
||||||
|
notify('Saved to Clipboard!', {
|
||||||
|
body: 'Paste it wherever you want!'
|
||||||
|
})
|
||||||
|
this.setState({openDropdown: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShareViaPublicURLClick (e) {
|
||||||
|
let { user } = this.props
|
||||||
|
let input = Object.assign({}, this.props.article, {
|
||||||
|
clientKey: clientKey.get(),
|
||||||
|
writerName: user.name
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
isSharing: true,
|
||||||
|
failed: false
|
||||||
|
}, () => {
|
||||||
|
api.shareViaPublicURL(input)
|
||||||
|
.then(res => {
|
||||||
|
let url = res.body.url
|
||||||
|
this.setState({url: url, isSharing: false})
|
||||||
|
activityRecord.emit('ARTICLE_SHARE')
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
|
this.setState({isSharing: false, failed: true})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCopyURLClick () {
|
||||||
|
clipboard.writeText(this.state.url)
|
||||||
|
this.setState({copied: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore copy url tooltip
|
||||||
|
handleCopyURLMouseLeave () {
|
||||||
|
this.setState({copied: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let hasPublicURL = this.state.url != null
|
||||||
|
return (
|
||||||
|
<div className='ShareButton'>
|
||||||
|
<button ref='openButton' onClick={e => this.handleOpenButtonClick(e)} className='ShareButton-open-button'>
|
||||||
|
<i className='fa fa-fw fa-share-alt'/>
|
||||||
|
{
|
||||||
|
this.state.openDropdown ? null : (
|
||||||
|
<span className='tooltip'>Share</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div ref='dropdown' className={'ShareButton-dropdown' + (this.state.openDropdown ? '' : ' hide')}>
|
||||||
|
{
|
||||||
|
!hasPublicURL ? (
|
||||||
|
<button
|
||||||
|
onClick={e => this.shareViaPublicURLHandler(e)}
|
||||||
|
ref='sharePublicURL'
|
||||||
|
disabled={this.state.isSharing}>
|
||||||
|
<i className='fa fa-fw fa-external-link'/> {this.state.failed ? 'Failed : Click to Try again' : !this.state.isSharing ? 'Share via public URL' : 'Sharing...'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className='ShareButton-url'>
|
||||||
|
<input className='ShareButton-url-input' value={this.state.url} readOnly/>
|
||||||
|
<button
|
||||||
|
onClick={e => this.handleCopyURLClick(e)}
|
||||||
|
className='ShareButton-url-button'
|
||||||
|
onMouseLeave={e => this.handleCopyURLMouseLeave(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-fw fa-clipboard'/>
|
||||||
|
<div className='ShareButton-url-button-tooltip'>{this.state.copied ? 'Copied!' : 'Copy URL'}</div>
|
||||||
|
</button>
|
||||||
|
<div className='ShareButton-url-alert'>This url is valid for 7 days.</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<button onClick={e => this.handleClipboardButtonClick(e)}>
|
||||||
|
<i className='fa fa-fw fa-clipboard'/> Copy to clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareButton.propTypes = {
|
||||||
|
article: PropTypes.shape({
|
||||||
|
publicURL: PropTypes.string,
|
||||||
|
content: PropTypes.string
|
||||||
|
}),
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
})
|
||||||
|
}
|
||||||
361
browser/main/HomePage/ArticleDetail/index.js
Normal file
361
browser/main/HomePage/ArticleDetail/index.js
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import moment from 'moment'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {
|
||||||
|
switchFolder,
|
||||||
|
updateArticle
|
||||||
|
} from '../../actions'
|
||||||
|
import linkState from 'browser/lib/linkState'
|
||||||
|
import TagSelect from 'browser/components/TagSelect'
|
||||||
|
import ModeSelect from 'browser/components/ModeSelect'
|
||||||
|
import ShareButton from './ShareButton'
|
||||||
|
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||||
|
import DeleteArticleModal from '../../modal/DeleteArticleModal'
|
||||||
|
import ArticleEditor from './ArticleEditor'
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
import fetchConfig from 'browser/lib/fetchConfig'
|
||||||
|
|
||||||
|
let config = fetchConfig()
|
||||||
|
ipc.on('config-apply', function (e, newConfig) {
|
||||||
|
config = newConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
const BRAND_COLOR = '#18AF90'
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const tagSelectTutorialElement = (
|
||||||
|
<svg width='500' height='500' className='tutorial'>
|
||||||
|
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
|
||||||
|
|
||||||
|
<svg x='0' y='-15'>
|
||||||
|
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
|
||||||
|
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
|
||||||
|
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
|
||||||
|
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
|
||||||
|
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
|
||||||
|
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
|
||||||
|
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
|
||||||
|
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
|
||||||
|
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const modeSelectTutorialElement = (
|
||||||
|
<svg width='500' height='500' className='tutorial'>
|
||||||
|
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
|
||||||
|
|
||||||
|
<svg x='300' y='0'>
|
||||||
|
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
|
||||||
|
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
|
||||||
|
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
|
||||||
|
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
|
||||||
|
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
|
||||||
|
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
|
||||||
|
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
|
||||||
|
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
|
||||||
|
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
|
||||||
|
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
|
||||||
|
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default class ArticleDetail extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.deleteHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleDeleteButtonClick()
|
||||||
|
}
|
||||||
|
this.uncacheHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleUncache()
|
||||||
|
}
|
||||||
|
this.titleHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
if (this.refs.title) {
|
||||||
|
this.focusTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
if (this.refs.editor) this.refs.editor.switchEditMode()
|
||||||
|
}
|
||||||
|
this.previewHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
if (this.refs.editor) this.refs.editor.switchPreviewMode()
|
||||||
|
}
|
||||||
|
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
article: Object.assign({content: ''}, props.activeArticle),
|
||||||
|
openShareDropdown: false,
|
||||||
|
fontFamily: config['editor-font-family']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||||
|
this.shareDropdownInterceptor = e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.on('detail-delete', this.deleteHandler)
|
||||||
|
ipc.on('detail-uncache', this.uncacheHandler)
|
||||||
|
ipc.on('detail-title', this.titleHandler)
|
||||||
|
ipc.on('detail-edit', this.editHandler)
|
||||||
|
ipc.on('detail-preview', this.previewHandler)
|
||||||
|
ipc.on('config-apply', this.configApplyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
clearInterval(this.refreshTimer)
|
||||||
|
|
||||||
|
ipc.removeListener('detail-delete', this.deleteHandler)
|
||||||
|
ipc.removeListener('detail-uncache', this.uncacheHandler)
|
||||||
|
ipc.removeListener('detail-title', this.titleHandler)
|
||||||
|
ipc.removeListener('detail-edit', this.editHandler)
|
||||||
|
ipc.removeListener('detail-preview', this.previewHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps, prevState) {
|
||||||
|
if (this.props.activeArticle == null || prevProps.activeArticle == null || this.props.activeArticle.key !== prevProps.activeArticle.key) {
|
||||||
|
if (this.refs.editor) this.refs.editor.resetCursorPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.activeArticle == null && this.props.activeArticle) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigApply (e, config) {
|
||||||
|
this.setState({
|
||||||
|
fontFamily: config['editor-font-family']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEmpty () {
|
||||||
|
return (
|
||||||
|
<div className='ArticleDetail empty'>
|
||||||
|
<div className='ArticleDetail-empty-box'>
|
||||||
|
<div className='ArticleDetail-empty-box-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOthersButtonClick (e) {
|
||||||
|
this.deleteHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderKeyChange (e) {
|
||||||
|
let { dispatch, activeArticle, status, folders } = this.props
|
||||||
|
let article = Object.assign({}, activeArticle, {
|
||||||
|
FolderKey: e.target.value,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateArticle(article))
|
||||||
|
|
||||||
|
let targetFolderKey = e.target.value
|
||||||
|
if (status.targetFolders.length > 0) {
|
||||||
|
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
|
||||||
|
dispatch(switchFolder(targetFolder.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTitleChange (e) {
|
||||||
|
let { dispatch, activeArticle } = this.props
|
||||||
|
let article = Object.assign({}, activeArticle, {
|
||||||
|
title: e.target.value,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
dispatch(updateArticle(article))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTagsChange (newTag, tags) {
|
||||||
|
let { dispatch, activeArticle } = this.props
|
||||||
|
let article = Object.assign({}, activeArticle, {
|
||||||
|
tags: tags,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateArticle(article))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModeChange (value) {
|
||||||
|
let { dispatch, activeArticle } = this.props
|
||||||
|
let article = Object.assign({}, activeArticle, {
|
||||||
|
mode: value,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateArticle(article))
|
||||||
|
this.switchEditMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContentChange (value) {
|
||||||
|
let { dispatch, activeArticle } = this.props
|
||||||
|
if (activeArticle.content !== value) {
|
||||||
|
let article = Object.assign({}, activeArticle, {
|
||||||
|
content: value,
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch(updateArticle(article))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteButtonClick (e) {
|
||||||
|
if (this.props.activeArticle) {
|
||||||
|
openModal(DeleteArticleModal, {articleKey: this.props.activeArticle.key})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTitleKeyDown (e) {
|
||||||
|
if (e.keyCode === 9 && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.refs.mode.handleIdleSelectClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModeSelectKeyDown (e) {
|
||||||
|
if (e.keyCode === 9 && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.switchEditMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 9 && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.focusTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
this.focusTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchEditMode () {
|
||||||
|
this.refs.editor.switchEditMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
focusTitle () {
|
||||||
|
if (this.refs.title) {
|
||||||
|
let titleEl = ReactDOM.findDOMNode(this.refs.title)
|
||||||
|
titleEl.focus()
|
||||||
|
titleEl.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { folders, status, tags, activeArticle, modified, user } = this.props
|
||||||
|
if (activeArticle == null) return this.renderEmpty()
|
||||||
|
let folderOptions = folders.map(folder => {
|
||||||
|
return (
|
||||||
|
<option key={folder.key} value={folder.key}>{folder.name}</option>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex='4' className='ArticleDetail'>
|
||||||
|
<div className='ArticleDetail-info'>
|
||||||
|
<div className='ArticleDetail-info-row'>
|
||||||
|
<select
|
||||||
|
className='ArticleDetail-info-folder'
|
||||||
|
value={activeArticle.FolderKey}
|
||||||
|
onChange={e => this.handleFolderKeyChange(e)}
|
||||||
|
>
|
||||||
|
{folderOptions}
|
||||||
|
</select>
|
||||||
|
<span className='ArticleDetail-info-status'
|
||||||
|
children={
|
||||||
|
isUnsaved
|
||||||
|
? <span> <span className='unsaved-mark'>●</span> Unsaved</span>
|
||||||
|
: `Created : ${moment(activeArticle.createdAt).format('YYYY/MM/DD')} Updated : ${moment(activeArticle.updatedAt).format('YYYY/MM/DD')}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='ArticleDetail-info-control'>
|
||||||
|
{/*<div className={'ArticleDetail-info-control-save' + (!isUnsaved ? ' hide' : '')}>
|
||||||
|
<button
|
||||||
|
onClick={e => this.handleSaveButtonClick(e)}
|
||||||
|
className='ArticleDetail-info-control-save-button'
|
||||||
|
disabled={!isUnsaved}
|
||||||
|
>
|
||||||
|
<i className='fa fa-fw fa-save'/> Save
|
||||||
|
<span className='tooltip' children={`Save Post (${OSX ? '⌘' : '^'} + S)`}/>
|
||||||
|
</button>
|
||||||
|
</div>*/}
|
||||||
|
|
||||||
|
<ShareButton
|
||||||
|
article={activeArticle}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button className='ArticleDetail-info-control-delete-button' onClick={e => this.handleOthersButtonClick(e)}>
|
||||||
|
<i className='fa fa-fw fa-trash'/>
|
||||||
|
<span className='tooltip' children={`Delete Post (^ + Del)`}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ArticleDetail-info-row2'>
|
||||||
|
<TagSelect
|
||||||
|
tags={activeArticle.tags}
|
||||||
|
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
|
||||||
|
suggestTags={tags}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status.isTutorialOpen ? tagSelectTutorialElement : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ArticleDetail-panel'>
|
||||||
|
<div className='ArticleDetail-panel-header'>
|
||||||
|
<div className='ArticleDetail-panel-header-title'>
|
||||||
|
<input
|
||||||
|
onKeyDown={e => this.handleTitleKeyDown(e)}
|
||||||
|
placeholder='(Untitled)'
|
||||||
|
ref='title'
|
||||||
|
value={activeArticle.title}
|
||||||
|
onChange={e => this.handleTitleChange(e)}
|
||||||
|
style={{
|
||||||
|
fontFamily: this.state.fontFamily
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ModeSelect
|
||||||
|
ref='mode'
|
||||||
|
onChange={e => this.handleModeChange(e)}
|
||||||
|
onKeyDown={e => this.handleModeSelectKeyDown(e)}
|
||||||
|
value={activeArticle.mode}
|
||||||
|
className='ArticleDetail-panel-header-mode'
|
||||||
|
/>
|
||||||
|
{status.isTutorialOpen ? modeSelectTutorialElement : null}
|
||||||
|
</div>
|
||||||
|
<ArticleEditor
|
||||||
|
ref='editor'
|
||||||
|
article={activeArticle}
|
||||||
|
onChange={content => this.handleContentChange(content)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticleDetail.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
status: PropTypes.shape(),
|
||||||
|
tags: PropTypes.array,
|
||||||
|
user: PropTypes.shape(),
|
||||||
|
folders: PropTypes.array,
|
||||||
|
modified: PropTypes.array,
|
||||||
|
activeArticle: PropTypes.shape()
|
||||||
|
}
|
||||||
|
ArticleDetail.prototype.linkState = linkState
|
||||||
@@ -1,19 +1,32 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ModeIcon from 'boost/components/ModeIcon'
|
import ModeIcon from 'browser/components/ModeIcon'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { switchArticle, NEW } from 'boost/actions'
|
import { switchArticle } from '../actions'
|
||||||
import FolderMark from 'boost/components/FolderMark'
|
import FolderMark from 'browser/components/FolderMark'
|
||||||
import TagLink from 'boost/components/TagLink'
|
import TagLink from './TagLink'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
export default class ArticleList extends React.Component {
|
export default class ArticleList extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.focusHandler = e => this.focus()
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||||
|
ipc.on('list-focus', this.focusHandler)
|
||||||
|
this.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
clearInterval(this.refreshTimer)
|
clearInterval(this.refreshTimer)
|
||||||
|
ipc.removeListener('list-focus', this.focusHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
@@ -36,6 +49,10 @@ export default class ArticleList extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focus () {
|
||||||
|
ReactDOM.findDOMNode(this).focus()
|
||||||
|
}
|
||||||
|
|
||||||
// 移動ができなかったらfalseを返す:
|
// 移動ができなかったらfalseを返す:
|
||||||
selectPriorArticle () {
|
selectPriorArticle () {
|
||||||
let { articles, activeArticle, dispatch } = this.props
|
let { articles, activeArticle, dispatch } = this.props
|
||||||
@@ -64,36 +81,107 @@ export default class ArticleList extends React.Component {
|
|||||||
handleArticleClick (article) {
|
handleArticleClick (article) {
|
||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
return function (e) {
|
return function (e) {
|
||||||
if (article.status === NEW) return null
|
|
||||||
dispatch(switchArticle(article.key))
|
dispatch(switchArticle(article.key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleArticleListKeyDown (e) {
|
||||||
|
if (e.metaKey || e.ctrlKey) return true
|
||||||
|
|
||||||
|
if (e.keyCode === 65 && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('top-new-post')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 65 && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('nav-new-folder')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 68) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('detail-delete')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 84) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('detail-title')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 69) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('detail-edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 83) {
|
||||||
|
e.preventDefault()
|
||||||
|
remote.getCurrentWebContents().send('detail-save')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 38) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectPriorArticle()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 40) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.selectNextArticle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { articles, activeArticle, folders } = this.props
|
let { articles, modified, activeArticle, folders } = this.props
|
||||||
|
|
||||||
let articleElements = articles.map(article => {
|
let articleElements = articles.map(article => {
|
||||||
|
let modifiedArticle = _.findWhere(modified, {key: article.key})
|
||||||
|
let originalArticle = article
|
||||||
|
if (modifiedArticle) {
|
||||||
|
article = Object.assign({}, article)
|
||||||
|
}
|
||||||
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
|
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
|
||||||
? article.tags.map(tag => {
|
? article.tags.slice().map(tag => {
|
||||||
return (<TagLink key={tag} tag={tag}/>)
|
return (<TagLink key={tag} tag={tag}/>)
|
||||||
})
|
})
|
||||||
: (<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 folderChanged = originalArticle.FolderKey !== article.FolderKey
|
||||||
|
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
|
||||||
|
|
||||||
|
let title = article.title.trim().length === 0
|
||||||
|
? <small>(Untitled)</small>
|
||||||
|
: article.title
|
||||||
|
|
||||||
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={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
|
||||||
<div className='top'>
|
<div className='ArticleList-item-top'>
|
||||||
{folder != null
|
{folder != null
|
||||||
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span>
|
? folderChanged
|
||||||
|
? <span className='folderName'>
|
||||||
|
<FolderMark color={originalFolder.color}/>{originalFolder.name}
|
||||||
|
->
|
||||||
|
<FolderMark color={folder.color}/>{folder.name}
|
||||||
|
</span>
|
||||||
|
: <span className='folderName'>
|
||||||
|
<FolderMark color={folder.color}/>{folder.name}
|
||||||
|
</span>
|
||||||
: <span><FolderMark color={-1}/>Unknown</span>
|
: <span><FolderMark color={-1}/>Unknown</span>
|
||||||
}
|
}
|
||||||
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
|
<span className='updatedAt'
|
||||||
|
children={
|
||||||
|
modifiedArticle != null
|
||||||
|
? <span><span className='unsaved-mark'>●</span> Unsaved</span>
|
||||||
|
: moment(article.updatedAt).fromNow()
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='middle'>
|
<div className='ArticleList-item-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' children={title}/>
|
||||||
</div>
|
</div>
|
||||||
<div className='bottom'>
|
<div className='ArticleList-item-middle2'>
|
||||||
|
<pre><code children={article.content.trim().length === 0 ? '(Empty content)' : article.content.substring(0, 50)}/></pre>
|
||||||
|
</div>
|
||||||
|
<div className='ArticleList-item-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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +191,7 @@ export default class ArticleList extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ArticleList'>
|
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
|
||||||
{articleElements}
|
{articleElements}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -111,8 +199,9 @@ export default class ArticleList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArticleList.propTypes = {
|
ArticleList.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
folders: PropTypes.array,
|
folders: PropTypes.array,
|
||||||
articles: PropTypes.array,
|
articles: PropTypes.array,
|
||||||
activeArticle: PropTypes.shape(),
|
modified: PropTypes.array,
|
||||||
dispatch: PropTypes.func
|
activeArticle: PropTypes.shape()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
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, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
|
||||||
import { openModal } from 'boost/modal'
|
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||||
import FolderMark from 'boost/components/FolderMark'
|
import FolderMark from 'browser/components/FolderMark'
|
||||||
import Preferences from 'boost/components/modal/Preferences'
|
import Preferences from '../modal/Preferences'
|
||||||
import CreateNewFolder from 'boost/components/modal/CreateNewFolder'
|
import CreateNewFolder from '../modal/CreateNewFolder'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import ModeIcon from 'browser/components/ModeIcon'
|
||||||
|
|
||||||
import remote from 'remote'
|
const ipc = require('electron').ipcRenderer
|
||||||
let userName = remote.getGlobal('process').env.USER
|
|
||||||
|
|
||||||
const BRAND_COLOR = '#18AF90'
|
const BRAND_COLOR = '#18AF90'
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
const preferenceTutorialElement = (
|
const preferenceTutorialElement = (
|
||||||
<svg width='300' height='300' className='tutorial'>
|
<svg width='300' height='300' className='tutorial'>
|
||||||
@@ -26,53 +28,44 @@ c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const newPostTutorialElement = (
|
|
||||||
<svg width='900' height='900' className='tutorial'>
|
|
||||||
<text x='290' y='155' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
|
|
||||||
<text x='300' y='180' fill={BRAND_COLOR} fontSize='16'>press `⌘ + Enter` or `a`</text>
|
|
||||||
<svg x='130' y='-20' width='400' height='400'>
|
|
||||||
<path fill='white' d='M56.2,132.5c11.7-2.9,23.9-6,36.1-4.1c8.7,1.4,16.6,5.5,23.7,10.5c13.3,9.4,24.5,21.5,40.2,27
|
|
||||||
c1.8,0.6,2.6-2.3,0.8-2.9c-17.1-6-28.9-20.3-44-29.7c-7-4.4-14.8-7.4-23-8.2c-11.7-1.1-23.3,1.7-34.5,4.5
|
|
||||||
C53.6,130.1,54.4,133,56.2,132.5L56.2,132.5 z'/>
|
|
||||||
</svg>
|
|
||||||
<svg x='130' y='-120' width='400' height='400'>
|
|
||||||
<path fill='white' d='M82.6,218c-7.7,4.5-15.3,9.3-22.7,14.3c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
|
|
||||||
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3
|
|
||||||
C85.7,219.7,84.2,217.1,82.6,218L82.6,218z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const newFolderTutorialElement = (
|
const newFolderTutorialElement = (
|
||||||
<svg width='800' height='500' className='tutorial'>
|
<svg width='800' height='500' className='tutorial'>
|
||||||
<text x='145' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
||||||
<svg x='115' y='-10' width='300' height='400'>
|
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
|
||||||
<path fill='white' d='M36.6,3.7C28.8,8.2,21.3,13,13.9,18c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
|
<svg x='50' y='10' width='300' height='400'>
|
||||||
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3C39.7,5.3,38.2,2.7,36.6,3.7
|
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
|
||||||
L36.6,3.7z'/>
|
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
|
||||||
<path fill='white' d='M16.8,21.5c13.3-6.9,29.5-7,42.6,0.6c5.6,3.2,10.4,7.7,14.1,13c3.8,5.4,10.3,16.2,2.2,20.6
|
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
|
||||||
c-1.2,0.7-2.5,1.2-3.9,1.6c-1.1,0.4-2.3,0.5-3.4,0.5c-1.3-1.4-2.6-2.8-3.9-4.2c-0.2-4.6,7.5-6,10.5-5.8
|
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
|
||||||
c7.4,0.7,13.7,6.2,18.4,11.6c9.4,10.7,14.7,24.3,15.6,38.5c0.1,1.9,3.1,1.9,3,0c-0.9-15.5-6.9-30.4-17.5-41.8
|
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
|
||||||
c-6.8-7.3-25.8-19.1-32.3-4.8c-1.9,4.1,0.3,8.5,4.8,9.4c4.6,0.8,11.6-1.8,14.3-5.7c3.6-5.3-0.1-12.8-2.8-17.6
|
|
||||||
c-3.4-6.1-8.2-11.3-13.8-15.4C50.2,11.6,31,10.9,15.3,19C13.6,19.8,15.1,22.4,16.8,21.5L16.8,21.5z'/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default class ArticleNavigator extends React.Component {
|
export default class ArticleNavigator extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.newFolderHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleNewFolderButton(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
ipc.on('nav-new-folder', this.newFolderHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('nav-new-folder', this.newFolderHandler)
|
||||||
|
}
|
||||||
|
|
||||||
handlePreferencesButtonClick (e) {
|
handlePreferencesButtonClick (e) {
|
||||||
openModal(Preferences)
|
openModal(Preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewPostButtonClick (e) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
|
|
||||||
dispatch(switchMode(CREATE_MODE))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewFolderButton (e) {
|
handleNewFolderButton (e) {
|
||||||
let { activeUser } = this.props
|
let { user } = this.props
|
||||||
openModal(CreateNewFolder, {user: activeUser})
|
openModal(CreateNewFolder, {user: user})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFolderButtonClick (name) {
|
handleFolderButtonClick (name) {
|
||||||
@@ -87,25 +80,70 @@ export default class ArticleNavigator extends React.Component {
|
|||||||
dispatch(setSearchFilter(''))
|
dispatch(setSearchFilter(''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUnsavedItemClick (article) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
return e => {
|
||||||
|
let { articles } = this.props
|
||||||
|
let isInArticleList = articles.some(_article => _article.key === article.key)
|
||||||
|
if (!isInArticleList) dispatch(clearSearch())
|
||||||
|
dispatch(switchArticle(article.key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUncacheButtonClick (article) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
return e => {
|
||||||
|
dispatch(uncacheArticle(article.key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveAllClick (e) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
dispatch(saveAllArticles())
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { status, folders } = this.props
|
let { status, user, folders, allArticles, modified, activeArticle } = this.props
|
||||||
let { targetFolders } = status
|
let { targetFolders } = status
|
||||||
if (targetFolders == null) targetFolders = []
|
if (targetFolders == null) targetFolders = []
|
||||||
|
|
||||||
|
let modifiedElements = modified.map(modifiedArticle => {
|
||||||
|
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
|
||||||
|
if (originalArticle == null) return false
|
||||||
|
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
|
||||||
|
|
||||||
|
let className = 'ArticleNavigator-unsaved-list-item'
|
||||||
|
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
|
||||||
|
<div className='ArticleNavigator-unsaved-list-item-label'>
|
||||||
|
<ModeIcon mode={combinedArticle.mode}/>
|
||||||
|
{combinedArticle.title.trim().length > 0
|
||||||
|
? combinedArticle.title
|
||||||
|
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
|
||||||
|
</div>
|
||||||
|
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
|
||||||
|
let hasModified = modifiedElements.length > 0
|
||||||
|
|
||||||
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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ArticleNavigator'>
|
<div tabIndex='1' className='ArticleNavigator'>
|
||||||
<div className='userInfo'>
|
<div className='userInfo'>
|
||||||
<div className='userProfileName'>{userName}</div>
|
<div className='userProfileName'>{user.name}</div>
|
||||||
<div className='userName'>localStorage</div>
|
<div className='userName'>localStorage</div>
|
||||||
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
|
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
|
||||||
<i className='fa fa-fw fa-chevron-down'/>
|
<i className='fa fa-fw fa-chevron-down'/>
|
||||||
@@ -116,26 +154,26 @@ export default class ArticleNavigator extends React.Component {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='controlSection'>
|
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
|
||||||
<button onClick={e => this.handleNewPostButtonClick(e)} className='newPostBtn'>
|
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
|
||||||
New Post
|
<div className='ArticleNavigator-unsaved-list'>
|
||||||
<span className='tooltip'>Create a new Post (⌘ + Enter or a)</span>
|
{modifiedElements}
|
||||||
</button>
|
</div>
|
||||||
|
<div className='ArticleNavigator-unsaved-control'>
|
||||||
|
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
|
||||||
|
</div>
|
||||||
|
</div>*/}
|
||||||
|
|
||||||
{status.isTutorialOpen ? newPostTutorialElement : null}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='folders'>
|
<div className={'ArticleNavigator-folders expand'}>
|
||||||
<div className='header'>
|
{status.isTutorialOpen ? newFolderTutorialElement : null}
|
||||||
|
<div className='ArticleNavigator-folders-header'>
|
||||||
<div className='title'>Folders</div>
|
<div className='title'>Folders</div>
|
||||||
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
|
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
|
||||||
<i className='fa fa-fw fa-plus'/>
|
<i className='fa fa-fw fa-plus'/>
|
||||||
<span className='tooltip'>Create a new folder</span>
|
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{status.isTutorialOpen ? newFolderTutorialElement : null}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className='folderList'>
|
<div className='folderList'>
|
||||||
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
|
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
|
||||||
@@ -148,11 +186,17 @@ export default class ArticleNavigator extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArticleNavigator.propTypes = {
|
ArticleNavigator.propTypes = {
|
||||||
activeUser: PropTypes.object,
|
dispatch: PropTypes.func,
|
||||||
folders: PropTypes.array,
|
|
||||||
status: PropTypes.shape({
|
status: PropTypes.shape({
|
||||||
folderId: PropTypes.number
|
folderId: PropTypes.number
|
||||||
}),
|
}),
|
||||||
dispatch: PropTypes.func
|
user: PropTypes.object,
|
||||||
|
folders: PropTypes.array,
|
||||||
|
allArticles: PropTypes.array,
|
||||||
|
articles: PropTypes.array,
|
||||||
|
modified: PropTypes.array,
|
||||||
|
activeArticle: PropTypes.shape({
|
||||||
|
key: PropTypes.string
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ExternalLink from 'boost/components/ExternalLink'
|
import ExternalLink from 'browser/components/ExternalLink'
|
||||||
import { setSearchFilter, clearSearch, toggleTutorial } from 'boost/actions'
|
import { setSearchFilter, clearSearch, toggleTutorial, saveArticle, switchFolder } from '../actions'
|
||||||
|
import { isModalOpen } from 'browser/lib/modal'
|
||||||
|
import keygen from 'browser/lib/keygen'
|
||||||
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
const BRAND_COLOR = '#18AF90'
|
const BRAND_COLOR = '#18AF90'
|
||||||
|
|
||||||
const searchTutorialElement = (
|
const searchTutorialElement = (
|
||||||
<svg width='750' height='120' className='tutorial'>
|
<svg width='750' height='300' className='tutorial'>
|
||||||
<text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
|
<text x='125' y='63' 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='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
|
||||||
<text x='450' y='85' fill={BRAND_COLOR} fontSize='18'>
|
<text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
|
||||||
{'- Search by folder : in:{folder_name}\n'}</text>
|
{'- Search by folder : /{folder_name}\n'}</text>
|
||||||
|
<text x='140' y='135' fill={BRAND_COLOR} fontSize='14'>
|
||||||
|
{'exact match : //{folder_name}'}</text>
|
||||||
|
|
||||||
<svg width='500' height='300'>
|
<svg x='90' 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='M27.2,6.9c-1.7,3.5-6,4.8-8,8.2c-1.8,3.1-2.1,6.8-1.8,10.2c0.7,7,4.2,16.7,10.3,20.7c0.5,0.4,1.4,0.2,1.8-0.2
|
||||||
c12.3-5.4,26.4-6.8,39.7-7.7C72.4,9.6,85.7,9.7,99,9.8c55.2,0.3,110.4,2.2,165.5-1.5C291,6.5,317.7,3.8,344.1,7
|
c0.1-0.1,0.2-0.2,0.3-0.3c0.6-0.6,0.6-1.5,0-2.1c-0.2-0.2-0.3-0.4-0.5-0.5c-1.3-1.4-3.2,0.7-1.9,2.1c0.2,0.2-0.3,0.4,0.7,0.5
|
||||||
c12.8,1.6,25.8,4.4,37.5,10c1.2,0.6,2.4,1.1,3.5,1.8c2.4,1.4,3.2,1.5,3.3,4.5c0.1,3.6-2.3,5.9-4.8,8.3c-3.9,3.8-8.6,6.8-13.5,9.2
|
c0-0.7,0-1.4,0-2.1c0,0.1-0.4,0.2-0.5,0.3c0.6-0.1,1.1-0.2,1.7-0.2c-5.7-3.7-9.2-14.5-9-20.9c0.1-4,1.6-6.7,4.8-9.1
|
||||||
c-12.6,6-26.5,7.2-40.3,7.7c-13.7,0.5-27.5,0.6-41.2,1.1c-27.7,0.9-55.3,2.2-82.9,4c-30.8,2-61.6,4.5-92.3,7.6
|
c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
|
||||||
c-15.4,1.5-30.8,3.7-46.3,4.9c-13.6,1.1-30.7,1.5-41.8-7.8c-1.5-1.2-3.6,0.9-2.1,2.1c8.9,7.5,21.4,9.2,32.7,9.2
|
<path fill='white' d='M9.5,24.4c2.4-2.7,4.9-5.4,7.3-8c2.5-2.8,5.7-7.6,9.9-7.8c-0.5-0.5-1-1-1.5-1.5c0.1,6.8,1.9,13.1,5.3,18.9
|
||||||
c15.3,0,30.6-2.6,45.8-4.2c31.3-3.3,62.7-6,94.2-8.1c30.9-2.1,61.8-3.7,92.8-4.7c15.7-0.5,31.4-0.5,47-1.3
|
c1,1.7,3.6,0.2,2.6-1.5c-3.2-5.4-4.8-11.1-4.9-17.4c0-0.8-0.7-1.5-1.5-1.5c-3.6,0.2-5.9,2.1-8.3,4.7c-3.7,3.9-7.3,8-11,12
|
||||||
c13.1-0.7,26.3-2.7,38.1-8.9c4.4-2.3,8.5-5.1,12-8.6c2.8-2.8,7.3-7.3,6.4-11.7c-0.8-4.3-6.4-6.3-9.8-7.9
|
C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
|
||||||
c-5.6-2.6-11.4-4.6-17.3-6.2c-28.3-7.5-58.1-5.6-87-3.6c-62.3,4.4-124.5,2.6-187,2.4c-16.4,0-32.8,0-49,2.4
|
</svg>
|
||||||
C29.9,11,13.4,13.8,5.5,24.6c-7.3,10,0.7,18.4,9.8,22.9c11.9,5.8,26.9,10.4,40,7C57.2,53.9,56.4,51,54.5,51.5L54.5,51.5z'/>
|
</svg>
|
||||||
<path fill='white' d='M446.5,21.4c-9.1-1.6-18.1-3.5-27.4-3.5c-10.2,0-20.4,1.4-30.5,2.8c-1.9,0.3-1.9,3.3,0,3
|
)
|
||||||
c9.5-1.3,19.1-2.6,28.8-2.7c9.6-0.2,18.9,1.7,28.3,3.4C447.6,24.7,448.4,21.8,446.5,21.4L446.5,21.4z'/>
|
|
||||||
|
const newPostTutorialElement = (
|
||||||
|
<svg width='900' height='900' className='tutorial'>
|
||||||
|
<text x='470' y='50' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
|
||||||
|
<text x='490' y='75' fill={BRAND_COLOR} fontSize='16' children={`press \`${OSX ? '⌘' : '^'} + n\``}/>
|
||||||
|
<svg x='415' y='20' width='400' height='400'>
|
||||||
|
<path fill='white' d='M11.6,14.7c1,5.5,2.9,10.7,5.7,15.5c1,1.7,3.5,0.2,2.6-1.5c-2.6-4.7-4.4-9.6-5.4-14.8
|
||||||
|
C14.1,12,11.3,12.8,11.6,14.7L11.6,14.7z'/>
|
||||||
|
<path fill='white' d='M16.8,17.1c4,0.2,7.6-1.1,10.7-3.6c1.5-1.2-0.6-3.3-2.1-2.1c-2.4,2-5.4,2.9-8.6,2.7C14.9,14,14.9,17,16.8,17.1
|
||||||
|
L16.8,17.1z'/>
|
||||||
|
<path fill='white' d='M13.8,17.6c11.9,3.5,24.1,4.9,36.4,3.9c1.9-0.1,1.9-3.1,0-3c-12.1,0.9-24-0.3-35.6-3.8
|
||||||
|
C12.7,14.1,11.9,17,13.8,17.6L13.8,17.6z'/>
|
||||||
</svg>
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
@@ -32,19 +54,55 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
this.saveAllHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleSaveAllButtonClick(e)
|
||||||
|
}
|
||||||
|
this.focusSearchHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.focusInput(e)
|
||||||
|
}
|
||||||
|
this.newPostHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleNewPostButtonClick(e)
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
// ipc.on('top-save-all', this.saveAllHandler)
|
||||||
|
ipc.on('top-focus-search', this.focusSearchHandler)
|
||||||
|
ipc.on('top-new-post', this.newPostHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
|
// ipc.removeListener('top-save-all', this.saveAllHandler)
|
||||||
|
ipc.removeListener('top-focus-search', this.focusSearchHandler)
|
||||||
|
ipc.removeListener('top-new-post', this.newPostHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTooltipRequest (e) {
|
handleTooltipRequest (e) {
|
||||||
@@ -65,7 +123,6 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
dispatch(clearSearch())
|
dispatch(clearSearch())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.blurInput()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput () {
|
focusInput () {
|
||||||
@@ -88,6 +145,32 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
this.focusInput()
|
this.focusInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNewPostButtonClick (e) {
|
||||||
|
let { dispatch, folders, status } = this.props
|
||||||
|
let { targetFolders } = status
|
||||||
|
|
||||||
|
let isFolderFilterApplied = targetFolders.length > 0
|
||||||
|
let FolderKey = isFolderFilterApplied
|
||||||
|
? targetFolders[0].key
|
||||||
|
: folders[0].key
|
||||||
|
|
||||||
|
let newArticle = {
|
||||||
|
key: keygen(),
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
mode: 'markdown',
|
||||||
|
tags: [],
|
||||||
|
FolderKey: FolderKey,
|
||||||
|
craetedAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(saveArticle(newArticle.key, newArticle, true))
|
||||||
|
if (isFolderFilterApplied) dispatch(switchFolder(targetFolders[0].name))
|
||||||
|
remote.getCurrentWebContents().send('detail-title')
|
||||||
|
activityRecord.emit('ARTICLE_CREATE')
|
||||||
|
}
|
||||||
|
|
||||||
handleTutorialButtonClick (e) {
|
handleTutorialButtonClick (e) {
|
||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
|
|
||||||
@@ -97,9 +180,9 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
let { status } = this.props
|
let { status } = this.props
|
||||||
return (
|
return (
|
||||||
<div className='ArticleTopBar'>
|
<div tabIndex='2' className='ArticleTopBar'>
|
||||||
<div className='left'>
|
<div className='ArticleTopBar-left'>
|
||||||
<div className='search'>
|
<div className='ArticleTopBar-left-search'>
|
||||||
<i className='fa fa-search fa-fw' />
|
<i className='fa fa-search fa-fw' />
|
||||||
<input
|
<input
|
||||||
ref='searchInput'
|
ref='searchInput'
|
||||||
@@ -112,25 +195,49 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
this.props.status.search != null && this.props.status.search.length > 0
|
this.props.status.search != null && this.props.status.search.length > 0
|
||||||
? <button onClick={e => this.handleSearchClearButton(e)} className='searchClearBtn'><i className='fa fa-times'/></button>
|
? <button onClick={e => this.handleSearchClearButton(e)} className='ArticleTopBar-left-search-clear-button'><i className='fa fa-times'/></button>
|
||||||
: 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}'}<br/><small>exact match : //{'{folder_name}'}</small></li>
|
||||||
|
<li>- Only unsaved : --unsaved</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status.isTutorialOpen ? searchTutorialElement : null}
|
{status.isTutorialOpen ? searchTutorialElement : null}
|
||||||
|
|
||||||
|
<div className={'ArticleTopBar-left-control'}>
|
||||||
|
<button className='ArticleTopBar-left-control-new-post-button' onClick={e => this.handleNewPostButtonClick(e)}>
|
||||||
|
<i className='fa fa-plus'/>
|
||||||
|
<span className='tooltip'>New Post ({OSX ? '⌘' : '^'} + n)</span>
|
||||||
|
</button>
|
||||||
|
{status.isTutorialOpen ? newPostTutorialElement : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='right'>
|
|
||||||
|
<div className='ArticleTopBar-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='ArticleTopBar-right-links-button' href>
|
||||||
<img src='../../resources/favicon-230x230.png' width='44' height='44'/>
|
<img src='../resources/app.png' width='44' height='44'/>
|
||||||
<span className='tooltip'>Boost official page</span>
|
</a>
|
||||||
</ExternalLink>
|
{
|
||||||
|
this.state.isLinksDropdownOpen
|
||||||
|
? (
|
||||||
|
<div className='ArticleTopBar-right-links-button-dropdown'>
|
||||||
|
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://b00st.io'>
|
||||||
|
<i className='fa fa-fw fa-home'/>Boost official page
|
||||||
|
</ExternalLink>
|
||||||
|
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-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 ? (
|
||||||
@@ -138,7 +245,7 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
|
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
|
||||||
<svg width='500' height='250' className='finder'>
|
<svg width='500' height='250' className='finder'>
|
||||||
<text x='100' y='25' fontSize='32' fill={BRAND_COLOR}>Also, you can open Finder!!</text>
|
<text x='100' y='25' fontSize='32' fill={BRAND_COLOR}>Also, you can open Finder!!</text>
|
||||||
<text x='120' y='55' fontSize='18' fill={BRAND_COLOR}>with pressing `Control` + `shift` + `tab`</text>
|
<text x='150' y='55' fontSize='18' fill={BRAND_COLOR} children={'with pressing ' + (OSX ? '`⌘ + Alt + s`' : '`Win + Alt + s`')}/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg width='450' className='global'>
|
<svg width='450' className='global'>
|
||||||
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
|
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
|
||||||
@@ -154,9 +261,9 @@ export default class ArticleTopBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArticleTopBar.propTypes = {
|
ArticleTopBar.propTypes = {
|
||||||
search: PropTypes.string,
|
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
status: PropTypes.shape({
|
status: PropTypes.shape({
|
||||||
search: PropTypes.string
|
search: PropTypes.string
|
||||||
})
|
}),
|
||||||
|
folders: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
import React, { Component, PropTypes } from 'react'
|
import React, { Component, PropTypes } from 'react'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
import ProfileImage from 'boost/components/ProfileImage'
|
import ProfileImage from 'browser/components/ProfileImage'
|
||||||
import { openModal } from 'boost/modal'
|
|
||||||
import CreateNewTeam from 'boost/components/modal/CreateNewTeam'
|
|
||||||
|
|
||||||
export default class UserNavigator extends Component {
|
export default class UserNavigator extends Component {
|
||||||
handleClick (e) {
|
|
||||||
openModal(CreateNewTeam)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for dev
|
|
||||||
componentDidMount () {
|
|
||||||
// openModal(CreateNewTeam)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderUserList () {
|
renderUserList () {
|
||||||
if (this.props.users == null) return null
|
if (this.props.users == null) return null
|
||||||
|
|
||||||
@@ -38,7 +27,7 @@ export default class UserNavigator extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className='UserNavigator'>
|
<div className='UserNavigator'>
|
||||||
{this.renderUserList()}
|
{this.renderUserList()}
|
||||||
<button className='createTeamBtn' onClick={e => this.handleClick(e)}>
|
<button className='createTeamBtn'>
|
||||||
+
|
+
|
||||||
<div className='tooltip'>Create a new team</div>
|
<div className='tooltip'>Create a new team</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
241
browser/main/HomePage/index.js
Normal file
241
browser/main/HomePage/index.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import React, { PropTypes} from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { toggleTutorial } from '../actions'
|
||||||
|
import ArticleNavigator from './ArticleNavigator'
|
||||||
|
import ArticleTopBar from './ArticleTopBar'
|
||||||
|
import ArticleList from './ArticleList'
|
||||||
|
import ArticleDetail from './ArticleDetail'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { isModalOpen, closeModal } from 'browser/lib/modal'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
const TEXT_FILTER = 'TEXT_FILTER'
|
||||||
|
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||||
|
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||||
|
const TAG_FILTER = 'TAG_FILTER'
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
class HomePage extends React.Component {
|
||||||
|
componentDidMount () {
|
||||||
|
// React自体のKey入力はfocusされていないElementからは動かないため、
|
||||||
|
// `window`に直接かける
|
||||||
|
this.keyHandler = e => this.handleKeyDown(e)
|
||||||
|
window.addEventListener('keydown', this.keyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('keydown', this.keyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
if (isModalOpen()) {
|
||||||
|
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
|
||||||
|
remote.getCurrentWebContents().send('modal-confirm')
|
||||||
|
}
|
||||||
|
if (e.keyCode === 27) closeModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { status, dispatch } = this.props
|
||||||
|
let { top, list } = this.refs
|
||||||
|
let listElement = ReactDOM.findDOMNode(list)
|
||||||
|
|
||||||
|
if (status.isTutorialOpen) {
|
||||||
|
dispatch(toggleTutorial())
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.keyCode === 13 && top.isInputFocused()) {
|
||||||
|
listElement.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.keyCode === 27 && top.isInputFocused()) {
|
||||||
|
if (status.search.length > 0) top.escape()
|
||||||
|
else listElement.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search inputがfocusされていたら大体のキー入力は無視される。
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
if (document.activeElement !== listElement) {
|
||||||
|
listElement.focus()
|
||||||
|
} else {
|
||||||
|
top.focusInput()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='HomePage'>
|
||||||
|
<ArticleNavigator
|
||||||
|
ref='nav'
|
||||||
|
dispatch={dispatch}
|
||||||
|
status={status}
|
||||||
|
user={user}
|
||||||
|
folders={folders}
|
||||||
|
allArticles={allArticles}
|
||||||
|
articles={articles}
|
||||||
|
modified={modified}
|
||||||
|
activeArticle={activeArticle}
|
||||||
|
/>
|
||||||
|
<ArticleTopBar
|
||||||
|
ref='top'
|
||||||
|
dispatch={dispatch}
|
||||||
|
status={status}
|
||||||
|
folders={folders}
|
||||||
|
/>
|
||||||
|
<ArticleList
|
||||||
|
ref='list'
|
||||||
|
dispatch={dispatch}
|
||||||
|
folders={folders}
|
||||||
|
articles={articles}
|
||||||
|
modified={modified}
|
||||||
|
activeArticle={activeArticle}
|
||||||
|
/>
|
||||||
|
<ArticleDetail
|
||||||
|
ref='detail'
|
||||||
|
dispatch={dispatch}
|
||||||
|
status={status}
|
||||||
|
tags={tags}
|
||||||
|
user={user}
|
||||||
|
folders={folders}
|
||||||
|
modified={modified}
|
||||||
|
activeArticle={activeArticle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore invalid key
|
||||||
|
function ignoreInvalidKey (key) {
|
||||||
|
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/) && !key.match(/^--/)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filter object by key
|
||||||
|
function buildFilter (key) {
|
||||||
|
if (key.match(/^\/\/.+/)) {
|
||||||
|
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
||||||
|
}
|
||||||
|
if (key.match(/^\/.+/)) {
|
||||||
|
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
||||||
|
}
|
||||||
|
if (key.match(/^#(.+)/)) {
|
||||||
|
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||||
|
}
|
||||||
|
return {type: TEXT_FILTER, value: key}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContaining (target, needle) {
|
||||||
|
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWith (target, needle) {
|
||||||
|
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function remap (state) {
|
||||||
|
let { user, folders, status } = state
|
||||||
|
let _articles = state.articles
|
||||||
|
|
||||||
|
let articles = _articles != null ? _articles.data : []
|
||||||
|
let modified = _articles != null ? _articles.modified : []
|
||||||
|
|
||||||
|
articles.sort((a, b) => {
|
||||||
|
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
|
||||||
|
if (match === 0) match = b.title.localeCompare(a.title)
|
||||||
|
if (match === 0) match = b.key.localeCompare(a.key)
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
let allArticles = articles.slice()
|
||||||
|
|
||||||
|
let tags = _.uniq(allArticles.reduce((sum, article) => {
|
||||||
|
if (!_.isArray(article.tags)) return sum
|
||||||
|
return sum.concat(article.tags)
|
||||||
|
}, []))
|
||||||
|
|
||||||
|
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
|
||||||
|
// Filter articles
|
||||||
|
let filters = status.search.split(' ')
|
||||||
|
.map(key => key.trim())
|
||||||
|
.filter(ignoreInvalidKey)
|
||||||
|
.map(buildFilter)
|
||||||
|
|
||||||
|
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
||||||
|
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||||
|
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||||
|
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||||
|
|
||||||
|
let targetFolders
|
||||||
|
if (folders != null) {
|
||||||
|
let exactTargetFolders = folders.filter(folder => {
|
||||||
|
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||||
|
})
|
||||||
|
let fuzzyTargetFolders = folders.filter(folder => {
|
||||||
|
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||||
|
})
|
||||||
|
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||||
|
|
||||||
|
if (targetFolders.length > 0) {
|
||||||
|
articles = articles.filter(article => {
|
||||||
|
return _.findWhere(targetFolders, {key: article.FolderKey})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textFilters.length > 0) {
|
||||||
|
articles = textFilters.reduce((articles, textFilter) => {
|
||||||
|
return articles.filter(article => {
|
||||||
|
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||||
|
})
|
||||||
|
}, articles)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagFilters.length > 0) {
|
||||||
|
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||||
|
return articles.filter(article => {
|
||||||
|
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||||
|
})
|
||||||
|
}, articles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab active article
|
||||||
|
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||||
|
if (activeArticle == null) activeArticle = articles[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
folders,
|
||||||
|
status,
|
||||||
|
articles,
|
||||||
|
allArticles,
|
||||||
|
modified,
|
||||||
|
activeArticle,
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomePage.propTypes = {
|
||||||
|
status: PropTypes.shape(),
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
}),
|
||||||
|
articles: PropTypes.array,
|
||||||
|
allArticles: PropTypes.array,
|
||||||
|
modified: PropTypes.array,
|
||||||
|
activeArticle: PropTypes.shape(),
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
folders: PropTypes.array,
|
||||||
|
tags: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(remap)(HomePage)
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import { login } from 'boost/api'
|
|
||||||
import auth from 'boost/auth'
|
|
||||||
|
|
||||||
export default class LoginPage extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
user: {},
|
|
||||||
isSending: false,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
this.linkState = linkState
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit (e) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.setState({
|
|
||||||
isSending: true,
|
|
||||||
error: null
|
|
||||||
}, function () {
|
|
||||||
login(this.state.user)
|
|
||||||
.then(res => {
|
|
||||||
let { user, token } = res.body
|
|
||||||
auth.user(user, token)
|
|
||||||
|
|
||||||
this.props.history.pushState('home')
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
if (err.code === 'ECONNREFUSED') {
|
|
||||||
return this.setState({
|
|
||||||
error: {
|
|
||||||
name: 'CunnectionRefused',
|
|
||||||
message: 'Can\'t cznnect to API server.'
|
|
||||||
},
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
} else if (err.status != null) {
|
|
||||||
return this.setState({
|
|
||||||
error: {
|
|
||||||
name: err.response.body.name,
|
|
||||||
message: err.response.body.message
|
|
||||||
},
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else throw err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='LoginContainer'>
|
|
||||||
<img className='logo' src='../../resources/favicon-230x230.png'/>
|
|
||||||
|
|
||||||
<nav className='authNavigator text-center'>
|
|
||||||
<Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<form onSubmit={e => this.handleSubmit(e)}>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.password')} onChange={this.handleChange} type='password' placeholder='Password'/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.isSending
|
|
||||||
? (
|
|
||||||
<p className='alertInfo'>Logging in...</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
|
|
||||||
|
|
||||||
<div className='formField'>
|
|
||||||
<button className='logInButton' type='submit'>Log In</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginPage.propTypes = {
|
|
||||||
history: PropTypes.shape({
|
|
||||||
pushState: PropTypes.func
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import ipc from 'ipc'
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
|
import HomePage from './HomePage'
|
||||||
var ContactModal = require('boost/components/modal/ContactModal')
|
|
||||||
|
|
||||||
export default class MainContainer extends React.Component {
|
export default class MainContainer extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -19,21 +19,31 @@ export default class MainContainer extends React.Component {
|
|||||||
ipc.send('update-app', 'Deal with it.')
|
ipc.send('update-app', 'Deal with it.')
|
||||||
}
|
}
|
||||||
|
|
||||||
openContactModal () {
|
handleWheel (e) {
|
||||||
this.openModal(ContactModal)
|
if (e.ctrlKey && global.process.platform !== 'darwin') {
|
||||||
|
if (window.document.body.style.zoom == null) {
|
||||||
|
window.document.body.style.zoom = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let zoom = Number(window.document.body.style.zoom)
|
||||||
|
if (e.deltaY > 0 && zoom < 4) {
|
||||||
|
document.body.style.zoom = zoom + 0.05
|
||||||
|
} else if (e.deltaY < 0 && zoom > 0.5) {
|
||||||
|
document.body.style.zoom = zoom - 0.05
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='Main'>
|
<div
|
||||||
|
className='Main'
|
||||||
|
onWheel={(e) => this.handleWheel(e)}
|
||||||
|
>
|
||||||
{this.state.updateAvailable ? (
|
{this.state.updateAvailable ? (
|
||||||
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
||||||
) : null}
|
) : null}
|
||||||
{/* <button onClick={this.openContactModal} className='contactButton'>
|
<HomePage/>
|
||||||
<i className='fa fa-paper-plane-o'/>
|
|
||||||
<div className='tooltip'>Contact us</div>
|
|
||||||
</button> */}
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import openExternal from 'boost/openExternal'
|
|
||||||
import { signup } from 'boost/api'
|
|
||||||
import auth from 'boost/auth'
|
|
||||||
|
|
||||||
export default class SignupContainer extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
user: {},
|
|
||||||
connectionFailed: false,
|
|
||||||
emailConflicted: false,
|
|
||||||
nameConflicted: false,
|
|
||||||
validationFailed: false,
|
|
||||||
isSending: false,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
this.linkState = linkState
|
|
||||||
this.openExternal = openExternal
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit (e) {
|
|
||||||
this.setState({
|
|
||||||
isSending: true,
|
|
||||||
error: null
|
|
||||||
}, function () {
|
|
||||||
signup(this.state.user)
|
|
||||||
.then(res => {
|
|
||||||
let { user, token } = res.body
|
|
||||||
auth.user(user, token)
|
|
||||||
|
|
||||||
this.props.history.pushState('home')
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
if (err.code === 'ECONNREFUSED') {
|
|
||||||
return this.setState({
|
|
||||||
error: {
|
|
||||||
name: 'CunnectionRefused',
|
|
||||||
message: 'Can\'t connect to API server.'
|
|
||||||
},
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
} else if (err.status != null) {
|
|
||||||
return this.setState({
|
|
||||||
error: {
|
|
||||||
name: err.response.body.name,
|
|
||||||
message: err.response.body.message
|
|
||||||
},
|
|
||||||
isSending: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else throw err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='SignupContainer'>
|
|
||||||
<img className='logo' src='../../resources/favicon-230x230.png'/>
|
|
||||||
|
|
||||||
<nav className='authNavigator text-center'><Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link></nav>
|
|
||||||
|
|
||||||
<form onSubmit={e => this.handleSubmit(e)}>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.password')} type='password' placeholder='Password'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.name')} type='text' placeholder='name'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<input valueLink={this.linkState('user.profileName')} type='text' placeholder='Profile name'/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.isSending ? (
|
|
||||||
<p className='alertInfo'>Signing up...</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
|
|
||||||
|
|
||||||
<div className='formField'>
|
|
||||||
<button className='logInButton' type='submit'>Sign Up</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p className='alert'>会員登録することで、<a onClick={this.openExternal} href='http://boostio.github.io/regulations.html'>当サイトの利用規約</a>及び<a onClick={this.openExternal} href='http://boostio.github.io/privacypolicies.html'>Cookieの使用を含むデータに関するポリシー</a>に同意するものとします。</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SignupContainer.propTypes = {
|
|
||||||
history: PropTypes.shape({
|
|
||||||
pushState: PropTypes.func
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,71 @@
|
|||||||
// Action types
|
// Action types
|
||||||
|
export const USER_UPDATE = 'USER_UPDATE'
|
||||||
|
|
||||||
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 ARTICLE_SAVE = 'ARTICLE_SAVE'
|
||||||
|
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
|
||||||
|
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
|
||||||
|
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
|
||||||
|
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
|
||||||
|
|
||||||
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_ARTICLE = 'SWITCH_ARTICLE'
|
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 TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
|
|
||||||
|
|
||||||
// Status - mode
|
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
|
||||||
export const IDLE_MODE = 'IDLE_MODE'
|
|
||||||
export const CREATE_MODE = 'CREATE_MODE'
|
|
||||||
export const EDIT_MODE = 'EDIT_MODE'
|
|
||||||
|
|
||||||
// Article status
|
// Article status
|
||||||
export const NEW = 'NEW'
|
export const NEW = 'NEW'
|
||||||
|
|
||||||
|
export function updateUser (input) {
|
||||||
|
return {
|
||||||
|
type: USER_UPDATE,
|
||||||
|
data: input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
|
export function cacheArticle (key, article) {
|
||||||
|
return {
|
||||||
|
type: ARTICLE_CACHE,
|
||||||
|
data: { key, article }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uncacheArticle (key) {
|
||||||
|
return {
|
||||||
|
type: ARTICLE_UNCACHE,
|
||||||
|
data: { key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uncacheAllArticles () {
|
||||||
|
return {
|
||||||
|
type: ARTICLE_UNCACHE_ALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveArticle (key, article, forceSwitch) {
|
||||||
|
return {
|
||||||
|
type: ARTICLE_SAVE,
|
||||||
|
data: { key, article, forceSwitch }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveAllArticles () {
|
||||||
|
return {
|
||||||
|
type: ARTICLE_SAVE_ALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function updateArticle (article) {
|
export function updateArticle (article) {
|
||||||
return {
|
return {
|
||||||
type: ARTICLE_UPDATE,
|
type: ARTICLE_UPDATE,
|
||||||
@@ -57,6 +101,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,
|
||||||
@@ -64,17 +118,12 @@ export function switchFolder (folderName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchMode (mode) {
|
|
||||||
return {
|
|
||||||
type: SWITCH_MODE,
|
|
||||||
data: mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function switchArticle (articleKey) {
|
export function switchArticle (articleKey) {
|
||||||
return {
|
return {
|
||||||
type: SWITCH_ARTICLE,
|
type: SWITCH_ARTICLE,
|
||||||
data: articleKey
|
data: {
|
||||||
|
key: articleKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +147,32 @@ export function clearSearch () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleTutorial() {
|
export function toggleTutorial () {
|
||||||
return {
|
return {
|
||||||
type: TOGGLE_TUTORIAL
|
type: TOGGLE_TUTORIAL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
updateUser,
|
||||||
|
|
||||||
|
updateArticle,
|
||||||
|
destroyArticle,
|
||||||
|
cacheArticle,
|
||||||
|
uncacheArticle,
|
||||||
|
uncacheAllArticles,
|
||||||
|
saveArticle,
|
||||||
|
saveAllArticles,
|
||||||
|
|
||||||
|
createFolder,
|
||||||
|
updateFolder,
|
||||||
|
destroyFolder,
|
||||||
|
replaceFolder,
|
||||||
|
|
||||||
|
switchFolder,
|
||||||
|
switchArticle,
|
||||||
|
setSearchFilter,
|
||||||
|
setTagFilter,
|
||||||
|
clearSearch,
|
||||||
|
toggleTutorial
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<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/devicon/devicon.min.css">
|
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Lato';
|
|
||||||
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
|
||||||
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
|
||||||
url('../../resources/Lato-Regular.ttf') format('truetype');
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
#loadingCover{
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 65px 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
#loadingCover img{
|
|
||||||
display: block;
|
|
||||||
margin: 75px auto 5px;
|
|
||||||
width: 160px;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
#loadingCover .message{
|
|
||||||
font-size: 30px;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-weight: 100;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="loadingCover">
|
|
||||||
<img src="../../resources/favicon-230x230.png">
|
|
||||||
<div class='message'>Loading...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content"></div>
|
|
||||||
|
|
||||||
<script src="../../submodules/ace/src-min/ace.js"></script>
|
|
||||||
<script type='text/javascript'>
|
|
||||||
require('web-frame').setZoomLevelLimits(1, 1)
|
|
||||||
var version = require('remote').require('app').getVersion()
|
|
||||||
document.title = 'Boost' + ((version == null || version.length === 0) ? ' DEV' : '')
|
|
||||||
var scriptUrl = process.env.BOOST_ENV === 'development'
|
|
||||||
? 'http://localhost:8080/assets/main.js'
|
|
||||||
: '../../compiled/main.js'
|
|
||||||
var scriptEl=document.createElement('script')
|
|
||||||
scriptEl.setAttribute("type","text/javascript")
|
|
||||||
scriptEl.setAttribute("src", scriptUrl)
|
|
||||||
document.getElementsByTagName("head")[0].appendChild(scriptEl)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,30 +1,57 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
// import { updateUser } from 'boost/actions'
|
|
||||||
import { Router, Route, IndexRoute } from 'react-router'
|
|
||||||
import MainPage from './MainPage'
|
import MainPage from './MainPage'
|
||||||
import HomePage from './HomePage'
|
import store from './store'
|
||||||
// import auth from 'boost/auth'
|
import React from 'react'
|
||||||
import store from 'boost/store'
|
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
require('../styles/main/index.styl')
|
require('../styles/main/index.styl')
|
||||||
import { openModal } from 'boost/modal'
|
import { openModal } from 'browser/lib/modal'
|
||||||
import Tutorial from 'boost/components/modal/Tutorial'
|
import Tutorial from './modal/Tutorial'
|
||||||
import activityRecord from 'boost/activityRecord'
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
const path = require('path')
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
window.addEventListener('keydown', function (e) {
|
||||||
|
if (e.keyCode === 73 && e.metaKey && e.altKey) {
|
||||||
|
remote.getCurrentWindow().toggleDevTools()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
activityRecord.init()
|
activityRecord.init()
|
||||||
|
window.addEventListener('online', function () {
|
||||||
|
ipc.send('check-update', 'check-update')
|
||||||
|
})
|
||||||
|
|
||||||
let routes = (
|
function notify (title, options) {
|
||||||
<Route path='/' component={MainPage}>
|
if (process.platform === 'win32') {
|
||||||
<IndexRoute name='home' component={HomePage}/>
|
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||||
</Route>
|
options.silent = false
|
||||||
)
|
}
|
||||||
|
console.log(options)
|
||||||
|
return new window.Notification(title, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.on('notify', function (e, payload) {
|
||||||
|
notify(payload.title, {
|
||||||
|
body: payload.body
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipc.on('copy-finder', function () {
|
||||||
|
activityRecord.emit('FINDER_COPY')
|
||||||
|
})
|
||||||
|
ipc.on('open-finder', function () {
|
||||||
|
activityRecord.emit('FINDER_OPEN')
|
||||||
|
})
|
||||||
|
|
||||||
let el = document.getElementById('content')
|
let el = document.getElementById('content')
|
||||||
ReactDOM.render((
|
ReactDOM.render((
|
||||||
<div>
|
<div>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router>{routes}</Router>
|
<MainPage/>
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
), el, function () {
|
), el, function () {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import linkState from 'boost/linkState'
|
import ReactDOM from 'react-dom'
|
||||||
import { createFolder } from 'boost/actions'
|
import linkState from 'browser/lib/linkState'
|
||||||
import store from 'boost/store'
|
import { createFolder } from '../actions'
|
||||||
|
import store from '../store'
|
||||||
|
import FolderMark from 'browser/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,11 +27,12 @@ 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 = {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let input = {
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
store.dispatch(createFolder(input))
|
store.dispatch(createFolder(input))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -38,6 +46,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 +67,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 +88,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>
|
||||||
54
browser/main/modal/DeleteArticleModal.js
Normal file
54
browser/main/modal/DeleteArticleModal.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import store from '../store'
|
||||||
|
import { destroyArticle } from '../actions'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
|
export default class DeleteArticleModal extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.confirmHandler = e => this.handleYesButtonClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
ReactDOM.findDOMNode(this.refs.no).focus()
|
||||||
|
ipc.on('modal-confirm', this.confirmHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('modal-confirm', this.confirmHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNoButtonClick (e) {
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleYesButtonClick (e) {
|
||||||
|
store.dispatch(destroyArticle(this.props.articleKey))
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='DeleteArticleModal modal'>
|
||||||
|
<div className='title'><i className='fa fa-fw fa-trash'/> Delete an article.</div>
|
||||||
|
|
||||||
|
<div className='message'>Do you really want to delete?</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='danger'><i className='fa fa-fw fa-check'/> Yes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteArticleModal.propTypes = {
|
||||||
|
action: PropTypes.object,
|
||||||
|
articleKey: PropTypes.string,
|
||||||
|
close: PropTypes.func
|
||||||
|
}
|
||||||
232
browser/main/modal/Preference/AppSettingTab.js
Normal file
232
browser/main/modal/Preference/AppSettingTab.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import linkState from 'browser/lib/linkState'
|
||||||
|
import { updateUser } from '../../actions'
|
||||||
|
import fetchConfig from 'browser/lib/fetchConfig'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const ipc = electron.ipcRenderer
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
export default class AppSettingTab extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
let keymap = Object.assign({}, remote.getGlobal('keymap'))
|
||||||
|
let config = Object.assign({}, fetchConfig())
|
||||||
|
let userName = props.user != null ? props.user.name : null
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
user: {
|
||||||
|
name: userName,
|
||||||
|
alert: null
|
||||||
|
},
|
||||||
|
userAlert: null,
|
||||||
|
keymap: keymap,
|
||||||
|
keymapAlert: null,
|
||||||
|
config: config,
|
||||||
|
configAlert: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.handleSettingDone = () => {
|
||||||
|
this.setState({keymapAlert: {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Successfully done!'
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
this.handleSettingError = err => {
|
||||||
|
this.setState({keymapAlert: {
|
||||||
|
type: 'error',
|
||||||
|
message: err.message != null ? err.message : 'Error occurs!'
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||||
|
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||||
|
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitHotKey () {
|
||||||
|
ipc.send('hotkeyUpdated', this.state.keymap)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitConfig () {
|
||||||
|
ipc.send('configUpdated', this.state.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveButtonClick (e) {
|
||||||
|
this.submitHotKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigSaveButtonClick (e) {
|
||||||
|
this.submitConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.submitHotKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfigKeyDown (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.submitConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisableDirectWriteClick (e) {
|
||||||
|
let config = this.state.config
|
||||||
|
config['disable-direct-write'] = !config['disable-direct-write']
|
||||||
|
this.setState({
|
||||||
|
config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNameSaveButtonClick (e) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
|
||||||
|
dispatch(updateUser({name: this.state.user.name}))
|
||||||
|
this.setState({
|
||||||
|
userAlert: {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Successfully done!'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let keymapAlert = this.state.keymapAlert
|
||||||
|
let keymapAlertElement = keymapAlert != null
|
||||||
|
? (
|
||||||
|
<p className={`alert ${keymapAlert.type}`}>
|
||||||
|
{keymapAlert.message}
|
||||||
|
</p>
|
||||||
|
) : null
|
||||||
|
let userAlert = this.state.userAlert
|
||||||
|
let userAlertElement = userAlert != null
|
||||||
|
? (
|
||||||
|
<p className={`alert ${userAlert.type}`}>
|
||||||
|
{userAlert.message}
|
||||||
|
</p>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='AppSettingTab content'>
|
||||||
|
<div className='section'>
|
||||||
|
<div className='sectionTitle'>User's info</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>User name</label>
|
||||||
|
<input valueLink={this.linkState('user.name')} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionConfirm'>
|
||||||
|
<button onClick={e => this.handleNameSaveButtonClick(e)}>Save</button>
|
||||||
|
{userAlertElement}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='section'>
|
||||||
|
<div className='sectionTitle'>Text</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Editor Font Size</label>
|
||||||
|
<input valueLink={this.linkState('config.editor-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Editor Font Family</label>
|
||||||
|
<input valueLink={this.linkState('config.editor-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionMultiSelect'>
|
||||||
|
<label>Editor Indent Style</label>
|
||||||
|
<div className='sectionMultiSelect-input'>
|
||||||
|
type
|
||||||
|
<select valueLink={this.linkState('config.editor-indent-type')}>
|
||||||
|
<option value='space'>Space</option>
|
||||||
|
<option value='tab'>Tab</option>
|
||||||
|
</select>
|
||||||
|
size
|
||||||
|
<select valueLink={this.linkState('config.editor-indent-size')}>
|
||||||
|
<option value='2'>2</option>
|
||||||
|
<option value='4'>4</option>
|
||||||
|
<option value='8'>8</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Preview Font Size</label>
|
||||||
|
<input valueLink={this.linkState('config.preview-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Preview Font Family</label>
|
||||||
|
<input valueLink={this.linkState('config.preview-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionSelect'>
|
||||||
|
<label>Switching Preview</label>
|
||||||
|
<select valueLink={this.linkState('config.switch-preview')}>
|
||||||
|
<option value='blur'>When Editor Blurred</option>
|
||||||
|
<option value='rightclick'>When Right Clicking</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
global.process.platform === 'win32'
|
||||||
|
? (
|
||||||
|
<div className='sectionCheck'>
|
||||||
|
<label><input onClick={e => this.handleDisableDirectWriteClick(e)} checked={this.state.config['disable-direct-write']} disabled={OSX} type='checkbox'/>Disable Direct Write<span className='sectionCheck-warn'>It will be applied after restarting</span></label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className='sectionConfirm'>
|
||||||
|
<button onClick={e => this.handleConfigSaveButtonClick(e)}>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='section'>
|
||||||
|
<div className='sectionTitle'>Hotkey</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Toggle Main</label>
|
||||||
|
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleMain')} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionInput'>
|
||||||
|
<label>Toggle Finder(popup)</label>
|
||||||
|
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='sectionConfirm'>
|
||||||
|
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
|
||||||
|
{keymapAlertElement}
|
||||||
|
</div>
|
||||||
|
<div className='description'>
|
||||||
|
<ul>
|
||||||
|
<li><code>0</code> to <code>9</code></li>
|
||||||
|
<li><code>A</code> to <code>Z</code></li>
|
||||||
|
<li><code>F1</code> to <code>F24</code></li>
|
||||||
|
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
|
||||||
|
<li><code>Plus</code></li>
|
||||||
|
<li><code>Space</code></li>
|
||||||
|
<li><code>Backspace</code></li>
|
||||||
|
<li><code>Delete</code></li>
|
||||||
|
<li><code>Insert</code></li>
|
||||||
|
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
|
||||||
|
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
|
||||||
|
<li><code>Home</code> and <code>End</code></li>
|
||||||
|
<li><code>PageUp</code> and <code>PageDown</code></li>
|
||||||
|
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
|
||||||
|
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
|
||||||
|
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettingTab.prototype.linkState = linkState
|
||||||
|
AppSettingTab.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
}),
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { getClientKey } from 'boost/activityRecord'
|
import clientKey from 'browser/lib/clientKey'
|
||||||
import linkState from 'boost/linkState'
|
import linkState from 'browser/lib/linkState'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { request, WEB_URL } from 'boost/api'
|
import { request, SERVER_URL } from 'browser/lib/api'
|
||||||
|
|
||||||
const FORM_MODE = 'FORM_MODE'
|
const FORM_MODE = 'FORM_MODE'
|
||||||
const DONE_MODE = 'DONE_MODE'
|
const DONE_MODE = 'DONE_MODE'
|
||||||
@@ -34,7 +34,7 @@ export default class ContactTab extends React.Component {
|
|||||||
|
|
||||||
handleSendButtonClick (e) {
|
handleSendButtonClick (e) {
|
||||||
let input = _.pick(this.state, ['title', 'content', 'email'])
|
let input = _.pick(this.state, ['title', 'content', 'email'])
|
||||||
input.clientKey = getClientKey()
|
input.clientKey = clientKey.get()
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
alert: {
|
alert: {
|
||||||
@@ -42,7 +42,7 @@ export default class ContactTab extends React.Component {
|
|||||||
message: 'Sending...'
|
message: 'Sending...'
|
||||||
}
|
}
|
||||||
}, () => {
|
}, () => {
|
||||||
request.post(WEB_URL + 'apis/inquiry')
|
request.post(SERVER_URL + 'apis/inquiry')
|
||||||
.send(input)
|
.send(input)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
console.log('sent')
|
console.log('sent')
|
||||||
187
browser/main/modal/Preference/FolderRow.js
Normal file
187
browser/main/modal/Preference/FolderRow.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import linkState from 'browser/lib/linkState'
|
||||||
|
import FolderMark from 'browser/components/FolderMark'
|
||||||
|
import store from '../../store'
|
||||||
|
import { updateFolder, destroyFolder, replaceFolder } from '../../actions'
|
||||||
|
|
||||||
|
const IDLE = 'IDLE'
|
||||||
|
const EDIT = 'EDIT'
|
||||||
|
const DELETE = 'DELETE'
|
||||||
|
|
||||||
|
export default class FolderRow extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
mode: IDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.setState({
|
||||||
|
mode: IDLE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
mode: EDIT,
|
||||||
|
name: this.props.folder.name,
|
||||||
|
color: this.props.folder.color,
|
||||||
|
isColorEditing: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteButtonClick (e) {
|
||||||
|
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) {
|
||||||
|
let { folder, setAlert } = this.props
|
||||||
|
|
||||||
|
setAlert(null, () => {
|
||||||
|
let input = {
|
||||||
|
name: this.state.name,
|
||||||
|
color: this.state.color
|
||||||
|
}
|
||||||
|
folder = Object.assign({}, folder, input)
|
||||||
|
|
||||||
|
try {
|
||||||
|
store.dispatch(updateFolder(folder))
|
||||||
|
this.setState({
|
||||||
|
mode: IDLE
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setAlert({
|
||||||
|
type: 'error',
|
||||||
|
message: e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteConfirmButtonClick (e) {
|
||||||
|
let { folder } = this.props
|
||||||
|
store.dispatch(destroyFolder(folder.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let folder = this.props.folder
|
||||||
|
|
||||||
|
switch (this.state.mode) {
|
||||||
|
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 (
|
||||||
|
<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'>
|
||||||
|
<input onKeyDown={e => this.handleNameInputKeyDown(e)} valueLink={this.linkState('name')} type='text'/>
|
||||||
|
</div>
|
||||||
|
<div className='folderControl'>
|
||||||
|
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
|
||||||
|
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case DELETE:
|
||||||
|
return (
|
||||||
|
<div className='FolderRow delete'>
|
||||||
|
<div className='folderDeleteLabel'>Are you sure to delete <strong>{folder.name}</strong> folder?</div>
|
||||||
|
<div className='folderControl'>
|
||||||
|
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>Sure</button>
|
||||||
|
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case IDLE:
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className='FolderRow'>
|
||||||
|
<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'>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderRow.propTypes = {
|
||||||
|
folder: PropTypes.shape(),
|
||||||
|
index: PropTypes.number,
|
||||||
|
count: PropTypes.number,
|
||||||
|
setAlert: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderRow.prototype.linkState = linkState
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import FolderRow from './FolderRow'
|
import FolderRow from './FolderRow'
|
||||||
import linkState from 'boost/linkState'
|
import linkState from 'browser/lib/linkState'
|
||||||
import { createFolder } from 'boost/actions'
|
import { createFolder } from '../../actions'
|
||||||
|
|
||||||
export default class FolderSettingTab extends React.Component {
|
export default class FolderSettingTab extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -12,10 +12,14 @@ 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
|
|
||||||
|
|
||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -40,10 +44,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 +69,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>
|
||||||
121
browser/main/modal/Preferences.js
Normal file
121
browser/main/modal/Preferences.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import { connect, Provider } from 'react-redux'
|
||||||
|
import linkState from 'browser/lib/linkState'
|
||||||
|
import store from '../store'
|
||||||
|
import AppSettingTab from './Preference/AppSettingTab'
|
||||||
|
import FolderSettingTab from './Preference/FolderSettingTab'
|
||||||
|
import ContactTab from './Preference/ContactTab'
|
||||||
|
import { closeModal } from 'browser/lib/modal'
|
||||||
|
|
||||||
|
const APP = 'APP'
|
||||||
|
const HELP = 'HELP'
|
||||||
|
const FOLDER = 'FOLDER'
|
||||||
|
const CONTACT = 'CONTACT'
|
||||||
|
|
||||||
|
class Preferences extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
currentTab: APP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchTeam (teamId) {
|
||||||
|
this.setState({currentTeamId: teamId})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNavButtonClick (tab) {
|
||||||
|
return e => {
|
||||||
|
this.setState({currentTab: tab})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let content = this.renderContent()
|
||||||
|
|
||||||
|
let tabs = [
|
||||||
|
{target: APP, label: 'Preferences'},
|
||||||
|
{target: FOLDER, label: 'Manage folder'},
|
||||||
|
{target: CONTACT, label: 'Contact form'}
|
||||||
|
]
|
||||||
|
|
||||||
|
let navButtons = tabs.map(tab => (
|
||||||
|
<button key={tab.target} onClick={e => this.handleNavButtonClick(tab.target)(e)} className={this.state.currentTab === tab.target ? 'active' : ''}>{tab.label}</button>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='Preferences modal'>
|
||||||
|
<div className='header'>
|
||||||
|
<div className='title'>Setting</div>
|
||||||
|
<button onClick={e => closeModal()} className='closeBtn'>Done</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='nav'>
|
||||||
|
{navButtons}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent () {
|
||||||
|
let { user, folders, dispatch } = this.props
|
||||||
|
|
||||||
|
switch (this.state.currentTab) {
|
||||||
|
case HELP:
|
||||||
|
return (<HelpTab/>)
|
||||||
|
case FOLDER:
|
||||||
|
return (
|
||||||
|
<FolderSettingTab
|
||||||
|
dispatch={dispatch}
|
||||||
|
folders={folders}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case CONTACT:
|
||||||
|
return (
|
||||||
|
<ContactTab/>
|
||||||
|
)
|
||||||
|
case APP:
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<AppSettingTab
|
||||||
|
user={user}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Preferences.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
}),
|
||||||
|
folders: PropTypes.array,
|
||||||
|
dispatch: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
Preferences.prototype.linkState = linkState
|
||||||
|
|
||||||
|
function remap (state) {
|
||||||
|
let { user, folders, status } = state
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
folders,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let RootComponent = connect(remap)(Preferences)
|
||||||
|
export default class PreferencesModal extends React.Component {
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<RootComponent/>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import MarkdownPreview from 'boost/components/MarkdownPreview'
|
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||||
import CodeEditor from 'boost/components/CodeEditor'
|
import CodeEditor from 'browser/components/CodeEditor'
|
||||||
|
|
||||||
export default class Tutorial extends React.Component {
|
export default class Tutorial extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -80,7 +80,7 @@ export default class Tutorial extends React.Component {
|
|||||||
Boost supports code syntax highlighting.<br/>
|
Boost supports code syntax highlighting.<br/>
|
||||||
There are more than 100 different type of language.
|
There are more than 100 different type of language.
|
||||||
<div className='code'>
|
<div className='code'>
|
||||||
<CodeEditor readOnly mode='jsx' code={code}/>
|
<CodeEditor readOnly article={{content:code, mode: 'jsx'}}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
@@ -88,12 +88,13 @@ 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 [⌘ + alt + s] 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 [{process.platform === 'darwin' ? 'Command' : 'Control'}-V]
|
||||||
|
|
||||||
<img width='480' src='../../resources/finder.png'/>
|
<img width='480' src='../resources/finder.png'/>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
case 4:
|
case 4:
|
||||||
306
browser/main/reducer.js
Normal file
306
browser/main/reducer.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import { combineReducers } from 'redux'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {
|
||||||
|
// Status action type
|
||||||
|
SWITCH_FOLDER,
|
||||||
|
SWITCH_ARTICLE,
|
||||||
|
SET_SEARCH_FILTER,
|
||||||
|
SET_TAG_FILTER,
|
||||||
|
CLEAR_SEARCH,
|
||||||
|
TOGGLE_TUTORIAL,
|
||||||
|
|
||||||
|
// user
|
||||||
|
USER_UPDATE,
|
||||||
|
|
||||||
|
// Article action type
|
||||||
|
ARTICLE_UPDATE,
|
||||||
|
ARTICLE_DESTROY,
|
||||||
|
ARTICLE_CACHE,
|
||||||
|
ARTICLE_UNCACHE,
|
||||||
|
ARTICLE_UNCACHE_ALL,
|
||||||
|
ARTICLE_SAVE,
|
||||||
|
ARTICLE_SAVE_ALL,
|
||||||
|
|
||||||
|
// Folder action type
|
||||||
|
FOLDER_CREATE,
|
||||||
|
FOLDER_UPDATE,
|
||||||
|
FOLDER_DESTROY,
|
||||||
|
FOLDER_REPLACE
|
||||||
|
} from './actions'
|
||||||
|
import dataStore from 'browser/lib/dataStore'
|
||||||
|
import keygen from 'browser/lib/keygen'
|
||||||
|
import activityRecord from 'browser/lib/activityRecord'
|
||||||
|
|
||||||
|
const initialStatus = {
|
||||||
|
search: '',
|
||||||
|
isTutorialOpen: false
|
||||||
|
}
|
||||||
|
|
||||||
|
dataStore.init()
|
||||||
|
let data = dataStore.getData()
|
||||||
|
let initialArticles = {
|
||||||
|
data: data && data.articles ? data.articles : [],
|
||||||
|
modified: []
|
||||||
|
}
|
||||||
|
let initialFolders = data && data.folders ? data.folders : []
|
||||||
|
let initialUser = dataStore.getUser().user
|
||||||
|
|
||||||
|
function user (state = initialUser, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case USER_UPDATE:
|
||||||
|
let updated = Object.assign(state, action.data)
|
||||||
|
dataStore.saveUser(null, updated)
|
||||||
|
return updated
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function folders (state = initialFolders, action) {
|
||||||
|
state = state.slice()
|
||||||
|
switch (action.type) {
|
||||||
|
case FOLDER_CREATE:
|
||||||
|
{
|
||||||
|
let newFolder = action.data.folder
|
||||||
|
if (!_.isString(newFolder.name)) throw new Error('Folder name must be a string')
|
||||||
|
newFolder.name = newFolder.name.trim().replace(/\s/g, '_')
|
||||||
|
|
||||||
|
Object.assign(newFolder, {
|
||||||
|
key: keygen(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
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 = _.find(state, folder => folder.name.toLowerCase() === newFolder.name.toLowerCase())
|
||||||
|
if (conflictFolder != null) throw new Error(`${conflictFolder.name} already exists!`)
|
||||||
|
state.push(newFolder)
|
||||||
|
|
||||||
|
dataStore.setFolders(state)
|
||||||
|
activityRecord.emit('FOLDER_CREATE')
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case FOLDER_UPDATE:
|
||||||
|
{
|
||||||
|
let folder = action.data.folder
|
||||||
|
let targetFolder = _.findWhere(state, {key: folder.key})
|
||||||
|
|
||||||
|
if (!_.isString(folder.name)) throw new Error('Folder name must be a string')
|
||||||
|
folder.name = folder.name.trim().replace(/\s/g, '_')
|
||||||
|
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
|
||||||
|
if (targetFolder == null) throw new Error('Folder doesnt exist')
|
||||||
|
// Name conflict check
|
||||||
|
if (targetFolder.name !== folder.name) {
|
||||||
|
let conflictFolder = _.find(state, _folder => {
|
||||||
|
return folder.name.toLowerCase() === _folder.name.toLowerCase() && folder.key !== _folder.key
|
||||||
|
})
|
||||||
|
if (conflictFolder != null) throw new Error('Name conflicted')
|
||||||
|
}
|
||||||
|
Object.assign(targetFolder, folder, {
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
dataStore.setFolders(state)
|
||||||
|
activityRecord.emit('FOLDER_UPDATE')
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case FOLDER_DESTROY:
|
||||||
|
{
|
||||||
|
if (state.length < 2) throw new Error('Folder must exist more than one')
|
||||||
|
|
||||||
|
let targetKey = action.data.key
|
||||||
|
let targetIndex = _.findIndex(state, folder => folder.key === targetKey)
|
||||||
|
if (targetIndex >= 0) {
|
||||||
|
state.splice(targetIndex, 1)
|
||||||
|
}
|
||||||
|
dataStore.setFolders(state)
|
||||||
|
activityRecord.emit('FOLDER_DESTROY')
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
dataStore.setFolders(state)
|
||||||
|
return state
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareArticle (original, modified) {
|
||||||
|
var keys = _.keys(_.pick(modified, ['mode', 'title', 'tags', 'content', 'FolderKey']))
|
||||||
|
|
||||||
|
return keys.reduce((sum, key) => {
|
||||||
|
if ((key === 'tags' && !_.isEqual(original[key], modified[key])) || (key !== 'tags' && original[key] !== modified[key])) {
|
||||||
|
if (sum == null) {
|
||||||
|
sum = {
|
||||||
|
key: original.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum[key] = modified[key]
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function articles (state = initialArticles, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ARTICLE_CACHE:
|
||||||
|
{
|
||||||
|
let modified = action.data.article
|
||||||
|
let targetKey = action.data.key
|
||||||
|
let originalIndex = _.findIndex(state.data, _article => targetKey === _article.key)
|
||||||
|
if (originalIndex === -1) return state
|
||||||
|
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
|
||||||
|
|
||||||
|
modified = compareArticle(state.data[originalIndex], modified)
|
||||||
|
if (modified == null) {
|
||||||
|
if (modifiedIndex !== -1) state.modified.splice(modifiedIndex, 1)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiedIndex === -1) state.modified.push(modified)
|
||||||
|
else Object.assign(state.modified[modifiedIndex], modified)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case ARTICLE_UNCACHE:
|
||||||
|
{
|
||||||
|
let targetKey = action.data.key
|
||||||
|
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
|
||||||
|
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case ARTICLE_UNCACHE_ALL:
|
||||||
|
state.modified = []
|
||||||
|
return state
|
||||||
|
case ARTICLE_SAVE:
|
||||||
|
{
|
||||||
|
let targetKey = action.data.key
|
||||||
|
let override = action.data.article
|
||||||
|
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
|
||||||
|
let modified = modifiedIndex !== -1 ? state.modified.splice(modifiedIndex, 1)[0] : null
|
||||||
|
|
||||||
|
let targetIndex = _.findIndex(state.data, _article => targetKey === _article.key)
|
||||||
|
// Make a new if target article is not found.
|
||||||
|
if (targetIndex === -1) {
|
||||||
|
state.data.push(Object.assign({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
mode: 'markdown',
|
||||||
|
tags: [],
|
||||||
|
craetedAt: new Date()
|
||||||
|
}, modified, override, {key: targetKey, updatedAt: new Date()}))
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(state.data[targetIndex], modified, override, {key: targetKey, updatedAt: new Date()})
|
||||||
|
|
||||||
|
dataStore.setArticles(state.data)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case ARTICLE_SAVE_ALL:
|
||||||
|
if (state.modified.length > 0) {
|
||||||
|
state.modified.forEach(modifiedArticle => {
|
||||||
|
let targetIndex = _.findIndex(state.data, _article => modifiedArticle.key === _article.key)
|
||||||
|
Object.assign(state.data[targetIndex], modifiedArticle, {key: modifiedArticle.key, updatedAt: new Date()})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.modified = []
|
||||||
|
dataStore.setArticles(state.data)
|
||||||
|
|
||||||
|
return state
|
||||||
|
case ARTICLE_UPDATE:
|
||||||
|
{
|
||||||
|
let article = action.data.article
|
||||||
|
|
||||||
|
let targetIndex = _.findIndex(state.data, _article => article.key === _article.key)
|
||||||
|
if (targetIndex < 0) state.data.unshift(article)
|
||||||
|
else Object.assign(state.data[targetIndex], article)
|
||||||
|
|
||||||
|
dataStore.setArticles(state.data)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case ARTICLE_DESTROY:
|
||||||
|
{
|
||||||
|
let articleKey = action.data.key
|
||||||
|
|
||||||
|
let targetIndex = _.findIndex(state.data, _article => articleKey === _article.key)
|
||||||
|
if (targetIndex >= 0) state.data.splice(targetIndex, 1)
|
||||||
|
let modifiedIndex = _.findIndex(state.modified, _article => articleKey === _article.key)
|
||||||
|
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
|
||||||
|
|
||||||
|
dataStore.setArticles(state.data)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case FOLDER_DESTROY:
|
||||||
|
{
|
||||||
|
let folderKey = action.data.key
|
||||||
|
|
||||||
|
state.data = state.data.filter(article => article.FolderKey !== folderKey)
|
||||||
|
|
||||||
|
dataStore.setArticles(state.data)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function status (state = initialStatus, action) {
|
||||||
|
state = Object.assign({}, state)
|
||||||
|
switch (action.type) {
|
||||||
|
case TOGGLE_TUTORIAL:
|
||||||
|
state.isTutorialOpen = !state.isTutorialOpen
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ARTICLE_SAVE:
|
||||||
|
if (action.data.forceSwitch) {
|
||||||
|
let article = action.data.article
|
||||||
|
state.articleKey = article.key
|
||||||
|
state.search = ''
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
case SWITCH_FOLDER:
|
||||||
|
state.search = `\/\/${action.data} `
|
||||||
|
|
||||||
|
return state
|
||||||
|
case SWITCH_ARTICLE:
|
||||||
|
state.articleKey = action.data.key
|
||||||
|
|
||||||
|
return state
|
||||||
|
case SET_SEARCH_FILTER:
|
||||||
|
state.search = action.data
|
||||||
|
|
||||||
|
return state
|
||||||
|
case SET_TAG_FILTER:
|
||||||
|
state.search = `#${action.data}`
|
||||||
|
|
||||||
|
return state
|
||||||
|
case CLEAR_SEARCH:
|
||||||
|
state.search = ''
|
||||||
|
|
||||||
|
return state
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default combineReducers({
|
||||||
|
user,
|
||||||
|
folders,
|
||||||
|
articles,
|
||||||
|
status
|
||||||
|
})
|
||||||
@@ -7,13 +7,17 @@ global-reset()
|
|||||||
iptBgColor = #E6E6E6
|
iptBgColor = #E6E6E6
|
||||||
iptFocusBorderColor = #369DCD
|
iptFocusBorderColor = #369DCD
|
||||||
|
|
||||||
|
DEFAULT_FONTS = 'Lato', 'MS Gothic', 'Malgun Gothic', 'Sans-serif'
|
||||||
|
|
||||||
body
|
body
|
||||||
font-family "Lato"
|
font-family DEFAULT_FONTS
|
||||||
color textColor
|
color textColor
|
||||||
font-size fontSize
|
font-size fontSize
|
||||||
width 100%
|
width 100%
|
||||||
height 100%
|
height 100%
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
button, input
|
||||||
|
font-family "Lato"
|
||||||
|
|
||||||
.Finder
|
.Finder
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
@@ -46,6 +50,7 @@ body
|
|||||||
width 250px
|
width 250px
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
z-index 0
|
z-index 0
|
||||||
|
user-select none
|
||||||
&>ul>li
|
&>ul>li
|
||||||
.articleItem
|
.articleItem
|
||||||
padding 10px
|
padding 10px
|
||||||
@@ -79,6 +84,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
|
||||||
@@ -90,5 +120,9 @@ body
|
|||||||
overflow-y auto
|
overflow-y auto
|
||||||
.MarkdownPreview
|
.MarkdownPreview
|
||||||
marked()
|
marked()
|
||||||
|
&.empty
|
||||||
|
color lighten(inactiveTextColor, 10%)
|
||||||
|
user-select none
|
||||||
|
font-size 14px
|
||||||
.CodeEditor
|
.CodeEditor
|
||||||
absolute top bottom left right
|
absolute top bottom left right
|
||||||
|
|||||||
391
browser/styles/main/ArticleDetail.styl
Normal file
391
browser/styles/main/ArticleDetail.styl
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
noTagsColor = #999
|
||||||
|
|
||||||
|
infoButton()
|
||||||
|
display inline-block
|
||||||
|
border-radius 16.5px
|
||||||
|
cursor pointer
|
||||||
|
height 33px
|
||||||
|
width 33px
|
||||||
|
line-height 33px
|
||||||
|
margin-right 5px
|
||||||
|
font-size 18px
|
||||||
|
color inactiveTextColor
|
||||||
|
background-color white
|
||||||
|
padding 0
|
||||||
|
border 1px solid white
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
|
&:hover
|
||||||
|
color inherit
|
||||||
|
|
||||||
|
.ArticleDetail
|
||||||
|
absolute right bottom
|
||||||
|
top 60px
|
||||||
|
left 450px
|
||||||
|
padding 10px
|
||||||
|
background-color #E6E6E6
|
||||||
|
border-top 1px solid borderColor
|
||||||
|
border-left 1px solid borderColor
|
||||||
|
&.empty
|
||||||
|
.ArticleDetail-empty-box
|
||||||
|
line-height 72px
|
||||||
|
font-size 42px
|
||||||
|
height 320px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
.ArticleDetail-empty-box-message
|
||||||
|
text-align center
|
||||||
|
width 100%
|
||||||
|
color inactiveTextColor
|
||||||
|
.ArticleDetail-info
|
||||||
|
height 70px
|
||||||
|
width 100%
|
||||||
|
font-size 12px
|
||||||
|
user-select none
|
||||||
|
&>.tutorial
|
||||||
|
position fixed
|
||||||
|
z-index 35
|
||||||
|
.ArticleDetail-info-folder
|
||||||
|
display inline-block
|
||||||
|
max-width 100px
|
||||||
|
overflow ellipsis
|
||||||
|
height 10px
|
||||||
|
width 150px
|
||||||
|
height 27px
|
||||||
|
outline none
|
||||||
|
background-color darken(white, 5%)
|
||||||
|
border 1px solid transparent
|
||||||
|
&:hover
|
||||||
|
background-color white
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
|
&>.tutorial
|
||||||
|
position fixed
|
||||||
|
z-index 35
|
||||||
|
.ArticleDetail-info-status
|
||||||
|
padding 0 5px
|
||||||
|
.unsaved-mark
|
||||||
|
color brandColor
|
||||||
|
.ArticleDetail-info-control
|
||||||
|
float right
|
||||||
|
clearfix
|
||||||
|
.ShareButton
|
||||||
|
display block
|
||||||
|
float left
|
||||||
|
&>button, .ShareButton-open-button
|
||||||
|
infoButton()
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
margin-top 30px
|
||||||
|
&:hover
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&>button
|
||||||
|
float left
|
||||||
|
&:nth-child(1) .tooltip
|
||||||
|
margin-left -65px
|
||||||
|
.ArticleDetail-info-control-delete-button
|
||||||
|
.tooltip
|
||||||
|
right 5px
|
||||||
|
.ArticleDetail-info-control-save
|
||||||
|
float left
|
||||||
|
width 80px
|
||||||
|
margin-right 5px
|
||||||
|
overflow hidden
|
||||||
|
transition width 0.15s ease-in-out
|
||||||
|
border-radius 16.5px
|
||||||
|
&.hide
|
||||||
|
width 0px
|
||||||
|
opacity 0.2
|
||||||
|
.ArticleDetail-info-control-save-button
|
||||||
|
infoButton()
|
||||||
|
background-color brandColor
|
||||||
|
color white
|
||||||
|
font-size 12px
|
||||||
|
width 100%
|
||||||
|
border 1px solid brandBorderColor
|
||||||
|
white-space nowrap
|
||||||
|
.fa
|
||||||
|
font-size 18px
|
||||||
|
&:hover
|
||||||
|
color white
|
||||||
|
background-color lighten(brandColor, 15%)
|
||||||
|
&:focus
|
||||||
|
color white
|
||||||
|
background-color lighten(brandColor, 15%)
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
margin-top 30px
|
||||||
|
margin-left -90px
|
||||||
|
&:hover .tooltip
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.ShareButton-open-button .tooltip
|
||||||
|
margin-left -40px
|
||||||
|
.ShareButton-dropdown
|
||||||
|
position fixed
|
||||||
|
width 185px
|
||||||
|
z-index 35
|
||||||
|
background-color #F0F0F0
|
||||||
|
padding 4px 0
|
||||||
|
border-radius 5px
|
||||||
|
right 5px
|
||||||
|
top 95px
|
||||||
|
box-shadow 0px 0px 10px 1px alpha(#bbb, 0.8)
|
||||||
|
border 1px solid #bcbcbc
|
||||||
|
&.hide
|
||||||
|
display none
|
||||||
|
&>button
|
||||||
|
background-color transparent
|
||||||
|
height 21px
|
||||||
|
width 100%
|
||||||
|
border none
|
||||||
|
padding-left 20px
|
||||||
|
text-align left
|
||||||
|
font-size 13px
|
||||||
|
font-family '.HelveticaNeueDeskInterface-Regular', sans-serif
|
||||||
|
&:hover
|
||||||
|
background-color #4297FE
|
||||||
|
color white
|
||||||
|
.ShareButton-url
|
||||||
|
height 40px
|
||||||
|
width 100%
|
||||||
|
position relative
|
||||||
|
padding 0 5px
|
||||||
|
.ShareButton-url-input
|
||||||
|
height 21px
|
||||||
|
border none
|
||||||
|
width 143px
|
||||||
|
float left
|
||||||
|
border-top-left-radius 3px
|
||||||
|
border-bottom-left-radius 3px
|
||||||
|
border 1px solid borderColor
|
||||||
|
border-right none
|
||||||
|
.ShareButton-url-button
|
||||||
|
height 21px
|
||||||
|
border none
|
||||||
|
width 30px
|
||||||
|
float left
|
||||||
|
background-color #F0F0F0
|
||||||
|
border-top-right-radius 3px
|
||||||
|
border-bottom-right-radius 3px
|
||||||
|
border 1px solid borderColor
|
||||||
|
.ShareButton-url-button-tooltip
|
||||||
|
tooltip()
|
||||||
|
right 10px
|
||||||
|
margin-top 5px
|
||||||
|
&:hover
|
||||||
|
.ShareButton-url-button-tooltip
|
||||||
|
opacity 1
|
||||||
|
&:active
|
||||||
|
background-color #4297FE
|
||||||
|
color white
|
||||||
|
.ShareButton-url-alert
|
||||||
|
padding 10px
|
||||||
|
line-height 16px
|
||||||
|
|
||||||
|
|
||||||
|
.ArticleDetail-info-row2
|
||||||
|
.tutorial
|
||||||
|
position fixed
|
||||||
|
z-index 35
|
||||||
|
font-style italic
|
||||||
|
.TagSelect
|
||||||
|
margin-top 5px
|
||||||
|
.TagSelect-tags
|
||||||
|
white-space nowrap
|
||||||
|
overflow-x auto
|
||||||
|
position relative
|
||||||
|
noSelect()
|
||||||
|
z-index 30
|
||||||
|
background-color #E6E6E6
|
||||||
|
clearfix()
|
||||||
|
.TagSelect-tags-item
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
margin 0 2px
|
||||||
|
padding 0
|
||||||
|
height 17px
|
||||||
|
float left
|
||||||
|
button.TagSelect-tags-item-remove
|
||||||
|
display block
|
||||||
|
float left
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
font-size 8px
|
||||||
|
color white
|
||||||
|
width 15px
|
||||||
|
height 17px
|
||||||
|
text-align center
|
||||||
|
line-height 12px
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
border-top solid 1px darken(brandColor, 5%)
|
||||||
|
border-bottom solid 1px darken(brandColor, 5%)
|
||||||
|
border-left solid 1px darken(brandColor, 5%)
|
||||||
|
border-right solid 1px transparent
|
||||||
|
border-radius left 2px
|
||||||
|
background-color brandColor
|
||||||
|
&:hover
|
||||||
|
background-color lighten(brandColor, 10%)
|
||||||
|
border-color lighten(brandColor, 10%)
|
||||||
|
&:focus
|
||||||
|
background-color lighten(brandColor, 10%)
|
||||||
|
border-color focusBorderColor
|
||||||
|
.TagSelect-tags-item-label
|
||||||
|
background-color brandColor
|
||||||
|
float left
|
||||||
|
font-size 12px
|
||||||
|
border-top solid 1px darken(brandColor, 5%)
|
||||||
|
border-bottom solid 1px darken(brandColor, 5%)
|
||||||
|
border-right solid 1px darken(brandColor, 5%)
|
||||||
|
line-height 15px
|
||||||
|
padding 0 5px
|
||||||
|
border-radius right 2px
|
||||||
|
input.TagSelect-input
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
border-bottom 1px solid transparent
|
||||||
|
outline none
|
||||||
|
margin 0 2px
|
||||||
|
transition 0.15s
|
||||||
|
height 18px
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
|
.TagSelect-suggest
|
||||||
|
position fixed
|
||||||
|
width 150px
|
||||||
|
max-height 150px
|
||||||
|
background-color white
|
||||||
|
z-index 50
|
||||||
|
border 1px solid borderColor
|
||||||
|
border-radius 5px
|
||||||
|
overflow-y auto
|
||||||
|
&>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
|
||||||
|
background-color darken(white, 10%)
|
||||||
|
.ArticleDetail-panel
|
||||||
|
position absolute
|
||||||
|
top 70px
|
||||||
|
left 10px
|
||||||
|
right 10px
|
||||||
|
bottom 10px
|
||||||
|
overflow-x hidden
|
||||||
|
overflow-y auto
|
||||||
|
background-color white
|
||||||
|
border-radius 5px
|
||||||
|
border solid 1px lighten(borderColor, 15%)
|
||||||
|
&>.ArticleDetail-panel-header
|
||||||
|
display block
|
||||||
|
height 60px
|
||||||
|
&>.tutorial
|
||||||
|
fixed right
|
||||||
|
z-index 35
|
||||||
|
font-style italic
|
||||||
|
.ArticleDetail-panel-header-mode
|
||||||
|
z-index 30
|
||||||
|
background-color white
|
||||||
|
absolute top bottom
|
||||||
|
right 10px
|
||||||
|
display block
|
||||||
|
height 33px
|
||||||
|
margin-top 14px
|
||||||
|
width 120px
|
||||||
|
margin-right 15px
|
||||||
|
border solid 1px borderColor
|
||||||
|
border-radius 5px
|
||||||
|
transition width 0.15s
|
||||||
|
user-select none
|
||||||
|
&.idle
|
||||||
|
cursor pointer
|
||||||
|
&:hover
|
||||||
|
background-color darken(white, 5%)
|
||||||
|
.ModeIcon
|
||||||
|
padding 0 5px
|
||||||
|
line-height 33px
|
||||||
|
&.edit
|
||||||
|
border-color focusBorderColor
|
||||||
|
input
|
||||||
|
width 120px
|
||||||
|
line-height 31px
|
||||||
|
padding 0 10px
|
||||||
|
border none
|
||||||
|
outline none
|
||||||
|
background-color transparent
|
||||||
|
font-size 14px
|
||||||
|
.ModeSelect-options
|
||||||
|
position fixed
|
||||||
|
width 120px
|
||||||
|
z-index 10
|
||||||
|
border 1px solid borderColor
|
||||||
|
border-radius 5px
|
||||||
|
background-color white
|
||||||
|
max-height 250px
|
||||||
|
overflow-y auto
|
||||||
|
margin-top 5px
|
||||||
|
.ModeSelect-options-item
|
||||||
|
height 33px
|
||||||
|
line-height 33px
|
||||||
|
cursor pointer
|
||||||
|
&.active, &:hover.active
|
||||||
|
background-color brandColor
|
||||||
|
color white
|
||||||
|
.ModeIcon
|
||||||
|
width 30px
|
||||||
|
text-align center
|
||||||
|
display inline-block
|
||||||
|
&:hover
|
||||||
|
background-color darken(white, 10%)
|
||||||
|
.ArticleDetail-panel-header-title
|
||||||
|
absolute left top
|
||||||
|
right 145px
|
||||||
|
padding 0 15px
|
||||||
|
background-color transparent
|
||||||
|
input
|
||||||
|
border none
|
||||||
|
line-height 60px
|
||||||
|
width 100%
|
||||||
|
font-size 24px
|
||||||
|
outline none
|
||||||
|
.ArticleEditor
|
||||||
|
absolute left right bottom
|
||||||
|
top 60px
|
||||||
|
.ArticleDetail-panel-content-tooltip
|
||||||
|
absolute bottom right
|
||||||
|
height 24px
|
||||||
|
background-color alpha(black, 0.5)
|
||||||
|
line-height 24px
|
||||||
|
color white
|
||||||
|
padding 0 15px
|
||||||
|
opacity 0
|
||||||
|
transition 0.1s
|
||||||
|
z-index 35
|
||||||
|
&:hover .ArticleDetail-panel-content-tooltip
|
||||||
|
opacity 1
|
||||||
|
.MarkdownPreview
|
||||||
|
absolute top left right bottom
|
||||||
|
marked()
|
||||||
|
box-sizing border-box
|
||||||
|
padding 5px 15px
|
||||||
|
border-top solid 1px borderColor
|
||||||
|
overflow-y auto
|
||||||
|
user-select all
|
||||||
|
&.empty
|
||||||
|
color lighten(inactiveTextColor, 10%)
|
||||||
|
user-select none
|
||||||
|
font-size 14px
|
||||||
|
.CodeEditor
|
||||||
|
absolute top left right bottom
|
||||||
|
border-top solid 1px borderColor
|
||||||
|
min-height 300px
|
||||||
|
border-bottom-left-radius 5px
|
||||||
|
border-bottom-right-radius 5px
|
||||||
@@ -8,36 +8,42 @@ articleItemColor = #777
|
|||||||
width 250px
|
width 250px
|
||||||
border-top 1px solid borderColor
|
border-top 1px solid borderColor
|
||||||
border-right 1px solid borderColor
|
border-right 1px solid borderColor
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
noSelect()
|
noSelect()
|
||||||
&>div
|
&>div
|
||||||
.articleItem
|
.ArticleList-item
|
||||||
border solid 2px transparent
|
border solid 2px transparent
|
||||||
position relative
|
position relative
|
||||||
height 88px
|
min-height 110px
|
||||||
width 100%
|
width 100%
|
||||||
cursor pointer
|
cursor pointer
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
background-color white
|
background-color white
|
||||||
padding 0 10px
|
padding 0 10px
|
||||||
font-size 12px
|
font-size 12px
|
||||||
.top
|
.ArticleList-item-top
|
||||||
clearfix()
|
clearfix()
|
||||||
line-height 20px
|
padding-top 2px
|
||||||
padding 5px 0
|
line-height 18px
|
||||||
|
height 20px
|
||||||
color articleItemColor
|
color articleItemColor
|
||||||
|
font-size 11px
|
||||||
.folderName
|
.folderName
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
display inline-block
|
display inline-block
|
||||||
width 120px
|
width 120px
|
||||||
.updatedAt
|
.updatedAt
|
||||||
float right
|
float right
|
||||||
line-height 20px
|
line-height 18px
|
||||||
.middle
|
.unsaved-mark
|
||||||
padding 3px 0 7px
|
color brandColor
|
||||||
|
.ArticleList-item-middle
|
||||||
font-size 16px
|
font-size 16px
|
||||||
position relative
|
position relative
|
||||||
height 26px
|
padding-top 6px
|
||||||
|
height 22px
|
||||||
.mode
|
.mode
|
||||||
position absolute
|
position absolute
|
||||||
left 0
|
left 0
|
||||||
@@ -48,18 +54,41 @@ articleItemColor = #777
|
|||||||
left 19px
|
left 19px
|
||||||
right 0
|
right 0
|
||||||
overflow ellipsis
|
overflow ellipsis
|
||||||
.bottom
|
small
|
||||||
padding 5px 0
|
color #AAA
|
||||||
overflow-x auto
|
.ArticleList-item-middle2
|
||||||
white-space nowrap
|
padding-top 8px
|
||||||
|
pre
|
||||||
|
color lighten(inactiveTextColor, 10%)
|
||||||
|
white-space pre-wrap
|
||||||
|
overflow hidden
|
||||||
|
height 33px
|
||||||
|
line-height 14px
|
||||||
|
font-size 10px
|
||||||
|
code
|
||||||
|
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
||||||
|
.ArticleList-item-bottom
|
||||||
|
padding-bottom 5px
|
||||||
.tags
|
.tags
|
||||||
color articleItemColor
|
color articleItemColor
|
||||||
|
line-height 18px
|
||||||
|
word-wrap break-word
|
||||||
|
clearfix()
|
||||||
|
i.fa-tags
|
||||||
|
display inline
|
||||||
|
float left
|
||||||
|
padding 2px 2px 0 0
|
||||||
|
height 14px
|
||||||
|
line-height 13px
|
||||||
a
|
a
|
||||||
background-color brandColor
|
background-color brandColor
|
||||||
|
float left
|
||||||
color white
|
color white
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
padding 1.5px 5px
|
padding 1px 5px
|
||||||
margin 2px
|
margin 2px
|
||||||
|
height 14px
|
||||||
|
line-height 13px
|
||||||
font-size 10px
|
font-size 10px
|
||||||
opacity 0.8
|
opacity 0.8
|
||||||
&:hover
|
&:hover
|
||||||
212
browser/styles/main/ArticleNavigator.styl
Normal file
212
browser/styles/main/ArticleNavigator.styl
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
articleNavBgColor = #353535
|
||||||
|
articleCount = #999
|
||||||
|
|
||||||
|
.ArticleNavigator
|
||||||
|
background-color articleNavBgColor
|
||||||
|
absolute top bottom left
|
||||||
|
width 200px
|
||||||
|
border-right 1px solid borderColor
|
||||||
|
color white
|
||||||
|
user-select none
|
||||||
|
.userInfo
|
||||||
|
height 60px
|
||||||
|
display block
|
||||||
|
border-bottom 1px solid borderColor
|
||||||
|
.userProfileName
|
||||||
|
color brandColor
|
||||||
|
font-size 28px
|
||||||
|
padding 6px 37px 0 10px
|
||||||
|
white-space nowrap
|
||||||
|
text-overflow ellipsis
|
||||||
|
overflow hidden
|
||||||
|
.userName
|
||||||
|
color white
|
||||||
|
padding-left 20px
|
||||||
|
margin-top 3px
|
||||||
|
.tutorial
|
||||||
|
position fixed
|
||||||
|
z-index 35
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
pointer-event none
|
||||||
|
font-style italic
|
||||||
|
transition 0.1s
|
||||||
|
&.hide
|
||||||
|
opacity 0
|
||||||
|
.settingBtn
|
||||||
|
width 22px
|
||||||
|
height 22px
|
||||||
|
line-height 22px
|
||||||
|
border-radius 11px
|
||||||
|
position absolute
|
||||||
|
top 19px
|
||||||
|
right 14px
|
||||||
|
color white
|
||||||
|
padding 0
|
||||||
|
background-color transparent
|
||||||
|
border 1px solid white
|
||||||
|
z-index 31
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
margin-top -5px
|
||||||
|
margin-left 10px
|
||||||
|
&:hover
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&:active
|
||||||
|
background-color brandColor
|
||||||
|
border-color brandColor
|
||||||
|
.ArticleNavigator-unsaved
|
||||||
|
position absolute
|
||||||
|
top 100px
|
||||||
|
width 100%
|
||||||
|
height 225px
|
||||||
|
transition opacity 0.2s ease-in-out
|
||||||
|
&.hide
|
||||||
|
opacity 0.2
|
||||||
|
.ArticleNavigator-unsaved-header
|
||||||
|
border-bottom 1px solid alpha(borderColor, 0.5)
|
||||||
|
padding-bottom 5px
|
||||||
|
clearfix()
|
||||||
|
position relative
|
||||||
|
padding-left 10px
|
||||||
|
font-size 18px
|
||||||
|
line-height 22px
|
||||||
|
.ArticleNavigator-unsaved-list
|
||||||
|
height 165px
|
||||||
|
padding 5px 0
|
||||||
|
overflow-y scroll
|
||||||
|
.ArticleNavigator-unsaved-list-item
|
||||||
|
height 33px
|
||||||
|
padding-left 15px
|
||||||
|
clearfix()
|
||||||
|
transition 0.1s
|
||||||
|
cursor pointer
|
||||||
|
overflow hidden
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.05)
|
||||||
|
&.active, &:active
|
||||||
|
background-color alpha(lighten(brandColor, 25%), 70%)
|
||||||
|
.ArticleNavigator-unsaved-list-item-label
|
||||||
|
float left
|
||||||
|
width 151px
|
||||||
|
line-height 33px
|
||||||
|
overflow ellipsis
|
||||||
|
.ArticleNavigator-unsaved-list-item-label-untitled
|
||||||
|
color inactiveTextColor
|
||||||
|
.ArticleNavigator-unsaved-list-item-discard-button
|
||||||
|
float right
|
||||||
|
width 33px
|
||||||
|
line-height 30px
|
||||||
|
height 33px
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
font-size 18px
|
||||||
|
opacity 0.5
|
||||||
|
&:hover
|
||||||
|
opacity 1
|
||||||
|
.ArticleNavigator-unsaved-list-empty
|
||||||
|
height 33px
|
||||||
|
padding-left 15px
|
||||||
|
color alpha(white, 0.4)
|
||||||
|
transition 0.1s
|
||||||
|
line-height 33px
|
||||||
|
&:hover
|
||||||
|
color alpha(white, 0.6)
|
||||||
|
.ArticleNavigator-unsaved-control
|
||||||
|
absolute bottom
|
||||||
|
height 33px
|
||||||
|
border-top 1px solid alpha(borderColor, 0.5)
|
||||||
|
width 100%
|
||||||
|
.ArticleNavigator-unsaved-control-save-all-button
|
||||||
|
border none
|
||||||
|
background-color transparent
|
||||||
|
font-size 14px
|
||||||
|
color brandColor
|
||||||
|
padding-left 15px
|
||||||
|
width 100%
|
||||||
|
height 33px
|
||||||
|
text-align left
|
||||||
|
&:hover
|
||||||
|
color lighten(brandColor, 15%)
|
||||||
|
background-color alpha(white, 0.05)
|
||||||
|
&:active
|
||||||
|
color white
|
||||||
|
&:disabled
|
||||||
|
color alpha(brandColor, 0.5)
|
||||||
|
&:hover
|
||||||
|
color alpha(lighten(brandColor, 25%), 0.5)
|
||||||
|
background-color transparent
|
||||||
|
|
||||||
|
|
||||||
|
.ArticleNavigator-folders
|
||||||
|
absolute bottom
|
||||||
|
top 365px
|
||||||
|
width 100%
|
||||||
|
transition top 0.15s ease-in-out
|
||||||
|
background-color articleNavBgColor
|
||||||
|
.tutorial
|
||||||
|
position fixed
|
||||||
|
z-index 35
|
||||||
|
font-style italic
|
||||||
|
&.expand
|
||||||
|
top 100px
|
||||||
|
.ArticleNavigator-folders-header
|
||||||
|
border-bottom 1px solid alpha(borderColor, 0.5)
|
||||||
|
padding-bottom 5px
|
||||||
|
clearfix()
|
||||||
|
position relative
|
||||||
|
z-index 30
|
||||||
|
.title
|
||||||
|
float left
|
||||||
|
padding-left 10px
|
||||||
|
font-size 18px
|
||||||
|
line-height 22px
|
||||||
|
.addBtn
|
||||||
|
float right
|
||||||
|
margin-right 15px
|
||||||
|
width 22px
|
||||||
|
height 22px
|
||||||
|
font-size 10px
|
||||||
|
padding 0
|
||||||
|
line-height 22px
|
||||||
|
border 1px solid white
|
||||||
|
border-radius 11px
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
padding 0
|
||||||
|
font-weight bold
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
margin-top -6px
|
||||||
|
margin-left 11px
|
||||||
|
&:hover
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&:active
|
||||||
|
background-color brandColor
|
||||||
|
border-color brandColor
|
||||||
|
.folderList
|
||||||
|
absolute bottom
|
||||||
|
top 33px
|
||||||
|
overflow-y auto
|
||||||
|
.folderList button
|
||||||
|
height 33px
|
||||||
|
width 199px
|
||||||
|
border none
|
||||||
|
text-align left
|
||||||
|
font-size 14px
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
padding-left 15px
|
||||||
|
overflow ellipsis
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.05)
|
||||||
|
&.active, &:active
|
||||||
|
background-color alpha(lighten(brandColor, 25%), 70%)
|
||||||
|
.articleCount
|
||||||
|
color white
|
||||||
|
.articleCount
|
||||||
|
color articleCount
|
||||||
|
font-size 12px
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
bgColor = #E6E6E6
|
bgColor = #E6E6E6
|
||||||
inputBgColor = white
|
inputBgColor = white
|
||||||
iptFocusBorderColor = #369DCD
|
|
||||||
|
|
||||||
topBarBtnColor = #B3B3B3
|
topBarBtnColor = #B3B3B3
|
||||||
topBarBtnBgColor = #B3B3B3
|
topBarBtnBgColor = #B3B3B3
|
||||||
@@ -15,6 +14,7 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
left 200px
|
left 200px
|
||||||
height 60px
|
height 60px
|
||||||
background-color bgColor
|
background-color bgColor
|
||||||
|
user-select none
|
||||||
&>.tutorial
|
&>.tutorial
|
||||||
.clickJammer
|
.clickJammer
|
||||||
fixed top left bottom right
|
fixed top left bottom right
|
||||||
@@ -36,14 +36,15 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
fixed top left bottom right
|
fixed top left bottom right
|
||||||
z-index 20
|
z-index 20
|
||||||
background-color transparentify(black, 80%)
|
background-color transparentify(black, 80%)
|
||||||
&>.left
|
&>.ArticleTopBar-left
|
||||||
float left
|
float left
|
||||||
&>.tutorial
|
&>.tutorial
|
||||||
fixed top
|
fixed top
|
||||||
left 200px
|
left 100px
|
||||||
|
top 30px
|
||||||
z-index 36
|
z-index 36
|
||||||
font-style italic
|
font-style italic
|
||||||
&>.search
|
&>.ArticleTopBar-left-search
|
||||||
position relative
|
position relative
|
||||||
float left
|
float left
|
||||||
height 33px
|
height 33px
|
||||||
@@ -62,6 +63,17 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
opacity 1
|
opacity 1
|
||||||
&.hide
|
&.hide
|
||||||
opacity 0
|
opacity 0
|
||||||
|
ul
|
||||||
|
li
|
||||||
|
line-height 18px
|
||||||
|
li:last-child
|
||||||
|
line-height 10px
|
||||||
|
margin-bottom 3px
|
||||||
|
small
|
||||||
|
font-size 10px
|
||||||
|
position relative
|
||||||
|
top -2px
|
||||||
|
margin-left 15px
|
||||||
input
|
input
|
||||||
absolute top left
|
absolute top left
|
||||||
width 350px
|
width 350px
|
||||||
@@ -75,7 +87,7 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
line-height 33px
|
line-height 33px
|
||||||
z-index 0
|
z-index 0
|
||||||
&:focus
|
&:focus
|
||||||
border-color iptFocusBorderColor
|
border-color focusBorderColor
|
||||||
i.fa.fa-search
|
i.fa.fa-search
|
||||||
position absolute
|
position absolute
|
||||||
display block
|
display block
|
||||||
@@ -84,7 +96,7 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
line-height 33px
|
line-height 33px
|
||||||
z-index 1
|
z-index 1
|
||||||
pointer-events none
|
pointer-events none
|
||||||
.searchClearBtn
|
.ArticleTopBar-left-search-clear-button
|
||||||
position absolute
|
position absolute
|
||||||
top 6px
|
top 6px
|
||||||
right 10px
|
right 10px
|
||||||
@@ -98,24 +110,56 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
line-height 20px
|
line-height 20px
|
||||||
text-align center
|
text-align center
|
||||||
padding 0
|
padding 0
|
||||||
|
&:focus
|
||||||
|
color textColor
|
||||||
&:hover
|
&:hover
|
||||||
color white
|
color white
|
||||||
background-color topBarBtnBgColor
|
background-color topBarBtnBgColor
|
||||||
&>.refreshBtn
|
&:active
|
||||||
|
color white
|
||||||
|
background-color darken(topBarBtnBgColor, 35%)
|
||||||
|
.ArticleTopBar-left-control
|
||||||
|
line-height 33px
|
||||||
float left
|
float left
|
||||||
width 33px
|
|
||||||
height 33px
|
height 33px
|
||||||
margin-top 13.5px
|
margin-top 13.5px
|
||||||
margin-left 15px
|
margin-left 20px
|
||||||
border none
|
.tutorial
|
||||||
color refreshBtColor
|
fixed top
|
||||||
background transparent
|
left 200px
|
||||||
font-size 18px
|
z-index 36
|
||||||
line-height 18px
|
font-style italic
|
||||||
transition 0.1s
|
button.ArticleTopBar-left-control-new-post-button
|
||||||
&:hover
|
position fixed
|
||||||
color refreshBtnActiveColor
|
background bgColor
|
||||||
&>.right
|
font-size 20px
|
||||||
|
border none
|
||||||
|
outline none
|
||||||
|
color inactiveTextColor
|
||||||
|
width 33px
|
||||||
|
height 33px
|
||||||
|
border-radius 16.5px
|
||||||
|
transition 0.1s
|
||||||
|
border 1px solid transparent
|
||||||
|
z-index 30
|
||||||
|
&:hover
|
||||||
|
color textColor
|
||||||
|
&:active
|
||||||
|
color textColor
|
||||||
|
background-color lighten(topBarBtnBgColor, 15%)
|
||||||
|
&:disabled
|
||||||
|
color inactiveTextColor
|
||||||
|
background transparent
|
||||||
|
&:focus
|
||||||
|
color textColor
|
||||||
|
.tooltip
|
||||||
|
tooltip()
|
||||||
|
margin-left -80px
|
||||||
|
margin-top 40px
|
||||||
|
&:hover
|
||||||
|
.tooltip
|
||||||
|
opacity 1
|
||||||
|
&>.ArticleTopBar-right
|
||||||
float right
|
float right
|
||||||
&>button
|
&>button
|
||||||
display block
|
display block
|
||||||
@@ -129,28 +173,52 @@ infoBtnActiveBgColor = #3A3A3A
|
|||||||
background-color infoBtnBgColor
|
background-color infoBtnBgColor
|
||||||
color bgColor
|
color bgColor
|
||||||
border-radius 11px
|
border-radius 11px
|
||||||
border none
|
border 1px solid bgColor
|
||||||
transition 0.1s
|
transition 0.1s
|
||||||
|
&:focus
|
||||||
|
background-color lighten(infoBtnActiveBgColor, 15%)
|
||||||
.tooltip
|
.tooltip
|
||||||
tooltip()
|
tooltip()
|
||||||
margin-left -50px
|
margin-left -50px
|
||||||
margin-top 29px
|
margin-top 20px
|
||||||
&:hover
|
&:hover
|
||||||
background-color infoBtnActiveBgColor
|
background-color infoBtnActiveBgColor
|
||||||
.tooltip
|
.tooltip
|
||||||
opacity 1
|
opacity 1
|
||||||
|
|
||||||
&>.logo
|
&>.ArticleTopBar-right-links-button
|
||||||
display block
|
display block
|
||||||
position absolute
|
position absolute
|
||||||
top 8px
|
top 8px
|
||||||
right 15px
|
right 15px
|
||||||
opacity 0.7
|
opacity 0.7
|
||||||
.tooltip
|
border-radius 23px
|
||||||
tooltip()
|
height 46px
|
||||||
margin-top 44px
|
width 46px
|
||||||
margin-left -120px
|
border 1px solid transparent
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
&:hover
|
&:hover
|
||||||
opacity 1
|
opacity 1
|
||||||
.tooltip
|
.tooltip
|
||||||
opacity 1
|
opacity 1
|
||||||
|
&>.ArticleTopBar-right-links-button-dropdown
|
||||||
|
position fixed
|
||||||
|
z-index 50
|
||||||
|
right 10px
|
||||||
|
top 40px
|
||||||
|
background-color transparentify(invBackgroundColor, 80%)
|
||||||
|
padding 5px 0
|
||||||
|
.ArticleTopBar-right-links-button-dropdown-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%)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
noTagsColor = #999
|
|
||||||
iptFocusBorderColor = #369DCD
|
|
||||||
|
|
||||||
.ArticleDetail
|
|
||||||
absolute right bottom
|
|
||||||
top 60px
|
|
||||||
left 450px
|
|
||||||
padding 10px
|
|
||||||
background-color #E6E6E6
|
|
||||||
border-top 1px solid borderColor
|
|
||||||
border-left 1px solid borderColor
|
|
||||||
*
|
|
||||||
-webkit-user-select all
|
|
||||||
.deleteConfirm
|
|
||||||
width 100%
|
|
||||||
height 70px
|
|
||||||
.right
|
|
||||||
float right
|
|
||||||
button
|
|
||||||
cursor pointer
|
|
||||||
height 33px
|
|
||||||
padding 0 10px
|
|
||||||
margin-left 5px
|
|
||||||
font-size 14px
|
|
||||||
color inactiveTextColor
|
|
||||||
background-color darken(white, 5%)
|
|
||||||
border solid 1px borderColor
|
|
||||||
border-radius 5px
|
|
||||||
&:hover
|
|
||||||
background-color white
|
|
||||||
&.primary
|
|
||||||
border none
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
&:hover
|
|
||||||
color white
|
|
||||||
background-color lighten(brandColor, 10%)
|
|
||||||
|
|
||||||
.detailInfo
|
|
||||||
height 70px
|
|
||||||
width 100%
|
|
||||||
font-size 12px
|
|
||||||
position relative
|
|
||||||
.left
|
|
||||||
absolute top left bottom
|
|
||||||
right 120px
|
|
||||||
.folderName
|
|
||||||
display inline-block
|
|
||||||
max-width 100px
|
|
||||||
overflow ellipsis
|
|
||||||
height 10px
|
|
||||||
.right
|
|
||||||
absolute top right
|
|
||||||
.detailBody
|
|
||||||
absolute left right bottom
|
|
||||||
top 70px
|
|
||||||
overflow-x hidden
|
|
||||||
overflow-y auto
|
|
||||||
.detailPanel
|
|
||||||
absolute top
|
|
||||||
left 10px
|
|
||||||
right 10px
|
|
||||||
bottom 10px
|
|
||||||
background-color white
|
|
||||||
border-radius 5px
|
|
||||||
border solid 1px borderColor
|
|
||||||
&>.header
|
|
||||||
absolute top left right
|
|
||||||
height 60px
|
|
||||||
.MarkdownPreview
|
|
||||||
absolute left right bottom
|
|
||||||
top 60px
|
|
||||||
marked()
|
|
||||||
box-sizing border-box
|
|
||||||
padding 5px 15px
|
|
||||||
border-top solid 1px borderColor
|
|
||||||
overflow-y auto
|
|
||||||
.CodeEditor
|
|
||||||
absolute left right bottom
|
|
||||||
top 60px
|
|
||||||
border-top solid 1px borderColor
|
|
||||||
min-height 300px
|
|
||||||
border-bottom-left-radius 5px
|
|
||||||
border-bottom-right-radius 5px
|
|
||||||
&.edit
|
|
||||||
.detailInfo
|
|
||||||
.left
|
|
||||||
&>.tutorial
|
|
||||||
position fixed
|
|
||||||
z-index 35
|
|
||||||
font-style italic
|
|
||||||
.folder
|
|
||||||
border none
|
|
||||||
width 150px
|
|
||||||
height 27px
|
|
||||||
outline none
|
|
||||||
background-color darken(white, 5%)
|
|
||||||
&:hover
|
|
||||||
background-color white
|
|
||||||
.TagSelect
|
|
||||||
white-space nowrap
|
|
||||||
overflow-x auto
|
|
||||||
position relative
|
|
||||||
margin-top 5px
|
|
||||||
noSelect()
|
|
||||||
z-index 30
|
|
||||||
background-color #E6E6E6
|
|
||||||
.tagItem
|
|
||||||
background-color brandColor
|
|
||||||
border-radius 2px
|
|
||||||
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 none
|
|
||||||
transition 0.1s
|
|
||||||
height 18px
|
|
||||||
.right
|
|
||||||
button
|
|
||||||
cursor pointer
|
|
||||||
height 33px
|
|
||||||
width 55px
|
|
||||||
margin-left 5px
|
|
||||||
font-size 14px
|
|
||||||
color inactiveTextColor
|
|
||||||
background-color darken(white, 5%)
|
|
||||||
border solid 1px borderColor
|
|
||||||
border-radius 5px
|
|
||||||
&.preview
|
|
||||||
width inherit
|
|
||||||
&:hover
|
|
||||||
background-color white
|
|
||||||
&.primary
|
|
||||||
border none
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
&:hover
|
|
||||||
color white
|
|
||||||
background-color lighten(brandColor, 10%)
|
|
||||||
.detailBody
|
|
||||||
.detailPanel
|
|
||||||
&>.header
|
|
||||||
&>.tutorial
|
|
||||||
fixed right
|
|
||||||
z-index 35
|
|
||||||
font-style italic
|
|
||||||
.mode
|
|
||||||
position relative
|
|
||||||
z-index 30
|
|
||||||
absolute top bottom right
|
|
||||||
display block
|
|
||||||
height 33px
|
|
||||||
margin-top 12px
|
|
||||||
width 150px
|
|
||||||
margin-right 15px
|
|
||||||
border-radius 5px
|
|
||||||
border solid 1px borderColor
|
|
||||||
transition 0.1s
|
|
||||||
&.idle
|
|
||||||
background-color darken(white, 5%)
|
|
||||||
cursor pointer
|
|
||||||
&:hover
|
|
||||||
background-color white
|
|
||||||
.ModeIcon
|
|
||||||
float left
|
|
||||||
width 25px
|
|
||||||
line-height 33px
|
|
||||||
text-align center
|
|
||||||
.modeLabel
|
|
||||||
line-height 30px
|
|
||||||
&.edit
|
|
||||||
background-color white
|
|
||||||
input
|
|
||||||
width 150px
|
|
||||||
line-height 30px
|
|
||||||
padding 0 10px
|
|
||||||
border none
|
|
||||||
outline none
|
|
||||||
background-color transparent
|
|
||||||
font-size 14px
|
|
||||||
.modeOptions
|
|
||||||
position fixed
|
|
||||||
width 150px
|
|
||||||
z-index 10
|
|
||||||
margin-top 5px
|
|
||||||
border 1px solid borderColor
|
|
||||||
border-radius 5px
|
|
||||||
background-color white
|
|
||||||
max-height 250px
|
|
||||||
overflow-y auto
|
|
||||||
.option
|
|
||||||
height 33px
|
|
||||||
line-height 33px
|
|
||||||
cursor pointer
|
|
||||||
&.active, &:hover.active
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
.ModeIcon
|
|
||||||
width 30px
|
|
||||||
text-align center
|
|
||||||
display inline-block
|
|
||||||
&:hover
|
|
||||||
background-color darken(white, 10%)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.title
|
|
||||||
absolute left top bottom
|
|
||||||
right 150px
|
|
||||||
padding 0 15px
|
|
||||||
input
|
|
||||||
width 100%
|
|
||||||
border none
|
|
||||||
background-color transparent
|
|
||||||
line-height 60px
|
|
||||||
font-size 24px
|
|
||||||
outline none
|
|
||||||
&.idle
|
|
||||||
.detailInfo
|
|
||||||
&>.tutorial
|
|
||||||
fixed top right
|
|
||||||
z-index 35
|
|
||||||
font-style italic
|
|
||||||
.left
|
|
||||||
right 99px
|
|
||||||
.info
|
|
||||||
padding 5px
|
|
||||||
overflow ellipsis
|
|
||||||
.tags
|
|
||||||
padding 10px 10px 5px
|
|
||||||
color articleItemColor
|
|
||||||
a
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
border-radius 2px
|
|
||||||
padding 1.5px 5px
|
|
||||||
margin 2px
|
|
||||||
font-size 10px
|
|
||||||
opacity 0.8
|
|
||||||
cursor pointer
|
|
||||||
&:hover
|
|
||||||
opacity 1
|
|
||||||
span.noTags
|
|
||||||
color noTagsColor
|
|
||||||
.right
|
|
||||||
z-index 30
|
|
||||||
button
|
|
||||||
border-radius 16.5px
|
|
||||||
cursor pointer
|
|
||||||
height 33px
|
|
||||||
width 33px
|
|
||||||
border none
|
|
||||||
margin-right 5px
|
|
||||||
font-size 18px
|
|
||||||
color inactiveTextColor
|
|
||||||
background-color darken(white, 5%)
|
|
||||||
padding 0
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
&.editBtn .tooltip
|
|
||||||
margin-top 25px
|
|
||||||
margin-left -45px
|
|
||||||
&.deleteBtn .tooltip
|
|
||||||
margin-top 25px
|
|
||||||
margin-left -73px
|
|
||||||
&:hover
|
|
||||||
color textColor
|
|
||||||
.tooltip
|
|
||||||
opacity 1
|
|
||||||
.detailBody
|
|
||||||
.detailPanel
|
|
||||||
&>.header
|
|
||||||
.mode
|
|
||||||
display block
|
|
||||||
line-height 60px
|
|
||||||
width 45px
|
|
||||||
height 60px
|
|
||||||
font-size 18px
|
|
||||||
text-align center
|
|
||||||
.title
|
|
||||||
absolute top bottom
|
|
||||||
left 45px
|
|
||||||
right 15px
|
|
||||||
font-size 24px
|
|
||||||
line-height 60px
|
|
||||||
|
|
||||||
white-space nowrap
|
|
||||||
overflow-x auto
|
|
||||||
overflow-y hidden
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
articleNavBgColor = #353535
|
|
||||||
|
|
||||||
.ArticleNavigator
|
|
||||||
background-color articleNavBgColor
|
|
||||||
absolute top bottom left
|
|
||||||
width 200px
|
|
||||||
border-right 1px solid borderColor
|
|
||||||
color white
|
|
||||||
.userInfo
|
|
||||||
height 60px
|
|
||||||
display block
|
|
||||||
border-bottom 1px solid borderColor
|
|
||||||
.userProfileName
|
|
||||||
color brandColor
|
|
||||||
font-size 28px
|
|
||||||
padding 6px 0 0 10px
|
|
||||||
white-space nowrap
|
|
||||||
text-overflow ellipsis
|
|
||||||
overflow hidden
|
|
||||||
.userName
|
|
||||||
color white
|
|
||||||
padding-left 20px
|
|
||||||
margin-top 3px
|
|
||||||
.tutorial
|
|
||||||
position fixed
|
|
||||||
z-index 35
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
pointer-event none
|
|
||||||
font-style italic
|
|
||||||
transition 0.1s
|
|
||||||
&.hide
|
|
||||||
opacity 0
|
|
||||||
.settingBtn
|
|
||||||
width 22px
|
|
||||||
height 22px
|
|
||||||
line-height 22px
|
|
||||||
border-radius 11px
|
|
||||||
position absolute
|
|
||||||
top 19px
|
|
||||||
right 14px
|
|
||||||
color white
|
|
||||||
padding 0
|
|
||||||
background-color transparent
|
|
||||||
border 1px solid white
|
|
||||||
z-index 31
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top -5px
|
|
||||||
margin-left 10px
|
|
||||||
&:hover
|
|
||||||
.tooltip
|
|
||||||
opacity 1
|
|
||||||
&:active
|
|
||||||
background-color brandColor
|
|
||||||
border-color brandColor
|
|
||||||
.controlSection
|
|
||||||
height 88px
|
|
||||||
padding 22px 15px
|
|
||||||
margin-bottom 44px
|
|
||||||
.tutorial
|
|
||||||
fixed top left
|
|
||||||
z-index 35
|
|
||||||
pointer-event none
|
|
||||||
font-style italic
|
|
||||||
transition 0.1s
|
|
||||||
&.hide
|
|
||||||
opacity 0
|
|
||||||
.newPostBtn
|
|
||||||
position relative
|
|
||||||
border none
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
height 44px
|
|
||||||
width 170px
|
|
||||||
border-radius 5px
|
|
||||||
font-size 20px
|
|
||||||
transition 0.1s
|
|
||||||
z-index 30
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-left 48px
|
|
||||||
margin-top -3px
|
|
||||||
&:hover
|
|
||||||
background-color lighten(brandColor, 7%)
|
|
||||||
.tooltip
|
|
||||||
opacity 1
|
|
||||||
.folders, .members
|
|
||||||
.header
|
|
||||||
border-bottom 1px solid borderColor
|
|
||||||
padding-bottom 5px
|
|
||||||
margin-bottom 10px
|
|
||||||
clearfix()
|
|
||||||
position relative
|
|
||||||
z-index 30
|
|
||||||
.title
|
|
||||||
float left
|
|
||||||
padding-left 10px
|
|
||||||
font-size 18px
|
|
||||||
line-height 22px
|
|
||||||
.addBtn
|
|
||||||
float right
|
|
||||||
margin-right 15px
|
|
||||||
width 22px
|
|
||||||
height 22px
|
|
||||||
font-size 10px
|
|
||||||
padding 0
|
|
||||||
line-height 22px
|
|
||||||
border 1px solid white
|
|
||||||
border-radius 11px
|
|
||||||
background-color transparent
|
|
||||||
color white
|
|
||||||
padding 0
|
|
||||||
font-weight bold
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top -6px
|
|
||||||
margin-left 11px
|
|
||||||
&:hover
|
|
||||||
.tooltip
|
|
||||||
opacity 1
|
|
||||||
&:active
|
|
||||||
background-color brandColor
|
|
||||||
border-color brandColor
|
|
||||||
.folders
|
|
||||||
absolute bottom
|
|
||||||
top 200px
|
|
||||||
width 100%
|
|
||||||
.header
|
|
||||||
.tutorial
|
|
||||||
position fixed
|
|
||||||
z-index 35px
|
|
||||||
top 200px
|
|
||||||
font-style italic
|
|
||||||
.folderList
|
|
||||||
absolute bottom
|
|
||||||
top 38px
|
|
||||||
overflow-y auto
|
|
||||||
.folderList button
|
|
||||||
height 33px
|
|
||||||
width 199px
|
|
||||||
border none
|
|
||||||
text-align left
|
|
||||||
font-size 14px
|
|
||||||
background-color transparent
|
|
||||||
color white
|
|
||||||
padding-left 15px
|
|
||||||
overflow ellipsis
|
|
||||||
&:hover
|
|
||||||
background-color transparentify(white, 5%)
|
|
||||||
&.active, &:active
|
|
||||||
background-color brandColor
|
|
||||||
.members
|
|
||||||
.memberList>div
|
|
||||||
height 33px
|
|
||||||
width 200px
|
|
||||||
margin-bottom 5px
|
|
||||||
padding-left 15px
|
|
||||||
.memberImage
|
|
||||||
float left
|
|
||||||
margin-top 5.5px
|
|
||||||
border-radius 11px
|
|
||||||
.memberProfileName
|
|
||||||
float left
|
|
||||||
line-height 33px
|
|
||||||
margin-left 7px
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
@require './components/UserNavigator'
|
|
||||||
@require './components/ArticleNavigator'
|
|
||||||
@require './components/ArticleTopBar'
|
|
||||||
@require './components/ArticleList'
|
|
||||||
@require './components/ArticleDetail'
|
|
||||||
|
|
||||||
@require './lib/modal'
|
|
||||||
@require './lib/CreateNewTeam'
|
|
||||||
@require './lib/CreateNewFolder'
|
|
||||||
@require './lib/Preferences'
|
|
||||||
@require './lib/Tutorial'
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
/**
|
|
||||||
* React Select
|
|
||||||
* ============
|
|
||||||
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
|
|
||||||
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
|
|
||||||
* MIT License: https://github.com/keystonejs/react-select
|
|
||||||
*/
|
|
||||||
.Select {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.Select-control {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-color: #d9d9d9 #cccccc #b3b3b3;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #333333;
|
|
||||||
cursor: default;
|
|
||||||
outline: none;
|
|
||||||
padding: 8px 52px 8px 10px;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
.Select-control:hover {
|
|
||||||
// box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
.is-searchable.is-open > .Select-control {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
.is-open > .Select-control {
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
background: #ffffff;
|
|
||||||
border-color: #b3b3b3 #cccccc #d9d9d9;
|
|
||||||
}
|
|
||||||
.is-open > .Select-control > .Select-arrow {
|
|
||||||
border-color: transparent transparent #999999;
|
|
||||||
border-width: 0 5px 5px;
|
|
||||||
}
|
|
||||||
.is-searchable.is-focused:not(.is-open) > .Select-control {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
.is-focused:not(.is-open) > .Select-control {
|
|
||||||
// border-color: #0088cc #0099e6 #0099e6;
|
|
||||||
// box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
|
|
||||||
}
|
|
||||||
.Select-placeholder {
|
|
||||||
color: #aaaaaa;
|
|
||||||
padding: 8px 52px 8px 10px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: -15px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.has-value > .Select-control > .Select-placeholder {
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
.Select-input > input {
|
|
||||||
cursor: default;
|
|
||||||
background: none transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
height: auto;
|
|
||||||
border: 0 none;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
outline: none;
|
|
||||||
display: inline-block;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
.is-focused .Select-input > input {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
.Select-control:not(.is-searchable) > .Select-input {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.Select-loading {
|
|
||||||
-webkit-animation: Select-animation-spin 400ms infinite linear;
|
|
||||||
-o-animation: Select-animation-spin 400ms infinite linear;
|
|
||||||
animation: Select-animation-spin 400ms infinite linear;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid #cccccc;
|
|
||||||
border-right-color: #333333;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
margin-top: -8px;
|
|
||||||
position: absolute;
|
|
||||||
right: 30px;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
.has-value > .Select-control > .Select-loading {
|
|
||||||
right: 46px;
|
|
||||||
}
|
|
||||||
.Select-clear {
|
|
||||||
color: #999999;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
position: absolute;
|
|
||||||
right: 17px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.Select-clear:hover {
|
|
||||||
color: #c0392b;
|
|
||||||
}
|
|
||||||
.Select-clear > span {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
.Select-arrow-zone {
|
|
||||||
content: " ";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.Select-arrow {
|
|
||||||
border-color: #999999 transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 5px 0;
|
|
||||||
content: " ";
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
margin-top: -ceil(2.5px);
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 14px;
|
|
||||||
width: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.Select-menu-outer {
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid #cccccc;
|
|
||||||
border-top-color: #e6e6e6;
|
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: -1px;
|
|
||||||
max-height: 200px;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
.Select-menu {
|
|
||||||
max-height: 198px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.Select-option {
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #666666;
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
.Select-option:last-child {
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
}
|
|
||||||
.Select-option.is-focused {
|
|
||||||
background-color: #f2f9fc;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
.Select-option.is-disabled {
|
|
||||||
color: #cccccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.Select-noresults {
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #999999;
|
|
||||||
cursor: default;
|
|
||||||
display: block;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
.Select.is-multi .Select-control {
|
|
||||||
padding: 2px 52px 2px 3px;
|
|
||||||
}
|
|
||||||
.Select.is-multi .Select-input {
|
|
||||||
vertical-align: middle;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
margin: 2px;
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
.Select-item {
|
|
||||||
background-color: brandColor;
|
|
||||||
border-radius: 2px;
|
|
||||||
// border: 1px solid #c9e6f2;
|
|
||||||
color: white;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1em;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
.Select-item-icon,
|
|
||||||
.Select-item-label {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.Select-item-label {
|
|
||||||
cursor: default;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
padding: 3px 5px;
|
|
||||||
}
|
|
||||||
.Select-item-label .Select-item-label__a {
|
|
||||||
color: white;
|
|
||||||
cursor: white;
|
|
||||||
}
|
|
||||||
.Select-item-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom-left-radius: 2px;
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-right: 1px solid darken(brandColor, 10%)
|
|
||||||
padding: 2px 5px 4px;
|
|
||||||
}
|
|
||||||
.Select-item-icon:hover,
|
|
||||||
.Select-item-icon:focus {
|
|
||||||
background-color: lighten(brandColor, 10%)
|
|
||||||
}
|
|
||||||
.Select-item-icon:active {
|
|
||||||
background-color: #c9e6f2;
|
|
||||||
}
|
|
||||||
.Select.is-multi.is-disabled .Select-item {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
.Select.is-multi.is-disabled .Select-item-icon {
|
|
||||||
cursor: not-allowed;
|
|
||||||
border-right: 1px solid #d9d9d9;
|
|
||||||
}
|
|
||||||
.Select.is-multi.is-disabled .Select-item-icon:hover,
|
|
||||||
.Select.is-multi.is-disabled .Select-item-icon:focus,
|
|
||||||
.Select.is-multi.is-disabled .Select-item-icon:active {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
@keyframes Select-animation-spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(1turn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-webkit-keyframes Select-animation-spin {
|
|
||||||
to {
|
|
||||||
-webkit-transform: rotate(1turn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
.LoginContainer, .SignupContainer
|
|
||||||
margin 0 auto
|
|
||||||
padding 105px 15px
|
|
||||||
box-sizing border-box
|
|
||||||
color inactiveTextColor
|
|
||||||
.logo
|
|
||||||
width 150px
|
|
||||||
height 150px
|
|
||||||
display block
|
|
||||||
margin 0 auto
|
|
||||||
.authNavigator
|
|
||||||
margin 15px 0 25px
|
|
||||||
a
|
|
||||||
font-size 1.5em
|
|
||||||
text-decoration none
|
|
||||||
color inactiveTextColor
|
|
||||||
&:hover, &.hover, &:active, &.active
|
|
||||||
color brandColor
|
|
||||||
.socialControl
|
|
||||||
text-align center
|
|
||||||
margin 25px 0
|
|
||||||
p
|
|
||||||
margin-bottom 25px
|
|
||||||
.facebookBtn, .githubBtn
|
|
||||||
margin 0 45px
|
|
||||||
width 50px
|
|
||||||
height 50px
|
|
||||||
line-height 50px
|
|
||||||
font-size 25px
|
|
||||||
text-align center
|
|
||||||
background-image none
|
|
||||||
color white
|
|
||||||
border none
|
|
||||||
border-radius 25px
|
|
||||||
cursor pointer
|
|
||||||
.facebookBtn
|
|
||||||
background-color facebookColor
|
|
||||||
&:hover, &.hover
|
|
||||||
background-color lighten(facebookColor, 25%)
|
|
||||||
.githubBtn
|
|
||||||
background-color githubBtn
|
|
||||||
font-size 30px
|
|
||||||
line-height 30px
|
|
||||||
&:hover, &.hover
|
|
||||||
background-color lighten(githubBtn, 25%)
|
|
||||||
.divider
|
|
||||||
.dividerLabel
|
|
||||||
text-align center
|
|
||||||
position relative
|
|
||||||
top -27px
|
|
||||||
font-size 1.3em
|
|
||||||
background-color backgroundColor
|
|
||||||
margin 0 auto
|
|
||||||
width 50px
|
|
||||||
form
|
|
||||||
width 400px
|
|
||||||
margin 0 auto 45px
|
|
||||||
.alertInfo, .alertError
|
|
||||||
margin-top 15px
|
|
||||||
margin-bottom 15px
|
|
||||||
padding 10px
|
|
||||||
border-radius 5px
|
|
||||||
line-height 1.6
|
|
||||||
text-align center
|
|
||||||
.alertInfo
|
|
||||||
alertInfo()
|
|
||||||
.alertError
|
|
||||||
alertError()
|
|
||||||
div.formField
|
|
||||||
input
|
|
||||||
stripInput()
|
|
||||||
height 33px
|
|
||||||
width 100%
|
|
||||||
margin-bottom 10px
|
|
||||||
text-align center
|
|
||||||
font-size 1.1em
|
|
||||||
&:last-child
|
|
||||||
margin-top 15px
|
|
||||||
button.logInButton
|
|
||||||
btnPrimary()
|
|
||||||
height 44px
|
|
||||||
border-radius 22px
|
|
||||||
display block
|
|
||||||
width 200px
|
|
||||||
font-size 1em
|
|
||||||
margin 0 auto
|
|
||||||
p.alert
|
|
||||||
text-align center
|
|
||||||
font-size 0.8em
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
navigationWidth= 200px
|
|
||||||
articleListWidth= 275px
|
|
||||||
|
|
||||||
.PlanetContainer
|
|
||||||
absolute top bottom right left
|
|
||||||
.tags
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow-x: auto;
|
|
||||||
a
|
|
||||||
margin 0 2px
|
|
||||||
text-decoration underline
|
|
||||||
cursor pointer
|
|
||||||
font-size 0.95em
|
|
||||||
&.noTag
|
|
||||||
color inactiveTextColor
|
|
||||||
font-size 0.8em
|
|
||||||
|
|
||||||
.PlanetHeader
|
|
||||||
absolute left right top
|
|
||||||
overflow-y hidden
|
|
||||||
height 55px
|
|
||||||
background-color white
|
|
||||||
border-bottom solid 1px borderColor
|
|
||||||
box-sizing border-box
|
|
||||||
padding 5px 15px
|
|
||||||
clearfix()
|
|
||||||
.headerLabel
|
|
||||||
noSelect()
|
|
||||||
absolute top left bottom
|
|
||||||
overflow hidden
|
|
||||||
display inline-block
|
|
||||||
width navigationWidth
|
|
||||||
.userName
|
|
||||||
position absolute
|
|
||||||
left 15px
|
|
||||||
top 30px
|
|
||||||
width 140px
|
|
||||||
font-size 1em
|
|
||||||
color textColor
|
|
||||||
text-decoration none
|
|
||||||
&:hover
|
|
||||||
color darken(lightButtonColor, 50%)
|
|
||||||
text-decoration underline
|
|
||||||
.planetName
|
|
||||||
position absolute
|
|
||||||
top 5px
|
|
||||||
left 10px
|
|
||||||
width 145px
|
|
||||||
font-size 1.6em
|
|
||||||
color brandColor
|
|
||||||
overflow hidden
|
|
||||||
text-overflow ellipsis
|
|
||||||
white-space nowrap
|
|
||||||
&:hover
|
|
||||||
color darken(brandBorderColor, 30%)
|
|
||||||
.private
|
|
||||||
position absolute
|
|
||||||
top 12px
|
|
||||||
right 38px
|
|
||||||
width 33px
|
|
||||||
height 33px
|
|
||||||
line-height 33px
|
|
||||||
text-align center
|
|
||||||
color inactiveColor
|
|
||||||
&:hover
|
|
||||||
color textColor
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-left -30px
|
|
||||||
&:hover .tooltip
|
|
||||||
opacity 1
|
|
||||||
|
|
||||||
|
|
||||||
.planetSettingButton
|
|
||||||
position absolute
|
|
||||||
top 15px
|
|
||||||
right 5px
|
|
||||||
font-size 0.8em
|
|
||||||
btnDefault()
|
|
||||||
box-sizing border-box
|
|
||||||
circle()
|
|
||||||
width 26px
|
|
||||||
height 26px
|
|
||||||
text-align center
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
&:focus, &.focus
|
|
||||||
outline none
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top 11px
|
|
||||||
margin-left -36px
|
|
||||||
&:hover .tooltip
|
|
||||||
opacity 1
|
|
||||||
|
|
||||||
.headerControl
|
|
||||||
noSelect()
|
|
||||||
absolute top bottom right
|
|
||||||
left navigationWidth
|
|
||||||
.searchInput
|
|
||||||
display block
|
|
||||||
position absolute
|
|
||||||
top 12px
|
|
||||||
left 0
|
|
||||||
input
|
|
||||||
padding-left 32px
|
|
||||||
width 300px
|
|
||||||
.fa
|
|
||||||
position absolute
|
|
||||||
top 8px
|
|
||||||
left 12px
|
|
||||||
color inactiveTextColor
|
|
||||||
.refreshButton
|
|
||||||
display block
|
|
||||||
position absolute
|
|
||||||
top 15px
|
|
||||||
right 55px
|
|
||||||
width 26px
|
|
||||||
height 26px
|
|
||||||
font-size 0.8em
|
|
||||||
btnDefault()
|
|
||||||
circle()
|
|
||||||
text-align center
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
&:focus, &.focus
|
|
||||||
outline none
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top 11px
|
|
||||||
margin-left -39px
|
|
||||||
&:hover .tooltip
|
|
||||||
opacity 1
|
|
||||||
.logo
|
|
||||||
display block
|
|
||||||
position absolute
|
|
||||||
top 4px
|
|
||||||
right 10px
|
|
||||||
cursor pointer
|
|
||||||
img
|
|
||||||
transition 0.1s
|
|
||||||
opacity 0.9
|
|
||||||
&:hover img, &:hover .tooltip
|
|
||||||
opacity 1
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top -5px
|
|
||||||
margin-left -67px
|
|
||||||
|
|
||||||
|
|
||||||
.PlanetNavigator
|
|
||||||
absolute bottom left
|
|
||||||
noSelect()
|
|
||||||
top 55px
|
|
||||||
width navigationWidth
|
|
||||||
border-right solid 1px highlightenBorderColor
|
|
||||||
padding 10px
|
|
||||||
box-sizing border-box
|
|
||||||
.launchButton
|
|
||||||
border-radius 22px
|
|
||||||
font-size 1.1em
|
|
||||||
nav
|
|
||||||
a
|
|
||||||
display block
|
|
||||||
box-sizing border-box
|
|
||||||
padding 15px 15px
|
|
||||||
margin 10px 0
|
|
||||||
border-radius 10px
|
|
||||||
text-decoration none
|
|
||||||
background-color transparent
|
|
||||||
color textColor
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
btnDefault()
|
|
||||||
border none
|
|
||||||
|
|
||||||
.PlanetArticleList
|
|
||||||
absolute bottom right
|
|
||||||
left navigationWidth
|
|
||||||
top 55px
|
|
||||||
width articleListWidth
|
|
||||||
border-right solid 1px highlightenBorderColor
|
|
||||||
|
|
||||||
&>ul
|
|
||||||
absolute top bottom left right
|
|
||||||
overflow-y auto
|
|
||||||
li
|
|
||||||
.articleItem
|
|
||||||
noSelect()
|
|
||||||
border solid 2px transparent
|
|
||||||
position relative
|
|
||||||
height 94px
|
|
||||||
width 100%
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
.itemLeft
|
|
||||||
position absolute
|
|
||||||
top 4px
|
|
||||||
bottom 4px
|
|
||||||
width 38px
|
|
||||||
padding 3px 0 3px 3px
|
|
||||||
text-align center
|
|
||||||
.profileImage
|
|
||||||
margin-bottom 5px
|
|
||||||
circle()
|
|
||||||
.fa
|
|
||||||
line-height 25px
|
|
||||||
.itemRight
|
|
||||||
position absolute
|
|
||||||
top 4px
|
|
||||||
bottom 4px
|
|
||||||
right 2px
|
|
||||||
left 40px
|
|
||||||
overflow-x hidden
|
|
||||||
padding 3px 10px 3px 3px
|
|
||||||
.itemInfo
|
|
||||||
margin 5px 0 13px
|
|
||||||
color lighten(textColor, 25%)
|
|
||||||
font-size 0.7em
|
|
||||||
.userProfileName
|
|
||||||
color brandColor
|
|
||||||
font-size 1.2em
|
|
||||||
.description
|
|
||||||
line-height 120%
|
|
||||||
margin-bottom 10px
|
|
||||||
font-size 1em
|
|
||||||
overflow-x hidden
|
|
||||||
white-space nowrap
|
|
||||||
text-overflow ellipsis
|
|
||||||
.tags
|
|
||||||
position absolute
|
|
||||||
bottom 5px
|
|
||||||
font-size 0.9em
|
|
||||||
&:hover, &.hover
|
|
||||||
background-color hoverBackgroundColor
|
|
||||||
&:active, &.active
|
|
||||||
background-color white
|
|
||||||
&:active, &.active
|
|
||||||
border-color brandBorderColor
|
|
||||||
.divider
|
|
||||||
border-bottom solid 1px borderColor
|
|
||||||
|
|
||||||
.PlanetArticleDetail
|
|
||||||
absolute right bottom
|
|
||||||
top 55px
|
|
||||||
left navigationWidth + articleListWidth
|
|
||||||
.detailHeader
|
|
||||||
border solid 2px transparent
|
|
||||||
position relative
|
|
||||||
height 105px
|
|
||||||
width 100%
|
|
||||||
transition 0.1s
|
|
||||||
.itemLeft
|
|
||||||
position absolute
|
|
||||||
top 7px
|
|
||||||
bottom 4px
|
|
||||||
width 38px
|
|
||||||
padding 3px 0 3px 3px
|
|
||||||
text-align center
|
|
||||||
.profileImage
|
|
||||||
margin-bottom 5px
|
|
||||||
circle()
|
|
||||||
.fa
|
|
||||||
line-height 25px
|
|
||||||
.itemRight
|
|
||||||
position absolute
|
|
||||||
top 7px
|
|
||||||
bottom 4px
|
|
||||||
right 2px
|
|
||||||
left 40px
|
|
||||||
overflow-x hidden
|
|
||||||
padding 3px 10px 3px 3px
|
|
||||||
.itemInfo
|
|
||||||
margin 5px 0 13px
|
|
||||||
color lighten(textColor, 25%)
|
|
||||||
font-size 0.7em
|
|
||||||
.userProfileName
|
|
||||||
color brandColor
|
|
||||||
font-size 1.2em
|
|
||||||
.description
|
|
||||||
line-height 120%
|
|
||||||
margin-bottom 10px
|
|
||||||
font-size 1em
|
|
||||||
overflow-x auto
|
|
||||||
white-space nowrap
|
|
||||||
.tags
|
|
||||||
position absolute
|
|
||||||
bottom 5px
|
|
||||||
font-size 0.9em
|
|
||||||
.itemControl
|
|
||||||
position absolute
|
|
||||||
z-index 1
|
|
||||||
top 2px
|
|
||||||
right 2px
|
|
||||||
.deleteButton, .editButton
|
|
||||||
btnDefault()
|
|
||||||
text-align center
|
|
||||||
width 33px
|
|
||||||
height 33px
|
|
||||||
border-radius 16.5px
|
|
||||||
font-size 15px
|
|
||||||
margin 0 3px
|
|
||||||
.tooltip
|
|
||||||
tooltip()
|
|
||||||
margin-top 10px
|
|
||||||
&:hover .tooltip
|
|
||||||
opacity 1
|
|
||||||
.editButton .tooltip
|
|
||||||
margin-left -12px
|
|
||||||
.deleteButton .tooltip
|
|
||||||
margin-left -26px
|
|
||||||
.detailBody
|
|
||||||
absolute left right bottom
|
|
||||||
top 105px
|
|
||||||
.content
|
|
||||||
position absolute
|
|
||||||
top 5px
|
|
||||||
bottom 5px
|
|
||||||
left 2px
|
|
||||||
right 2px
|
|
||||||
box-sizing border-box
|
|
||||||
padding 5px
|
|
||||||
border-top solid 1px borderColor
|
|
||||||
&.noteDetail
|
|
||||||
.detailBody .content
|
|
||||||
overflow-x hidden
|
|
||||||
overflow-y auto
|
|
||||||
marked()
|
|
||||||
&.codeDetail
|
|
||||||
.detailBody .content
|
|
||||||
.ace_editor
|
|
||||||
absolute left right top bottom
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
userNavigatorWidth = 200px
|
|
||||||
userNavigatorBgColor = #333
|
|
||||||
userNavigatorColor = #DDD
|
|
||||||
userNavigatorProfileNameColor = brandColor
|
|
||||||
userNavigatorBorderColor = #666
|
|
||||||
|
|
||||||
userContentBgColor = #E6E6E6
|
|
||||||
|
|
||||||
.UserContainer
|
|
||||||
absolute top bottom right
|
|
||||||
left 60px
|
|
||||||
.content
|
|
||||||
absolute top bottom right
|
|
||||||
left userNavigatorWidth
|
|
||||||
background-color userContentBgColor
|
|
||||||
.UserNavigator
|
|
||||||
absolute left top bottom
|
|
||||||
width userNavigatorWidth
|
|
||||||
background-color userNavigatorBgColor
|
|
||||||
color userNavigatorColor
|
|
||||||
noSelect()
|
|
||||||
&>.profile
|
|
||||||
height 60px
|
|
||||||
padding 10px 15px 0
|
|
||||||
box-sizing border-box
|
|
||||||
position relative
|
|
||||||
border-bottom solid 1px userNavigatorBorderColor
|
|
||||||
cursor pointer
|
|
||||||
&>.profileName
|
|
||||||
color userNavigatorProfileNameColor
|
|
||||||
font-size 22px
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
&>.name
|
|
||||||
padding 5px 10px
|
|
||||||
font-size 14px
|
|
||||||
color userNavigatorColor
|
|
||||||
cursor pointer
|
|
||||||
transition 0.1s
|
|
||||||
&>.dropdownIcon
|
|
||||||
position absolute
|
|
||||||
top 20px
|
|
||||||
right 25px
|
|
||||||
float right
|
|
||||||
width 20px
|
|
||||||
height 20px
|
|
||||||
line-height 20px
|
|
||||||
font-size 8px
|
|
||||||
border solid 1px userNavigatorColor
|
|
||||||
border-radius 12.5px
|
|
||||||
text-align center
|
|
||||||
transition 0.1s
|
|
||||||
&:hover
|
|
||||||
&>.profileName
|
|
||||||
color lighten(brandColor, 10%)
|
|
||||||
&>.name
|
|
||||||
color white
|
|
||||||
&>.dropdownIcon
|
|
||||||
border-color white
|
|
||||||
&:active
|
|
||||||
&>.dropdownIcon
|
|
||||||
background-color brandColor
|
|
||||||
border-color brandColor
|
|
||||||
&>.control
|
|
||||||
padding 15px 15px
|
|
||||||
&>.newPostButton
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
height 44px
|
|
||||||
width 100%
|
|
||||||
border none
|
|
||||||
border-radius 5px
|
|
||||||
font-size 16px
|
|
||||||
font-weight 600
|
|
||||||
transition 0.1s
|
|
||||||
&:hover
|
|
||||||
background-color lighten(brandColor, 10%)
|
|
||||||
&>.menu
|
|
||||||
absolute left right bottom
|
|
||||||
top 134px
|
|
||||||
padding 15px 0
|
|
||||||
overflow auto
|
|
||||||
&>.menuGruop
|
|
||||||
&>.label
|
|
||||||
border-bottom 1px solid userNavigatorBorderColor
|
|
||||||
padding 10px 15px
|
|
||||||
font-size 18px
|
|
||||||
margin-bottom 10px
|
|
||||||
&>.plusButton
|
|
||||||
float right
|
|
||||||
width 20px
|
|
||||||
height 20px
|
|
||||||
margin-top -2.5px
|
|
||||||
margin-right -5px
|
|
||||||
line-height 15px
|
|
||||||
font-size 8px
|
|
||||||
border solid 1px userNavigatorColor
|
|
||||||
border-radius 10px
|
|
||||||
background-color transparent
|
|
||||||
text-align center
|
|
||||||
color userNavigatorColor
|
|
||||||
&:hover
|
|
||||||
border-color white
|
|
||||||
color white
|
|
||||||
&:active
|
|
||||||
background-color brandColor
|
|
||||||
border-color brandColor
|
|
||||||
&>.folders
|
|
||||||
.folderButton
|
|
||||||
padding 10px 25px
|
|
||||||
width 100%
|
|
||||||
background-color transparent
|
|
||||||
border none
|
|
||||||
font-size 14px
|
|
||||||
color userNavigatorColor
|
|
||||||
transition 0.1s
|
|
||||||
text-align left
|
|
||||||
&:hover
|
|
||||||
background-color transparentify(white, 20%)
|
|
||||||
color white
|
|
||||||
&.active
|
|
||||||
background-color brandColor
|
|
||||||
color white
|
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
@import '../mixins/*'
|
@import '../mixins/*'
|
||||||
global-reset()
|
global-reset()
|
||||||
@import '../shared/*'
|
@import '../shared/*'
|
||||||
@import './components/*'
|
@import './ArticleNavigator'
|
||||||
@import './containers/*'
|
@import './ArticleTopbar'
|
||||||
@import './HomeContainer'
|
@import './ArticleList'
|
||||||
|
@import './ArticleDetail'
|
||||||
|
@import './modal/*'
|
||||||
|
|
||||||
*
|
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||||
-webkit-app-region no-drag
|
|
||||||
-webkit-user-select none
|
|
||||||
|
|
||||||
html, body
|
html, body
|
||||||
width 100%
|
width 100%
|
||||||
@@ -17,13 +17,13 @@ html, body
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
|
|
||||||
body
|
body
|
||||||
font-family "Lato"
|
font-family DEFAULT_FONTS
|
||||||
color textColor
|
color textColor
|
||||||
font-size fontSize
|
font-size fontSize
|
||||||
font-weight 400
|
font-weight 400
|
||||||
|
|
||||||
button, input, select
|
button, input, select, textarea
|
||||||
font-family "Lato"
|
font-family DEFAULT_FONTS
|
||||||
|
|
||||||
div, span, a, button, input, textarea
|
div, span, a, button, input, textarea
|
||||||
box-sizing border-box
|
box-sizing border-box
|
||||||
@@ -31,7 +31,7 @@ div, span, a, button, input, textarea
|
|||||||
a
|
a
|
||||||
color brandColor
|
color brandColor
|
||||||
&:hover
|
&:hover
|
||||||
color darken(brandColor, 15%)
|
color lighten(brandColor, 5%)
|
||||||
&:visited
|
&:visited
|
||||||
color brandColor
|
color brandColor
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
33
browser/styles/main/modal/DeleteArticleModal.styl
Normal file
33
browser/styles/main/modal/DeleteArticleModal.styl
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.DeleteArticleModal.modal
|
||||||
|
width 350px !important
|
||||||
|
top 100px
|
||||||
|
user-select none
|
||||||
|
.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%)
|
||||||
|
&:focus
|
||||||
|
border-color focusBorderColor
|
||||||
|
&.danger
|
||||||
|
border-color #E9432A
|
||||||
|
background-color #E9432A
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color lighten(#E9432A, 15%)
|
||||||
|
&:focus
|
||||||
|
background-color lighten(#E9432A, 15%)
|
||||||
@@ -60,7 +60,7 @@ iptFocusBorderColor = #369DCD
|
|||||||
left 180px
|
left 180px
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
&>.section
|
&>.section
|
||||||
padding 10px
|
padding 10px 20px
|
||||||
border-bottom 1px solid borderColor
|
border-bottom 1px solid borderColor
|
||||||
overflow-y auto
|
overflow-y auto
|
||||||
&:nth-last-child(1)
|
&:nth-last-child(1)
|
||||||
@@ -69,17 +69,29 @@ iptFocusBorderColor = #369DCD
|
|||||||
font-size 18px
|
font-size 18px
|
||||||
margin 10px 0 5px
|
margin 10px 0 5px
|
||||||
color brandColor
|
color brandColor
|
||||||
&>.sectionInput
|
&>.sectionCheck
|
||||||
|
margin-bottom 5px
|
||||||
height 33px
|
height 33px
|
||||||
|
label
|
||||||
|
width 150px
|
||||||
|
padding-left 15px
|
||||||
|
line-height 33px
|
||||||
|
.sectionCheck-warn
|
||||||
|
font-size 12px
|
||||||
|
margin-left 10px
|
||||||
|
border-left 2px solid brandColor
|
||||||
|
padding-left 5px
|
||||||
|
&>.sectionInput
|
||||||
margin-bottom 5px
|
margin-bottom 5px
|
||||||
clearfix()
|
clearfix()
|
||||||
|
height 33px
|
||||||
label
|
label
|
||||||
width 180px
|
width 150px
|
||||||
padding-left 15px
|
padding-left 15px
|
||||||
float left
|
float left
|
||||||
line-height 33px
|
line-height 33px
|
||||||
input
|
input
|
||||||
width 300px
|
width 250px
|
||||||
float left
|
float left
|
||||||
height 33px
|
height 33px
|
||||||
border-radius 5px
|
border-radius 5px
|
||||||
@@ -89,6 +101,51 @@ iptFocusBorderColor = #369DCD
|
|||||||
outline none
|
outline none
|
||||||
&:focus
|
&:focus
|
||||||
border-color iptFocusBorderColor
|
border-color iptFocusBorderColor
|
||||||
|
&>.sectionSelect
|
||||||
|
margin-bottom 5px
|
||||||
|
clearfix()
|
||||||
|
height 33px
|
||||||
|
label
|
||||||
|
width 150px
|
||||||
|
padding-left 15px
|
||||||
|
float left
|
||||||
|
line-height 33px
|
||||||
|
select
|
||||||
|
float left
|
||||||
|
width 200px
|
||||||
|
height 25px
|
||||||
|
margin-top 4px
|
||||||
|
border-radius 5px
|
||||||
|
border 1px solid borderColor
|
||||||
|
padding 0 10px
|
||||||
|
font-size 14px
|
||||||
|
outline none
|
||||||
|
&:focus
|
||||||
|
border-color iptFocusBorderColor
|
||||||
|
&>.sectionMultiSelect
|
||||||
|
margin-bottom 5px
|
||||||
|
clearfix()
|
||||||
|
height 33px
|
||||||
|
label
|
||||||
|
width 150px
|
||||||
|
padding-left 15px
|
||||||
|
float left
|
||||||
|
line-height 33px
|
||||||
|
.sectionMultiSelect-input
|
||||||
|
float left
|
||||||
|
select
|
||||||
|
width 80px
|
||||||
|
height 25px
|
||||||
|
margin-top 4px
|
||||||
|
border-radius 5px
|
||||||
|
border 1px solid borderColor
|
||||||
|
padding 0 10px
|
||||||
|
font-size 14px
|
||||||
|
outline none
|
||||||
|
margin-left 5px
|
||||||
|
margin-right 15px
|
||||||
|
&:focus
|
||||||
|
border-color iptFocusBorderColor
|
||||||
&>.sectionConfirm
|
&>.sectionConfirm
|
||||||
clearfix()
|
clearfix()
|
||||||
padding 5px 15px
|
padding 5px 15px
|
||||||
@@ -440,19 +497,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 +562,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 +596,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
|
||||||
@@ -561,10 +681,3 @@ iptFocusBorderColor = #369DCD
|
|||||||
color brandColor
|
color brandColor
|
||||||
&:hover
|
&:hover
|
||||||
color lighten(brandColor, 10%)
|
color lighten(brandColor, 10%)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ slideBgColor4 = #00B493
|
|||||||
|
|
||||||
.Tutorial.modal
|
.Tutorial.modal
|
||||||
background-color slideBgColor0
|
background-color slideBgColor0
|
||||||
color white
|
color white !important
|
||||||
width 720px
|
width 720px
|
||||||
height 480px
|
height 480px
|
||||||
margin-top 75px
|
margin-top 75px
|
||||||
@@ -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
|
||||||
@@ -1,83 +1,137 @@
|
|||||||
marked()
|
marked()
|
||||||
h1, h2, h3, h4, h5, h6, p
|
font-size 14px
|
||||||
&:first-child
|
div.math-rendered
|
||||||
margin-top 0
|
text-align center
|
||||||
|
.math-failed
|
||||||
|
background-color alpha(red, 0.1)
|
||||||
|
color darken(red, 15%)
|
||||||
|
padding 5px
|
||||||
|
margin -5px
|
||||||
|
border-radius 5px
|
||||||
|
sup
|
||||||
|
position relative
|
||||||
|
top -.4em
|
||||||
|
font-size 0.8em
|
||||||
|
vertical-align top
|
||||||
|
sub
|
||||||
|
position relative
|
||||||
|
bottom -.4em
|
||||||
|
font-size 0.8em
|
||||||
|
vertical-align top
|
||||||
|
a
|
||||||
|
color brandColor
|
||||||
|
text-decoration none
|
||||||
|
padding 0 5px
|
||||||
|
border-radius 5px
|
||||||
|
margin -5px
|
||||||
|
transition .1s
|
||||||
|
display inline-block
|
||||||
|
img
|
||||||
|
vertical-align sub
|
||||||
|
&:hover
|
||||||
|
color lighten(brandColor, 5%)
|
||||||
|
text-decoration underline
|
||||||
|
background-color alpha(#FFC95C, 0.3)
|
||||||
|
&:visited
|
||||||
|
color brandColor
|
||||||
|
&.lineAnchor
|
||||||
|
padding 0
|
||||||
|
margin 0
|
||||||
|
display block
|
||||||
|
font-size 0
|
||||||
|
height 0
|
||||||
hr
|
hr
|
||||||
border-top none
|
border-top none
|
||||||
border-bottom solid 1px borderColor
|
border-bottom solid 1px borderColor
|
||||||
margin 15px 0
|
margin 15px 0
|
||||||
h1
|
|
||||||
font-size 2em
|
|
||||||
border-bottom solid 2px borderColor
|
|
||||||
margin 0.33em auto 0.67em
|
|
||||||
h2
|
|
||||||
font-size 1.5em
|
|
||||||
margin 0.42em auto 0.83em
|
|
||||||
h3
|
|
||||||
font-size 1.17em
|
|
||||||
margin 0.5em auto 1em
|
|
||||||
h4
|
|
||||||
font-size 1em
|
|
||||||
margin 0.67em auto 1.33em
|
|
||||||
h5
|
|
||||||
font-size 0.83em
|
|
||||||
margin 0.84em auto 1.67em
|
|
||||||
h6
|
|
||||||
font-size 0.67em
|
|
||||||
margin 1.16em auto 2.33em
|
|
||||||
h1, h2, h3, h4, h5, h6
|
h1, h2, h3, h4, h5, h6
|
||||||
font-weight 700
|
margin 0 0 15px
|
||||||
|
font-weight 600
|
||||||
|
*:not(a.lineAnchor) + h1, *:not(a.lineAnchor) + h2, *:not(a.lineAnchor) + h3, *:not(a.lineAnchor) + h4, *:not(a.lineAnchor) + h5, *:not(a.lineAnchor) + h6
|
||||||
|
margin-top 25px
|
||||||
|
h1
|
||||||
|
font-size 1.8em
|
||||||
|
border-bottom solid 2px borderColor
|
||||||
|
line-height 2em
|
||||||
|
h2
|
||||||
|
font-size 1.66em
|
||||||
line-height 1.8em
|
line-height 1.8em
|
||||||
|
h3
|
||||||
|
font-size 1.33em
|
||||||
|
line-height 1.6625em
|
||||||
|
h4
|
||||||
|
font-size 1.15em
|
||||||
|
line-height 1.4375em
|
||||||
|
h5
|
||||||
|
font-size 1em
|
||||||
|
line-height 1.25em
|
||||||
|
h6
|
||||||
|
font-size 0.8em
|
||||||
|
line-height 1em
|
||||||
|
|
||||||
|
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
|
||||||
|
margin-top 15px
|
||||||
p
|
p
|
||||||
line-height 1.8em
|
line-height 1.9em
|
||||||
margin 15px 0 25px
|
margin 0 0 15px
|
||||||
img
|
img
|
||||||
max-width 100%
|
max-width 100%
|
||||||
strong
|
strong, b
|
||||||
font-weight bold
|
font-weight bold
|
||||||
em
|
em, i
|
||||||
font-style italic
|
font-style italic
|
||||||
s
|
s, del, strike
|
||||||
text-decoration line-through
|
text-decoration line-through
|
||||||
|
u
|
||||||
|
text-decoration underline
|
||||||
blockquote
|
blockquote
|
||||||
border-left solid 4px brandBorderColor
|
border-left solid 4px brandBorderColor
|
||||||
margin 15px 0 25px
|
margin 0 0 15px
|
||||||
padding 0 25px
|
padding 0 25px
|
||||||
ul
|
ul
|
||||||
list-style-type disc
|
list-style-type disc
|
||||||
padding-left 35px
|
padding-left 25px
|
||||||
margin-bottom 35px
|
margin-bottom 15px
|
||||||
li
|
li
|
||||||
display list-item
|
display list-item
|
||||||
line-height 1.8em
|
line-height 1.8em
|
||||||
|
&>li>ul, &>li>ol
|
||||||
|
margin 0
|
||||||
&>li>ul
|
&>li>ul
|
||||||
list-style-type circle
|
list-style-type circle
|
||||||
&>li>ul
|
&>li>ul
|
||||||
list-style-type square
|
list-style-type square
|
||||||
ol
|
ol
|
||||||
list-style-type decimal
|
list-style-type decimal
|
||||||
padding-left 35px
|
padding-left 25px
|
||||||
margin-bottom 35px
|
margin-bottom 15px
|
||||||
li
|
li
|
||||||
display list-item
|
display list-item
|
||||||
line-height 1.8em
|
line-height 1.8em
|
||||||
|
&>li>ul, &>li>ol
|
||||||
|
margin 0
|
||||||
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 alpha(borderColor, 0.3)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
font-size 0.9em
|
font-size 0.9em
|
||||||
color black
|
color black
|
||||||
text-decoration none
|
text-decoration none
|
||||||
background-color #F6F6F6
|
background-color #F6F6F6
|
||||||
|
margin-right 2px
|
||||||
|
*:not(a.lineAnchor) + code
|
||||||
|
margin-left 2px
|
||||||
pre
|
pre
|
||||||
padding 5px
|
padding 5px
|
||||||
border solid 1px borderColor
|
border solid 1px alpha(borderColor, 0.3)
|
||||||
border-radius 5px
|
border-radius 5px
|
||||||
overflow-x auto
|
overflow-x auto
|
||||||
margin 15px 0 25px
|
margin 0 0 15px
|
||||||
background-color #F6F6F6
|
background-color #F6F6F6
|
||||||
|
line-height 1.35em
|
||||||
&>code
|
&>code
|
||||||
|
margin 0
|
||||||
padding 0
|
padding 0
|
||||||
border none
|
border none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ highlightenBorderColor = darken(borderColor, 20%)
|
|||||||
invBorderColor = #404849
|
invBorderColor = #404849
|
||||||
brandBorderColor = #3FB399
|
brandBorderColor = #3FB399
|
||||||
|
|
||||||
|
focusBorderColor = #369DCD
|
||||||
|
|
||||||
buttonBorderColor = #4C4C4C
|
buttonBorderColor = #4C4C4C
|
||||||
|
|
||||||
lightButtonColor = #898989
|
lightButtonColor = #898989
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// export const API_URL = 'http://localhost:8000/'
|
|
||||||
export const API_URL = 'http://boost-api4.elasticbeanstalk.com/'
|
|
||||||
// export API_URL 'https://api2.b00st.io/'
|
|
||||||
35
contributing.md
Normal file
35
contributing.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Contributing to Boostnote
|
||||||
|
|
||||||
|
> English below.
|
||||||
|
|
||||||
|
## About Pull Request
|
||||||
|
|
||||||
|
### やり方
|
||||||
|
|
||||||
|
現状特に`dev`ブランチを用意しないつもりなので、最新VersionのブランチにPullrequestを送ってください。
|
||||||
|
|
||||||
|
### Pull requsetの著作権
|
||||||
|
|
||||||
|
Pull requestをすることはその変化分のコードの著作権をMAISIN&CO.に譲渡することに同意することになります。
|
||||||
|
|
||||||
|
アプリケーションのLicenseのをいつでも変える選択肢を残したいからです。
|
||||||
|
しかし、これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
|
||||||
|
もし、このアプリケーションで金を稼ごうとするならBoostnote専用のCloud storageの提供やMobile appとの連動、何か特殊なプレミアム機能の提供など形になると思います。
|
||||||
|
現在考えられているのは、GPL v3の場合、他のライセンスとの互換が不可能であるため、もしより自由なLicense(BSD, MIT)に変える時に改めて著作権者としてライセンスし直す選択肢を残したいぐらいのイメージです。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Contributing to Boostnote(ENG)
|
||||||
|
|
||||||
|
## About Pull Request
|
||||||
|
|
||||||
|
### How to
|
||||||
|
|
||||||
|
Make a new PR to the branch named latest version. This is because there is no `dev` branch currently.
|
||||||
|
|
||||||
|
### Copyright of Pull Request
|
||||||
|
|
||||||
|
If you make a pull request, It means you agree to transfer the copyright of the code changes to MAISIN&CO.
|
||||||
|
|
||||||
|
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
|
||||||
|
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
|
||||||
212
gruntfile.js
Normal file
212
gruntfile.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const ChildProcess = require('child_process')
|
||||||
|
const packager = require('electron-packager')
|
||||||
|
|
||||||
|
module.exports = function (grunt) {
|
||||||
|
var auth_code
|
||||||
|
try {
|
||||||
|
auth_code = grunt.file.readJSON('secret/auth_code.json')
|
||||||
|
} catch (e) {
|
||||||
|
if (e.origError.code === 'ENOENT') {
|
||||||
|
console.warn('secret/auth_code.json is not found. CodeSigning is not available.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const OSX_COMMON_NAME = auth_code != null ? auth_code.OSX_COMMON_NAME : ''
|
||||||
|
const WIN_CERT_PASSWORD = auth_code != null ? auth_code.WIN_CERT_PASSWORD : ''
|
||||||
|
|
||||||
|
var initConfig = {
|
||||||
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
|
'create-windows-installer': {
|
||||||
|
x64: {
|
||||||
|
appDirectory: path.join(__dirname, 'dist', 'Boostnote-win32-x64'),
|
||||||
|
outputDirectory: path.join(__dirname, 'dist'),
|
||||||
|
authors: 'MAISIN&CO., Inc.',
|
||||||
|
exe: 'Boostnote.exe',
|
||||||
|
loadingGif: path.join(__dirname, 'resources/boostnote-install.gif'),
|
||||||
|
iconUrl: path.join(__dirname, 'resources/app.ico'),
|
||||||
|
setupIcon: path.join(__dirname, 'resources/dmg.ico'),
|
||||||
|
certificateFile: path.join(__dirname, 'secret', 'authenticode_cer.p12'),
|
||||||
|
certificatePassword: WIN_CERT_PASSWORD,
|
||||||
|
noMsi: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grunt.initConfig(initConfig)
|
||||||
|
grunt.loadNpmTasks('grunt-electron-installer')
|
||||||
|
|
||||||
|
grunt.registerTask('compile', function () {
|
||||||
|
var done = this.async()
|
||||||
|
var execPath = path.join('node_modules', '.bin', 'webpack') + ' --config webpack-production.config.js'
|
||||||
|
grunt.log.writeln(execPath)
|
||||||
|
ChildProcess.exec(execPath,
|
||||||
|
{
|
||||||
|
env: Object.assign({}, process.env, {
|
||||||
|
BABEL_ENV: 'production',
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
function (err, stdout, stderr) {
|
||||||
|
grunt.log.writeln(stdout)
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
grunt.log.writeln(stderr)
|
||||||
|
done(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('pack', function (platform) {
|
||||||
|
grunt.log.writeln(path.join(__dirname, 'dist'))
|
||||||
|
var done = this.async()
|
||||||
|
var opts = {
|
||||||
|
name: 'Boostnote',
|
||||||
|
arch: 'x64',
|
||||||
|
dir: __dirname,
|
||||||
|
version: grunt.config.get('pkg.config.electron-version'),
|
||||||
|
'app-version': grunt.config.get('pkg.version'),
|
||||||
|
'app-bundle-id': 'com.maisin.boost',
|
||||||
|
asar: true,
|
||||||
|
prune: true,
|
||||||
|
overwrite: true,
|
||||||
|
out: path.join(__dirname, 'dist'),
|
||||||
|
ignore: /submodules\/ace\/(?!src-min)|submodules\/ace\/(?=src-min-noconflict)|node_modules\/devicon\/icons|dist|^\/browser|^\/secret|\.babelrc|\.gitignore|^\/\.gitmodules|^\/gruntfile|^\/readme.md|^\/webpack|^\/appdmg\.json/
|
||||||
|
}
|
||||||
|
switch (platform) {
|
||||||
|
case 'win':
|
||||||
|
Object.assign(opts, {
|
||||||
|
platform: 'win32',
|
||||||
|
icon: path.join(__dirname, 'resources/app.ico'),
|
||||||
|
'version-string': {
|
||||||
|
CompanyName: 'MAISIN&CO., Inc.',
|
||||||
|
LegalCopyright: '© 2015 MAISIN&CO., Inc. All rights reserved.',
|
||||||
|
FileDescription: 'Boostnote',
|
||||||
|
OriginalFilename: 'Boostnote',
|
||||||
|
FileVersion: grunt.config.get('pkg.version'),
|
||||||
|
ProductVersion: grunt.config.get('pkg.version'),
|
||||||
|
ProductName: 'Boostnote',
|
||||||
|
InternalName: 'Boostnote'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
packager(opts, function (err, appPath) {
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
done(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'osx':
|
||||||
|
Object.assign(opts, {
|
||||||
|
platform: 'darwin',
|
||||||
|
icon: path.join(__dirname, 'resources/app.icns'),
|
||||||
|
'app-category-type': 'public.app-category.developer-tools'
|
||||||
|
})
|
||||||
|
packager(opts, function (err, appPath) {
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
done(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('codesign', function (platform) {
|
||||||
|
var done = this.async()
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
done(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildProcess.exec(`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
|
||||||
|
function (err, stdout, stderr) {
|
||||||
|
grunt.log.writeln(stdout)
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
grunt.log.writeln(stderr)
|
||||||
|
done(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('create-osx-installer', function () {
|
||||||
|
var done = this.async()
|
||||||
|
var execPath = 'appdmg appdmg.json dist/Boostnote-mac.dmg'
|
||||||
|
grunt.log.writeln(execPath)
|
||||||
|
ChildProcess.exec(execPath,
|
||||||
|
function (err, stdout, stderr) {
|
||||||
|
grunt.log.writeln(stdout)
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
grunt.log.writeln(stderr)
|
||||||
|
done(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('zip', function (platform) {
|
||||||
|
var done = this.async()
|
||||||
|
switch (platform) {
|
||||||
|
case 'osx':
|
||||||
|
var execPath = 'cd dist/Boostnote-darwin-x64 && zip -r -y -q ../Boostnote-mac.zip Boostnote.app'
|
||||||
|
grunt.log.writeln(execPath)
|
||||||
|
ChildProcess.exec(execPath,
|
||||||
|
function (err, stdout, stderr) {
|
||||||
|
grunt.log.writeln(stdout)
|
||||||
|
if (err) {
|
||||||
|
grunt.log.writeln(err)
|
||||||
|
grunt.log.writeln(stderr)
|
||||||
|
done(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('build', function (platform) {
|
||||||
|
if (!platform) {
|
||||||
|
platform = process.platform === 'darwin' ? 'osx' : process.platform === 'win32' ? 'win' : null
|
||||||
|
}
|
||||||
|
switch (platform) {
|
||||||
|
case 'win':
|
||||||
|
grunt.task.run(['compile', 'pack:win', 'create-windows-installer'])
|
||||||
|
break
|
||||||
|
case 'osx':
|
||||||
|
grunt.task.run(['compile', 'pack:osx', 'codesign', 'create-osx-installer', 'zip:osx'])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('pre-build', function (platform) {
|
||||||
|
if (!platform) {
|
||||||
|
platform = process.platform === 'darwin' ? 'osx' : process.platform === 'win32' ? 'win' : null
|
||||||
|
}
|
||||||
|
switch (platform) {
|
||||||
|
case 'win':
|
||||||
|
grunt.task.run(['compile', 'pack:win'])
|
||||||
|
break
|
||||||
|
case 'osx':
|
||||||
|
grunt.task.run(['compile', 'pack:osx'])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
grunt.registerTask('default', ['build'])
|
||||||
|
}
|
||||||
10
index.js
Normal file
10
index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
function isFinderCalled () {
|
||||||
|
var argv = process.argv.slice(1)
|
||||||
|
return argv.some(arg => arg.match(/--finder/))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFinderCalled()) {
|
||||||
|
require('./lib/finder-app')
|
||||||
|
} else {
|
||||||
|
require('./lib/main-app')
|
||||||
|
}
|
||||||
191
lib/api.js
191
lib/api.js
@@ -1,191 +0,0 @@
|
|||||||
import superagent from 'superagent'
|
|
||||||
import superagentPromise from 'superagent-promise'
|
|
||||||
import auth from 'boost/auth'
|
|
||||||
|
|
||||||
export const API_URL = 'http://boost-api4.elasticbeanstalk.com/'
|
|
||||||
export const WEB_URL = 'https://b00st.io/'
|
|
||||||
// export const WEB_URL = 'http://localhost:3333/'
|
|
||||||
|
|
||||||
export const request = superagentPromise(superagent, Promise)
|
|
||||||
|
|
||||||
export function login (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'auth/login')
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function signup (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'auth/register')
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateUserInfo (input) {
|
|
||||||
return request
|
|
||||||
.put(API_URL + 'auth/user')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePassword (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'auth/password')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchCurrentUser () {
|
|
||||||
return request
|
|
||||||
.get(API_URL + 'auth/user')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchArticles (userId) {
|
|
||||||
return request
|
|
||||||
.get(API_URL + 'teams/' + userId + '/articles')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createArticle (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'articles/')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveArticle (input) {
|
|
||||||
return request
|
|
||||||
.put(API_URL + 'articles/' + input.id)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyArticle (articleId) {
|
|
||||||
return request
|
|
||||||
.del(API_URL + 'articles/' + articleId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTeam (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'teams')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTeamInfo (teamId, input) {
|
|
||||||
return request
|
|
||||||
.put(API_URL + 'teams/' + teamId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyTeam (teamId) {
|
|
||||||
return request
|
|
||||||
.del(API_URL + 'teams/' + teamId)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchUser (key) {
|
|
||||||
return request
|
|
||||||
.get(API_URL + 'search/users')
|
|
||||||
.query({key: key})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setMember (teamId, input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'teams/' + teamId + '/members')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteMember (teamId, input) {
|
|
||||||
return request
|
|
||||||
.del(API_URL + 'teams/' + teamId + '/members')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFolder (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'folders/')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateFolder (id, input) {
|
|
||||||
return request
|
|
||||||
.put(API_URL + 'folders/' + id)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyFolder (id) {
|
|
||||||
return request
|
|
||||||
.del(API_URL + 'folders/' + id)
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendEmail (input) {
|
|
||||||
return request
|
|
||||||
.post(API_URL + 'mail')
|
|
||||||
.set({
|
|
||||||
Authorization: 'Bearer ' + auth.token()
|
|
||||||
})
|
|
||||||
.send(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
API_URL,
|
|
||||||
WEB_URL,
|
|
||||||
request,
|
|
||||||
login,
|
|
||||||
signup,
|
|
||||||
updateUserInfo,
|
|
||||||
updatePassword,
|
|
||||||
fetchCurrentUser,
|
|
||||||
fetchArticles,
|
|
||||||
createArticle,
|
|
||||||
saveArticle,
|
|
||||||
destroyArticle,
|
|
||||||
createTeam,
|
|
||||||
updateTeamInfo,
|
|
||||||
destroyTeam,
|
|
||||||
searchUser,
|
|
||||||
setMember,
|
|
||||||
deleteMember,
|
|
||||||
createFolder,
|
|
||||||
updateFolder,
|
|
||||||
destroyFolder,
|
|
||||||
sendEmail
|
|
||||||
}
|
|
||||||
34
lib/auth.js
34
lib/auth.js
@@ -1,34 +0,0 @@
|
|||||||
// initial value
|
|
||||||
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
var currentToken = localStorage.getItem('token')
|
|
||||||
|
|
||||||
function user (user, newToken) {
|
|
||||||
if (user != null) {
|
|
||||||
localStorage.setItem('currentUser', JSON.stringify(user))
|
|
||||||
currentUser = user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newToken != null) {
|
|
||||||
localStorage.setItem('token', newToken)
|
|
||||||
currentToken = newToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentUser
|
|
||||||
}
|
|
||||||
|
|
||||||
function token () {
|
|
||||||
return currentToken
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear () {
|
|
||||||
localStorage.removeItem('currentUser')
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
currentUser = null
|
|
||||||
currentToken = null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
clear
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import modes from 'boost/vars/modes'
|
|
||||||
import _ from 'lodash'
|
|
||||||
var ace = window.ace
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
code: React.PropTypes.string,
|
|
||||||
mode: React.PropTypes.string,
|
|
||||||
className: React.PropTypes.string,
|
|
||||||
onChange: React.PropTypes.func,
|
|
||||||
readOnly: React.PropTypes.bool
|
|
||||||
},
|
|
||||||
getDefaultProps: function () {
|
|
||||||
return {
|
|
||||||
readOnly: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
|
||||||
if (nextProps.readOnly !== this.props.readOnly) {
|
|
||||||
this.editor.setReadOnly(!!nextProps.readOnly)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function () {
|
|
||||||
var el = ReactDOM.findDOMNode(this.refs.target)
|
|
||||||
var editor = this.editor = ace.edit(el)
|
|
||||||
editor.$blockScrolling = Infinity
|
|
||||||
editor.setValue(this.props.code)
|
|
||||||
editor.renderer.setShowGutter(true)
|
|
||||||
editor.setTheme('ace/theme/xcode')
|
|
||||||
editor.clearSelection()
|
|
||||||
|
|
||||||
editor.setReadOnly(!!this.props.readOnly)
|
|
||||||
|
|
||||||
var session = editor.getSession()
|
|
||||||
let mode = _.findWhere(modes, {name: this.props.mode})
|
|
||||||
let syntaxMode = mode != null
|
|
||||||
? mode.mode
|
|
||||||
: 'text'
|
|
||||||
session.setMode('ace/mode/' + syntaxMode)
|
|
||||||
|
|
||||||
session.setUseSoftTabs(true)
|
|
||||||
session.setOption('useWorker', false)
|
|
||||||
session.setUseWrapMode(true)
|
|
||||||
|
|
||||||
session.on('change', function (e) {
|
|
||||||
if (this.props.onChange != null) {
|
|
||||||
var value = editor.getValue()
|
|
||||||
this.props.onChange(e, value)
|
|
||||||
}
|
|
||||||
}.bind(this))
|
|
||||||
|
|
||||||
this.setState({editor: editor})
|
|
||||||
},
|
|
||||||
componentDidUpdate: function (prevProps) {
|
|
||||||
if (this.state.editor.getValue() !== this.props.code) {
|
|
||||||
this.state.editor.setValue(this.props.code)
|
|
||||||
this.state.editor.clearSelection()
|
|
||||||
}
|
|
||||||
if (prevProps.mode !== this.props.mode) {
|
|
||||||
var session = this.state.editor.getSession()
|
|
||||||
let mode = _.findWhere(modes, {name: this.props.mode})
|
|
||||||
let syntaxMode = mode != null
|
|
||||||
? mode.mode
|
|
||||||
: 'text'
|
|
||||||
session.setMode('ace/mode/' + syntaxMode)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render: function () {
|
|
||||||
return (
|
|
||||||
<div ref='target' className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import shell from 'shell'
|
|
||||||
var React = require('react')
|
|
||||||
var { PropTypes } = React
|
|
||||||
import markdown from 'boost/markdown'
|
|
||||||
var ReactDOM = require('react-dom')
|
|
||||||
|
|
||||||
function handleAnchorClick (e) {
|
|
||||||
shell.openExternal(e.target.href)
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MarkdownPreview extends React.Component {
|
|
||||||
componentDidMount () {
|
|
||||||
this.addListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this.addListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.removeListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUpdate () {
|
|
||||||
this.removeListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener () {
|
|
||||||
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
|
|
||||||
|
|
||||||
for (var i = 0; i < anchors.length; i++) {
|
|
||||||
anchors[i].addEventListener('click', handleAnchorClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeListener () {
|
|
||||||
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
|
|
||||||
|
|
||||||
for (var i = 0; i < anchors.length; i++) {
|
|
||||||
anchors[i].removeEventListener('click', handleAnchorClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + markdown(this.props.content)}}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MarkdownPreview.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
content: PropTypes.string
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
|
|
||||||
export default class TagSelect extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
input: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown (e) {
|
|
||||||
if (e.keyCode !== 13) return false
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
let tags = this.props.tags.slice(0)
|
|
||||||
let newTag = this.state.input.trim()
|
|
||||||
|
|
||||||
if (newTag.length === 0) {
|
|
||||||
this.setState({input: ''})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(newTag)
|
|
||||||
tags = _.uniq(tags)
|
|
||||||
|
|
||||||
if (_.isFunction(this.props.onChange)) {
|
|
||||||
this.props.onChange(newTag, tags)
|
|
||||||
}
|
|
||||||
this.setState({input: ''})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleThisClick (e) {
|
|
||||||
ReactDOM.findDOMNode(this.refs.tagInput).focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleItemRemoveButton (tag) {
|
|
||||||
return e => {
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
let tags = this.props.tags.slice(0)
|
|
||||||
tags.splice(tags.indexOf(tag), 1)
|
|
||||||
|
|
||||||
if (_.isFunction(this.props.onChange)) {
|
|
||||||
this.props.onChange(null, tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
var tagElements = _.isArray(this.props.tags)
|
|
||||||
? this.props.tags.map(tag => (
|
|
||||||
<span key={tag} className='tagItem'>
|
|
||||||
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
|
|
||||||
<span className='tagLabel'>{tag}</span>
|
|
||||||
</span>))
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
|
|
||||||
{tagElements}
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
onKeyDown={e => this.handleKeyDown(e)}
|
|
||||||
ref='tagInput'
|
|
||||||
valueLink={this.linkState('input')}
|
|
||||||
placeholder='Click here to add tags'
|
|
||||||
className='tagInput'/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TagSelect.propTypes = {
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
onChange: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
TagSelect.prototype.linkState = linkState
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import React, { PropTypes, findDOMNode } from 'react'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import { sendEmail } from 'boost/api'
|
|
||||||
|
|
||||||
export default class ContactModal extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.linkState = linkState
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isSent: false,
|
|
||||||
mail: {
|
|
||||||
title: '',
|
|
||||||
content: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyCast (e) {
|
|
||||||
switch (e.status) {
|
|
||||||
case 'closeModal':
|
|
||||||
this.props.close()
|
|
||||||
break
|
|
||||||
case 'submitContactModal':
|
|
||||||
if (this.state.isSent) {
|
|
||||||
this.props.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.sendEmail()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
findDOMNode(this.refs.title).focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
sendEmail () {
|
|
||||||
sendEmail(this.state.mail)
|
|
||||||
.then(function (res) {
|
|
||||||
this.setState({isSent: !this.state.isSent})
|
|
||||||
}.bind(this))
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='ContactModal modal'>
|
|
||||||
<div className='modal-header'><h1>Contact form</h1></div>
|
|
||||||
|
|
||||||
{!this.state.isSent ? (
|
|
||||||
<div className='contactForm'>
|
|
||||||
<div className='modal-body'>
|
|
||||||
<div className='formField'>
|
|
||||||
<input ref='title' valueLink={this.linkState('mail.title')} placeholder='Title'/>
|
|
||||||
</div>
|
|
||||||
<div className='formField'>
|
|
||||||
<textarea valueLink={this.linkState('mail.content')} placeholder='Content'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='modal-footer'>
|
|
||||||
<div className='formControl'>
|
|
||||||
<button onClick={this.sendEmail} className='sendButton'>Send</button>
|
|
||||||
<button onClick={this.props.close}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='confirmation'>
|
|
||||||
<div className='confirmationMessage'>Thanks for sharing your opinion!</div>
|
|
||||||
<button className='doneButton' onClick={this.props.close}>Done</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactModal.propTypes = {
|
|
||||||
close: PropTypes.func
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import ProfileImage from 'boost/components/ProfileImage'
|
|
||||||
import { searchUser, createTeam, setMember, deleteMember } from 'boost/api'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import Select from 'react-select'
|
|
||||||
|
|
||||||
function getUsers (input, cb) {
|
|
||||||
searchUser(input)
|
|
||||||
.then(function (res) {
|
|
||||||
let users = res.body
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
options: users.map(user => {
|
|
||||||
return { value: user.name, label: user.name }
|
|
||||||
}),
|
|
||||||
complete: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class CreateNewTeam extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
create: {
|
|
||||||
name: '',
|
|
||||||
alert: null
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
team: null,
|
|
||||||
newMember: null,
|
|
||||||
alert: null
|
|
||||||
},
|
|
||||||
currentTab: 'create',
|
|
||||||
currentUser: JSON.parse(localStorage.getItem('currentUser'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseClick (e) {
|
|
||||||
this.props.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContinueClick (e) {
|
|
||||||
let createState = this.state.create
|
|
||||||
createState.isSending = true
|
|
||||||
createState.alert = {
|
|
||||||
type: 'info',
|
|
||||||
message: 'sending...'
|
|
||||||
}
|
|
||||||
this.setState({create: createState})
|
|
||||||
|
|
||||||
function onTeamCreate (res) {
|
|
||||||
let createState = this.state.create
|
|
||||||
createState.isSending = false
|
|
||||||
createState.alert = null
|
|
||||||
|
|
||||||
let selectState = this.state.select
|
|
||||||
selectState.team = res.body
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
currentTab: 'select',
|
|
||||||
create: createState,
|
|
||||||
select: {
|
|
||||||
team: res.body
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError (err) {
|
|
||||||
let errorMessage = err.response != null ? err.response.body.message : 'Can\'t connect to API server.'
|
|
||||||
|
|
||||||
let createState = this.state.create
|
|
||||||
createState.isSending = false
|
|
||||||
createState.alert = {
|
|
||||||
type: 'error',
|
|
||||||
message: errorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
create: createState
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
createTeam({name: this.state.create.name})
|
|
||||||
.then(onTeamCreate.bind(this))
|
|
||||||
.catch(onError.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCreateTab () {
|
|
||||||
let createState = this.state.create
|
|
||||||
let alertEl = createState.alert != null ? (
|
|
||||||
<p className={['alert'].concat([createState.alert.type]).join(' ')}>{createState.alert.message}</p>
|
|
||||||
) : null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='createTab'>
|
|
||||||
<div className='title'>Create new team</div>
|
|
||||||
|
|
||||||
<input valueLink={this.linkState('create.name')} className='ipt' type='text' placeholder='Enter your team name'/>
|
|
||||||
{alertEl}
|
|
||||||
<button onClick={e => this.handleContinueClick(e)} disabled={createState.isSending} className='confirmBtn'>Continue <i className='fa fa-arrow-right fa-fw'/></button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
handleNewMemberChange (value) {
|
|
||||||
let selectState = this.state.select
|
|
||||||
selectState.newMember = value
|
|
||||||
this.setState({select: selectState})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClickAddMemberButton (e) {
|
|
||||||
let selectState = this.state.select
|
|
||||||
let input = {
|
|
||||||
name: selectState.newMember,
|
|
||||||
role: 'member'
|
|
||||||
}
|
|
||||||
|
|
||||||
setMember(selectState.team.id, input)
|
|
||||||
.then(res => {
|
|
||||||
let selectState = this.state.select
|
|
||||||
let team = res.body
|
|
||||||
team.Members = team.Members.sort((a, b) => {
|
|
||||||
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
|
|
||||||
})
|
|
||||||
selectState.team = team
|
|
||||||
selectState.newMember = ''
|
|
||||||
|
|
||||||
this.setState({select: selectState})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.status != null) throw err
|
|
||||||
else console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMemberDeleteButtonClick (name) {
|
|
||||||
let selectState = this.state.select
|
|
||||||
let input = {
|
|
||||||
name: name
|
|
||||||
}
|
|
||||||
|
|
||||||
return e => {
|
|
||||||
deleteMember(selectState.team.id, input)
|
|
||||||
.then(res => {
|
|
||||||
let selectState = this.state.select
|
|
||||||
let team = res.body
|
|
||||||
team.Members = team.Members.sort((a, b) => {
|
|
||||||
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
|
|
||||||
})
|
|
||||||
selectState.team = team
|
|
||||||
selectState.newMember = ''
|
|
||||||
|
|
||||||
this.setState({select: selectState})
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log(err, err.response)
|
|
||||||
if (err.status != null) throw err
|
|
||||||
else console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMemberRoleChange (name) {
|
|
||||||
return function (e) {
|
|
||||||
let selectState = this.state.select
|
|
||||||
let input = {
|
|
||||||
name: name,
|
|
||||||
role: e.target.value
|
|
||||||
}
|
|
||||||
|
|
||||||
setMember(selectState.team.id, input)
|
|
||||||
.then(res => {
|
|
||||||
console.log(res.body)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.status != null) throw err
|
|
||||||
else console.error(err)
|
|
||||||
})
|
|
||||||
}.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSelectTab () {
|
|
||||||
let selectState = this.state.select
|
|
||||||
|
|
||||||
let membersEl = selectState.team.Members.map(member => {
|
|
||||||
let isCurrentUser = this.state.currentUser.id === member.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={'user-' + member.id}>
|
|
||||||
<ProfileImage className='userPhoto' email={member.email} size='30'/>
|
|
||||||
<div className='userInfo'>
|
|
||||||
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
|
|
||||||
<div className='userEmail'>{member.email}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='userControl'>
|
|
||||||
<select onChange={e => this.handleMemberRoleChange(member.name)(e)} disabled={isCurrentUser} value={member._pivot_role} className='userRole'>
|
|
||||||
<option value='owner'>Owner</option>
|
|
||||||
<option value='member'>Member</option>
|
|
||||||
</select>
|
|
||||||
<button onClick={e => this.handleMemberDeleteButtonClick(member.name)(e)} disabled={isCurrentUser}><i className='fa fa-times fa-fw'/></button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='selectTab'>
|
|
||||||
<div className='title'>Select member</div>
|
|
||||||
<div className='memberForm'>
|
|
||||||
<Select
|
|
||||||
className='memberName'
|
|
||||||
autoload={false}
|
|
||||||
asyncOptions={getUsers}
|
|
||||||
onChange={val => this.handleNewMemberChange(val)}
|
|
||||||
value={selectState.newMember}
|
|
||||||
/>
|
|
||||||
<button onClick={e => this.handleClickAddMemberButton(e)} className='addMemberBtn'>add</button>
|
|
||||||
</div>
|
|
||||||
<ul className='memberList'>
|
|
||||||
{membersEl}
|
|
||||||
</ul>
|
|
||||||
<button onClick={e => this.props.close(e)}className='confirmBtn'>Done</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let currentTab = this.state.currentTab === 'create'
|
|
||||||
? this.renderCreateTab()
|
|
||||||
: this.renderSelectTab()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='CreateNewTeam modal'>
|
|
||||||
<button onClick={e => this.handleCloseClick(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
|
|
||||||
|
|
||||||
{currentTab}
|
|
||||||
|
|
||||||
<div className='tabNav'>
|
|
||||||
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'create' ? ' active' : '')}/>
|
|
||||||
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'select' ? ' active' : '')}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateNewTeam.propTypes = {
|
|
||||||
close: PropTypes.func
|
|
||||||
}
|
|
||||||
CreateNewTeam.prototype.linkState = linkState
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import remote from 'remote'
|
|
||||||
import ipc from 'ipc'
|
|
||||||
|
|
||||||
export default class AppSettingTab extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
let keymap = remote.getGlobal('keymap')
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
toggleFinder: keymap.toggleFinder,
|
|
||||||
alert: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.handleSettingDone = () => {
|
|
||||||
this.setState({alert: {
|
|
||||||
type: 'success',
|
|
||||||
message: 'Successfully done!'
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
this.handleSettingError = err => {
|
|
||||||
this.setState({alert: {
|
|
||||||
type: 'error',
|
|
||||||
message: err.message
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
|
||||||
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
|
|
||||||
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveButtonClick (e) {
|
|
||||||
ipc.send('hotkeyUpdated', {
|
|
||||||
toggleFinder: this.state.toggleFinder
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let alert = this.state.alert
|
|
||||||
let alertElement = alert != null ? (
|
|
||||||
<p className={`alert ${alert.type}`}>
|
|
||||||
{alert.message}
|
|
||||||
</p>
|
|
||||||
) : null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='AppSettingTab content'>
|
|
||||||
<div className='section'>
|
|
||||||
<div className='sectionTitle'>Hotkey</div>
|
|
||||||
<div className='sectionInput'>
|
|
||||||
<label>Toggle Finder(popup)</label>
|
|
||||||
<input valueLink={this.linkState('toggleFinder')} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='sectionConfirm'>
|
|
||||||
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
|
|
||||||
{alertElement}
|
|
||||||
</div>
|
|
||||||
<div className='description'>
|
|
||||||
<ul>
|
|
||||||
<li><code>0</code> to <code>9</code></li>
|
|
||||||
<li><code>A</code> to <code>Z</code></li>
|
|
||||||
<li><code>F1</code> to <code>F24</code></li>
|
|
||||||
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
|
|
||||||
<li><code>Plus</code></li>
|
|
||||||
<li><code>Space</code></li>
|
|
||||||
<li><code>Backspace</code></li>
|
|
||||||
<li><code>Delete</code></li>
|
|
||||||
<li><code>Insert</code></li>
|
|
||||||
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
|
|
||||||
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
|
|
||||||
<li><code>Home</code> and <code>End</code></li>
|
|
||||||
<li><code>PageUp</code> and <code>PageDown</code></li>
|
|
||||||
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
|
|
||||||
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
|
|
||||||
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppSettingTab.prototype.linkState = linkState
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import linkState from 'boost/linkState'
|
|
||||||
import FolderMark from 'boost/components/FolderMark'
|
|
||||||
import store from 'boost/store'
|
|
||||||
import { updateFolder, destroyFolder } from 'boost/actions'
|
|
||||||
|
|
||||||
const IDLE = 'IDLE'
|
|
||||||
const EDIT = 'EDIT'
|
|
||||||
const DELETE = 'DELETE'
|
|
||||||
|
|
||||||
export default class FolderRow extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
mode: IDLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
mode: IDLE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEditButtonClick (e) {
|
|
||||||
this.setState({
|
|
||||||
mode: EDIT,
|
|
||||||
name: this.props.folder.name
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteButtonClick (e) {
|
|
||||||
this.setState({mode: DELETE})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveButtonClick (e) {
|
|
||||||
let { folder, setAlert } = this.props
|
|
||||||
|
|
||||||
setAlert(null, () => {
|
|
||||||
let input = {
|
|
||||||
name: this.state.name
|
|
||||||
}
|
|
||||||
folder = Object.assign({}, folder, input)
|
|
||||||
|
|
||||||
try {
|
|
||||||
store.dispatch(updateFolder(folder))
|
|
||||||
this.setState({
|
|
||||||
mode: IDLE
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
setAlert({
|
|
||||||
type: 'error',
|
|
||||||
message: e.message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteConfirmButtonClick (e) {
|
|
||||||
let { folder } = this.props
|
|
||||||
store.dispatch(destroyFolder(folder.key))
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let folder = this.props.folder
|
|
||||||
|
|
||||||
switch (this.state.mode) {
|
|
||||||
case EDIT:
|
|
||||||
return (
|
|
||||||
<div className='FolderRow edit'>
|
|
||||||
<div className='folderName'>
|
|
||||||
<input valueLink={this.linkState('name')} type='text'/>
|
|
||||||
</div>
|
|
||||||
<div className='folderControl'>
|
|
||||||
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
|
|
||||||
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case DELETE:
|
|
||||||
return (
|
|
||||||
<div className='FolderRow delete'>
|
|
||||||
<div className='folderDeleteLabel'>Are you sure to delete <strong>{folder.name}</strong> folder?</div>
|
|
||||||
<div className='folderControl'>
|
|
||||||
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>Sure</button>
|
|
||||||
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
case IDLE:
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<div className='FolderRow'>
|
|
||||||
<div className='folderName'><FolderMark color={folder.color}/> {folder.name}</div>
|
|
||||||
<div className='folderControl'>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderRow.propTypes = {
|
|
||||||
folder: PropTypes.shape(),
|
|
||||||
setAlert: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
FolderRow.prototype.linkState = linkState
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
|
|
||||||
export default class HelpTab extends React.Component {
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='content help'>
|
|
||||||
Comming soon
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import ProfileImage from 'boost/components/ProfileImage'
|
|
||||||
import api from 'boost/api'
|
|
||||||
|
|
||||||
const IDLE = 'IDLE'
|
|
||||||
const DELETE = 'DELETE'
|
|
||||||
|
|
||||||
export default class MemberRow extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
mode: IDLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleMemberRoleChange (e) {
|
|
||||||
let input = {
|
|
||||||
name: this.props.member.name,
|
|
||||||
role: e.target.value
|
|
||||||
}
|
|
||||||
|
|
||||||
api.setMember(this.props.team.id, input)
|
|
||||||
.then(res => {
|
|
||||||
console.log(res.body)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.status != null) throw err
|
|
||||||
else console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteButtonClick (e) {
|
|
||||||
this.setState({mode: DELETE})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelButtonClick (e) {
|
|
||||||
this.setState({mode: IDLE})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteConfirmButtonClick (e) {
|
|
||||||
let input = {
|
|
||||||
name: this.props.member.name
|
|
||||||
}
|
|
||||||
|
|
||||||
api.deleteMember(this.props.team.id, input)
|
|
||||||
.then(res => {
|
|
||||||
console.log(res.body)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err.status != null) throw err
|
|
||||||
else console.error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let member = this.props.member
|
|
||||||
let currentUser = this.props.currentUser
|
|
||||||
let isDisabled = (currentUser.id === member.id)
|
|
||||||
|
|
||||||
switch (this.state.mode) {
|
|
||||||
case DELETE:
|
|
||||||
return (
|
|
||||||
<li className='MemberRow edit'>
|
|
||||||
<div className='colDescription'>
|
|
||||||
Are you sure to remove <strong>{member.profileName}</strong> ?
|
|
||||||
</div>
|
|
||||||
<div className='colDeleteConfirm'>
|
|
||||||
<button className='deleteButton primary' onClick={e => this.handleDeleteConfirmButtonClick(e)}>Sure</button>
|
|
||||||
<button className='deleteButton' onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
case IDLE:
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<li className='MemberRow'>
|
|
||||||
<div className='colUserName'>
|
|
||||||
<ProfileImage className='userPhoto' email={member.email} size='30'/>
|
|
||||||
<div className='userInfo'>
|
|
||||||
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
|
|
||||||
<div className='userEmail'>{member.email}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='colRole'>
|
|
||||||
<select onChange={e => this.handleMemberRoleChange(e)} disabled={isDisabled} value={member._pivot_role} className='userRole'>
|
|
||||||
<option value='owner'>Owner</option>
|
|
||||||
<option value='member'>Member</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className='colDelete'>
|
|
||||||
<button className='deleteButton' onClick={e => this.handleDeleteButtonClick(e)} disabled={isDisabled}><i className='fa fa-times fa-fw'/></button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MemberRow.propTypes = {
|
|
||||||
member: PropTypes.shape(),
|
|
||||||
currentUser: PropTypes.shape(),
|
|
||||||
team: PropTypes.shape({
|
|
||||||
id: PropTypes.number
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user