mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
427 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
61cc44cc83 | ||
|
|
c20cbe7d66 | ||
|
|
2f4af3223b | ||
|
|
e4b2c42897 | ||
|
|
746df9277c | ||
|
|
8428588a4c | ||
|
|
83a8f4b911 | ||
|
|
2736024cb7 | ||
|
|
9a32ca893e | ||
|
|
59d3c6c94f | ||
|
|
388027b731 | ||
|
|
8abdedc11d | ||
|
|
9758f5baa8 | ||
|
|
248262a597 | ||
|
|
cc0f2c7c7f | ||
|
|
72f6468d12 | ||
|
|
522c0edd90 | ||
|
|
d338f217fe | ||
|
|
ca79857386 | ||
|
|
60e551e273 | ||
|
|
954e148be3 | ||
|
|
3d0b79f674 | ||
|
|
d9442aa23c | ||
|
|
ba0daf4452 | ||
|
|
8d9cd5bbd1 | ||
|
|
186b877c09 | ||
|
|
5ed2dfccd1 | ||
|
|
911cfd8642 | ||
|
|
3539bd1e79 | ||
|
|
f56df7c16d | ||
|
|
c507dfa6c4 | ||
|
|
f6d2e898dc | ||
|
|
326c7a93fb | ||
|
|
58381b8062 | ||
|
|
0899cea4b4 | ||
|
|
7459e937b5 | ||
|
|
55db0bebbb | ||
|
|
0bdb8142c6 | ||
|
|
88ee94d4b6 | ||
|
|
1df4ed0fe9 | ||
|
|
2a339a2935 | ||
|
|
a1810e6023 | ||
|
|
832ca3347c | ||
|
|
9d2b64e82b | ||
|
|
9a5e4b3f54 | ||
|
|
e5e8032ba1 | ||
|
|
5356e68b51 | ||
|
|
cd94c625a7 | ||
|
|
972a3746a1 | ||
|
|
a9e12e4384 | ||
|
|
1690e6420f | ||
|
|
acdf61f7ab | ||
|
|
2e4fc557ea | ||
|
|
979dcead49 | ||
|
|
116ddf345d | ||
|
|
366805a64f | ||
|
|
1fee2a846a | ||
|
|
3f54eb52b2 | ||
|
|
a3847ce1c9 | ||
|
|
1e7415b692 | ||
|
|
ff950ef28a | ||
|
|
51bd12c6cf | ||
|
|
5fa37dbffb | ||
|
|
c6307e4ad3 | ||
|
|
06a54d451c | ||
|
|
e317075815 | ||
|
|
45541a255b | ||
|
|
3ab423d695 | ||
|
|
345d7b427a | ||
|
|
de6d6b692e | ||
|
|
b2845e2284 | ||
|
|
47383c347c | ||
|
|
4bda84d69c | ||
|
|
b510aa11f5 | ||
|
|
8dab6d5e04 | ||
|
|
85f833c865 | ||
|
|
15133d00c7 | ||
|
|
b93990d10b | ||
|
|
a0bcb8edbe | ||
|
|
bfdf691bed | ||
|
|
f60856b998 | ||
|
|
3308eeaf82 | ||
|
|
19930a2472 | ||
|
|
e75d95b1fc | ||
|
|
4319711dc6 | ||
|
|
9712be909d | ||
|
|
04060ce252 | ||
|
|
da066fe694 | ||
|
|
3b7215b36a | ||
|
|
b88d5cfb06 | ||
|
|
8ab96cf2fb | ||
|
|
63af2fd8b1 | ||
|
|
fcf26f7844 | ||
|
|
657ebc99df | ||
|
|
9500f9a8c9 | ||
|
|
a05bba15e7 | ||
|
|
d88ad0f6be | ||
|
|
4e34f16e33 | ||
|
|
503a806446 | ||
|
|
caf7606893 | ||
|
|
2fb51fe37c | ||
|
|
fa1c48e480 | ||
|
|
3d9a631786 | ||
|
|
0b4cfd6563 | ||
|
|
484dfe6726 | ||
|
|
d8cb93fb10 | ||
|
|
23b8b49c00 | ||
|
|
932997259f | ||
|
|
1bebb66165 | ||
|
|
ac0d81f9b3 | ||
|
|
45b99d13cd | ||
|
|
920704075e | ||
|
|
9e929f80ad | ||
|
|
9696a6cba1 | ||
|
|
211fd8b28a | ||
|
|
c6ef86cbbe | ||
|
|
90c2ff7480 | ||
|
|
49057810fb | ||
|
|
e5e6e2e1b8 | ||
|
|
c8851ecd2a | ||
|
|
bd2d77fef7 | ||
|
|
43403f8bb1 | ||
|
|
0ac7839f11 | ||
|
|
714b5f7b4b | ||
|
|
51fb43d624 | ||
|
|
a79cbb2d5c | ||
|
|
e88c197f86 | ||
|
|
b1be92e6c9 | ||
|
|
d20f005c5d | ||
|
|
5dbfb24f1c | ||
|
|
4df489bd10 | ||
|
|
8c3510413a | ||
|
|
bea9dfdfc7 | ||
|
|
465b315ae0 | ||
|
|
054daac6db | ||
|
|
867ec25e54 | ||
|
|
2e4aaf7345 | ||
|
|
2f754bbb87 | ||
|
|
cdf6ed47dd | ||
|
|
c31432fe3f | ||
|
|
f0b2e91091 | ||
|
|
72a08e8fec | ||
|
|
864001bdff | ||
|
|
bd2816b2ac | ||
|
|
03918527f6 | ||
|
|
6140e93cc8 | ||
|
|
0f8eaaf750 | ||
|
|
7afad6ac49 | ||
|
|
e9308bdd69 | ||
|
|
16b60ada50 | ||
|
|
aa71251edd | ||
|
|
89cfd35d72 | ||
|
|
9ea16a39df | ||
|
|
4fee2586e4 | ||
|
|
b30511eb51 | ||
|
|
05325e7276 | ||
|
|
0dde2eb20f | ||
|
|
c2fcc72e62 | ||
|
|
ec686c9452 | ||
|
|
863de33f63 | ||
|
|
f56dd10106 | ||
|
|
8b10eb130a | ||
|
|
b0d9895e5e | ||
|
|
1d3e3f3c87 | ||
|
|
36eaebcbc7 | ||
|
|
7870c60ab4 | ||
|
|
e0d52d3578 | ||
|
|
9422825aec | ||
|
|
e467862c29 | ||
|
|
6b03ea2fe5 | ||
|
|
472d79cbf2 | ||
|
|
27701bbe1b | ||
|
|
9e1dcf8b64 | ||
|
|
c74de88ca3 | ||
|
|
361e9c629e | ||
|
|
3b907627f7 | ||
|
|
2284fd41b9 | ||
|
|
1a832c1fc4 | ||
|
|
ee139ca36d | ||
|
|
549ce7f299 | ||
|
|
c72b5449bd | ||
|
|
debfa6323b | ||
|
|
0e6fe35ca4 | ||
|
|
7c2cbfb32e | ||
|
|
25eccacb4c |
8
.babelrc
Normal file
8
.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": ["react", "es2015"],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
/build
|
||||
/node_modules
|
||||
/electron_build
|
||||
.env
|
||||
dist/
|
||||
node_modules/*
|
||||
!node_modules/boost
|
||||
/dist
|
||||
/compiled
|
||||
/secret
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "submodules/ace"]
|
||||
path = submodules/ace
|
||||
url = https://github.com/ajaxorg/ace-builds.git
|
||||
branch = master
|
||||
161
Gulpfile.js
161
Gulpfile.js
@@ -1,161 +0,0 @@
|
||||
require('dotenv').load()
|
||||
var env = process.env
|
||||
|
||||
var gulp = require('gulp')
|
||||
var styl = require('gulp-stylus')
|
||||
var autoprefixer = require('gulp-autoprefixer')
|
||||
var templateCache = require('gulp-angular-templatecache')
|
||||
var globby = require('globby')
|
||||
var template = require('gulp-template')
|
||||
var del = require('del')
|
||||
var runSequence = require('run-sequence')
|
||||
var plumber = require('gulp-plumber')
|
||||
var notify = require('gulp-notify')
|
||||
var changed = require('gulp-changed')
|
||||
var livereload = require('gulp-livereload')
|
||||
|
||||
// for Dist
|
||||
var rev = require('gulp-rev')
|
||||
var ngAnnotate = require('gulp-ng-annotate')
|
||||
var uglify = require('gulp-uglify')
|
||||
var minifyCss = require('gulp-minify-css')
|
||||
var merge = require('merge-stream')
|
||||
var concat = require('gulp-concat')
|
||||
var streamqueue = require('streamqueue')
|
||||
var minifyHtml = require('gulp-minify-html')
|
||||
|
||||
var config = require('./build.config.js')
|
||||
|
||||
gulp.task('js', function () {
|
||||
return streamqueue({objectMode: true},
|
||||
gulp.src('tpls/env.js')
|
||||
.pipe(template({
|
||||
apiUrl: env.BUILD_API_URL
|
||||
})),
|
||||
gulp.src(['src/**/*.js'])
|
||||
)
|
||||
.pipe(changed('build'))
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
|
||||
gulp.task('dist', function () {
|
||||
var js = streamqueue({objectMode: true},
|
||||
gulp.src(['src/**/*.js']),
|
||||
gulp.src('tpls/env.js')
|
||||
.pipe(template({
|
||||
apiUrl: env.DIST_API_URL
|
||||
})),
|
||||
gulp.src('src/**/*.tpl.html')
|
||||
.pipe(templateCache())
|
||||
)
|
||||
.pipe(ngAnnotate())
|
||||
.pipe(uglify())
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(gulp.dest('dist'))
|
||||
|
||||
var css = gulp.src('src/styles/main.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(minifyCss())
|
||||
.pipe(gulp.dest('dist'))
|
||||
|
||||
var index = gulp.src('src/index.html')
|
||||
.pipe(template({
|
||||
scripts: ['app.js'],
|
||||
styles: ['main.css'],
|
||||
env: 'dist'
|
||||
}))
|
||||
.pipe(minifyHtml())
|
||||
.pipe(gulp.dest('dist'))
|
||||
|
||||
return merge(js, css, index)
|
||||
})
|
||||
|
||||
gulp.task('styl', function () {
|
||||
return gulp.src('src/styles/main.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest('build'))
|
||||
.pipe(notify('Stylus!!'))
|
||||
.pipe(livereload())
|
||||
})
|
||||
|
||||
gulp.task('tpls', function () {
|
||||
return gulp.src('src/**/*.tpl.html')
|
||||
.pipe(templateCache())
|
||||
.pipe(notify('Tpls Done!! :)'))
|
||||
.pipe(gulp.dest('build'))
|
||||
})
|
||||
|
||||
gulp.task('index', function () {
|
||||
var files = globby.sync(['build/**/*', '!build/vendor/**/*'])
|
||||
|
||||
var filter = function (files, ext) {
|
||||
return files.filter(function (file) {
|
||||
var reg = new RegExp('.+\.' + ext + '$')
|
||||
return file.match(reg)
|
||||
}).map(function (file) {
|
||||
return file.replace('build/', '')
|
||||
})
|
||||
}
|
||||
var scripts = filter(files, 'js')
|
||||
var styles = filter(files, 'css')
|
||||
|
||||
return gulp.src('src/index.html')
|
||||
.pipe(template({
|
||||
scripts: scripts,
|
||||
styles: styles,
|
||||
env: 'build'
|
||||
}))
|
||||
.pipe(gulp.dest('build'))
|
||||
.pipe(livereload())
|
||||
})
|
||||
|
||||
gulp.task('vendor', function () {
|
||||
var vendors = config.vendors
|
||||
|
||||
var vendorFiles = vendors.map(function (vendor) {
|
||||
return vendor.src
|
||||
})
|
||||
|
||||
vendorFiles.push('node_modules/font-awesome/**/font-awesome.css')
|
||||
vendorFiles.push('node_modules/font-awesome/**/fontawesome-webfont.*')
|
||||
vendorFiles.push('node_modules/font-awesome/**/FontAwesome.*')
|
||||
|
||||
return gulp.src(vendorFiles)
|
||||
.pipe(gulp.dest('build/vendor'))
|
||||
})
|
||||
|
||||
gulp.task('resources', function () {
|
||||
return gulp.src('resources/**/*')
|
||||
.pipe(changed('build/resources'))
|
||||
.pipe(gulp.dest('build/resources'))
|
||||
})
|
||||
|
||||
gulp.task('build', function (cb) {
|
||||
runSequence(['js', 'styl', 'tpls', 'vendor', 'resources'], 'index', cb)
|
||||
})
|
||||
|
||||
gulp.task('watch', function (cb) {
|
||||
gulp.watch(['.env', 'tpls/env.js', 'src/**/*.js'], ['js'])
|
||||
|
||||
gulp.watch('src/styles/**/*.styl', ['styl'])
|
||||
|
||||
gulp.watch('src/**/*.tpl.html', ['tpls'])
|
||||
|
||||
gulp.watch(['build/**/*.js', 'src/index.html'], ['index'])
|
||||
|
||||
livereload.listen()
|
||||
})
|
||||
|
||||
gulp.task('del', function (cb) {
|
||||
del(['build/**/*'], cb)
|
||||
})
|
||||
|
||||
gulp.task('default', function (cb) {
|
||||
runSequence('del', 'build', 'watch', cb)
|
||||
})
|
||||
|
||||
require('./gulp-electron')(gulp)
|
||||
2
LICENSE
Normal file
2
LICENSE
Normal file
@@ -0,0 +1,2 @@
|
||||
本製品をインストール、または使用することによって、お客様は利用規約(
|
||||
https://b00st.io/regulations.html)より拘束されることに承諾されたものとします。利用規約に同意されない場合、Boostnoteは、お客様に本製品のインストール、使用のいずれも許諾できません。
|
||||
BIN
app-logo.png
BIN
app-logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
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" }
|
||||
]
|
||||
|
||||
}
|
||||
203
browser/components/CodeEditor.js
Normal file
203
browser/components/CodeEditor.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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: 'Focus title',
|
||||
bindKey: {win: 'Esc', mac: 'Esc'},
|
||||
exec: function (editor, e) {
|
||||
remote.getCurrentWebContents().send('list-focus')
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
|
||||
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('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
|
||||
20
browser/components/ExternalLink.js
Normal file
20
browser/components/ExternalLink.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default class ExternalLink extends React.Component {
|
||||
handleClick (e) {
|
||||
shell.openExternal(this.props.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<a onClick={e => this.handleClick(e)} {...this.props}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
href: PropTypes.string
|
||||
}
|
||||
52
browser/components/FolderMark.js
Normal file
52
browser/components/FolderMark.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
const BLUE = '#3460C7'
|
||||
const LIGHTBLUE = '#2BA5F7'
|
||||
const ORANGE = '#FF8E00'
|
||||
const YELLOW = '#E8D252'
|
||||
const GREEN = '#3FD941'
|
||||
const DARKGREEN = '#1FAD85'
|
||||
const RED = '#E10051'
|
||||
const PURPLE = '#B013A4'
|
||||
|
||||
function getColorByIndex (index) {
|
||||
switch (index % 8) {
|
||||
case 0:
|
||||
return RED
|
||||
case 1:
|
||||
return ORANGE
|
||||
case 2:
|
||||
return YELLOW
|
||||
case 3:
|
||||
return GREEN
|
||||
case 4:
|
||||
return DARKGREEN
|
||||
case 5:
|
||||
return LIGHTBLUE
|
||||
case 6:
|
||||
return BLUE
|
||||
case 7:
|
||||
return PURPLE
|
||||
default:
|
||||
return DARKGREEN
|
||||
}
|
||||
}
|
||||
|
||||
export default class FolderMark extends React.Component {
|
||||
render () {
|
||||
let color = getColorByIndex(this.props.color)
|
||||
let className = 'FolderMark fa fa-square fa-fw'
|
||||
if (this.props.className != null) {
|
||||
className += ' active'
|
||||
}
|
||||
|
||||
return (
|
||||
<i className={className} style={{color: color}}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FolderMark.propTypes = {
|
||||
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 (e.target.attributes.href && e.target.attributes.href.nodeValue.match(/#.+/)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let href = e.target.href
|
||||
if (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
|
||||
}
|
||||
82
browser/components/ModeIcon.js
Normal file
82
browser/components/ModeIcon.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class ModeIcon extends React.Component {
|
||||
getClassName () {
|
||||
var mode = this.props.mode
|
||||
switch (mode) {
|
||||
// Script
|
||||
case 'javascript':
|
||||
return 'devicon-javascript-plain'
|
||||
case 'jsx':
|
||||
return 'devicon-react-original'
|
||||
case 'coffee':
|
||||
return 'devicon-coffeescript-original'
|
||||
case 'ruby':
|
||||
return 'devicon-ruby-plain'
|
||||
case 'erlang':
|
||||
return 'devicon-erlang-plain'
|
||||
case 'php':
|
||||
return 'devicon-php-plain'
|
||||
|
||||
// HTML
|
||||
case 'html':
|
||||
return 'devicon-html5-plain'
|
||||
|
||||
// Stylesheet
|
||||
case 'css':
|
||||
return 'devicon-css3-plain'
|
||||
case 'less':
|
||||
return 'devicon-less-plain-wordmark'
|
||||
case 'sass':
|
||||
case 'scss':
|
||||
return 'devicon-sass-original'
|
||||
|
||||
// Compile
|
||||
case 'c':
|
||||
return 'devicon-c-plain'
|
||||
case 'cpp':
|
||||
return 'devicon-cplusplus-plain'
|
||||
case 'csharp':
|
||||
return 'devicon-csharp-plain'
|
||||
case 'objc':
|
||||
return 'devicon-apple-original'
|
||||
case 'golang':
|
||||
return 'devicon-go-plain'
|
||||
case 'java':
|
||||
return 'devicon-java-plain'
|
||||
|
||||
// Framework
|
||||
case 'django':
|
||||
return 'devicon-django-plain'
|
||||
|
||||
// Config
|
||||
case 'dockerfile':
|
||||
return 'devicon-docker-plain'
|
||||
case 'gitignore':
|
||||
return 'devicon-git-plain'
|
||||
|
||||
// Shell
|
||||
case 'sh':
|
||||
case 'batchfile':
|
||||
case 'powershell':
|
||||
return 'fa fa-fw fa-terminal'
|
||||
|
||||
case 'text':
|
||||
case 'markdown':
|
||||
return 'fa fa-fw fa-file-text-o'
|
||||
}
|
||||
return 'fa fa-fw fa-code'
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
|
||||
return (
|
||||
<i className={className}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeIcon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
}
|
||||
172
browser/components/ModeSelect.js
Normal file
172
browser/components/ModeSelect.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from './ModeIcon'
|
||||
import modes from '../lib/modes'
|
||||
import _ from 'lodash'
|
||||
|
||||
const IDLE_MODE = 'IDLE_MODE'
|
||||
const EDIT_MODE = 'EDIT_MODE'
|
||||
|
||||
export default class ModeSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.blurHandler = e => {
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
|
||||
this.handleBlur(e)
|
||||
}
|
||||
}
|
||||
window.addEventListener('click', this.blurHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('click', this.blurHandler)
|
||||
let searchElement = ReactDOM.findDOMNode(this.refs.search)
|
||||
if (searchElement != null && this.searchKeyDownListener != null) {
|
||||
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
|
||||
}
|
||||
}
|
||||
|
||||
handleIdleSelectClick (e) {
|
||||
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
|
||||
ReactDOM.findDOMNode(this.refs.search).select()
|
||||
})
|
||||
}
|
||||
|
||||
handleModeOptionClick (modeName) {
|
||||
return e => {
|
||||
this.props.onChange(modeName)
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// up
|
||||
case 38:
|
||||
e.preventDefault()
|
||||
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
|
||||
break
|
||||
// down
|
||||
case 40:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
|
||||
else this.setState({focusIndex: this.state.focusIndex + 1})
|
||||
}
|
||||
break
|
||||
// enter
|
||||
case 13:
|
||||
e.preventDefault()
|
||||
{
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredModes = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
let targetMode = filteredModes[this.state.focusIndex]
|
||||
if (targetMode != null) {
|
||||
this.props.onChange(targetMode.name)
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
break
|
||||
// esc
|
||||
case 27:
|
||||
case 9:
|
||||
e.stopPropagation()
|
||||
this.setIdle()
|
||||
}
|
||||
if (this.props.onKeyDown) this.props.onKeyDown(e)
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
this.setState({
|
||||
search: e.target.value,
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
handleBlur (e) {
|
||||
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
|
||||
this.setIdle()
|
||||
}
|
||||
}
|
||||
|
||||
setIdle () {
|
||||
this.setState({
|
||||
mode: IDLE_MODE,
|
||||
search: '',
|
||||
focusIndex: 0
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
let className = this.props.className != null
|
||||
? `ModeSelect ${this.props.className}`
|
||||
: this.props.className
|
||||
|
||||
if (this.state.mode === IDLE_MODE) {
|
||||
let mode = _.findWhere(modes, {name: this.props.value})
|
||||
let modeName = mode != null ? mode.name : 'text'
|
||||
let modeLabel = mode != null ? mode.label : this.props.value
|
||||
|
||||
return (
|
||||
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
|
||||
<ModeIcon mode={modeName}/>{modeLabel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let search = _.escapeRegExp(this.state.search)
|
||||
let filteredOptions = modes
|
||||
.filter(mode => {
|
||||
let nameMatched = mode.name.match(search)
|
||||
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
|
||||
return nameMatched || aliasMatched
|
||||
})
|
||||
.map((mode, index) => {
|
||||
return (
|
||||
<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 (
|
||||
<div className={className + ' edit'}>
|
||||
<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='ModeSelect-options hide'>
|
||||
{filteredOptions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ModeSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onKeyDown: PropTypes.func
|
||||
}
|
||||
24
browser/components/ProfileImage.js
Normal file
24
browser/components/ProfileImage.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import md5 from 'md5'
|
||||
|
||||
export default class ProfileImage extends React.Component {
|
||||
render () {
|
||||
let className = this.props.className == null ? 'ProfileImage' : 'ProfileImage ' + this.props.className
|
||||
let email = this.props.email != null ? this.props.email : ''
|
||||
let src = 'http://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=' + this.props.size
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
width={this.props.size}
|
||||
height={this.props.size}
|
||||
src={src}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProfileImage.propTypes = {
|
||||
email: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
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
|
||||
44
browser/finder/FinderDetail.js
Normal file
44
browser/finder/FinderDetail.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
export default class FinderDetail extends React.Component {
|
||||
render () {
|
||||
let { activeArticle } = this.props
|
||||
|
||||
if (activeArticle != null) {
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='header'>
|
||||
<div className='left'>
|
||||
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
|
||||
</div>
|
||||
<div className='right'>
|
||||
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
|
||||
<i className='fa fa-clipboard fa-fw'/>
|
||||
<span className='tooltip'>Copy to clipboard (Enter)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='content'>
|
||||
{activeArticle.mode === 'markdown'
|
||||
? <MarkdownPreview content={activeArticle.content}/>
|
||||
: <CodeEditor readOnly article={activeArticle}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='FinderDetail'>
|
||||
<div className='nothing'>Nothing selected</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderDetail.propTypes = {
|
||||
activeArticle: PropTypes.shape(),
|
||||
saveToClipboard: PropTypes.func
|
||||
}
|
||||
16
browser/finder/FinderInput.js
Normal file
16
browser/finder/FinderInput.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class FinderInput extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='FinderInput'>
|
||||
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderInput.propTypes = {
|
||||
handleSearchChange: PropTypes.func,
|
||||
value: PropTypes.string
|
||||
}
|
||||
71
browser/finder/FinderList.js
Normal file
71
browser/finder/FinderList.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import { selectArticle } from './actions'
|
||||
|
||||
export default class FinderList extends React.Component {
|
||||
componentDidUpdate () {
|
||||
var index = this.props.articles.indexOf(this.props.activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('li')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
return (e) => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(selectArticle(article.key))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let articleElements = this.props.articles.map(function (article) {
|
||||
if (article == null) {
|
||||
return (
|
||||
<li className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>Undefined</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
|
||||
return (
|
||||
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
|
||||
<div className='articleItem'>
|
||||
<ModeIcon mode={article.mode}/> {article.title}</div>
|
||||
<div className='divider'/>
|
||||
</li>
|
||||
)
|
||||
}.bind(this))
|
||||
|
||||
return (
|
||||
<div className='FinderList'>
|
||||
<ul>
|
||||
{articleElements}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderList.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
key: PropTypes.string
|
||||
}),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
39
browser/finder/actions.js
Normal file
39
browser/finder/actions.js
Normal file
@@ -0,0 +1,39 @@
|
||||
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
|
||||
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
|
||||
export const REFRESH_DATA = 'REFRESH_DATA'
|
||||
|
||||
export function selectArticle (key) {
|
||||
return {
|
||||
type: SELECT_ARTICLE,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function searchArticle (input) {
|
||||
return {
|
||||
type: SEARCH_ARTICLE,
|
||||
data: { input }
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshData (data) {
|
||||
console.log('refreshing data')
|
||||
let { folders, articles } = data
|
||||
|
||||
return {
|
||||
type: REFRESH_DATA,
|
||||
data: {
|
||||
articles,
|
||||
folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
SELECT_ARTICLE,
|
||||
SEARCH_ARTICLE,
|
||||
REFRESH_DATA,
|
||||
selectArticle,
|
||||
searchArticle,
|
||||
refreshData
|
||||
}
|
||||
268
browser/finder/index.js
Normal file
268
browser/finder/index.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
import FinderInput from './FinderInput'
|
||||
import FinderList from './FinderList'
|
||||
import FinderDetail from './FinderDetail'
|
||||
import actions, { selectArticle, searchArticle } from './actions'
|
||||
import _ from 'lodash'
|
||||
import dataStore from 'browser/lib/dataStore'
|
||||
|
||||
const electron = require('electron')
|
||||
const { clipboard, ipcRenderer, remote } = electron
|
||||
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')
|
||||
options.silent = false
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
require('../styles/finder/index.styl')
|
||||
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
class FinderMain extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.keyDownHandler = e => this.handleKeyDown(e)
|
||||
document.addEventListener('keydown', this.keyDownHandler)
|
||||
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
|
||||
this.focusHandler = e => {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(''))
|
||||
dispatch(selectArticle(null))
|
||||
}
|
||||
window.addEventListener('focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('keydown', this.keyDownHandler)
|
||||
window.removeEventListener('focus', this.focusHandler)
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 38) {
|
||||
this.selectPrevious()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) {
|
||||
this.selectNext()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
this.saveToClipboard()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
hideFinder()
|
||||
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!'
|
||||
})
|
||||
hideFinder()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(searchArticle(e.target.value))
|
||||
}
|
||||
|
||||
selectArticle (article) {
|
||||
this.setState({currentArticle: article})
|
||||
}
|
||||
|
||||
selectPrevious () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index - 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
}
|
||||
|
||||
selectNext () {
|
||||
let { activeArticle, dispatch } = this.props
|
||||
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
|
||||
let previousArticle = this.refs.finderList.props.articles[index + 1]
|
||||
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
|
||||
}
|
||||
|
||||
render () {
|
||||
let { articles, activeArticle, status, dispatch } = this.props
|
||||
let saveToClipboard = () => this.saveToClipboard()
|
||||
return (
|
||||
<div className='Finder'>
|
||||
<FinderInput
|
||||
handleSearchChange={e => this.handleSearchChange(e)}
|
||||
ref='finderInput'
|
||||
onChange={this.handleChange}
|
||||
value={status.search}
|
||||
/>
|
||||
<FinderList
|
||||
ref='finderList'
|
||||
activeArticle={activeArticle}
|
||||
articles={articles}
|
||||
dispatch={dispatch}
|
||||
selectArticle={article => this.selectArticle(article)}
|
||||
/>
|
||||
<FinderDetail
|
||||
activeArticle={activeArticle}
|
||||
saveToClipboard={saveToClipboard}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FinderMain.propTypes = {
|
||||
articles: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
tags: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
content: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.shape(),
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
// Ignore invalid key
|
||||
function ignoreInvalidKey (key) {
|
||||
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
|
||||
}
|
||||
|
||||
// Build filter object by key
|
||||
function buildFilter (key) {
|
||||
if (key.match(/^\/\/.+/)) {
|
||||
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^\/.+/)) {
|
||||
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
||||
}
|
||||
if (key.match(/^#(.+)/)) {
|
||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
||||
}
|
||||
return {type: TEXT_FILTER, value: key}
|
||||
}
|
||||
|
||||
function isContaining (target, needle) {
|
||||
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function startsWith (target, needle) {
|
||||
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
||||
}
|
||||
|
||||
function remap (state) {
|
||||
let { articles, folders, status } = state
|
||||
|
||||
let filters = status.search.split(' ')
|
||||
.map(key => key.trim())
|
||||
.filter(ignoreInvalidKey)
|
||||
.map(buildFilter)
|
||||
|
||||
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
||||
|
||||
let targetFolders
|
||||
if (folders != null) {
|
||||
let exactTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
||||
})
|
||||
let fuzzyTargetFolders = folders.filter(folder => {
|
||||
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
||||
})
|
||||
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
||||
|
||||
if (targetFolders.length > 0) {
|
||||
articles = articles.filter(article => {
|
||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
||||
})
|
||||
}
|
||||
|
||||
if (textFilters.length > 0) {
|
||||
articles = textFilters.reduce((articles, textFilter) => {
|
||||
return articles.filter(article => {
|
||||
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
if (tagFilters.length > 0) {
|
||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
||||
return articles.filter(article => {
|
||||
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
}
|
||||
|
||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||
if (activeArticle == null) activeArticle = articles[0]
|
||||
|
||||
return {
|
||||
articles,
|
||||
activeArticle,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
var Finder = connect(remap)(FinderMain)
|
||||
var store = createStore(reducer)
|
||||
|
||||
function refreshData () {
|
||||
let data = dataStore.getData(true)
|
||||
store.dispatch(actions.refreshData(data))
|
||||
}
|
||||
|
||||
window.onfocus = e => {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
ReactDOM.render((
|
||||
<Provider store={store}>
|
||||
<Finder/>
|
||||
</Provider>
|
||||
), document.getElementById('content'), function () {
|
||||
refreshData()
|
||||
})
|
||||
47
browser/finder/reducer.js
Normal file
47
browser/finder/reducer.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
|
||||
|
||||
let initialArticles = []
|
||||
let initialFolders = []
|
||||
let initialStatus = {
|
||||
articleKey: null,
|
||||
search: ''
|
||||
}
|
||||
|
||||
function status (state = initialStatus, action) {
|
||||
switch (action.type) {
|
||||
case SELECT_ARTICLE:
|
||||
state.articleKey = action.data.key
|
||||
return Object.assign({}, state)
|
||||
case SEARCH_ARTICLE:
|
||||
state.search = action.data.input
|
||||
return Object.assign({}, state)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function articles (state = initialArticles, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
return action.data.articles
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function folders (state = initialFolders, action) {
|
||||
switch (action.type) {
|
||||
case REFRESH_DATA:
|
||||
console.log(action)
|
||||
return action.data.folders
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
status,
|
||||
folders,
|
||||
articles
|
||||
})
|
||||
138
browser/lib/activityRecord.js
Normal file
138
browser/lib/activityRecord.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import dataStore from './dataStore'
|
||||
import { request, SERVER_URL } from './api'
|
||||
import clientKey from './clientKey'
|
||||
|
||||
const electron = require('electron')
|
||||
const version = electron.remote.app.getVersion()
|
||||
|
||||
function isSameDate (a, b) {
|
||||
a = moment(a).utcOffset(+540).format('YYYYMMDD')
|
||||
b = moment(b).utcOffset(+540).format('YYYYMMDD')
|
||||
|
||||
return a === b
|
||||
}
|
||||
|
||||
export function init () {
|
||||
let records = getAllRecords()
|
||||
if (records == null) {
|
||||
saveAllRecords([])
|
||||
}
|
||||
emit(null)
|
||||
|
||||
postRecords()
|
||||
if (window != null) {
|
||||
window.addEventListener('online', postRecords)
|
||||
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllRecords () {
|
||||
return JSON.parse(localStorage.getItem('activityRecords'))
|
||||
}
|
||||
|
||||
export function saveAllRecords (records) {
|
||||
localStorage.setItem('activityRecords', JSON.stringify(records))
|
||||
}
|
||||
|
||||
/*
|
||||
Post all records(except today)
|
||||
and remove all posted records
|
||||
*/
|
||||
export function postRecords (data) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('post failed - NOT PRODUCTION ')
|
||||
return
|
||||
}
|
||||
|
||||
let records = getAllRecords()
|
||||
records = records.filter(record => {
|
||||
return !isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
if (records.length === 0) {
|
||||
console.log('No records to post')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('posting...', records)
|
||||
let input = {
|
||||
clientKey: clientKey.get(),
|
||||
records
|
||||
}
|
||||
return request.post(SERVER_URL + 'apis/activity')
|
||||
.send(input)
|
||||
.then(res => {
|
||||
let records = getAllRecords()
|
||||
let todayRecord = _.find(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
if (todayRecord != null) saveAllRecords([todayRecord])
|
||||
else saveAllRecords([])
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
export function emit (type, data = {}) {
|
||||
let records = getAllRecords()
|
||||
|
||||
let index = _.findIndex(records, record => {
|
||||
return isSameDate(new Date(), record.date)
|
||||
})
|
||||
|
||||
let todayRecord
|
||||
if (index < 0) {
|
||||
todayRecord = {date: new Date()}
|
||||
records.push(todayRecord)
|
||||
}
|
||||
else todayRecord = records[index]
|
||||
switch (type) {
|
||||
case 'ARTICLE_CREATE':
|
||||
case 'ARTICLE_UPDATE':
|
||||
case 'ARTICLE_DESTROY':
|
||||
case 'FOLDER_CREATE':
|
||||
case 'FOLDER_UPDATE':
|
||||
case 'FOLDER_DESTROY':
|
||||
case 'FINDER_OPEN':
|
||||
case 'FINDER_COPY':
|
||||
case 'MAIN_DETAIL_COPY':
|
||||
case 'ARTICLE_SHARE':
|
||||
todayRecord[type] = todayRecord[type] == null
|
||||
? 1
|
||||
: todayRecord[type] + 1
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
|
||||
if ((type === 'ARTICLE_CREATE' || 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()
|
||||
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.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)
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
emit,
|
||||
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
|
||||
}
|
||||
10
browser/lib/fetchConfig.js
Normal file
10
browser/lib/fetchConfig.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const jetpack = require('fs-jetpack')
|
||||
|
||||
const userDataPath = remote.app.getPath('userData')
|
||||
const configFile = 'config.json'
|
||||
|
||||
export default function fetchConfig () {
|
||||
return Object.assign({}, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
|
||||
}
|
||||
7
browser/lib/keygen.js
Normal file
7
browser/lib/keygen.js
Normal file
@@ -0,0 +1,7 @@
|
||||
var crypto = require('crypto')
|
||||
|
||||
module.exports = function () {
|
||||
var shasum = crypto.createHash('sha1')
|
||||
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
|
||||
return shasum.digest('hex')
|
||||
}
|
||||
36
browser/lib/linkState.js
Normal file
36
browser/lib/linkState.js
Normal file
@@ -0,0 +1,36 @@
|
||||
function getIn (object, path) {
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
object = object[stack.shift()]
|
||||
}
|
||||
return object[stack.shift()]
|
||||
}
|
||||
|
||||
function updateIn (object, path, value) {
|
||||
let current = object
|
||||
let stack = path.split('.')
|
||||
while (stack.length > 1) {
|
||||
current = current[stack.shift()]
|
||||
}
|
||||
current[stack.shift()] = value
|
||||
return object
|
||||
}
|
||||
|
||||
function setPartialState (component, path, value) {
|
||||
component.setState(
|
||||
updateIn(component.state, path, value))
|
||||
}
|
||||
|
||||
export default function linkState (path) {
|
||||
return {
|
||||
value: getIn(this.state, path),
|
||||
requestChange: setPartialState.bind(null, this, path)
|
||||
}
|
||||
}
|
||||
|
||||
export function linkState2 (el, path) {
|
||||
return {
|
||||
value: getIn(el.state, path),
|
||||
requestChange: setPartialState.bind(null, el, path)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
})
|
||||
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())
|
||||
}
|
||||
56
browser/lib/modal.js
Normal file
56
browser/lib/modal.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
const remote = require('electron').remote
|
||||
|
||||
class ModalBase extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
component: null,
|
||||
componentProps: {},
|
||||
isHidden: true
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
|
||||
|
||||
remote.getCurrentWebContents().send('list-focus')
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
|
||||
<div onClick={e => this.close(e)} className='modalBack'/>
|
||||
{this.state.component == null ? null : (
|
||||
<this.state.component {...this.state.componentProps} close={this.close}/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
let modalBase = ReactDOM.render(<ModalBase/>, el)
|
||||
|
||||
export function openModal (component, props) {
|
||||
if (modalBase == null) { return }
|
||||
modalBase.setState({component: component, componentProps: props, isHidden: false})
|
||||
}
|
||||
|
||||
export function closeModal () {
|
||||
if (modalBase == null) { return }
|
||||
modalBase.close()
|
||||
}
|
||||
|
||||
export function isModalOpen () {
|
||||
return !modalBase.state.isHidden
|
||||
}
|
||||
|
||||
export default {
|
||||
open: openModal,
|
||||
close: closeModal,
|
||||
isOpen: isModalOpen
|
||||
}
|
||||
756
browser/lib/modes.js
Normal file
756
browser/lib/modes.js
Normal file
@@ -0,0 +1,756 @@
|
||||
const modes = [
|
||||
// Major
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Plain text',
|
||||
mode: 'text'
|
||||
},
|
||||
{
|
||||
name: 'markdown',
|
||||
label: 'Markdown',
|
||||
alias: ['md'],
|
||||
mode: 'markdown'
|
||||
},
|
||||
{
|
||||
name: 'javascript',
|
||||
label: 'JavaScript',
|
||||
alias: ['js', 'jscript', 'babel', 'es'],
|
||||
mode: 'javascript'
|
||||
},
|
||||
{
|
||||
name: 'html',
|
||||
label: 'HTML',
|
||||
alias: [],
|
||||
mode: 'html'
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
label: 'CSS',
|
||||
alias: ['cascade', 'stylesheet'],
|
||||
mode: 'css'
|
||||
},
|
||||
{
|
||||
name: 'php',
|
||||
label: 'PHP',
|
||||
alias: [],
|
||||
mode: 'php'
|
||||
},
|
||||
{
|
||||
name: 'python',
|
||||
label: 'Python',
|
||||
alias: ['py'],
|
||||
mode: 'python'
|
||||
},
|
||||
{
|
||||
name: 'ruby',
|
||||
label: 'Ruby',
|
||||
alias: ['rb'],
|
||||
mode: 'ruby'
|
||||
},
|
||||
{
|
||||
name: 'java',
|
||||
label: 'Java',
|
||||
alias: [],
|
||||
mode: 'java'
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
label: 'C',
|
||||
alias: ['c', 'h', 'clang', 'clang'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'cpp',
|
||||
label: 'C++',
|
||||
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
|
||||
mode: 'c_cpp'
|
||||
},
|
||||
{
|
||||
name: 'csharp',
|
||||
label: 'C#',
|
||||
alias: ['cs', 'c#'],
|
||||
mode: 'csharp'
|
||||
},
|
||||
{
|
||||
name: 'swift',
|
||||
label: 'Swift',
|
||||
alias: [],
|
||||
mode: 'swift'
|
||||
},
|
||||
{
|
||||
name: 'golang',
|
||||
label: 'Go',
|
||||
alias: ['go'],
|
||||
mode: 'golang'
|
||||
},
|
||||
|
||||
// Minor
|
||||
{
|
||||
name: 'abap',
|
||||
label: 'ABAP',
|
||||
alias: [],
|
||||
mode: 'abap'
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
label: 'ABC',
|
||||
alias: [],
|
||||
mode: 'abc'
|
||||
},
|
||||
{
|
||||
name: 'actionscript',
|
||||
label: 'ActionScript',
|
||||
alias: ['as'],
|
||||
mode: 'actionscript'
|
||||
},
|
||||
{
|
||||
name: 'ada',
|
||||
label: 'Ada',
|
||||
alias: [],
|
||||
mode: 'ada'
|
||||
},
|
||||
{
|
||||
name: 'apache_conf',
|
||||
label: 'Apache config',
|
||||
alias: ['apache', 'conf'],
|
||||
mode: 'apache_conf'
|
||||
},
|
||||
{
|
||||
name: 'applescript',
|
||||
label: 'AppleScript',
|
||||
alias: ['scpt'],
|
||||
mode: 'applescript'
|
||||
},
|
||||
{
|
||||
name: 'asciidoc',
|
||||
label: 'AsciiDoc',
|
||||
alias: ['ascii', 'doc', 'txt'],
|
||||
mode: 'asciidoc'
|
||||
},
|
||||
{
|
||||
name: 'assembly_x86',
|
||||
label: 'Assembly x86',
|
||||
alias: ['assembly', 'x86', 'asm'],
|
||||
mode: 'assembly_x86'
|
||||
},
|
||||
{
|
||||
name: 'autohotkey',
|
||||
label: 'AutoHotkey',
|
||||
alias: ['ahk'],
|
||||
mode: 'autohotkey'
|
||||
},
|
||||
{
|
||||
name: 'batchfile',
|
||||
label: 'Batch file',
|
||||
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
|
||||
mode: 'batchfile'
|
||||
},
|
||||
{
|
||||
name: 'cirru',
|
||||
label: 'Cirru',
|
||||
alias: [],
|
||||
mode: 'cirru'
|
||||
},
|
||||
{
|
||||
name: 'clojure',
|
||||
label: 'Clojure',
|
||||
alias: ['clj', 'cljs', 'cljc', 'edn'],
|
||||
mode: 'clojure'
|
||||
},
|
||||
{
|
||||
name: 'cobol',
|
||||
label: 'COBOL',
|
||||
alias: ['cbl', 'cob', 'cpy'],
|
||||
mode: 'cobol'
|
||||
},
|
||||
{
|
||||
name: 'coffee',
|
||||
label: 'CoffeeScript',
|
||||
alias: ['coffee'],
|
||||
mode: 'coffee'
|
||||
},
|
||||
{
|
||||
name: 'coldfusion',
|
||||
label: 'ColdFusion',
|
||||
alias: ['cfm', 'cfc'],
|
||||
mode: 'coldfusion'
|
||||
},
|
||||
{
|
||||
name: 'curly',
|
||||
label: 'Curly',
|
||||
alias: [],
|
||||
mode: 'curly'
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
label: 'D',
|
||||
alias: ['dlang'],
|
||||
mode: 'd'
|
||||
},
|
||||
{
|
||||
name: 'dockerfile',
|
||||
label: 'DockerFile',
|
||||
alias: ['docker'],
|
||||
mode: 'docker'
|
||||
},
|
||||
{
|
||||
name: 'dart',
|
||||
label: 'Dart',
|
||||
alias: [],
|
||||
mode: 'dart'
|
||||
},
|
||||
{
|
||||
name: 'diff',
|
||||
label: 'Diff',
|
||||
alias: [],
|
||||
mode: 'diff'
|
||||
},
|
||||
{
|
||||
name: 'django',
|
||||
label: 'Django',
|
||||
alias: [],
|
||||
mode: 'djt'
|
||||
},
|
||||
{
|
||||
name: 'dot',
|
||||
label: 'DOT',
|
||||
alias: ['gv'],
|
||||
mode: 'dot'
|
||||
},
|
||||
{
|
||||
name: 'eiffel',
|
||||
label: 'Eiffel',
|
||||
alias: [],
|
||||
mode: 'eiffel'
|
||||
},
|
||||
{
|
||||
name: 'ejs',
|
||||
label: 'EJS',
|
||||
alias: [],
|
||||
mode: 'ejs'
|
||||
},
|
||||
{
|
||||
name: 'elixir',
|
||||
label: 'Elixir',
|
||||
alias: ['ex', 'exs'],
|
||||
mode: 'elixir'
|
||||
},
|
||||
{
|
||||
name: 'elm',
|
||||
label: 'Elm',
|
||||
alias: [],
|
||||
mode: 'elm'
|
||||
},
|
||||
{
|
||||
name: 'erlang',
|
||||
label: 'Erlang',
|
||||
alias: ['erl', 'hrl'],
|
||||
mode: 'erlang'
|
||||
},
|
||||
{
|
||||
name: 'forth',
|
||||
label: 'Forth',
|
||||
alias: ['fs', 'fth'],
|
||||
mode: 'forth'
|
||||
},
|
||||
{
|
||||
name: 'freemaker',
|
||||
label: 'Freemaker',
|
||||
alias: ['ftl'],
|
||||
mode: 'ftl'
|
||||
},
|
||||
{
|
||||
name: 'gcode',
|
||||
label: 'G-code',
|
||||
alias: ['mpt', 'mpf', 'nc'],
|
||||
mode: 'gcode'
|
||||
},
|
||||
{
|
||||
name: 'gherkin',
|
||||
label: 'Gherkin',
|
||||
alias: ['cucumber'],
|
||||
mode: 'gherkin'
|
||||
},
|
||||
{
|
||||
name: 'gitignore',
|
||||
label: 'Gitignore',
|
||||
alias: ['git'],
|
||||
mode: 'gitignore'
|
||||
},
|
||||
{
|
||||
name: 'glsl',
|
||||
label: 'GLSL',
|
||||
alias: ['opengl', 'shading'],
|
||||
mode: 'glsl'
|
||||
},
|
||||
{
|
||||
name: 'groovy',
|
||||
label: 'Groovy',
|
||||
alias: [],
|
||||
mode: 'grooby'
|
||||
},
|
||||
{
|
||||
name: 'haml',
|
||||
label: 'Haml',
|
||||
alias: [],
|
||||
mode: 'haml'
|
||||
},
|
||||
{
|
||||
name: 'handlebars',
|
||||
label: 'Handlebars',
|
||||
alias: ['hbs'],
|
||||
mode: 'handlebars'
|
||||
},
|
||||
{
|
||||
name: 'haskell',
|
||||
label: 'Haskell',
|
||||
alias: ['hs', 'lhs'],
|
||||
mode: 'haskell'
|
||||
},
|
||||
{
|
||||
name: 'haxe',
|
||||
label: 'Haxe',
|
||||
alias: ['hx', 'hxml'],
|
||||
mode: 'haxe'
|
||||
},
|
||||
{
|
||||
name: 'html_ruby',
|
||||
label: 'HTML (Ruby)',
|
||||
alias: ['erb', 'rhtml'],
|
||||
mode: 'html_ruby'
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
label: 'JSX',
|
||||
alias: ['es', 'babel', 'js', 'jsx', 'react'],
|
||||
mode: 'jsx'
|
||||
},
|
||||
{
|
||||
name: 'typescript',
|
||||
label: 'TypeScript',
|
||||
alias: ['ts'],
|
||||
mode: 'typescript'
|
||||
},
|
||||
{
|
||||
name: 'ini',
|
||||
label: 'INI file',
|
||||
alias: [],
|
||||
mode: 'ini'
|
||||
},
|
||||
{
|
||||
name: 'io',
|
||||
label: 'Io',
|
||||
alias: [],
|
||||
mode: 'io'
|
||||
},
|
||||
{
|
||||
name: 'jack',
|
||||
label: 'Jack',
|
||||
alias: [],
|
||||
mode: 'jack'
|
||||
},
|
||||
{
|
||||
name: 'jade',
|
||||
label: 'Jade',
|
||||
alias: [],
|
||||
mode: 'jade'
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
label: 'JSON',
|
||||
alias: [],
|
||||
mode: 'json'
|
||||
},
|
||||
{
|
||||
name: 'jsoniq',
|
||||
label: 'JSONiq',
|
||||
alias: ['query'],
|
||||
mode: 'jsoniq'
|
||||
},
|
||||
{
|
||||
name: 'jsp',
|
||||
label: 'JSP',
|
||||
alias: [],
|
||||
mode: 'jsp'
|
||||
},
|
||||
{
|
||||
name: 'julia',
|
||||
label: 'Julia',
|
||||
alias: [],
|
||||
mode: 'julia'
|
||||
},
|
||||
{
|
||||
name: 'latex',
|
||||
label: 'Latex',
|
||||
alias: ['tex'],
|
||||
mode: 'latex'
|
||||
},
|
||||
{
|
||||
name: 'lean',
|
||||
label: 'Lean',
|
||||
alias: [],
|
||||
mode: 'lean'
|
||||
},
|
||||
{
|
||||
name: 'less',
|
||||
label: 'Less',
|
||||
alias: [],
|
||||
mode: 'less'
|
||||
},
|
||||
{
|
||||
name: 'liquid',
|
||||
label: 'Liquid',
|
||||
alias: [],
|
||||
mode: 'liquid'
|
||||
},
|
||||
{
|
||||
name: 'lisp',
|
||||
label: 'Lisp',
|
||||
alias: ['lsp'],
|
||||
mode: 'lisp'
|
||||
},
|
||||
{
|
||||
name: 'livescript',
|
||||
label: 'LiveScript',
|
||||
alias: ['ls'],
|
||||
mode: 'livescript'
|
||||
},
|
||||
{
|
||||
name: 'logiql',
|
||||
label: 'LogiQL',
|
||||
alias: [],
|
||||
mode: 'logiql'
|
||||
},
|
||||
{
|
||||
name: 'lsl',
|
||||
label: 'LSL',
|
||||
alias: [],
|
||||
mode: 'lsl'
|
||||
},
|
||||
{
|
||||
name: 'lua',
|
||||
label: 'Lua',
|
||||
alias: [],
|
||||
mode: 'lua'
|
||||
},
|
||||
{
|
||||
name: 'luapage',
|
||||
label: 'Luapage',
|
||||
alias: [],
|
||||
mode: 'luapage'
|
||||
},
|
||||
{
|
||||
name: 'lucene',
|
||||
label: 'Lucene',
|
||||
alias: [],
|
||||
mode: 'lucene'
|
||||
},
|
||||
{
|
||||
name: 'makefile',
|
||||
label: 'Makefile',
|
||||
alias: [],
|
||||
mode: 'makefile'
|
||||
},
|
||||
{
|
||||
name: 'mask',
|
||||
label: 'Mask',
|
||||
alias: [],
|
||||
mode: 'mask'
|
||||
},
|
||||
{
|
||||
name: 'matlab',
|
||||
label: 'MATLAB',
|
||||
alias: [],
|
||||
mode: 'matlab'
|
||||
},
|
||||
{
|
||||
name: 'maze',
|
||||
label: 'Maze',
|
||||
alias: [],
|
||||
mode: 'maze'
|
||||
},
|
||||
{
|
||||
name: 'mel',
|
||||
label: 'MEL',
|
||||
alias: [],
|
||||
mode: 'mel'
|
||||
},
|
||||
{
|
||||
name: 'mipsassembler',
|
||||
label: 'MIPS assembly',
|
||||
alias: [],
|
||||
mode: 'mipsassembler'
|
||||
},
|
||||
{
|
||||
name: 'mushcode',
|
||||
label: 'MUSHCode',
|
||||
alias: [],
|
||||
mode: 'mushcode'
|
||||
},
|
||||
{
|
||||
name: 'mysql',
|
||||
label: 'MySQL',
|
||||
alias: [],
|
||||
mode: 'mysql'
|
||||
},
|
||||
{
|
||||
name: 'nix',
|
||||
label: 'Nix',
|
||||
alias: [],
|
||||
mode: 'nix'
|
||||
},
|
||||
{
|
||||
name: 'objectivec',
|
||||
label: 'Objective C',
|
||||
alias: ['objc'],
|
||||
mode: 'objectivec'
|
||||
},
|
||||
{
|
||||
name: 'ocaml',
|
||||
label: 'OCaml',
|
||||
alias: [],
|
||||
mode: 'ocaml'
|
||||
},
|
||||
{
|
||||
name: 'pascal',
|
||||
label: 'Pascal',
|
||||
alias: [],
|
||||
mode: 'pascal'
|
||||
},
|
||||
{
|
||||
name: 'perl',
|
||||
label: 'Perl',
|
||||
alias: [],
|
||||
mode: 'perl'
|
||||
},
|
||||
{
|
||||
name: 'pgsql',
|
||||
label: 'Postgres SQL',
|
||||
alias: ['postgres'],
|
||||
mode: 'pgsql'
|
||||
},
|
||||
{
|
||||
name: 'powershell',
|
||||
label: 'PowerShell',
|
||||
alias: ['ps1'],
|
||||
mode: 'powershell'
|
||||
},
|
||||
{
|
||||
name: 'praat',
|
||||
label: 'Praat',
|
||||
alias: [],
|
||||
mode: 'praat'
|
||||
},
|
||||
{
|
||||
name: 'prolog',
|
||||
label: 'Prolog',
|
||||
alias: ['pl', 'pro'],
|
||||
mode: 'prolog'
|
||||
},
|
||||
{
|
||||
name: 'properties',
|
||||
label: 'Properties',
|
||||
alias: [],
|
||||
mode: 'properties'
|
||||
},
|
||||
{
|
||||
name: 'protobuf',
|
||||
label: 'Protocol Buffers',
|
||||
alias: ['protocol', 'buffers'],
|
||||
mode: 'protobuf'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
label: 'R',
|
||||
alias: ['rlang'],
|
||||
mode: 'r'
|
||||
},
|
||||
{
|
||||
name: 'rdoc',
|
||||
label: 'RDoc',
|
||||
alias: [],
|
||||
mode: 'rdoc'
|
||||
},
|
||||
{
|
||||
name: 'rust',
|
||||
label: 'Rust',
|
||||
alias: [],
|
||||
mode: 'rust'
|
||||
},
|
||||
{
|
||||
name: 'sass',
|
||||
label: 'Sass',
|
||||
alias: [],
|
||||
mode: 'sass'
|
||||
},
|
||||
{
|
||||
name: 'scad',
|
||||
label: 'SCAD',
|
||||
alias: [],
|
||||
mode: 'scad'
|
||||
},
|
||||
{
|
||||
name: 'scala',
|
||||
label: 'Scala',
|
||||
alias: [],
|
||||
mode: 'scala'
|
||||
},
|
||||
{
|
||||
name: 'scheme',
|
||||
label: 'Scheme',
|
||||
alias: ['scm', 'ss'],
|
||||
mode: 'scheme'
|
||||
},
|
||||
{
|
||||
name: 'scss',
|
||||
label: 'Scss',
|
||||
alias: [],
|
||||
mode: 'scss'
|
||||
},
|
||||
{
|
||||
name: 'sh',
|
||||
label: 'Shell',
|
||||
alias: ['shell'],
|
||||
mode: 'sh'
|
||||
},
|
||||
{
|
||||
name: 'sjs',
|
||||
label: 'StratifiedJS',
|
||||
alias: ['stratified'],
|
||||
mode: 'sjs'
|
||||
},
|
||||
{
|
||||
name: 'smarty',
|
||||
label: 'Smarty',
|
||||
alias: [],
|
||||
mode: 'smarty'
|
||||
},
|
||||
{
|
||||
name: 'snippets',
|
||||
label: 'Snippets',
|
||||
alias: [],
|
||||
mode: 'snippets'
|
||||
},
|
||||
{
|
||||
name: 'soy_template',
|
||||
label: 'Soy Template',
|
||||
alias: ['soy'],
|
||||
mode: 'soy_template'
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
label: 'Space',
|
||||
alias: [],
|
||||
mode: 'space'
|
||||
},
|
||||
{
|
||||
name: 'sql',
|
||||
label: 'SQL',
|
||||
alias: [],
|
||||
mode: 'sql'
|
||||
},
|
||||
{
|
||||
name: 'sqlserver',
|
||||
label: 'SQL Server',
|
||||
alias: [],
|
||||
mode: 'sqlserver'
|
||||
},
|
||||
{
|
||||
name: 'stylus',
|
||||
label: 'Stylus',
|
||||
alias: [],
|
||||
mode: 'stylus'
|
||||
},
|
||||
{
|
||||
name: 'svg',
|
||||
label: 'SVG',
|
||||
alias: [],
|
||||
mode: 'svg'
|
||||
},
|
||||
{
|
||||
name: 'swig',
|
||||
label: 'SWIG',
|
||||
alias: [],
|
||||
mode: 'swig'
|
||||
},
|
||||
{
|
||||
name: 'tcl',
|
||||
label: 'Tcl',
|
||||
alias: [],
|
||||
mode: 'tcl'
|
||||
},
|
||||
{
|
||||
name: 'tex',
|
||||
label: 'TeX',
|
||||
alias: [],
|
||||
mode: 'tex'
|
||||
},
|
||||
{
|
||||
name: 'textile',
|
||||
label: 'Textile',
|
||||
alias: [],
|
||||
mode: 'textile'
|
||||
},
|
||||
{
|
||||
name: 'toml',
|
||||
label: 'TOML',
|
||||
alias: [],
|
||||
mode: 'toml'
|
||||
},
|
||||
{
|
||||
name: 'twig',
|
||||
label: 'Twig',
|
||||
alias: [],
|
||||
mode: 'twig'
|
||||
},
|
||||
{
|
||||
name: 'vala',
|
||||
label: 'Vala',
|
||||
alias: [],
|
||||
mode: 'vala'
|
||||
},
|
||||
{
|
||||
name: 'vbscript',
|
||||
label: 'VBScript',
|
||||
alias: ['vbs', 'vbe'],
|
||||
mode: 'vbscript'
|
||||
},
|
||||
{
|
||||
name: 'velocity',
|
||||
label: 'Velocity',
|
||||
alias: [],
|
||||
mode: 'velocity'
|
||||
},
|
||||
{
|
||||
name: 'verilog',
|
||||
label: 'Verilog',
|
||||
alias: [],
|
||||
mode: 'verilog'
|
||||
},
|
||||
{
|
||||
name: 'vhdl',
|
||||
label: 'VHDL',
|
||||
alias: [],
|
||||
mode: 'vhdl'
|
||||
},
|
||||
{
|
||||
name: 'xml',
|
||||
label: 'XML',
|
||||
alias: [],
|
||||
mode: 'xml'
|
||||
},
|
||||
{
|
||||
name: 'xquery',
|
||||
label: 'XQuery',
|
||||
alias: [],
|
||||
mode: 'xquery'
|
||||
},
|
||||
{
|
||||
name: 'yaml',
|
||||
label: 'YAML',
|
||||
alias: [],
|
||||
mode: 'yaml'
|
||||
}
|
||||
]
|
||||
|
||||
export default modes
|
||||
7
browser/lib/openExternal.js
Normal file
7
browser/lib/openExternal.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const electron = require('electron')
|
||||
const shell = electron.shell
|
||||
|
||||
export default function (e) {
|
||||
shell.openExternal(e.currentTarget.href)
|
||||
e.preventDefault()
|
||||
}
|
||||
40
browser/lib/search.js
Normal file
40
browser/lib/search.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict'
|
||||
|
||||
var _ = require('lodash')
|
||||
|
||||
const TEXT_FILTER = 'TEXT_FILTER'
|
||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
||||
const TAG_FILTER = 'TAG_FILTER'
|
||||
|
||||
export default function search (articles, search) {
|
||||
let filters = 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 (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.name.match(new RegExp(tagFilter.value, 'i')))
|
||||
})
|
||||
}, articles)
|
||||
}
|
||||
|
||||
return articles
|
||||
}
|
||||
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 () {
|
||||
console.log(this.state.switchPreview)
|
||||
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) {
|
||||
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
|
||||
})
|
||||
}
|
||||
362
browser/main/HomePage/ArticleDetail/index.js
Normal file
362
browser/main/HomePage/ArticleDetail/index.js
Normal file
@@ -0,0 +1,362 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
import {
|
||||
switchFolder,
|
||||
updateArticle,
|
||||
// cacheArticle,
|
||||
// saveArticle,
|
||||
// uncacheArticle
|
||||
} 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
|
||||
|
||||
// const remote = electron.remote
|
||||
// const { Menu, MenuItem } = remote
|
||||
// const othersMenu = new Menu()
|
||||
// othersMenu.append(new MenuItem({
|
||||
// label: 'Delete Post',
|
||||
// click: function () {
|
||||
// remote.getCurrentWebContents().send('detail-delete')
|
||||
// }
|
||||
// }))
|
||||
// othersMenu.append(new MenuItem({
|
||||
// label: 'Discard Change',
|
||||
// click: function (item) {
|
||||
// remote.getCurrentWebContents().send('detail-uncache')
|
||||
// }
|
||||
// }))
|
||||
|
||||
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.saveHandler = e => {
|
||||
if (isModalOpen()) return true
|
||||
this.handleSaveButtonClick()
|
||||
}
|
||||
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.state = {
|
||||
article: Object.assign({content: ''}, props.activeArticle),
|
||||
openShareDropdown: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
this.shareDropdownInterceptor = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// ipc.on('detail-save', this.saveHandler)
|
||||
ipc.on('detail-delete', this.deleteHandler)
|
||||
ipc.on('detail-uncache', this.uncacheHandler)
|
||||
ipc.on('detail-title', this.titleHandler)
|
||||
ipc.on('detail-edit', this.editHandler)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
|
||||
// ipc.removeListener('detail-save', this.saveHandler)
|
||||
ipc.removeListener('detail-delete', this.deleteHandler)
|
||||
ipc.removeListener('detail-uncache', this.uncacheHandler)
|
||||
ipc.removeListener('detail-title', this.titleHandler)
|
||||
ipc.removeListener('detail-edit', this.editHandler)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)}
|
||||
/>
|
||||
</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
|
||||
207
browser/main/HomePage/ArticleList.js
Normal file
207
browser/main/HomePage/ArticleList.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
import moment from 'moment'
|
||||
import { switchArticle } from '../actions'
|
||||
import FolderMark from 'browser/components/FolderMark'
|
||||
import TagLink from './TagLink'
|
||||
import _ from 'lodash'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
export default class ArticleList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.focusHandler = e => this.focus()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
|
||||
ipc.on('list-focus', this.focusHandler)
|
||||
this.focus()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.refreshTimer)
|
||||
ipc.removeListener('list-focus', this.focusHandler)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
let { articles, activeArticle } = this.props
|
||||
var index = articles.indexOf(activeArticle)
|
||||
var el = ReactDOM.findDOMNode(this)
|
||||
var li = el.querySelectorAll('.ArticleList>div')[index]
|
||||
|
||||
if (li == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
|
||||
if (overflowBelow) {
|
||||
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
|
||||
}
|
||||
var overflowAbove = el.scrollTop > li.offsetTop
|
||||
if (overflowAbove) {
|
||||
el.scrollTop = li.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
focus () {
|
||||
ReactDOM.findDOMNode(this).focus()
|
||||
}
|
||||
|
||||
// 移動ができなかったらfalseを返す:
|
||||
selectPriorArticle () {
|
||||
let { articles, activeArticle, dispatch } = this.props
|
||||
let targetIndex = articles.indexOf(activeArticle) - 1
|
||||
let targetArticle = articles[targetIndex]
|
||||
|
||||
if (targetArticle != null) {
|
||||
dispatch(switchArticle(targetArticle.key))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
selectNextArticle () {
|
||||
let { articles, activeArticle, dispatch } = this.props
|
||||
let targetIndex = articles.indexOf(activeArticle) + 1
|
||||
let targetArticle = articles[targetIndex]
|
||||
|
||||
if (targetArticle != null) {
|
||||
dispatch(switchArticle(targetArticle.key))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
handleArticleClick (article) {
|
||||
let { dispatch } = this.props
|
||||
return function (e) {
|
||||
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 () {
|
||||
let { articles, modified, activeArticle, folders } = this.props
|
||||
|
||||
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
|
||||
? article.tags.slice().map(tag => {
|
||||
return (<TagLink key={tag} tag={tag}/>)
|
||||
})
|
||||
: (<span>Not tagged yet</span>)
|
||||
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 (
|
||||
<div key={'article-' + article.key}>
|
||||
<div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
|
||||
<div className='ArticleList-item-top'>
|
||||
{folder != null
|
||||
? 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 className='updatedAt'
|
||||
children={
|
||||
modifiedArticle != null
|
||||
? <span><span className='unsaved-mark'>●</span> Unsaved</span>
|
||||
: moment(article.updatedAt).fromNow()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='ArticleList-item-middle'>
|
||||
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
|
||||
{articleElements}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleList.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
folders: PropTypes.array,
|
||||
articles: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape()
|
||||
}
|
||||
202
browser/main/HomePage/ArticleNavigator.js
Normal file
202
browser/main/HomePage/ArticleNavigator.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import { findWhere } from 'lodash'
|
||||
import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
|
||||
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||
import FolderMark from 'browser/components/FolderMark'
|
||||
import Preferences from '../modal/Preferences'
|
||||
import CreateNewFolder from '../modal/CreateNewFolder'
|
||||
import _ from 'lodash'
|
||||
import ModeIcon from 'browser/components/ModeIcon'
|
||||
|
||||
const ipc = require('electron').ipcRenderer
|
||||
|
||||
const BRAND_COLOR = '#18AF90'
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const preferenceTutorialElement = (
|
||||
<svg width='300' height='300' className='tutorial'>
|
||||
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
|
||||
<svg x='-30' y='-270' width='400' height='400'>
|
||||
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
|
||||
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
|
||||
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
|
||||
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
|
||||
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
|
||||
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
|
||||
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const newFolderTutorialElement = (
|
||||
<svg width='800' height='500' className='tutorial'>
|
||||
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
||||
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
|
||||
<svg x='50' y='10' width='300' height='400'>
|
||||
<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
|
||||
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='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
|
||||
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
|
||||
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
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) {
|
||||
openModal(Preferences)
|
||||
}
|
||||
|
||||
handleNewFolderButton (e) {
|
||||
let { user } = this.props
|
||||
openModal(CreateNewFolder, {user: user})
|
||||
}
|
||||
|
||||
handleFolderButtonClick (name) {
|
||||
return e => {
|
||||
let { dispatch } = this.props
|
||||
dispatch(switchFolder(name))
|
||||
}
|
||||
}
|
||||
|
||||
handleAllFoldersButtonClick (e) {
|
||||
let { dispatch } = this.props
|
||||
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 () {
|
||||
let { status, user, folders, allArticles, modified, activeArticle } = this.props
|
||||
let { targetFolders } = status
|
||||
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 isActive = findWhere(targetFolders, {key: folder.key})
|
||||
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
|
||||
|
||||
return (
|
||||
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
|
||||
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div tabIndex='1' className='ArticleNavigator'>
|
||||
<div className='userInfo'>
|
||||
<div className='userProfileName'>{user.name}</div>
|
||||
<div className='userName'>localStorage</div>
|
||||
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
|
||||
<i className='fa fa-fw fa-chevron-down'/>
|
||||
<span className='tooltip'>Preferences</span>
|
||||
</button>
|
||||
|
||||
{status.isTutorialOpen ? preferenceTutorialElement : null}
|
||||
|
||||
</div>
|
||||
|
||||
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
|
||||
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
|
||||
<div className='ArticleNavigator-unsaved-list'>
|
||||
{modifiedElements}
|
||||
</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>*/}
|
||||
|
||||
|
||||
|
||||
<div className={'ArticleNavigator-folders expand'}>
|
||||
{status.isTutorialOpen ? newFolderTutorialElement : null}
|
||||
<div className='ArticleNavigator-folders-header'>
|
||||
<div className='title'>Folders</div>
|
||||
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
|
||||
<i className='fa fa-fw fa-plus'/>
|
||||
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className='folderList'>
|
||||
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
|
||||
{folderElememts}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleNavigator.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
status: PropTypes.shape({
|
||||
folderId: PropTypes.number
|
||||
}),
|
||||
user: PropTypes.object,
|
||||
folders: PropTypes.array,
|
||||
allArticles: PropTypes.array,
|
||||
articles: PropTypes.array,
|
||||
modified: PropTypes.array,
|
||||
activeArticle: PropTypes.shape({
|
||||
key: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
267
browser/main/HomePage/ArticleTopBar.js
Normal file
267
browser/main/HomePage/ArticleTopBar.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ExternalLink from 'browser/components/ExternalLink'
|
||||
import { setSearchFilter, clearSearch, toggleTutorial, saveArticle, switchFolder } from '../actions'
|
||||
import { isModalOpen } from 'browser/lib/modal'
|
||||
import keygen from 'browser/lib/keygen'
|
||||
|
||||
const electron = require('electron')
|
||||
const remote = electron.remote
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const BRAND_COLOR = '#18AF90'
|
||||
|
||||
const searchTutorialElement = (
|
||||
<svg width='750' height='300' className='tutorial'>
|
||||
<text x='125' y='63' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
|
||||
<text x='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
|
||||
<text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
|
||||
{'- Search by folder : /{folder_name}\n'}</text>
|
||||
<text x='140' y='135' fill={BRAND_COLOR} fontSize='14'>
|
||||
{'exact match : //{folder_name}'}</text>
|
||||
|
||||
<svg x='90' width='500' height='300'>
|
||||
<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
|
||||
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
|
||||
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
|
||||
c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
|
||||
<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
|
||||
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
|
||||
C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
|
||||
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>
|
||||
)
|
||||
|
||||
export default class ArticleTopBar extends React.Component {
|
||||
constructor (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 = {
|
||||
isTooltipHidden: true,
|
||||
isLinksDropdownOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
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 () {
|
||||
document.removeEventListener('click', this.hideLinksDropdown)
|
||||
this.linksButton.removeEventListener('click', this.showLinksDropdown())
|
||||
|
||||
// ipc.removeListener('top-save-all', this.saveAllHandler)
|
||||
ipc.removeListener('top-focus-search', this.focusSearchHandler)
|
||||
ipc.removeListener('top-new-post', this.newPostHandler)
|
||||
}
|
||||
|
||||
handleTooltipRequest (e) {
|
||||
if (this.searchInput.value.length === 0 && (document.activeElement === this.searchInput)) {
|
||||
this.setState({isTooltipHidden: false})
|
||||
} else {
|
||||
this.setState({isTooltipHidden: true})
|
||||
}
|
||||
}
|
||||
|
||||
isInputFocused () {
|
||||
return document.activeElement === ReactDOM.findDOMNode(this.refs.searchInput)
|
||||
}
|
||||
|
||||
escape () {
|
||||
let { status, dispatch } = this.props
|
||||
if (status.search.length > 0) {
|
||||
dispatch(clearSearch())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
focusInput () {
|
||||
this.searchInput.focus()
|
||||
}
|
||||
|
||||
blurInput () {
|
||||
this.searchInput.blur()
|
||||
}
|
||||
|
||||
handleSearchChange (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(setSearchFilter(e.target.value))
|
||||
this.handleTooltipRequest()
|
||||
}
|
||||
|
||||
handleSearchClearButton (e) {
|
||||
this.searchInput.value = ''
|
||||
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')
|
||||
}
|
||||
|
||||
handleTutorialButtonClick (e) {
|
||||
let { dispatch } = this.props
|
||||
|
||||
dispatch(toggleTutorial())
|
||||
}
|
||||
|
||||
render () {
|
||||
let { status } = this.props
|
||||
return (
|
||||
<div tabIndex='2' className='ArticleTopBar'>
|
||||
<div className='ArticleTopBar-left'>
|
||||
<div className='ArticleTopBar-left-search'>
|
||||
<i className='fa fa-search fa-fw' />
|
||||
<input
|
||||
ref='searchInput'
|
||||
onFocus={e => this.handleSearchChange(e)}
|
||||
onBlur={e => this.handleSearchChange(e)}
|
||||
value={this.props.status.search}
|
||||
onChange={e => this.handleSearchChange(e)}
|
||||
placeholder='Search'
|
||||
type='text'
|
||||
/>
|
||||
{
|
||||
this.props.status.search != null && this.props.status.search.length > 0
|
||||
? <button onClick={e => this.handleSearchClearButton(e)} className='ArticleTopBar-left-search-clear-button'><i className='fa fa-times'/></button>
|
||||
: null
|
||||
}
|
||||
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
|
||||
<ul>
|
||||
<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>
|
||||
|
||||
{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 className='ArticleTopBar-right'>
|
||||
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
|
||||
</button>
|
||||
<a ref='links' className='ArticleTopBar-right-links-button' href>
|
||||
<img src='../resources/app.png' width='44' height='44'/>
|
||||
</a>
|
||||
{
|
||||
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>
|
||||
|
||||
{status.isTutorialOpen ? (
|
||||
<div className='tutorial'>
|
||||
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
|
||||
<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='150' y='55' fontSize='18' fill={BRAND_COLOR} children={'with pressing ' + (OSX ? '`⌘ + Alt + s`' : '`Win + Alt + s`')}/>
|
||||
</svg>
|
||||
<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='50' y='75' fontSize='18' fill={BRAND_COLOR}>Press any key or click to escape tutorial mode</text>
|
||||
</svg>
|
||||
<div className='back'></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ArticleTopBar.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
status: PropTypes.shape({
|
||||
search: PropTypes.string
|
||||
}),
|
||||
folders: PropTypes.array
|
||||
}
|
||||
18
browser/main/HomePage/TagLink.js
Normal file
18
browser/main/HomePage/TagLink.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import store from '../store'
|
||||
import { setTagFilter } from '../actions'
|
||||
|
||||
export default class TagLink extends React.Component {
|
||||
handleClick (e) {
|
||||
store.dispatch(setTagFilter(this.props.tag))
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagLink.propTypes = {
|
||||
tag: PropTypes.string
|
||||
}
|
||||
41
browser/main/HomePage/UserNavigator.js
Normal file
41
browser/main/HomePage/UserNavigator.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
import ProfileImage from 'browser/components/ProfileImage'
|
||||
|
||||
export default class UserNavigator extends Component {
|
||||
renderUserList () {
|
||||
if (this.props.users == null) return null
|
||||
|
||||
var users = this.props.users.map((user, index) => (
|
||||
<li key={'user-' + user.id}>
|
||||
<Link to={'/users/' + user.id} activeClassName='active'>
|
||||
<ProfileImage email={user.email} size='44'/>
|
||||
<div className='userTooltip'>{user.name}</div>
|
||||
{index < 9 ? <div className='keyLabel'>{'⌘' + (index + 1)}</div> : null}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
|
||||
return (
|
||||
<ul className='userList'>
|
||||
{users}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='UserNavigator'>
|
||||
{this.renderUserList()}
|
||||
<button className='createTeamBtn'>
|
||||
+
|
||||
<div className='tooltip'>Create a new team</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UserNavigator.propTypes = {
|
||||
users: PropTypes.array
|
||||
}
|
||||
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)
|
||||
36
browser/main/MainPage.js
Normal file
36
browser/main/MainPage.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
import React, { PropTypes } from 'react'
|
||||
import HomePage from './HomePage'
|
||||
|
||||
export default class MainContainer extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {updateAvailable: false}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ipc.on('update-available', function (message) {
|
||||
this.setState({updateAvailable: true})
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
updateApp () {
|
||||
ipc.send('update-app', 'Deal with it.')
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='Main'>
|
||||
{this.state.updateAvailable ? (
|
||||
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
|
||||
) : null}
|
||||
<HomePage/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MainContainer.propTypes = {
|
||||
children: PropTypes.element
|
||||
}
|
||||
178
browser/main/actions.js
Normal file
178
browser/main/actions.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// Action types
|
||||
export const USER_UPDATE = 'USER_UPDATE'
|
||||
|
||||
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
|
||||
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_UPDATE = 'FOLDER_UPDATE'
|
||||
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
|
||||
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
|
||||
|
||||
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
|
||||
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
|
||||
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
|
||||
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
|
||||
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
|
||||
|
||||
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
|
||||
|
||||
// Article status
|
||||
export const NEW = 'NEW'
|
||||
|
||||
export function updateUser (input) {
|
||||
return {
|
||||
type: USER_UPDATE,
|
||||
data: input
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return {
|
||||
type: ARTICLE_UPDATE,
|
||||
data: { article }
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyArticle (key) {
|
||||
return {
|
||||
type: ARTICLE_DESTROY,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function createFolder (folder) {
|
||||
return {
|
||||
type: FOLDER_CREATE,
|
||||
data: { folder }
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFolder (folder) {
|
||||
return {
|
||||
type: FOLDER_UPDATE,
|
||||
data: { folder }
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyFolder (key) {
|
||||
return {
|
||||
type: FOLDER_DESTROY,
|
||||
data: { key }
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceFolder (a, b) {
|
||||
return {
|
||||
type: FOLDER_REPLACE,
|
||||
data: {
|
||||
a,
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function switchFolder (folderName) {
|
||||
return {
|
||||
type: SWITCH_FOLDER,
|
||||
data: folderName
|
||||
}
|
||||
}
|
||||
|
||||
export function switchArticle (articleKey) {
|
||||
return {
|
||||
type: SWITCH_ARTICLE,
|
||||
data: {
|
||||
key: articleKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setSearchFilter (search) {
|
||||
return {
|
||||
type: SET_SEARCH_FILTER,
|
||||
data: search
|
||||
}
|
||||
}
|
||||
|
||||
export function setTagFilter (tag) {
|
||||
return {
|
||||
type: SET_TAG_FILTER,
|
||||
data: tag
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSearch () {
|
||||
return {
|
||||
type: CLEAR_SEARCH
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleTutorial () {
|
||||
return {
|
||||
type: TOGGLE_TUTORIAL
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
updateUser,
|
||||
|
||||
updateArticle,
|
||||
destroyArticle,
|
||||
cacheArticle,
|
||||
uncacheArticle,
|
||||
uncacheAllArticles,
|
||||
saveArticle,
|
||||
saveAllArticles,
|
||||
|
||||
createFolder,
|
||||
updateFolder,
|
||||
destroyFolder,
|
||||
replaceFolder,
|
||||
|
||||
switchFolder,
|
||||
switchArticle,
|
||||
setSearchFilter,
|
||||
setTagFilter,
|
||||
clearSearch,
|
||||
toggleTutorial
|
||||
}
|
||||
67
browser/main/index.js
Normal file
67
browser/main/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Provider } from 'react-redux'
|
||||
import MainPage from './MainPage'
|
||||
import store from './store'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
require('../styles/main/index.styl')
|
||||
import { openModal } from 'browser/lib/modal'
|
||||
import Tutorial from './modal/Tutorial'
|
||||
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()
|
||||
window.addEventListener('online', function () {
|
||||
ipc.send('check-update', 'check-update')
|
||||
})
|
||||
|
||||
function notify (title, options) {
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
options.silent = false
|
||||
}
|
||||
console.log(options)
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
ipc.on('notify', function (e, payload) {
|
||||
notify(payload.title, {
|
||||
body: payload.body
|
||||
})
|
||||
})
|
||||
|
||||
ipc.on('copy-finder', function () {
|
||||
activityRecord.emit('FINDER_COPY')
|
||||
})
|
||||
ipc.on('open-finder', function () {
|
||||
activityRecord.emit('FINDER_OPEN')
|
||||
})
|
||||
|
||||
let el = document.getElementById('content')
|
||||
ReactDOM.render((
|
||||
<div>
|
||||
<Provider store={store}>
|
||||
<MainPage/>
|
||||
</Provider>
|
||||
</div>
|
||||
), el, function () {
|
||||
let loadingCover = document.getElementById('loadingCover')
|
||||
loadingCover.parentNode.removeChild(loadingCover)
|
||||
let status = JSON.parse(localStorage.getItem('status'))
|
||||
if (status == null) status = {}
|
||||
if (!status.introWatched) {
|
||||
openModal(Tutorial)
|
||||
status.introWatched = true
|
||||
localStorage.setItem('status', JSON.stringify(status))
|
||||
}
|
||||
})
|
||||
107
browser/main/modal/CreateNewFolder.js
Normal file
107
browser/main/modal/CreateNewFolder.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import linkState from 'browser/lib/linkState'
|
||||
import { createFolder } from '../actions'
|
||||
import store from '../store'
|
||||
import FolderMark from 'browser/components/FolderMark'
|
||||
|
||||
export default class CreateNewFolder extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
color: Math.round(Math.random() * 7),
|
||||
alert: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ReactDOM.findDOMNode(this.refs.folderName).focus()
|
||||
}
|
||||
|
||||
handleCloseButton (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
handleConfirmButton (e) {
|
||||
this.setState({alert: null}, () => {
|
||||
let { close } = this.props
|
||||
let { name, color } = this.state
|
||||
|
||||
let input = {
|
||||
name,
|
||||
color
|
||||
}
|
||||
try {
|
||||
store.dispatch(createFolder(input))
|
||||
} catch (e) {
|
||||
this.setState({alert: {
|
||||
type: 'error',
|
||||
message: e.message
|
||||
}})
|
||||
return
|
||||
}
|
||||
close()
|
||||
})
|
||||
}
|
||||
|
||||
handleColorClick (colorIndex) {
|
||||
return e => {
|
||||
this.setState({
|
||||
color: colorIndex
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.handleConfirmButton()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
let alert = this.state.alert
|
||||
let alertElement = alert != null ? (
|
||||
<p className={`alert ${alert.type}`}>
|
||||
{alert.message}
|
||||
</p>
|
||||
) : 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 (
|
||||
<div className='CreateNewFolder modal'>
|
||||
<button onClick={e => this.handleCloseButton(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
|
||||
|
||||
<div className='title'>Create new folder</div>
|
||||
|
||||
<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}
|
||||
|
||||
<button onClick={e => this.handleConfirmButton(e)} className='confirmBtn'>Create</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CreateNewFolder.propTypes = {
|
||||
close: PropTypes.func
|
||||
}
|
||||
|
||||
CreateNewFolder.prototype.linkState = linkState
|
||||
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
|
||||
}
|
||||
123
browser/main/modal/Preference/ContactTab.js
Normal file
123
browser/main/modal/Preference/ContactTab.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import clientKey from 'browser/lib/clientKey'
|
||||
import linkState from 'browser/lib/linkState'
|
||||
import _ from 'lodash'
|
||||
import { request, SERVER_URL } from 'browser/lib/api'
|
||||
|
||||
const FORM_MODE = 'FORM_MODE'
|
||||
const DONE_MODE = 'DONE_MODE'
|
||||
|
||||
export default class ContactTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
email: '',
|
||||
mode: FORM_MODE,
|
||||
alert: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
let titleInput = ReactDOM.findDOMNode(this.refs.title)
|
||||
if (titleInput != null) titleInput.focus()
|
||||
}
|
||||
|
||||
handleBackButtonClick (e) {
|
||||
this.setState({
|
||||
mode: FORM_MODE
|
||||
})
|
||||
}
|
||||
|
||||
handleSendButtonClick (e) {
|
||||
let input = _.pick(this.state, ['title', 'content', 'email'])
|
||||
input.clientKey = clientKey.get()
|
||||
|
||||
this.setState({
|
||||
alert: {
|
||||
type: 'info',
|
||||
message: 'Sending...'
|
||||
}
|
||||
}, () => {
|
||||
request.post(SERVER_URL + 'apis/inquiry')
|
||||
.send(input)
|
||||
.then(res => {
|
||||
console.log('sent')
|
||||
this.setState({
|
||||
title: '',
|
||||
content: '',
|
||||
mode: DONE_MODE,
|
||||
alert: null
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
this.setState({
|
||||
alert: {
|
||||
type: 'error',
|
||||
message: 'Can\'t connect to API server.'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error(err)
|
||||
this.setState({
|
||||
alert: {
|
||||
type: 'error',
|
||||
message: err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
switch (this.state.mode) {
|
||||
case DONE_MODE:
|
||||
return (
|
||||
<div className='ContactTab content done'>
|
||||
<div className='message'>
|
||||
<i className='checkIcon fa fa-check-circle'/><br/>
|
||||
Your message has been sent successfully!!
|
||||
</div>
|
||||
<div className='control'>
|
||||
<button onClick={e => this.handleBackButtonClick(e)}>Back to Contact form</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case FORM_MODE:
|
||||
default:
|
||||
let alertElement = this.state.alert != null
|
||||
? (
|
||||
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
|
||||
)
|
||||
: null
|
||||
return (
|
||||
<div className='ContactTab content form'>
|
||||
<div className='title'>Contact form</div>
|
||||
<div className='description'>
|
||||
Your feedback is highly appreciated and will help us to improve our app. :D
|
||||
</div>
|
||||
<div className='iptGroup'>
|
||||
<input ref='title' valueLink={this.linkState('title')} placeholder='Title' type='text'/>
|
||||
</div>
|
||||
<div className='iptGroup'>
|
||||
<textarea valueLink={this.linkState('content')} placeholder='Content'/>
|
||||
</div>
|
||||
<div className='iptGroup'>
|
||||
<input valueLink={this.linkState('email')} placeholder='E-mail (Optional)' type='email'/>
|
||||
</div>
|
||||
<div className='formControl'>
|
||||
<button onClick={e => this.handleSendButtonClick(e)} className='primary'>Send</button>
|
||||
{alertElement}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContactTab.prototype.linkState = linkState
|
||||
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
|
||||
97
browser/main/modal/Preference/FolderSettingTab.js
Normal file
97
browser/main/modal/Preference/FolderSettingTab.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import FolderRow from './FolderRow'
|
||||
import linkState from 'browser/lib/linkState'
|
||||
import { createFolder } from '../../actions'
|
||||
|
||||
export default class FolderSettingTab extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
handleNewFolderNameKeyDown (e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.handleSaveButtonClick()
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveButtonClick (e) {
|
||||
this.setState({alert: null}, () => {
|
||||
let { dispatch } = this.props
|
||||
|
||||
try {
|
||||
dispatch(createFolder({
|
||||
name: this.state.name
|
||||
}))
|
||||
} catch (e) {
|
||||
this.setState({alert: {
|
||||
type: 'error',
|
||||
message: e.message
|
||||
}})
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({name: ''})
|
||||
})
|
||||
}
|
||||
|
||||
setAlert (alert, cb) {
|
||||
this.setState({alert: alert}, cb)
|
||||
}
|
||||
|
||||
render () {
|
||||
let { folders } = this.props
|
||||
let folderElements = folders.map((folder, index) => {
|
||||
return (
|
||||
<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 alertElement = alert != null ? (
|
||||
<p className={`alert ${alert.type}`}>
|
||||
{alert.message}
|
||||
</p>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className='FolderSettingTab content'>
|
||||
<div className='section'>
|
||||
<div className='sectionTitle'>Manage folder</div>
|
||||
<div className='folderTable'>
|
||||
<div className='folderHeader'>
|
||||
<div className='folderName'>Folder</div>
|
||||
<div className='folderControl'>Edit/Delete</div>
|
||||
</div>
|
||||
{folderElements}
|
||||
<div className='newFolder'>
|
||||
<div className='folderName'>
|
||||
<input onKeyDown={e => this.handleNewFolderNameKeyDown(e)} valueLink={this.linkState('name')} type='text' placeholder='New Folder'/>
|
||||
</div>
|
||||
<div className='folderControl'>
|
||||
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
{alertElement}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FolderSettingTab.propTypes = {
|
||||
folders: PropTypes.array,
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
FolderSettingTab.prototype.linkState = linkState
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
115
browser/main/modal/Tutorial.js
Normal file
115
browser/main/modal/Tutorial.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import CodeEditor from 'browser/components/CodeEditor'
|
||||
|
||||
export default class Tutorial extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
slideIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
handlePriorSlideClick () {
|
||||
if (this.state.slideIndex > 0) this.setState({slideIndex: this.state.slideIndex - 1})
|
||||
}
|
||||
|
||||
handleNextSlideClick () {
|
||||
if (this.state.slideIndex < 4) this.setState({slideIndex: this.state.slideIndex + 1})
|
||||
}
|
||||
|
||||
startButtonClick (e) {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
render () {
|
||||
let content = this.renderContent(this.state.slideIndex)
|
||||
|
||||
let dotElements = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
dotElements.push(<i key={i} className={'fa fa-fw fa-circle' + (i === this.state.slideIndex ? ' active' : '')}/>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='Tutorial modal'>
|
||||
<button onClick={e => this.handlePriorSlideClick()} className={'priorBtn' + (this.state.slideIndex === 0 ? ' hide' : '')}>
|
||||
<i className='fa fa-fw fa-angle-left'/>
|
||||
</button>
|
||||
<button onClick={e => this.handleNextSlideClick()} className={'nextBtn' + (this.state.slideIndex === 4 ? ' hide' : '')}>
|
||||
<i className='fa fa-fw fa-angle-right'/>
|
||||
</button>
|
||||
{content}
|
||||
<div className='dots'>
|
||||
{dotElements}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return (<div className='slide slide0'>
|
||||
<div className='title'>Welcome to Boost</div>
|
||||
<div className='content'>
|
||||
Boost is a brand new note app for software<br/>
|
||||
Don't waste time cleaning up your data.<br/>
|
||||
devote that time to more creative work.<br/>
|
||||
Hack your memory.
|
||||
</div>
|
||||
</div>)
|
||||
case 1:
|
||||
let content = '## Boost is a note app for engineer.\n\n - Write with markdown\n - Stylize beautiful'
|
||||
return (<div className='slide slide1'>
|
||||
<div className='title'>Write with Markdown</div>
|
||||
<div className='content'>
|
||||
Markdown is available.<br/>
|
||||
Your notes will be stylized beautifully and quickly.
|
||||
<div className='markdown'>
|
||||
<pre className='left'>{content}</pre>
|
||||
<MarkdownPreview className='right' content={content}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
case 2:
|
||||
let code = 'import shell from \'shell\'\r\nvar React = require(\'react\')\r\nvar { PropTypes } = React\r\nimport markdown from \'boost\/markdown\'\r\nvar ReactDOM = require(\'react-dom\')\r\n\r\nfunction handleAnchorClick (e) {\r\n shell.openExternal(e.target.href)\r\n e.preventDefault()\r\n}\r\n\r\nexport default class MarkdownPreview extends React.Component {\r\n componentDidMount () {\r\n this.addListener()\r\n }\r\n\r\n componentDidUpdate () {\r\n this.addListener()\r\n }\r\n\r\n componentWillUnmount () {\r\n this.removeListener()\r\n }'
|
||||
return (<div className='slide slide2'>
|
||||
<div className='title'>Beautiful code highlighting</div>
|
||||
<div className='content'>
|
||||
Boost supports code syntax highlighting.<br/>
|
||||
There are more than 100 different type of language.
|
||||
<div className='code'>
|
||||
<CodeEditor readOnly article={{content:code, mode: 'jsx'}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
case 3:
|
||||
return (<div className='slide slide3'>
|
||||
<div className='title'>Easy to access with Finder</div>
|
||||
<div className='content'>
|
||||
The Finder helps you organize all of the files and documents.<br/>
|
||||
There is a short-cut key [⌘ + alt + s] to open the Finder.<br/>
|
||||
It is available to save your articles on the Clipboard<br/>
|
||||
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'/>
|
||||
</div>
|
||||
</div>)
|
||||
case 4:
|
||||
return (<div className='slide slide4'>
|
||||
<div className='title'>Are you ready?</div>
|
||||
<div className='content'>
|
||||
<button onClick={e => this.startButtonClick(e)}>Start<br/>Boost</button>
|
||||
</div>
|
||||
</div>)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tutorial.propTypes = {
|
||||
close: PropTypes.func
|
||||
}
|
||||
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
|
||||
})
|
||||
6
browser/main/store.js
Normal file
6
browser/main/store.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import reducer from './reducer'
|
||||
import { createStore } from 'redux'
|
||||
|
||||
let store = createStore(reducer)
|
||||
|
||||
export default store
|
||||
128
browser/styles/finder/index.styl
Normal file
128
browser/styles/finder/index.styl
Normal file
@@ -0,0 +1,128 @@
|
||||
@import '../../../node_modules/nib/lib/nib'
|
||||
@import '../vars'
|
||||
@import '../mixins/*'
|
||||
global-reset()
|
||||
@import '../shared/*'
|
||||
|
||||
iptBgColor = #E6E6E6
|
||||
iptFocusBorderColor = #369DCD
|
||||
|
||||
DEFAULT_FONTS = 'Lato', 'MS Gothic', 'Malgun Gothic', 'Sans-serif'
|
||||
|
||||
body
|
||||
font-family DEFAULT_FONTS
|
||||
color textColor
|
||||
font-size fontSize
|
||||
width 100%
|
||||
height 100%
|
||||
overflow hidden
|
||||
button, input
|
||||
font-family "Lato"
|
||||
|
||||
.Finder
|
||||
absolute top bottom left right
|
||||
.FinderInput
|
||||
padding 11px
|
||||
margin 0 auto
|
||||
height 55px
|
||||
box-sizing border-box
|
||||
border-bottom solid 1px borderColor
|
||||
background-color iptBgColor
|
||||
z-index 200
|
||||
input
|
||||
display block
|
||||
width 100%
|
||||
border solid 1px borderColor
|
||||
padding 0 10px
|
||||
font-size 1em
|
||||
height 33px
|
||||
border-radius 5px
|
||||
box-sizing border-box
|
||||
border-radius 5px
|
||||
&:focus, &.focus
|
||||
border-color iptFocusBorderColor
|
||||
outline none
|
||||
.FinderList
|
||||
absolute left bottom
|
||||
top 55px
|
||||
border-right solid 1px borderColor
|
||||
box-sizing border-box
|
||||
width 250px
|
||||
overflow-y auto
|
||||
z-index 0
|
||||
user-select none
|
||||
&>ul>li
|
||||
.articleItem
|
||||
padding 10px
|
||||
border solid 2px transparent
|
||||
box-sizing border-box
|
||||
cursor pointer
|
||||
white-space nowrap
|
||||
overflow-x hidden
|
||||
text-overflow ellipsis
|
||||
.divider
|
||||
box-sizing border-box
|
||||
border-bottom solid 1px borderColor
|
||||
&.active
|
||||
.articleItem
|
||||
border-color brandColor
|
||||
|
||||
.FinderDetail
|
||||
absolute right bottom
|
||||
top 55px
|
||||
left 250px
|
||||
box-shadow 0px 0px 10px 0 #CCC
|
||||
z-index 100
|
||||
.header
|
||||
absolute top left right
|
||||
height 55px
|
||||
box-sizing border-box
|
||||
padding 0 10px
|
||||
border-bottom solid 1px borderColor
|
||||
line-height 55px
|
||||
font-size 18px
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
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
|
||||
position absolute
|
||||
top 55px
|
||||
padding 10px
|
||||
bottom 0
|
||||
left 0
|
||||
right 0
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
.MarkdownPreview
|
||||
marked()
|
||||
&.empty
|
||||
color lighten(inactiveTextColor, 10%)
|
||||
user-select none
|
||||
font-size 14px
|
||||
.CodeEditor
|
||||
absolute top bottom left right
|
||||
390
browser/styles/main/ArticleDetail.styl
Normal file
390
browser/styles/main/ArticleDetail.styl
Normal file
@@ -0,0 +1,390 @@
|
||||
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 5
|
||||
border 1px solid borderColor
|
||||
border-radius 5px
|
||||
&>button
|
||||
width 100%
|
||||
display block
|
||||
padding 0 15px
|
||||
height 33px
|
||||
line-height 33px
|
||||
background-color transparent
|
||||
border none
|
||||
text-align left
|
||||
font-size 14px
|
||||
&:hover
|
||||
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
|
||||
94
browser/styles/main/ArticleList.styl
Normal file
94
browser/styles/main/ArticleList.styl
Normal file
@@ -0,0 +1,94 @@
|
||||
articleItemHoverBgColor = darken(white, 5%)
|
||||
articleItemColor = #777
|
||||
|
||||
.ArticleList
|
||||
absolute bottom
|
||||
top 60px
|
||||
left 200px
|
||||
width 250px
|
||||
border-top 1px solid borderColor
|
||||
border-right 1px solid borderColor
|
||||
&:focus
|
||||
border-color focusBorderColor
|
||||
overflow-y auto
|
||||
noSelect()
|
||||
&>div
|
||||
.ArticleList-item
|
||||
border solid 2px transparent
|
||||
position relative
|
||||
height 110px
|
||||
width 100%
|
||||
cursor pointer
|
||||
transition 0.1s
|
||||
background-color white
|
||||
padding 0 10px
|
||||
font-size 12px
|
||||
.ArticleList-item-top
|
||||
clearfix()
|
||||
padding-top 2px
|
||||
line-height 18px
|
||||
height 20px
|
||||
color articleItemColor
|
||||
font-size 11px
|
||||
.folderName
|
||||
overflow ellipsis
|
||||
display inline-block
|
||||
width 120px
|
||||
.updatedAt
|
||||
float right
|
||||
line-height 18px
|
||||
.unsaved-mark
|
||||
color brandColor
|
||||
.ArticleList-item-middle
|
||||
font-size 16px
|
||||
position relative
|
||||
padding-top 6px
|
||||
height 22px
|
||||
.mode
|
||||
position absolute
|
||||
left 0
|
||||
font-size 12px
|
||||
line-height 16px
|
||||
.title
|
||||
position absolute
|
||||
left 19px
|
||||
right 0
|
||||
overflow ellipsis
|
||||
small
|
||||
color #AAA
|
||||
.ArticleList-item-middle2
|
||||
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
|
||||
overflow-x auto
|
||||
white-space nowrap
|
||||
padding-top 6px
|
||||
.tags
|
||||
color articleItemColor
|
||||
height 14px
|
||||
a
|
||||
background-color brandColor
|
||||
color white
|
||||
border-radius 2px
|
||||
padding 1px 5px
|
||||
margin 2px
|
||||
font-size 10px
|
||||
opacity 0.8
|
||||
&:hover
|
||||
opacity 1
|
||||
&:hover, &.hover
|
||||
background-color articleItemHoverBgColor
|
||||
&:active, &.active
|
||||
background-color white
|
||||
&:active, &.active
|
||||
border-color brandBorderColor
|
||||
.divider
|
||||
border-bottom solid 1px borderColor
|
||||
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
|
||||
224
browser/styles/main/ArticleTopBar.styl
Normal file
224
browser/styles/main/ArticleTopBar.styl
Normal file
@@ -0,0 +1,224 @@
|
||||
bgColor = #E6E6E6
|
||||
inputBgColor = white
|
||||
|
||||
topBarBtnColor = #B3B3B3
|
||||
topBarBtnBgColor = #B3B3B3
|
||||
topBarBtnBgActiveColor = #3A3A3A
|
||||
|
||||
infoBtnColor = bgColor
|
||||
infoBtnBgColor = #B3B3B3
|
||||
infoBtnActiveBgColor = #3A3A3A
|
||||
|
||||
.ArticleTopBar
|
||||
absolute top right
|
||||
left 200px
|
||||
height 60px
|
||||
background-color bgColor
|
||||
user-select none
|
||||
&>.tutorial
|
||||
.clickJammer
|
||||
fixed top left bottom right
|
||||
z-index 40
|
||||
background transparent
|
||||
.global
|
||||
fixed bottom right
|
||||
height 100px
|
||||
z-index 35
|
||||
font-style italic
|
||||
.finder
|
||||
fixed bottom right
|
||||
height 250px
|
||||
left 50%
|
||||
margin-left -250px
|
||||
z-index 35
|
||||
font-style italic
|
||||
.back
|
||||
fixed top left bottom right
|
||||
z-index 20
|
||||
background-color transparentify(black, 80%)
|
||||
&>.ArticleTopBar-left
|
||||
float left
|
||||
&>.tutorial
|
||||
fixed top
|
||||
left 100px
|
||||
top 30px
|
||||
z-index 36
|
||||
font-style italic
|
||||
&>.ArticleTopBar-left-search
|
||||
position relative
|
||||
float left
|
||||
height 33px
|
||||
margin-top 13.5px
|
||||
margin-left 15px
|
||||
width 350px
|
||||
padding 5px 15px
|
||||
transition 0.1s
|
||||
font-size 16px
|
||||
border 1px solid transparent
|
||||
z-index 30
|
||||
.tooltip
|
||||
tooltip()
|
||||
margin-left -24px
|
||||
margin-top 35px
|
||||
opacity 1
|
||||
&.hide
|
||||
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
|
||||
absolute top left
|
||||
width 350px
|
||||
border-radius 16.5px
|
||||
background-color inputBgColor
|
||||
border 1px solid transparent
|
||||
padding-left 35px
|
||||
outline none
|
||||
font-size 14px
|
||||
height 33px
|
||||
line-height 33px
|
||||
z-index 0
|
||||
&:focus
|
||||
border-color focusBorderColor
|
||||
i.fa.fa-search
|
||||
position absolute
|
||||
display block
|
||||
top 0
|
||||
left 10px
|
||||
line-height 33px
|
||||
z-index 1
|
||||
pointer-events none
|
||||
.ArticleTopBar-left-search-clear-button
|
||||
position absolute
|
||||
top 6px
|
||||
right 10px
|
||||
width 20px
|
||||
height 20px
|
||||
border-radius 10px
|
||||
border none
|
||||
background-color transparent
|
||||
color topBarBtnColor
|
||||
transition 0.1s
|
||||
line-height 20px
|
||||
text-align center
|
||||
padding 0
|
||||
&:focus
|
||||
color textColor
|
||||
&:hover
|
||||
color white
|
||||
background-color topBarBtnBgColor
|
||||
&:active
|
||||
color white
|
||||
background-color darken(topBarBtnBgColor, 35%)
|
||||
.ArticleTopBar-left-control
|
||||
line-height 33px
|
||||
float left
|
||||
height 33px
|
||||
margin-top 13.5px
|
||||
margin-left 20px
|
||||
.tutorial
|
||||
fixed top
|
||||
left 200px
|
||||
z-index 36
|
||||
font-style italic
|
||||
button.ArticleTopBar-left-control-new-post-button
|
||||
position fixed
|
||||
background bgColor
|
||||
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
|
||||
&>button
|
||||
display block
|
||||
position absolute
|
||||
right 74px
|
||||
top 20px
|
||||
width 20px
|
||||
height 20px
|
||||
font-size 14px
|
||||
line-height 14px
|
||||
background-color infoBtnBgColor
|
||||
color bgColor
|
||||
border-radius 11px
|
||||
border 1px solid bgColor
|
||||
transition 0.1s
|
||||
&:focus
|
||||
background-color lighten(infoBtnActiveBgColor, 15%)
|
||||
.tooltip
|
||||
tooltip()
|
||||
margin-left -50px
|
||||
margin-top 20px
|
||||
&:hover
|
||||
background-color infoBtnActiveBgColor
|
||||
.tooltip
|
||||
opacity 1
|
||||
|
||||
&>.ArticleTopBar-right-links-button
|
||||
display block
|
||||
position absolute
|
||||
top 8px
|
||||
right 15px
|
||||
opacity 0.7
|
||||
border-radius 23px
|
||||
height 46px
|
||||
width 46px
|
||||
border 1px solid transparent
|
||||
&:focus
|
||||
border-color focusBorderColor
|
||||
&:hover
|
||||
opacity 1
|
||||
.tooltip
|
||||
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%)
|
||||
|
||||
|
||||
|
||||
86
browser/styles/main/UserNavigator.styl
Normal file
86
browser/styles/main/UserNavigator.styl
Normal file
@@ -0,0 +1,86 @@
|
||||
userNavigatorBgColor = #1B1C1C
|
||||
userNavigatorColor = #DDD
|
||||
userAnchorColor = #979797
|
||||
userAnchorBgColor = #BEBEBE
|
||||
userAnchorActiveColor = textColor
|
||||
userAnchorActiveBgColor = white
|
||||
|
||||
.UserNavigator
|
||||
noSelect()
|
||||
background-color userNavigatorBgColor
|
||||
absolute left top bottom
|
||||
width 60px
|
||||
text-align center
|
||||
box-sizing border-box
|
||||
ul.userList
|
||||
position absolute
|
||||
top 25px
|
||||
left 0
|
||||
right 0
|
||||
bottom 70px
|
||||
// overflow-y auto
|
||||
&>li
|
||||
a
|
||||
display block
|
||||
width 38px
|
||||
height 64px
|
||||
margin 0 auto 10px
|
||||
text-align center
|
||||
text-decoration none
|
||||
color userAnchorColor
|
||||
line-height 44px
|
||||
font-size 1.1em
|
||||
cursor pointer
|
||||
transition 0.1s
|
||||
|
||||
img.ProfileImage
|
||||
width 38px
|
||||
height 38px
|
||||
border-radius 22px
|
||||
opacity 0.7
|
||||
&:hover
|
||||
img.ProfileImage
|
||||
opacity 1
|
||||
.userTooltip
|
||||
opacity 1
|
||||
&.active
|
||||
img.ProfileImage
|
||||
opacity 1
|
||||
.userTooltip
|
||||
tooltip()
|
||||
position absolute
|
||||
margin-top -52px
|
||||
margin-left 44px
|
||||
.keyLabel
|
||||
margin-top -25px
|
||||
font-size 0.8em
|
||||
color userNavigatorColor
|
||||
button.createTeamBtn
|
||||
display block
|
||||
margin 0 auto
|
||||
width 30px
|
||||
height 30px
|
||||
border-radius 15px
|
||||
border 2px solid darken(white, 5%)
|
||||
color darken(white, 5%)
|
||||
text-align center
|
||||
background-image none
|
||||
background-color transparent
|
||||
box-sizing border-box
|
||||
absolute left right
|
||||
bottom 15px
|
||||
font-size 22px
|
||||
line-height 22px
|
||||
transition 0.1s
|
||||
.tooltip
|
||||
tooltip()
|
||||
margin-top -26px
|
||||
margin-left 30px
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
color white
|
||||
border-color white
|
||||
.tooltip
|
||||
opacity 1
|
||||
&:active
|
||||
background-color brandColor
|
||||
border-color brandColor
|
||||
128
browser/styles/main/index.styl
Normal file
128
browser/styles/main/index.styl
Normal file
@@ -0,0 +1,128 @@
|
||||
@import '../../../node_modules/nib/lib/nib'
|
||||
@import '../vars'
|
||||
@import '../mixins/*'
|
||||
global-reset()
|
||||
@import '../shared/*'
|
||||
@import './ArticleNavigator'
|
||||
@import './ArticleTopbar'
|
||||
@import './ArticleList'
|
||||
@import './ArticleDetail'
|
||||
@import './modal/*'
|
||||
|
||||
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
|
||||
|
||||
html, body
|
||||
width 100%
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
body
|
||||
font-family DEFAULT_FONTS
|
||||
color textColor
|
||||
font-size fontSize
|
||||
font-weight 400
|
||||
|
||||
button, input, select, textarea
|
||||
font-family DEFAULT_FONTS
|
||||
|
||||
div, span, a, button, input, textarea
|
||||
box-sizing border-box
|
||||
|
||||
a
|
||||
color brandColor
|
||||
&:hover
|
||||
color lighten(brandColor, 5%)
|
||||
&:visited
|
||||
color brandColor
|
||||
|
||||
hr
|
||||
border-top none
|
||||
border-bottom solid 1px borderColor
|
||||
margin 15px 0
|
||||
|
||||
button
|
||||
font-weight 400
|
||||
cursor pointer
|
||||
&:focus, &.focus
|
||||
outline none
|
||||
|
||||
.noSelect
|
||||
noSelect()
|
||||
|
||||
.text-center
|
||||
text-align center
|
||||
|
||||
.form-group
|
||||
margin-bottom 15px
|
||||
&>label
|
||||
display block
|
||||
margin-bottom 5px
|
||||
|
||||
.block-input, .inline-input
|
||||
border solid 1px borderColor
|
||||
padding 0 10px
|
||||
font-size 1em
|
||||
height 33px
|
||||
border-radius 5px
|
||||
box-sizing border-box
|
||||
&:focus, &.focus
|
||||
border solid 1px brandBorderColor
|
||||
outline none
|
||||
&.circleInput
|
||||
border-radius 16.5px
|
||||
|
||||
.block-input
|
||||
display block
|
||||
width 100%
|
||||
|
||||
.inline-input
|
||||
display inline-block
|
||||
margin-right 5px
|
||||
|
||||
.relative
|
||||
position relative
|
||||
|
||||
textarea.block-input
|
||||
resize vertical
|
||||
height 125px
|
||||
border-radius 5px
|
||||
padding 5px 10px
|
||||
|
||||
#content
|
||||
fullsize()
|
||||
|
||||
.Main
|
||||
.appUpdateButton
|
||||
position fixed
|
||||
z-index 2000
|
||||
bottom 5px
|
||||
right 53px
|
||||
padding 10px 15px
|
||||
border none
|
||||
border-radius 5px
|
||||
background-color brandColor
|
||||
color white
|
||||
opacity 0.7
|
||||
&:hover
|
||||
opacity 1
|
||||
background-color lighten(brandColor, 10%)
|
||||
.contactButton
|
||||
position fixed
|
||||
z-index 2000
|
||||
bottom 5px
|
||||
right 5px
|
||||
padding 10px 15px
|
||||
border none
|
||||
border-radius 5px
|
||||
background-color brandColor
|
||||
color white
|
||||
opacity 0.7
|
||||
&:hover
|
||||
opacity 1
|
||||
background-color lighten(brandColor, 10%)
|
||||
.tooltip
|
||||
tooltip()
|
||||
margin-top -22px
|
||||
margin-left -107px
|
||||
&:hover .tooltip
|
||||
opacity 1
|
||||
91
browser/styles/main/modal/CreateNewFolder.styl
Normal file
91
browser/styles/main/modal/CreateNewFolder.styl
Normal file
@@ -0,0 +1,91 @@
|
||||
tabNavColor = #999999
|
||||
iptFocusBorderColor = #369DCD
|
||||
|
||||
.CreateNewFolder.modal
|
||||
width 600px
|
||||
height 450px
|
||||
.closeBtn
|
||||
position absolute
|
||||
top 15px
|
||||
right 15px
|
||||
width 33px
|
||||
height 33px
|
||||
font-size 18px
|
||||
line-height 33px
|
||||
padding 0
|
||||
text-align center
|
||||
background-color transparent
|
||||
border none
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
.title
|
||||
font-size 32px
|
||||
text-align center
|
||||
font-weight bold
|
||||
margin-top 25px
|
||||
.ipt
|
||||
display block
|
||||
width 330px
|
||||
font-size 14px
|
||||
height 44px
|
||||
line-height 44px
|
||||
padding 0 15px
|
||||
border-radius 5px
|
||||
border solid 1px borderColor
|
||||
outline none
|
||||
margin 75px auto 20px
|
||||
&:focus
|
||||
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
|
||||
color infoTextColor
|
||||
background-color infoBackgroundColor
|
||||
font-size 14px
|
||||
padding 15px 15px
|
||||
width 330px
|
||||
border-radius 5px
|
||||
margin 15px auto 0
|
||||
&.error
|
||||
color errorTextColor
|
||||
background-color errorBackgroundColor
|
||||
.confirmBtn
|
||||
display block
|
||||
position absolute
|
||||
left 180px
|
||||
bottom 44px
|
||||
width 240px
|
||||
font-size 24px
|
||||
height 44px
|
||||
line-height 24px
|
||||
font-weight bold
|
||||
background-color brandColor
|
||||
color white
|
||||
border none
|
||||
border-radius 5px
|
||||
margin 0 auto
|
||||
transition 0.1s
|
||||
&:hover
|
||||
transform scale(1.1)
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
199
browser/styles/main/modal/CreateNewTeam.styl
Normal file
199
browser/styles/main/modal/CreateNewTeam.styl
Normal file
@@ -0,0 +1,199 @@
|
||||
tabNavColor = #999999
|
||||
iptFocusBorderColor = #369DCD
|
||||
stripHoverBtnColor = #333
|
||||
stripBtnColor = lighten(stripHoverBtnColor, 35%)
|
||||
|
||||
.CreateNewTeam.modal
|
||||
width 600px
|
||||
height 450px
|
||||
.closeBtn
|
||||
position absolute
|
||||
top 15px
|
||||
right 15px
|
||||
width 33px
|
||||
height 33px
|
||||
font-size 18px
|
||||
line-height 33px
|
||||
padding 0
|
||||
text-align center
|
||||
background-color transparent
|
||||
border none
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
.title
|
||||
font-size 32px
|
||||
text-align center
|
||||
font-weight bold
|
||||
margin-top 25px
|
||||
.ipt
|
||||
display block
|
||||
width 330px
|
||||
font-size 14px
|
||||
height 44px
|
||||
line-height 44px
|
||||
padding 0 15px
|
||||
border-radius 5px
|
||||
border solid 1px borderColor
|
||||
outline none
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.alert
|
||||
padding 0 15px
|
||||
height 44px
|
||||
line-height 44px
|
||||
width 300px
|
||||
margin 0 auto
|
||||
border-radius 5px
|
||||
color infoTextColor
|
||||
background-color infoBackgroundColor
|
||||
white-space nowrap
|
||||
overflow-x auto
|
||||
&.error
|
||||
color errorTextColor
|
||||
background-color errorBackgroundColor
|
||||
.confirmBtn
|
||||
display block
|
||||
position absolute
|
||||
left 180px
|
||||
bottom 44px
|
||||
width 240px
|
||||
font-size 24px
|
||||
height 44px
|
||||
line-height 24px
|
||||
font-weight bold
|
||||
background-color brandColor
|
||||
color white
|
||||
border none
|
||||
border-radius 5px
|
||||
margin 0 auto
|
||||
transition 0.1s
|
||||
&:hover
|
||||
transform scale(1.1)
|
||||
&:disabled
|
||||
opacity 0.7
|
||||
.tabNav
|
||||
absolute left right
|
||||
bottom 15px
|
||||
height 33px
|
||||
line-height 33px
|
||||
width 150px
|
||||
text-align center
|
||||
font-size 12px
|
||||
color tabNavColor
|
||||
margin 0 auto
|
||||
transition 0.1s
|
||||
i.active
|
||||
color brandColor
|
||||
.createTab
|
||||
.ipt
|
||||
margin 105px auto 15px
|
||||
.selectTab
|
||||
.memberForm
|
||||
display block
|
||||
margin 25px auto 15px
|
||||
width 330px
|
||||
clearfix()
|
||||
padding 0
|
||||
font-size 14px
|
||||
height 44px
|
||||
line-height 44px
|
||||
outline none
|
||||
.Select.memberName
|
||||
display block
|
||||
margin 0
|
||||
float left
|
||||
width 280px
|
||||
height 44px
|
||||
font-size 14px
|
||||
border none
|
||||
line-height 44px
|
||||
background-color transparent
|
||||
outline none
|
||||
&.is-focus
|
||||
.Select-control
|
||||
border-color iptFocusBorderColor
|
||||
.Select-control
|
||||
height 44px
|
||||
line-height 44px
|
||||
padding 0 0 0 15px
|
||||
border-radius 5px 0 0 5px
|
||||
border 1px solid borderColor
|
||||
border-right none
|
||||
.Select-placeholder
|
||||
padding 0 0 0 15px
|
||||
.Seleect-arrow
|
||||
top 21px
|
||||
.Select-clear
|
||||
padding 0 10px
|
||||
.Select-noresults, .Select-option
|
||||
line-height 44px
|
||||
padding 0 0 0 15px
|
||||
|
||||
&:focus, &.focus
|
||||
border-color iptFocusBorderColor
|
||||
button
|
||||
font-weight 400
|
||||
height 44px
|
||||
cursor pointer
|
||||
margin 0
|
||||
padding 0
|
||||
width 50px
|
||||
float right
|
||||
border none
|
||||
background-color brandColor
|
||||
border-top-right-radius 5px
|
||||
border-bottom-right-radius 5px
|
||||
color white
|
||||
font-size 14px
|
||||
.memberList
|
||||
width 480px
|
||||
margin 0 auto
|
||||
height 190px
|
||||
overflow scroll
|
||||
border-bottom 1px solid borderColor
|
||||
&>li
|
||||
border-bottom 1px solid borderColor
|
||||
height 44px
|
||||
padding 0 25px
|
||||
clearfix()
|
||||
&:nth-last-child(1)
|
||||
border-bottom-color transparent
|
||||
.userPhoto
|
||||
width 30px
|
||||
height 30px
|
||||
float left
|
||||
margin-top 7px
|
||||
margin-right 15px
|
||||
border-radius 15px
|
||||
.userInfo
|
||||
float left
|
||||
margin-top 7px
|
||||
.userName
|
||||
font-size 16px
|
||||
margin-bottom 2px
|
||||
.userEmail
|
||||
font-size 12px
|
||||
.userControl
|
||||
float right
|
||||
.userRole
|
||||
float left
|
||||
height 30px
|
||||
background-color transparent
|
||||
border 1px solid transparent
|
||||
margin-top 7px
|
||||
margin-right 35px
|
||||
outline none
|
||||
cursor pointer
|
||||
&:hover
|
||||
border-color borderColor
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
button
|
||||
border none
|
||||
height 30px
|
||||
margin-top 7px
|
||||
background-color transparent
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
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%)
|
||||
683
browser/styles/main/modal/Preferences.styl
Normal file
683
browser/styles/main/modal/Preferences.styl
Normal file
@@ -0,0 +1,683 @@
|
||||
menuColor = #808080
|
||||
menuBgColor = #E6E6E6
|
||||
closeBtnBgColor = #1790C6
|
||||
iptFocusBorderColor = #369DCD
|
||||
|
||||
.Preferences.modal
|
||||
padding 0
|
||||
border-radius 5px
|
||||
overflow hidden
|
||||
width 720px
|
||||
height 450px
|
||||
&>.header
|
||||
absolute top left right
|
||||
height 50px
|
||||
border-bottom 1px solid borderColor
|
||||
background-color menuBgColor
|
||||
&>.title
|
||||
font-size 22px
|
||||
font-weight bold
|
||||
float left
|
||||
padding-left 30px
|
||||
line-height 50px
|
||||
&>.closeBtn
|
||||
float right
|
||||
font-size 14px
|
||||
background-color closeBtnBgColor
|
||||
color white
|
||||
padding 0 15px
|
||||
height 33px
|
||||
margin-top 9px
|
||||
margin-right 15px
|
||||
border none
|
||||
border-radius 5px
|
||||
&:hover
|
||||
background-color lighten(closeBtnBgColor, 10%)
|
||||
&>.nav
|
||||
absolute left bottom
|
||||
top 50px
|
||||
width 180px
|
||||
background-color menuBgColor
|
||||
border-right 1px solid borderColor
|
||||
&>button
|
||||
width 100%
|
||||
height 44px
|
||||
font-size 18px
|
||||
color menuColor
|
||||
border none
|
||||
background-color transparent
|
||||
transition 0.1s
|
||||
text-align left
|
||||
padding-left 15px
|
||||
&:hover
|
||||
background-color darken(menuBgColor, 10%)
|
||||
&.active, &:active
|
||||
background-color brandColor
|
||||
color white
|
||||
&>.content
|
||||
absolute right bottom
|
||||
top 50px
|
||||
left 180px
|
||||
overflow-y auto
|
||||
&>.section
|
||||
padding 10px 20px
|
||||
border-bottom 1px solid borderColor
|
||||
overflow-y auto
|
||||
&:nth-last-child(1)
|
||||
border-bottom none
|
||||
&>.sectionTitle
|
||||
font-size 18px
|
||||
margin 10px 0 5px
|
||||
color brandColor
|
||||
&>.sectionCheck
|
||||
margin-bottom 5px
|
||||
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
|
||||
clearfix()
|
||||
height 33px
|
||||
label
|
||||
width 150px
|
||||
padding-left 15px
|
||||
float left
|
||||
line-height 33px
|
||||
input
|
||||
width 250px
|
||||
float left
|
||||
height 33px
|
||||
border-radius 5px
|
||||
border 1px solid borderColor
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
outline none
|
||||
&:focus
|
||||
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
|
||||
clearfix()
|
||||
padding 5px 15px
|
||||
button
|
||||
float right
|
||||
background-color brandColor
|
||||
color white
|
||||
border none
|
||||
border-radius 5px
|
||||
height 33px
|
||||
padding 0 15px
|
||||
font-size 14px
|
||||
&:hover
|
||||
background-color lighten(brandColor, 10%)
|
||||
.alert
|
||||
float right
|
||||
width 250px
|
||||
padding 10px 15px
|
||||
margin 0 10px 0
|
||||
.alert
|
||||
color infoTextColor
|
||||
background-color infoBackgroundColor
|
||||
font-size 14px
|
||||
padding 15px 15px
|
||||
width 330px
|
||||
border-radius 5px
|
||||
margin 10px auto
|
||||
&.error
|
||||
color errorTextColor
|
||||
background-color errorBackgroundColor
|
||||
&.ContactTab
|
||||
&.done
|
||||
.message
|
||||
margin-top 75px
|
||||
margin-bottom 15px
|
||||
text-align center
|
||||
font-size 22px
|
||||
.checkIcon
|
||||
margin-bottom 15px
|
||||
font-size 144px
|
||||
color brandColor
|
||||
text-align center
|
||||
.control
|
||||
text-align center
|
||||
button
|
||||
border solid 1px borderColor
|
||||
border-radius 5px
|
||||
background-color white
|
||||
padding 15px 15px
|
||||
font-size 14px
|
||||
&:hover
|
||||
background-color darken(white, 10%)
|
||||
&.form
|
||||
padding 10px
|
||||
.title
|
||||
font-size 18px
|
||||
color brandColor
|
||||
margin-top 10px
|
||||
margin-bottom 10px
|
||||
.description
|
||||
margin-bottom 15px
|
||||
.iptGroup
|
||||
margin-bottom 10px
|
||||
input, textarea
|
||||
border-radius 5px
|
||||
border 1px solid borderColor
|
||||
font-size 14px
|
||||
outline none
|
||||
padding 10px 15px
|
||||
width 100%
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
textarea
|
||||
resize vertical
|
||||
min-height 150px
|
||||
.formControl
|
||||
clearfix()
|
||||
.alert
|
||||
float right
|
||||
padding 10px 15px
|
||||
margin 0 5px 0
|
||||
font-size 14px
|
||||
line-height normal
|
||||
button
|
||||
padding 10px 15px
|
||||
background-color brandColor
|
||||
color white
|
||||
font-size 14px
|
||||
border-radius 5px
|
||||
border none
|
||||
float right
|
||||
&:hover
|
||||
background-color lighten(brandColor, 10%)
|
||||
|
||||
&.AppSettingTab
|
||||
.description
|
||||
marked()
|
||||
&.TeamSettingTab
|
||||
.header
|
||||
border-bottom 1px solid borderColor
|
||||
padding 10px
|
||||
font-size 18px
|
||||
color brandColor
|
||||
line-height 33px
|
||||
.teamSelect
|
||||
border 1px solid borderColor
|
||||
height 33px
|
||||
width 200px
|
||||
margin 0 10px
|
||||
outline none
|
||||
font-size 14px
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.teamDeleteConfirm
|
||||
|
||||
label
|
||||
line-height 33px
|
||||
font-size 14px
|
||||
.teamDelete
|
||||
label
|
||||
line-height 33px
|
||||
font-size 18px
|
||||
color brandColor
|
||||
.teamDelete, .teamDeleteConfirm
|
||||
padding 15px 20px 15px 15px
|
||||
button
|
||||
background-color white
|
||||
height 33px
|
||||
font-size 14px
|
||||
padding 0 15px
|
||||
border 1px solid borderColor
|
||||
float right
|
||||
margin 0 5px
|
||||
border-radius 5px
|
||||
&:hover
|
||||
background-color darken(white, 10%)
|
||||
button.deleteBtn
|
||||
background-color brandColor
|
||||
border none
|
||||
color white
|
||||
&:hover
|
||||
background-color lighten(brandColor, 10%)
|
||||
&.MemberSettingTab
|
||||
&>.header
|
||||
border-bottom 1px solid borderColor
|
||||
padding 10px
|
||||
font-size 18px
|
||||
color brandColor
|
||||
line-height 33px
|
||||
.teamSelect
|
||||
border 1px solid borderColor
|
||||
height 33px
|
||||
width 200px
|
||||
margin 0 10px
|
||||
outline none
|
||||
font-size 14px
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.membersTableSection
|
||||
.addMember
|
||||
clearfix()
|
||||
padding 10px
|
||||
.addMemberLabel
|
||||
font-size 14px
|
||||
line-height 33px
|
||||
float left
|
||||
.addMemberControl
|
||||
width 330px
|
||||
float left
|
||||
margin-left 25px
|
||||
.Select
|
||||
display block
|
||||
margin 0
|
||||
float left
|
||||
width 280px
|
||||
height 33px
|
||||
font-size 14px
|
||||
border none
|
||||
line-height 33px
|
||||
background-color transparent
|
||||
outline none
|
||||
&.is-focus
|
||||
.Select-control
|
||||
border-color iptFocusBorderColor
|
||||
.Select-control
|
||||
height 33px
|
||||
line-height 33px
|
||||
padding 0 0 0 15px
|
||||
border-radius 5px 0 0 5px
|
||||
border 1px solid borderColor
|
||||
border-right none
|
||||
.Select-placeholder
|
||||
padding 0 0 0 15px
|
||||
.Seleect-arrow
|
||||
top 21px
|
||||
.Select-clear
|
||||
padding 0 10px
|
||||
.Select-noresults, .Select-option
|
||||
line-height 33px
|
||||
padding 0 0 0 15px
|
||||
button
|
||||
font-weight 400
|
||||
height 33px
|
||||
cursor pointer
|
||||
margin 0
|
||||
padding 0
|
||||
width 50px
|
||||
float right
|
||||
border none
|
||||
background-color brandColor
|
||||
border-top-right-radius 5px
|
||||
border-bottom-right-radius 5px
|
||||
color white
|
||||
font-size 14px
|
||||
.memberList
|
||||
&>.header
|
||||
clearfix()
|
||||
&>.userName
|
||||
float left
|
||||
&>.role
|
||||
float left
|
||||
&>.control
|
||||
float right
|
||||
&>li
|
||||
&.edit
|
||||
.colDescription
|
||||
font-size 14px
|
||||
line-height 33px
|
||||
padding-left 15px
|
||||
float left
|
||||
strong
|
||||
font-size 16px
|
||||
color brandColor
|
||||
.colDeleteConfirm
|
||||
float right
|
||||
margin-right 15px
|
||||
button
|
||||
border none
|
||||
height 30px
|
||||
width 60px
|
||||
margin-top 1.5px
|
||||
font-size 14px
|
||||
background-color transparent
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
&:disabled
|
||||
color lighten(stripBtnColor, 10%)
|
||||
cursor not-allowed
|
||||
&.primary
|
||||
color brandColor
|
||||
&:hover
|
||||
color lighten(brandColor, 10%)
|
||||
|
||||
border-bottom 1px solid borderColor
|
||||
height 44px
|
||||
padding 0 25px
|
||||
width 420px
|
||||
margin 0 auto
|
||||
clearfix()
|
||||
&:nth-last-child(1)
|
||||
border-bottom-color transparent
|
||||
.colUserName
|
||||
float left
|
||||
width 250px
|
||||
clearfix()
|
||||
.userPhoto
|
||||
width 30px
|
||||
height 30px
|
||||
float left
|
||||
margin-top 7px
|
||||
margin-right 15px
|
||||
border-radius 15px
|
||||
.userInfo
|
||||
float left
|
||||
margin-top 7px
|
||||
width 205px
|
||||
.userName
|
||||
font-size 16px
|
||||
margin-bottom 2px
|
||||
overflow ellipsis
|
||||
.userEmail
|
||||
font-size 12px
|
||||
overflow ellipsis
|
||||
.colRole
|
||||
float left
|
||||
width 75px
|
||||
.userRole
|
||||
height 30px
|
||||
background-color transparent
|
||||
border 1px solid transparent
|
||||
margin-top 7px
|
||||
margin-right 35px
|
||||
outline none
|
||||
cursor pointer
|
||||
&:hover
|
||||
border-color borderColor
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
&:disabled
|
||||
border-color transparent
|
||||
cursor not-allowed
|
||||
.colDelete
|
||||
width 45px
|
||||
float right
|
||||
text-align center
|
||||
button.deleteButton
|
||||
border none
|
||||
height 30px
|
||||
width 30px
|
||||
margin-top 7px
|
||||
background-color transparent
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
&:disabled
|
||||
color lighten(stripBtnColor, 10%)
|
||||
cursor not-allowed
|
||||
&.header
|
||||
.colRole, .colDelete
|
||||
text-align center
|
||||
.colUserName, .colRole, .colDelete
|
||||
line-height 44px
|
||||
&.FolderSettingTab
|
||||
&>.header
|
||||
border-bottom 1px solid borderColor
|
||||
padding 10px
|
||||
font-size 18px
|
||||
color brandColor
|
||||
line-height 33px
|
||||
.teamSelect
|
||||
border 1px solid borderColor
|
||||
height 33px
|
||||
width 200px
|
||||
margin 0 10px
|
||||
outline none
|
||||
font-size 14px
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.section
|
||||
.folderTable
|
||||
width 420px
|
||||
margin 15px auto
|
||||
&>div
|
||||
border-bottom 1px solid borderColor
|
||||
clearfix()
|
||||
height 43px
|
||||
line-height 33px
|
||||
padding 5px 0
|
||||
&:last-child
|
||||
border-color transparent
|
||||
.folderColor
|
||||
float left
|
||||
margin-left 10px
|
||||
text-align center
|
||||
width 44px
|
||||
.folderName
|
||||
float left
|
||||
width 175px
|
||||
overflow ellipsis
|
||||
.folderControl
|
||||
float right
|
||||
width 125px
|
||||
text-align center
|
||||
&.folderHeader
|
||||
.folderName
|
||||
padding-left 25px
|
||||
&.newFolder
|
||||
.alert
|
||||
display block
|
||||
color infoTextColor
|
||||
background-color infoBackgroundColor
|
||||
font-size 14px
|
||||
padding 15px 15px
|
||||
width 330px
|
||||
border-radius 5px
|
||||
margin 0 auto
|
||||
&.error
|
||||
color errorTextColor
|
||||
background-color errorBackgroundColor
|
||||
.folderName input
|
||||
height 33px
|
||||
border 1px solid transparent
|
||||
border-radius 5px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
outline none
|
||||
width 150px
|
||||
overflow ellipsis
|
||||
&:hover
|
||||
border-color borderColor
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.folderPublic select
|
||||
height 33px
|
||||
border 1px solid transparent
|
||||
background-color white
|
||||
outline none
|
||||
display block
|
||||
margin 0 auto
|
||||
font-size 14px
|
||||
&:hover
|
||||
border-color borderColor
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.folderControl
|
||||
button
|
||||
border none
|
||||
height 30px
|
||||
margin-top 1.5px
|
||||
font-size 14px
|
||||
background-color transparent
|
||||
color brandColor
|
||||
&:hover
|
||||
color lighten(brandColor, 10%)
|
||||
&.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
|
||||
height 33px
|
||||
border 1px solid borderColor
|
||||
border-radius 5px
|
||||
padding 0 10px
|
||||
font-size 14px
|
||||
outline none
|
||||
width 150px
|
||||
&:focus
|
||||
border-color iptFocusBorderColor
|
||||
.folderColor
|
||||
.select
|
||||
height 33px
|
||||
width 33px
|
||||
border 1px solid borderColor
|
||||
background-color white
|
||||
outline none
|
||||
display block
|
||||
margin 0 auto
|
||||
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
|
||||
button
|
||||
border none
|
||||
height 30px
|
||||
width 30px
|
||||
margin-top 1.5px
|
||||
font-size 14px
|
||||
background-color transparent
|
||||
color stripBtnColor
|
||||
&:hover
|
||||
color stripHoverBtnColor
|
||||
&:disabled
|
||||
color lighten(stripBtnColor, 10%)
|
||||
cursor not-allowed
|
||||
&.edit
|
||||
.folderControl
|
||||
button
|
||||
width 60px
|
||||
&.primary
|
||||
color brandColor
|
||||
&:hover
|
||||
color lighten(brandColor, 10%)
|
||||
&.delete
|
||||
.folderDeleteLabel
|
||||
float left
|
||||
height 33px
|
||||
width 250px
|
||||
padding-left 15px
|
||||
overflow ellipsis
|
||||
strong
|
||||
font-size 16px
|
||||
color brandColor
|
||||
.folderControl
|
||||
button
|
||||
width 60px
|
||||
&.primary
|
||||
color brandColor
|
||||
&:hover
|
||||
color lighten(brandColor, 10%)
|
||||
132
browser/styles/main/modal/Tutorial.styl
Normal file
132
browser/styles/main/modal/Tutorial.styl
Normal file
@@ -0,0 +1,132 @@
|
||||
|
||||
slideBgColor0 = #2BAC8F
|
||||
slideBgColor1 = #F68F92
|
||||
slideBgColor2 = #D6AD56
|
||||
slideBgColor3 = #26969B
|
||||
slideBgColor4 = #00B493
|
||||
|
||||
.Tutorial.modal
|
||||
background-color slideBgColor0
|
||||
color white !important
|
||||
width 720px
|
||||
height 480px
|
||||
margin-top 75px
|
||||
border-radius 5px
|
||||
overflow hidden
|
||||
|
||||
.priorBtn, .nextBtn
|
||||
font-size 72px
|
||||
position absolute
|
||||
background-color transparent
|
||||
color transparentify(white, 50%)
|
||||
transition 0.1s
|
||||
border none
|
||||
line-height 72px
|
||||
padding 0
|
||||
width 93px
|
||||
height 72px
|
||||
z-index 2
|
||||
top 189px
|
||||
&:hover
|
||||
color white
|
||||
&.hide
|
||||
opacity 0
|
||||
.priorBtn
|
||||
left 15px
|
||||
.nextBtn
|
||||
right 15px
|
||||
.title
|
||||
text-align center
|
||||
font-size 54px
|
||||
margin 40px 0
|
||||
.content
|
||||
text-align center
|
||||
font-size 22px
|
||||
line-height 1.8
|
||||
.dots
|
||||
position absolute
|
||||
left 0
|
||||
right 0
|
||||
bottom 25px
|
||||
margin 0 auto
|
||||
color gray
|
||||
text-align center
|
||||
z-index 2
|
||||
&>i
|
||||
transition 0.3s
|
||||
&.active
|
||||
color white
|
||||
.slide
|
||||
absolute top bottom left right
|
||||
z-index 1
|
||||
.slide0
|
||||
background-color slideBgColor0
|
||||
.content
|
||||
margin-top 100px
|
||||
.slide1
|
||||
background-color slideBgColor1
|
||||
.content
|
||||
.markdown
|
||||
background-color white
|
||||
color textColor
|
||||
width 480px
|
||||
height 140px
|
||||
margin 45px auto 0
|
||||
clearfix()
|
||||
text-align left
|
||||
border-radius 5px
|
||||
overflow hidden
|
||||
.left
|
||||
float left
|
||||
width 240px
|
||||
height 140px
|
||||
box-sizing border-box
|
||||
font-size 0.5em
|
||||
padding 30px
|
||||
border-right 1px solid borderColor
|
||||
.right
|
||||
width 240px
|
||||
height 140px
|
||||
float right
|
||||
box-sizing border-box
|
||||
padding: 28px 0 0 10px
|
||||
font-size 0.45em
|
||||
marked()
|
||||
ul
|
||||
padding-left 20px
|
||||
.slide2
|
||||
background-color slideBgColor2
|
||||
.code
|
||||
border-radius 5px
|
||||
overflow hidden
|
||||
text-align left
|
||||
width 480px
|
||||
heght 140px
|
||||
margin 45px auto 0
|
||||
font-size 14px
|
||||
.ace_editor
|
||||
height 140px
|
||||
.slide3
|
||||
background-color slideBgColor3
|
||||
.title
|
||||
margin-bottom 15px
|
||||
.content
|
||||
font-size 18px
|
||||
&>img
|
||||
margin-top 25px
|
||||
.slide4
|
||||
background-color slideBgColor4
|
||||
.content
|
||||
&>button
|
||||
background-color white
|
||||
color brandColor
|
||||
font-size 60px
|
||||
width 250px
|
||||
height 250px
|
||||
border-radius 125px
|
||||
border none
|
||||
transition 0.1s
|
||||
&:hover
|
||||
transform scale(1.2)
|
||||
|
||||
|
||||
21
browser/styles/main/modal/modal.styl
Normal file
21
browser/styles/main/modal/modal.styl
Normal file
@@ -0,0 +1,21 @@
|
||||
modalZIndex= 1000
|
||||
modalBackColor = transparentify(black, 65%)
|
||||
|
||||
.ModalBase
|
||||
fixed top left bottom right
|
||||
z-index modalZIndex
|
||||
&.hide
|
||||
display none
|
||||
.modalBack
|
||||
absolute top left bottom right
|
||||
background-color modalBackColor
|
||||
z-index modalZIndex + 1
|
||||
.modal
|
||||
position relative
|
||||
width 650px
|
||||
margin 50px auto 0
|
||||
z-index modalZIndex + 2
|
||||
background-color white
|
||||
padding 15px
|
||||
color #666666
|
||||
border-radius 5px
|
||||
11
browser/styles/mixins/alert.styl
Normal file
11
browser/styles/mixins/alert.styl
Normal file
@@ -0,0 +1,11 @@
|
||||
alertSuccess()
|
||||
background-color successBackgroundColor
|
||||
color successTextColor
|
||||
|
||||
alertError()
|
||||
background-color errorBackgroundColor
|
||||
color errorTextColor
|
||||
|
||||
alertInfo()
|
||||
background-color infoBackgroundColor
|
||||
color infoTextColor
|
||||
40
browser/styles/mixins/btn.styl
Normal file
40
browser/styles/mixins/btn.styl
Normal file
@@ -0,0 +1,40 @@
|
||||
btnDefault()
|
||||
border-style solid
|
||||
border-width 1px
|
||||
border-color lightButtonColor
|
||||
background-color transparent
|
||||
color lightButtonColor
|
||||
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
border-color darken(lightButtonColor, 50%)
|
||||
color darken(lightButtonColor, 50%)
|
||||
&:active, &.active
|
||||
border-color darken(brandBorderColor, 10%)
|
||||
background-color brandColor
|
||||
color white
|
||||
&:disabled, &.disabled
|
||||
opacity 0.6
|
||||
|
||||
btnPrimary()
|
||||
border-style solid
|
||||
border-width 1px
|
||||
border-color brandBorderColor
|
||||
background-color transparent
|
||||
color brandColor
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
border-color darken(brandBorderColor, 30%)
|
||||
color darken(brandColor, 30%)
|
||||
&:active, &.active
|
||||
background-color brandColor
|
||||
color white
|
||||
&:disabled, &.disabled
|
||||
opacity 0.6
|
||||
|
||||
btnStripDefault()
|
||||
border none
|
||||
background-color transparent
|
||||
color lightButtonColor
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
color darken(lightButtonColor, 50%)
|
||||
&:active, &.active
|
||||
color brandColor
|
||||
3
browser/styles/mixins/circle.styl
Normal file
3
browser/styles/mixins/circle.styl
Normal file
@@ -0,0 +1,3 @@
|
||||
circle()
|
||||
border-radius 50%
|
||||
overflow hidden
|
||||
6
browser/styles/mixins/fullsize.styl
Normal file
6
browser/styles/mixins/fullsize.styl
Normal file
@@ -0,0 +1,6 @@
|
||||
fullsize()
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
18
browser/styles/mixins/input.styl
Normal file
18
browser/styles/mixins/input.styl
Normal file
@@ -0,0 +1,18 @@
|
||||
stripInput()
|
||||
border none
|
||||
border-bottom 1px solid borderColor
|
||||
padding 5px 15px
|
||||
transition 0.1s
|
||||
font-size 14px
|
||||
&:focus, &.focus
|
||||
border-bottom 1px solid brandBorderColor
|
||||
outline none
|
||||
|
||||
borderInput()
|
||||
border solid 1px borderColor
|
||||
padding 5px 15px
|
||||
transition 0.1s
|
||||
font-size 14px
|
||||
&:focus, &.focus
|
||||
border-color brandBorderColor
|
||||
outline none
|
||||
161
browser/styles/mixins/marked.styl
Normal file
161
browser/styles/mixins/marked.styl
Normal file
@@ -0,0 +1,161 @@
|
||||
marked()
|
||||
font-size 14px
|
||||
div.math-rendered
|
||||
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
|
||||
&: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
|
||||
border-top none
|
||||
border-bottom solid 1px borderColor
|
||||
margin 15px 0
|
||||
h1, h2, h3, h4, h5, h6
|
||||
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 2em
|
||||
border-bottom solid 2px borderColor
|
||||
line-height 2.333em
|
||||
h2
|
||||
font-size 1.66em
|
||||
line-height 2.07em
|
||||
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
|
||||
line-height 1.9em
|
||||
margin 0 0 15px
|
||||
img
|
||||
max-width 100%
|
||||
strong, b
|
||||
font-weight bold
|
||||
em, i
|
||||
font-style italic
|
||||
s, del, strike
|
||||
text-decoration line-through
|
||||
u
|
||||
text-decoration underline
|
||||
blockquote
|
||||
border-left solid 4px brandBorderColor
|
||||
margin 0 0 15px
|
||||
padding 0 25px
|
||||
ul
|
||||
list-style-type disc
|
||||
padding-left 25px
|
||||
margin-bottom 15px
|
||||
li
|
||||
display list-item
|
||||
line-height 1.8em
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
&>li>ul
|
||||
list-style-type circle
|
||||
&>li>ul
|
||||
list-style-type square
|
||||
ol
|
||||
list-style-type decimal
|
||||
padding-left 25px
|
||||
margin-bottom 15px
|
||||
li
|
||||
display list-item
|
||||
line-height 1.8em
|
||||
&>li>ul, &>li>ol
|
||||
margin 0
|
||||
code
|
||||
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
|
||||
padding 2px 4px
|
||||
border solid 1px alpha(borderColor, 0.3)
|
||||
border-radius 4px
|
||||
font-size 0.9em
|
||||
color black
|
||||
text-decoration none
|
||||
background-color #F6F6F6
|
||||
margin-right 2px
|
||||
*:not(a.lineAnchor) + code
|
||||
margin-left 2px
|
||||
pre
|
||||
padding 5px
|
||||
border solid 1px alpha(borderColor, 0.3)
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 15px
|
||||
background-color #F6F6F6
|
||||
line-height 1.35em
|
||||
&>code
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
color black
|
||||
table
|
||||
width 100%
|
||||
margin 15px 0 25px
|
||||
thead
|
||||
tr
|
||||
background-color tableHeadBgColor
|
||||
th
|
||||
border-style solid
|
||||
padding 15px 5px
|
||||
border-width 1px 0 2px 1px
|
||||
border-color borderColor
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
tbody
|
||||
tr:nth-child(2n + 1)
|
||||
background-color tableOddBgColor
|
||||
tr:nth-child(2n)
|
||||
background-color tableEvenBgColor
|
||||
td
|
||||
border-style solid
|
||||
padding 15px 5px
|
||||
border-width 0 0 1px 1px
|
||||
border-color borderColor
|
||||
&:last-child
|
||||
border-right solid 1px borderColor
|
||||
14
browser/styles/mixins/tooltip.styl
Normal file
14
browser/styles/mixins/tooltip.styl
Normal file
@@ -0,0 +1,14 @@
|
||||
tooltip()
|
||||
position fixed
|
||||
z-index popupZIndex
|
||||
background-color transparentify(invBackgroundColor, 80%)
|
||||
color invTextColor
|
||||
padding 6px 15px
|
||||
font-size 12px
|
||||
font-weight normal
|
||||
line-height 20px
|
||||
white-space nowrap
|
||||
opacity 0
|
||||
transition 0.1s
|
||||
pointer-events none
|
||||
|
||||
6
browser/styles/mixins/util.styl
Normal file
6
browser/styles/mixins/util.styl
Normal file
@@ -0,0 +1,6 @@
|
||||
borderBox()
|
||||
box-sizing border-box
|
||||
|
||||
noSelect()
|
||||
-webkit-user-select none
|
||||
cursor default
|
||||
55
browser/styles/shared/btn.styl
Normal file
55
browser/styles/shared/btn.styl
Normal file
@@ -0,0 +1,55 @@
|
||||
.btn-primary, .btn-default
|
||||
border-style solid
|
||||
border-width 1px
|
||||
background-image none
|
||||
height 44px
|
||||
padding 0 15px
|
||||
border-radius 5px
|
||||
box-sizing border-box
|
||||
font-size 1em
|
||||
font-family 'Lato'
|
||||
font-weight 400
|
||||
transition 0.1s
|
||||
cursor pointer
|
||||
margin 0 5px
|
||||
|
||||
.btn-block
|
||||
display block
|
||||
width 100%
|
||||
margin 0 auto
|
||||
|
||||
.btn-square
|
||||
display inline-block
|
||||
width 44px
|
||||
padding 0
|
||||
border-width 1px
|
||||
|
||||
.btn-sm
|
||||
height 32px
|
||||
border-radius 16px
|
||||
&.btn-square
|
||||
width 32px
|
||||
|
||||
.btn-primary
|
||||
border-color brandBorderColor
|
||||
background-color transparent
|
||||
color brandColor
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
border-color darken(brandBorderColor, 30%)
|
||||
color darken(brandColor, 30%)
|
||||
&:active, &.active
|
||||
background-color brandColor
|
||||
color white
|
||||
|
||||
.btn-default
|
||||
border-color lightButtonColor
|
||||
background-color transparent
|
||||
color lightButtonColor
|
||||
|
||||
&:hover, &.hover, &:focus, &.focus
|
||||
border-color darken(lightButtonColor, 50%)
|
||||
color darken(lightButtonColor, 50%)
|
||||
&:active, &.active
|
||||
border-color darken(brandBorderColor, 10%)
|
||||
background-color brandColor
|
||||
color white
|
||||
390
browser/styles/shared/modal.styl
Normal file
390
browser/styles/shared/modal.styl
Normal file
@@ -0,0 +1,390 @@
|
||||
// .ModalBase
|
||||
// fixed top left bottom right
|
||||
// z-index modalZIndex
|
||||
// &.hide
|
||||
// display none
|
||||
// .modalBack
|
||||
// absolute top left bottom right
|
||||
// background-color modalBackColor
|
||||
// z-index modalZIndex + 1
|
||||
// .modal
|
||||
// position relative
|
||||
// width 650px
|
||||
// margin 50px auto 0
|
||||
// z-index modalZIndex + 2
|
||||
// box-shadow popupShadow
|
||||
// background-color white
|
||||
// border-radius 10px
|
||||
// padding 15px
|
||||
// .modal-header
|
||||
// border-bottom solid 1px borderColor
|
||||
// margin-bottom 10px
|
||||
// h1
|
||||
// padding 10px 0 15px
|
||||
// font-size 1.5em
|
||||
// .modal-body
|
||||
// p
|
||||
// margin-bottom 10px
|
||||
// .modal-footer
|
||||
// clearfix()
|
||||
// border-top solid 1px borderColor
|
||||
// padding-top 10px
|
||||
// .modal-control
|
||||
// float right
|
||||
//
|
||||
// .sideNavModal
|
||||
// height 500px
|
||||
// .leftPane
|
||||
// absolute top bottom left
|
||||
// width 175px
|
||||
// padding 20px
|
||||
// border-right solid 1px borderColor
|
||||
// .modalLabel
|
||||
// font-size 1.5em
|
||||
// margin-top 25px
|
||||
// margin-bottom 35px
|
||||
// color brandColor
|
||||
// .tabList button
|
||||
// btnStripDefault()
|
||||
// display block
|
||||
// width 100%
|
||||
// font-size 1.1em
|
||||
// padding 10px 5px
|
||||
// margin-bottom 15px
|
||||
// text-align left
|
||||
// .rightPane
|
||||
// absolute top bottom right
|
||||
// left 175px
|
||||
// padding 15px
|
||||
// overflow-y auto
|
||||
// .tab
|
||||
// padding-top 45px
|
||||
// .formField
|
||||
// position relative
|
||||
// clearfix()
|
||||
// margin-bottom 15px
|
||||
// label
|
||||
// width 30%
|
||||
// display block
|
||||
// line-height 33px
|
||||
// float left
|
||||
// input
|
||||
// width 70%
|
||||
// display block
|
||||
// borderInput()
|
||||
// height 33px
|
||||
// font-size 1em
|
||||
// border-radius 5px
|
||||
// float left
|
||||
// .formRadioField
|
||||
// margin-bottom 15px
|
||||
// input
|
||||
// margin-left 25px
|
||||
// .formConfirm
|
||||
// position relative
|
||||
// clearfix()
|
||||
// margin-bottom 15px
|
||||
// button
|
||||
// float right
|
||||
// btnDefault()
|
||||
// padding 10px 15px
|
||||
// border-radius 5px
|
||||
// font-size 1em
|
||||
// margin-left 5px
|
||||
// .alertInfo, .alertSuccess, .alertError
|
||||
// float right
|
||||
// padding 12px 10px
|
||||
// border-radius 5px
|
||||
// width 320px
|
||||
// font-size 1em
|
||||
// overflow-x hidden
|
||||
// white-space nowrap
|
||||
// transition 0.1s
|
||||
// &.hide
|
||||
// width 0
|
||||
// padding 12px 0
|
||||
// .alertInfo
|
||||
// alertInfo()
|
||||
// .alertSuccess
|
||||
// alertSuccess()
|
||||
// .alertError
|
||||
// alertError()
|
||||
// .PreferencesModal
|
||||
// .settingsTab
|
||||
// .categoryLabel
|
||||
// font-size 1.5em
|
||||
// margin-bottom 25px
|
||||
// .example
|
||||
// marked()
|
||||
// .aboutTab
|
||||
// padding-top 30px
|
||||
// .about1
|
||||
// margin-bottom 25px
|
||||
// .logo
|
||||
// display block
|
||||
// margin 0 auto
|
||||
// .appInfo
|
||||
// font-size 1.5em
|
||||
// text-align center
|
||||
// .about2
|
||||
// width 200px
|
||||
// margin 0 auto
|
||||
// .externalLabel
|
||||
// font-size 1.2em
|
||||
// margin-bottom 15px
|
||||
// .externalList
|
||||
// li
|
||||
// margin-bottom 15px
|
||||
// .PlanetSettingModal
|
||||
// .planetDeleteTab
|
||||
// padding-top 65px
|
||||
// p
|
||||
// margin-bottom 25px
|
||||
// strong
|
||||
// color brandColor
|
||||
// font-size 1.1em
|
||||
// input
|
||||
// borderInput()
|
||||
// margin-right 5px
|
||||
// height 33px
|
||||
// font-size 1em
|
||||
// border-radius 10px
|
||||
// .formConfirm
|
||||
// position relative
|
||||
// clearfix()
|
||||
// margin-bottom 15px
|
||||
// button
|
||||
// float right
|
||||
// btnDefault()
|
||||
// padding 10px 15px
|
||||
// border-radius 5px
|
||||
// font-size 1em
|
||||
// margin-left 5px
|
||||
// .alertInfo, .alertSuccess, .alertError
|
||||
// float right
|
||||
// padding 12px 10px
|
||||
// border-radius 5px
|
||||
// width 320px
|
||||
// font-size 1em
|
||||
// overflow-x hidden
|
||||
// white-space nowrap
|
||||
// transition 0.1s
|
||||
// &.hide
|
||||
// width 0
|
||||
// padding 12px 0
|
||||
// .alertInfo
|
||||
// alertInfo()
|
||||
// .alertSuccess
|
||||
// alertSuccess()
|
||||
// .alertError
|
||||
// alertError()
|
||||
// .TeamSettingsModal
|
||||
// .membersTab
|
||||
// .memberTable
|
||||
// width 100%
|
||||
// margin-bottom 25px
|
||||
// th
|
||||
// border-bottom solid 2px borderColor
|
||||
// td
|
||||
// border-bottom solid 1px borderColor
|
||||
// height 38px
|
||||
// button
|
||||
// btnDefault()
|
||||
// padding 5px
|
||||
// border-radius 5px
|
||||
// .roleSelect
|
||||
// height 33px
|
||||
// border solid 1px borderColor
|
||||
// background-color backgroundColor
|
||||
// th, td
|
||||
// padding 5px 0
|
||||
// .addMemberForm
|
||||
// .formLabel
|
||||
// margin-bottom 5px
|
||||
// .formGroup
|
||||
// clearfix()
|
||||
// .userNameSelect
|
||||
// display block
|
||||
// width 200px
|
||||
// margin-right 5px
|
||||
// float left
|
||||
// .roleSelect
|
||||
// display block
|
||||
// height 33px
|
||||
// border solid 1px borderColor
|
||||
// background-color backgroundColor
|
||||
// float left
|
||||
// margin-right 5px
|
||||
// .confirmButton
|
||||
// display block
|
||||
// height 33px
|
||||
// btnDefault()
|
||||
// border-radius 5px
|
||||
// float left
|
||||
//
|
||||
// .LaunchModal
|
||||
// .modal-tab
|
||||
// text-align center
|
||||
// margin-bottom 10px
|
||||
// .btn-primary, .btn-default
|
||||
// margin 0
|
||||
// border-radius 0
|
||||
// border-width 1px
|
||||
// width 150px
|
||||
// border-radius 0
|
||||
// &:nth-child(1)
|
||||
// border-right solid 1px borderColor
|
||||
// border-top-left-radius 5px
|
||||
// border-bottom-left-radius 5px
|
||||
// &:nth-child(2)
|
||||
// border-left none
|
||||
// border-top-right-radius 5px
|
||||
// border-bottom-right-radius 5px
|
||||
// .Select
|
||||
// .Select-control
|
||||
// border-color borderColor
|
||||
// &.is-focused
|
||||
// .Select-control
|
||||
// border-color brandBorderColor
|
||||
// .Select-menu-outer
|
||||
// border-color borderColor
|
||||
// .ace_editor
|
||||
// border-radius 5px
|
||||
// border solid 1px borderColor
|
||||
// .CodeForm, .NoteForm
|
||||
// .form-group
|
||||
// margin-bottom 10px
|
||||
// .CodeForm
|
||||
// textarea.codeDescription
|
||||
// height 75px
|
||||
// font-size 0.9em
|
||||
// margin-bottom 10px
|
||||
// .modeSelect.Select
|
||||
// display inline-block
|
||||
// width 200px
|
||||
// height 37px
|
||||
// .Select-control
|
||||
// height 37px
|
||||
// .ace_editor
|
||||
// height 258px
|
||||
// .NoteForm
|
||||
// .ace_editor
|
||||
// height 358px
|
||||
// .previewMode
|
||||
// absolute top right
|
||||
// font-size 0.8em
|
||||
// line-height 24px
|
||||
// padding 5 15px
|
||||
// background-color transparentify(invBackgroundColor, 0.2)
|
||||
// color invTextColor
|
||||
// border-top-right-radius 5px
|
||||
// .marked
|
||||
// height 360px
|
||||
// overflow-x hidden
|
||||
// overflow-y auto
|
||||
// box-sizing border-box
|
||||
// padding 5px
|
||||
// border solid 1px borderColor
|
||||
// border-radius 5px
|
||||
// marked()
|
||||
//
|
||||
//
|
||||
// .PlanetCreateModal.modal, .TeamCreateModal.modal, .AddMemberModal.modal
|
||||
// padding 60px 0
|
||||
// .nameInput
|
||||
// width 80%
|
||||
// font-size 1.3em
|
||||
// margin 25px auto 15px
|
||||
// text-align center
|
||||
// .userNameSelect
|
||||
// width 80%
|
||||
// font-size 1.3em
|
||||
// margin 35px auto
|
||||
// text-align center
|
||||
// .formField
|
||||
// text-align center
|
||||
// margin 0 auto 25px
|
||||
// select
|
||||
// display inline-block
|
||||
// width 150px
|
||||
// height 33px
|
||||
// border solid 1px borderColor
|
||||
// background-color white
|
||||
// padding 0 10px
|
||||
// margin 0 15px
|
||||
// .submitButton
|
||||
// display block
|
||||
// margin 0 auto
|
||||
// box-sizing border-box
|
||||
// width 55px
|
||||
// height 55px
|
||||
// circle()
|
||||
// btnPrimary()
|
||||
// .errorAlert
|
||||
// alertError()
|
||||
// padding 12px 10px
|
||||
// border-radius 5px
|
||||
// text-align center
|
||||
// display block
|
||||
// width 360px
|
||||
// margin 0 auto 15px
|
||||
//
|
||||
// .ContactModal
|
||||
// padding 15px
|
||||
// .contactForm
|
||||
// .formField
|
||||
// width 100%
|
||||
// margin-bottom 10px
|
||||
// input, textarea
|
||||
// display block
|
||||
// width 100%
|
||||
// borderInput()
|
||||
// border-radius 5px
|
||||
// input
|
||||
// height 33px
|
||||
// font-size 1em
|
||||
// textarea
|
||||
// height 175px
|
||||
// font-size 1em
|
||||
// .formControl
|
||||
// clearfix()
|
||||
// button
|
||||
// float right
|
||||
// btnDefault()
|
||||
// height 44px
|
||||
// padding 0 15px
|
||||
// border-radius 5px
|
||||
// margin-left 5px
|
||||
// font-size 1em
|
||||
// button.sendButton
|
||||
// btnPrimary()
|
||||
// .confirmation
|
||||
// .confirmationMessage
|
||||
// padding 35px 0
|
||||
// text-align center
|
||||
// font-size 1.1em
|
||||
// .doneButton
|
||||
// btnDefault()
|
||||
// height 44px
|
||||
// padding 0 35px
|
||||
// border-radius 5px
|
||||
// display block
|
||||
// margin 0 auto 25px
|
||||
//
|
||||
// .LogoutModal
|
||||
// padding 65px 0 45px
|
||||
// width 350px
|
||||
// .messageLabel
|
||||
// text-align center
|
||||
// font-size 1.1em
|
||||
// margin-bottom 35px
|
||||
// .formControl
|
||||
// text-align center
|
||||
// button
|
||||
// btnDefault()
|
||||
// border-radius 5px
|
||||
// height 44px
|
||||
// margin 15px 5px
|
||||
// padding 0 15px
|
||||
// button.logoutButton
|
||||
// btnPrimary()
|
||||
47
browser/styles/vars.styl
Normal file
47
browser/styles/vars.styl
Normal file
@@ -0,0 +1,47 @@
|
||||
borderColor = #D0D0D0 // using
|
||||
highlightenBorderColor = darken(borderColor, 20%)
|
||||
invBorderColor = #404849
|
||||
brandBorderColor = #3FB399
|
||||
|
||||
focusBorderColor = #369DCD
|
||||
|
||||
buttonBorderColor = #4C4C4C
|
||||
|
||||
lightButtonColor = #898989
|
||||
|
||||
hoverBackgroundColor= transparentify(#444, 4%) // using
|
||||
|
||||
inactiveTextColor = #888 // using
|
||||
textColor = #4D4D4D // using
|
||||
backgroundColor= white
|
||||
fontSize= 14px // using
|
||||
|
||||
shadowColor= #C5C5C5
|
||||
|
||||
invBackgroundColor = #4C4C4C
|
||||
invTextColor = white
|
||||
|
||||
btnColor = #888
|
||||
btnHighlightenColor = #000
|
||||
|
||||
brandColor = #2BAC8F
|
||||
|
||||
popupShadow = 0 0 5px 0 #888
|
||||
|
||||
|
||||
tableHeadBgColor = white
|
||||
tableOddBgColor = #F9F9F9
|
||||
tableEvenBgColor = white
|
||||
|
||||
facebookColor= #3b5998
|
||||
githubBtn= #201F1F
|
||||
|
||||
// using
|
||||
successBackgroundColor= #E0F0D9
|
||||
successTextColor= #3E753F
|
||||
errorBackgroundColor= #F2DEDE
|
||||
errorTextColor= #A64444
|
||||
infoBackgroundColor= #D9EDF7
|
||||
infoTextColor= #34708E
|
||||
|
||||
popupZIndex= 500
|
||||
@@ -1,44 +0,0 @@
|
||||
module.exports = {
|
||||
vendors: [
|
||||
{
|
||||
name: 'ace',
|
||||
src: 'node_modules/@rokt33r/ace-builds/src/**/*'
|
||||
},
|
||||
{
|
||||
name: 'angular',
|
||||
src: 'node_modules/angular/angular.js'
|
||||
},
|
||||
{
|
||||
name: 'angular-bootstrap',
|
||||
src: 'node_modules/angular-bootstrap/dist/ui-bootstrap-tpls.js'
|
||||
},
|
||||
{
|
||||
name: 'angular-sanitize',
|
||||
src: 'node_modules/angular-sanitize/angular-sanitize.js'
|
||||
},
|
||||
{
|
||||
name: 'angular-ui-router',
|
||||
src: 'node_modules/angular-ui-router/build/angular-ui-router.js'
|
||||
},
|
||||
{
|
||||
name: 'ui-select',
|
||||
src: 'node_modules/ui-select/dist/select.js'
|
||||
},
|
||||
{
|
||||
name: 'satellizer',
|
||||
src: 'node_modules/satellizer/satellizer.js'
|
||||
},
|
||||
{
|
||||
name: 'angular-md5',
|
||||
src: 'node_modules/angular-md5/angular-md5.js'
|
||||
},
|
||||
{
|
||||
name: 'moment',
|
||||
src: 'node_modules/moment/moment.js'
|
||||
},
|
||||
{
|
||||
name: 'angular-hotkeys',
|
||||
src: 'node_modules/angular-hotkeys/build/hotkeys.js'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# Event List
|
||||
|
||||
|name|Description|Delivery|
|
||||
|----|----|----|
|
||||
|userSignIn|a user signed in||
|
||||
|userSignOut|a user signed out||
|
||||
|snippetUpdated|snippet has been updated or created|snippet|
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="codexen.popup">
|
||||
<head>
|
||||
<title>
|
||||
CodeXen App
|
||||
</title>
|
||||
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
||||
<meta name="description" content="CodeXen - Short code storage service">
|
||||
|
||||
<link rel="stylesheet" href="../../vendor/css/font-awesome.css" media="screen" title="no title" charset="utf-8">
|
||||
<link rel="stylesheet" href="../../main.css" media="screen" title="no title" charset="utf-8">
|
||||
</head>
|
||||
<body class="popup-body" ng-controller="PopUpController">
|
||||
|
||||
<div class="search-block">
|
||||
<input ng-change="filterList(searchNeedle)" search-input id="search-input" type="text" class="form-control" ng-model="searchNeedle" ng-change="refreshResult">
|
||||
</div>
|
||||
|
||||
<div class="result-block row-fluid">
|
||||
<ul id="result-list" class="result-list left-pane">
|
||||
<li ng-click="selectSnippet($index)" ng-repeat="snippet in filteredSnippets" ng-class="{active:$index == selectIndex}"><a href="#"> <span ng-bind="snippet.callSign"></span> <small ng-bind="snippet.description"></small></a></li>
|
||||
</ul>
|
||||
|
||||
<div class="right-pane">
|
||||
<div class="result-detail-control">
|
||||
<button ng-click="writeCode(selectedItem.content)" id="btnClipboard" type="button" name="button" class="btn btn-default"><i class="fa fa-clipboard"></i></button>
|
||||
<!-- <button ng-click="editSnippet(selectedItem.id)" id="btnEdit" type="button" name="button" class="btn btn-default"><i class="fa fa-edit"></i></button> -->
|
||||
<!-- <button id="btnShare" type="button" name="button" class="btn btn-default"><i class="fa fa-share"></i></button> -->
|
||||
</div>
|
||||
<div id="aceView" class="result-detail-content"
|
||||
ui-ace="{
|
||||
showGutter: false,
|
||||
useWrapMode: true,
|
||||
mode:selectedItem.mode.toLowerCase(),
|
||||
onLoad: aceLoaded,
|
||||
theme: 'solarized_dark'
|
||||
}"
|
||||
|
||||
readonly
|
||||
ng-model="selectedItem.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../vendor/ace.js"></script>
|
||||
<script src="../../vendor/angular.js" charset="utf-8"></script>
|
||||
<script src="../../directives/ui-ace.js"></script>
|
||||
<script src="../../vendor/satellizer.js"></script>
|
||||
<script src="../../vendor/hotkeys.js" charset="utf-8"></script>
|
||||
<script src="popup.js" charset="utf-8"></script>
|
||||
<script src="services/snippet.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,190 +0,0 @@
|
||||
/* global angular */
|
||||
|
||||
var remote = require('remote')
|
||||
var ipc = require('ipc')
|
||||
|
||||
var resultList = document.getElementById('result-list')
|
||||
|
||||
angular.module('codexen.popup', [
|
||||
'ui.ace',
|
||||
'satellizer',
|
||||
'cfp.hotkeys'
|
||||
])
|
||||
.controller('PopUpController', function ($scope, Snippet, $auth, $window, hotkeys, $document, $filter) {
|
||||
// Setup Events
|
||||
remote.getCurrentWindow().on('focus', function () {
|
||||
if (!$auth.isAuthenticated()) return hidePopUp()
|
||||
$scope.$apply(focusSearchInput)
|
||||
loadSnippets()
|
||||
})
|
||||
|
||||
hotkeys.bindTo($scope)
|
||||
.add('down', function (e) {
|
||||
nextSnippet()
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('up', function (e) {
|
||||
priorSnippet()
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('right', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('left', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('esc', function (e) {
|
||||
hidePopUp()
|
||||
})
|
||||
.add('shift+tab', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('tab', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
.add('enter', function (e) {
|
||||
writeCode($scope.selectedItem.content)
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
$scope.aceLoaded = function (editor) {
|
||||
editor.commands.addCommand({
|
||||
name: 'escape',
|
||||
bindKey: {win: 'esc', mac: 'esc'},
|
||||
exec: function (editor) {
|
||||
editor.blur()
|
||||
$scope.$apply()
|
||||
},
|
||||
readOnly: true
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$on('nextSnippetRequested', function (e) {
|
||||
e.stopPropagation()
|
||||
nextSnippet()
|
||||
})
|
||||
|
||||
$scope.$on('priorSnippetRequested', function (e) {
|
||||
e.stopPropagation()
|
||||
priorSnippet()
|
||||
})
|
||||
|
||||
$scope.$on('snippetSubmitted', function (e) {
|
||||
if ($scope.filteredSnippets.length > 0) ipc.send('writeCode', $scope.selectedItem.content)
|
||||
else console.log('\x07')
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
// Init Data
|
||||
$scope.snippets = []
|
||||
|
||||
Snippet.findMine()
|
||||
.success(function (data) {
|
||||
$scope.snippets = data
|
||||
filterList()
|
||||
})
|
||||
|
||||
// Result Item control
|
||||
$scope.selectIndex = 0
|
||||
|
||||
$scope.selectSnippet = selectSnippet
|
||||
$scope.filterList = filterList
|
||||
$scope.writeCode = writeCode
|
||||
$scope.focusSearchInput = focusSearchInput
|
||||
|
||||
// Search Filter
|
||||
function loadSnippets () {
|
||||
Snippet.findMine()
|
||||
.success(function (data) {
|
||||
$scope.snippets = data
|
||||
filterList()
|
||||
})
|
||||
}
|
||||
|
||||
function filterList (needle) {
|
||||
$scope.filteredSnippets = $filter('filter')($scope.snippets, needle)
|
||||
firstSnippet()
|
||||
}
|
||||
|
||||
function selectSnippet (index) {
|
||||
if (index !== undefined) $scope.selectIndex = index
|
||||
$scope.selectedItem = $scope.filteredSnippets[$scope.selectIndex]
|
||||
}
|
||||
|
||||
function firstSnippet () {
|
||||
$scope.selectIndex = 0
|
||||
selectSnippet($scope.selectIndex)
|
||||
}
|
||||
|
||||
function priorSnippet () {
|
||||
if ($scope.selectIndex > 0) $scope.selectIndex -= 1
|
||||
|
||||
if (resultList.children[$scope.selectIndex].offsetTop < resultList.scrollTop) {
|
||||
resultList.scrollTop -= 33
|
||||
}
|
||||
|
||||
selectSnippet()
|
||||
}
|
||||
|
||||
function nextSnippet () {
|
||||
if ($scope.selectIndex < $scope.filteredSnippets.length - 1) {
|
||||
$scope.selectIndex += 1
|
||||
}
|
||||
|
||||
if (resultList.clientHeight - 33 < resultList.children[$scope.selectIndex].offsetTop - resultList.scrollTop) {
|
||||
resultList.scrollTop += 33
|
||||
}
|
||||
|
||||
selectSnippet()
|
||||
}
|
||||
|
||||
function writeCode (code) {
|
||||
ipc.send('writeCode', code)
|
||||
}
|
||||
|
||||
// Focusing Search Input
|
||||
function focusSearchInput () {
|
||||
document.getElementById('search-input').focus()
|
||||
}
|
||||
|
||||
function hidePopUp () {
|
||||
ipc.send('hidePopUp')
|
||||
}
|
||||
|
||||
})
|
||||
.directive('searchInput', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, el, attr) {
|
||||
el.on('keydown', function (e) {
|
||||
// Down key => Focus on Result list
|
||||
if (e.keyCode === 40) {
|
||||
scope.$emit('nextSnippetRequested')
|
||||
// e.preventDefault()
|
||||
}
|
||||
|
||||
// Up key => Focus on Result list
|
||||
if (e.keyCode === 38) {
|
||||
scope.$emit('priorSnippetRequested')
|
||||
// e.preventDefault()
|
||||
}
|
||||
|
||||
// Up key => Focus on Result list
|
||||
if (e.keyCode === 13) {
|
||||
scope.$emit('snippetSubmitted')
|
||||
}
|
||||
|
||||
// Esc key => Dismiss popup
|
||||
if (e.keyCode === 27) {
|
||||
ipc.send('hidePopUp')
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// TODO: Tab key => Auto complete
|
||||
if (e.keyCode === 9) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
/* global angular */
|
||||
angular.module('codexen.popup')
|
||||
.constant('apiUrl', 'http://codexen-server-dev.elasticbeanstalk.com/')
|
||||
.config(function ($authProvider, $httpProvider, apiUrl) {
|
||||
$authProvider.baseUrl = apiUrl
|
||||
|
||||
$httpProvider.defaults.useXDomain = true
|
||||
delete $httpProvider.defaults.headers.common['X-Requested-With']
|
||||
})
|
||||
|
||||
|
||||
angular.module('codexen.popup')
|
||||
.factory('Snippet', function ($http, $auth, apiUrl) {
|
||||
var findByUser = function (user) {
|
||||
var url = apiUrl + 'snippets/search'
|
||||
|
||||
return $http.get(url, {
|
||||
params: {
|
||||
user: user
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var findMine = function (params) {
|
||||
var url = apiUrl + 'snippets/my'
|
||||
|
||||
return $http.get(url, {params: params})
|
||||
}
|
||||
|
||||
var create = function (params) {
|
||||
var url = apiUrl + 'snippets/create'
|
||||
|
||||
return $http.post(url, params)
|
||||
}
|
||||
|
||||
var show = function (id, params) {
|
||||
var url = apiUrl + 'snippets/id/' + id
|
||||
|
||||
return $http.get(url, {params: params})
|
||||
}
|
||||
|
||||
var update = function (id, params) {
|
||||
var url = apiUrl + 'snippets/id/' + id
|
||||
|
||||
return $http.put(url, params)
|
||||
}
|
||||
|
||||
var destroy = function (id) {
|
||||
var url = apiUrl + 'snippets/id/' + id
|
||||
|
||||
return $http.delete(url)
|
||||
}
|
||||
|
||||
return {
|
||||
findByUser: findByUser,
|
||||
findMine: findMine,
|
||||
create: create,
|
||||
show: show,
|
||||
delete: destroy,
|
||||
update: update
|
||||
}
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
.popup-body
|
||||
.search-block
|
||||
padding: 5px
|
||||
height:44px
|
||||
position:absolute
|
||||
top: 0
|
||||
width: 100%
|
||||
|
||||
.result-block
|
||||
position:absolute
|
||||
top: 44px
|
||||
bottom: 0
|
||||
width: 100%
|
||||
overflow: hidden
|
||||
.left-pane
|
||||
margin: 0
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
width: 40%
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
.result-list
|
||||
list-style:none
|
||||
padding: 0
|
||||
border-right: 1px solid $baseBorderColor
|
||||
li
|
||||
&:nth-child(even)
|
||||
background-color $baseBackgroundColor
|
||||
&:nth-child(odd)
|
||||
background-color lighten($baseBackgroundColor, 2%)
|
||||
&.active
|
||||
color: $textColorSelected
|
||||
background-color: $btnPrimary
|
||||
a
|
||||
display:block
|
||||
padding: 5px 10px
|
||||
border-bottom 1px solid $baseBorderColor
|
||||
|
||||
.right-pane
|
||||
position: absolute
|
||||
left: 40%
|
||||
top: 0
|
||||
bottom: 0
|
||||
width: 60%
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
.result-detail-control
|
||||
position: absolute
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 34px
|
||||
|
||||
.result-detail-content
|
||||
position: absolute
|
||||
top: 34px
|
||||
bottom: 0
|
||||
width: 100%
|
||||
@@ -1,13 +0,0 @@
|
||||
@import '../../src/styles/_vars'
|
||||
@import '../../src/styles/mixins/*'
|
||||
|
||||
@import '../../src/styles/_bootstrap'
|
||||
|
||||
@import '../../src/styles/_index'
|
||||
@import '../../src/styles/_shared'
|
||||
|
||||
@import '../../src/styles/modals/*'
|
||||
@import '../../src/styles/directives/*'
|
||||
@import '../../src/styles/states/*'
|
||||
|
||||
@import '_popup'
|
||||
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'])
|
||||
}
|
||||
139
gulp-electron.js
139
gulp-electron.js
@@ -1,139 +0,0 @@
|
||||
require('dotenv').load()
|
||||
var env = process.env
|
||||
|
||||
var styl = require('gulp-stylus')
|
||||
var autoprefixer = require('gulp-autoprefixer')
|
||||
var templateCache = require('gulp-angular-templatecache')
|
||||
var globby = require('globby')
|
||||
var template = require('gulp-template')
|
||||
var del = require('del')
|
||||
var runSequence = require('run-sequence')
|
||||
var plumber = require('gulp-plumber')
|
||||
var notify = require('gulp-notify')
|
||||
var changed = require('gulp-changed')
|
||||
var livereload = require('gulp-livereload')
|
||||
var merge = require('merge-stream')
|
||||
|
||||
var config = require('./build.config.js')
|
||||
|
||||
module.exports = function (gulp) {
|
||||
|
||||
gulp.task('elec-env', function () {
|
||||
return gulp.src('tpls/env.js')
|
||||
.pipe(template({
|
||||
apiUrl: env.ELEC_API_URL
|
||||
}))
|
||||
.pipe(gulp.dest('electron_build/config'))
|
||||
})
|
||||
|
||||
gulp.task('elec-js', function () {
|
||||
var main = gulp.src('src/**/*.js')
|
||||
.pipe(changed('electron_build'))
|
||||
.pipe(gulp.dest('electron_build'))
|
||||
|
||||
var electron = gulp.src('electron_src/**/*.js')
|
||||
.pipe(changed('electron_build/electron'))
|
||||
.pipe(gulp.dest('electron_build/electron'))
|
||||
|
||||
return merge(main, electron)
|
||||
})
|
||||
|
||||
gulp.task('elec-styl', function () {
|
||||
return gulp.src('electron_src/styles/main.styl')
|
||||
.pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
|
||||
.pipe(styl())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest('electron_build'))
|
||||
.pipe(notify('Stylus!!'))
|
||||
.pipe(livereload())
|
||||
})
|
||||
|
||||
gulp.task('elec-tpls', function () {
|
||||
var main = gulp.src('src/**/*.tpl.html')
|
||||
.pipe(templateCache())
|
||||
.pipe(gulp.dest('electron_build'))
|
||||
|
||||
var electron = gulp.src('electron_src/**/*.tpl.html')
|
||||
.pipe(templateCache())
|
||||
.pipe(gulp.dest('electron_build/electron'))
|
||||
|
||||
return merge(main, electron)
|
||||
})
|
||||
|
||||
gulp.task('elec-index', function () {
|
||||
var files = globby.sync(['electron_build/**/*', '!electron_build/vendor/**/*', '!electron_build/electron/**/*'])
|
||||
|
||||
var filter = function (files, ext) {
|
||||
return files.filter(function (file) {
|
||||
var reg = new RegExp('.+\.' + ext + '$')
|
||||
return file.match(reg)
|
||||
}).map(function (file) {
|
||||
return file.replace('electron_build/', '')
|
||||
})
|
||||
}
|
||||
var scripts = filter(files, 'js')
|
||||
var styles = filter(files, 'css')
|
||||
|
||||
var main = gulp.src('src/index.html')
|
||||
.pipe(template({
|
||||
scripts: scripts,
|
||||
styles: styles,
|
||||
env: 'build'
|
||||
}))
|
||||
.pipe(gulp.dest('electron_build'))
|
||||
.pipe(livereload())
|
||||
|
||||
var electron = gulp.src('electron_src/**/index.html')
|
||||
.pipe(gulp.dest('electron_build/electron'))
|
||||
|
||||
return merge(main, electron)
|
||||
})
|
||||
|
||||
gulp.task('elec-vendor', function () {
|
||||
var vendors = config.vendors
|
||||
|
||||
var vendorFiles = vendors.map(function (vendor) {
|
||||
return vendor.src
|
||||
})
|
||||
|
||||
vendorFiles.push('node_modules/font-awesome/**/font-awesome.css')
|
||||
vendorFiles.push('node_modules/font-awesome/**/fontawesome-webfont.*')
|
||||
vendorFiles.push('node_modules/font-awesome/**/FontAwesome.*')
|
||||
|
||||
return gulp.src(vendorFiles)
|
||||
.pipe(gulp.dest('electron_build/vendor'))
|
||||
})
|
||||
|
||||
gulp.task('elec-resources', function () {
|
||||
return gulp.src('resources/**/*')
|
||||
.pipe(changed('electron_build/resources'))
|
||||
.pipe(gulp.dest('electron_build/resources'))
|
||||
})
|
||||
|
||||
gulp.task('elec-build', function (cb) {
|
||||
runSequence(['elec-env', 'elec-js', 'elec-styl', 'elec-tpls', 'elec-vendor', 'elec-resources'], 'elec-index', cb)
|
||||
})
|
||||
|
||||
gulp.task('elec-watch', function (cb) {
|
||||
gulp.watch(['.env', 'tpls/env.js'], ['elec-env'])
|
||||
|
||||
gulp.watch(['src/**/*.js', 'electron_src/**/*.js'], ['elec-js'])
|
||||
|
||||
gulp.watch(['src/styles/**/*.styl', 'electron_src/styles/**/*.styl'], ['elec-styl'])
|
||||
|
||||
gulp.watch('src/**/*.tpl.html', ['elec-tpls'])
|
||||
|
||||
gulp.watch(['electron_build/**/*.js', 'src/index.html', 'src/index.html', 'electron_src/**/index.html'], ['elec-index'])
|
||||
|
||||
livereload.listen()
|
||||
})
|
||||
|
||||
gulp.task('elec-del', function (cb) {
|
||||
del(['electron_build/**/*'], cb)
|
||||
})
|
||||
|
||||
gulp.task('elec', function (cb) {
|
||||
runSequence('elec-del', 'elec-build', 'elec-watch', cb)
|
||||
})
|
||||
|
||||
}
|
||||
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')
|
||||
}
|
||||
79
lib/config.js
Normal file
79
lib/config.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const ipc = electron.ipcMain
|
||||
const jetpack = require('fs-jetpack')
|
||||
const nodeIpc = require('@rokt33r/node-ipc')
|
||||
|
||||
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
|
||||
}
|
||||
const configFile = 'config.json'
|
||||
|
||||
var userDataPath = app.getPath('userData')
|
||||
|
||||
function getConfig () {
|
||||
var userDataPath = app.getPath('userData')
|
||||
if (jetpack.cwd(userDataPath).exists(configFile)) {
|
||||
try {
|
||||
return JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8'))
|
||||
} catch (err) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var config = null
|
||||
|
||||
function saveConfig () {
|
||||
var content
|
||||
try {
|
||||
content = JSON.stringify(config)
|
||||
} catch (e) {
|
||||
config = {}
|
||||
content = JSON.stringify(config)
|
||||
}
|
||||
jetpack.cwd(userDataPath).file(configFile, { content })
|
||||
}
|
||||
|
||||
// Init
|
||||
config = getConfig()
|
||||
if (config == null) {
|
||||
config = Object.assign({}, defaultConfig)
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
config = Object.assign({}, defaultConfig, config)
|
||||
|
||||
if (config['disable-direct-write']) {
|
||||
app.commandLine.appendSwitch('disable-direct-write')
|
||||
}
|
||||
|
||||
function emitToFinder (type, data) {
|
||||
var payload = {
|
||||
type: type,
|
||||
data: data
|
||||
}
|
||||
|
||||
nodeIpc.server.broadcast('message', payload)
|
||||
}
|
||||
|
||||
app.on('ready', function () {
|
||||
const mainWindow = require('./main-window')
|
||||
function applyConfig () {
|
||||
mainWindow.webContents.send('config-apply', config)
|
||||
emitToFinder('config-apply', config)
|
||||
}
|
||||
|
||||
ipc.on('configUpdated', function (event, newConfig) {
|
||||
config = Object.assign({}, defaultConfig, config, newConfig)
|
||||
saveConfig()
|
||||
applyConfig()
|
||||
})
|
||||
})
|
||||
|
||||
19
lib/finder-app.js
Executable file
19
lib/finder-app.js
Executable file
@@ -0,0 +1,19 @@
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const Menu = electron.Menu
|
||||
|
||||
var finderWindow = null
|
||||
|
||||
app.on('ready', function () {
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.hide()
|
||||
}
|
||||
|
||||
var template = require('./finder-menu')
|
||||
var menu = Menu.buildFromTemplate(template)
|
||||
Menu.setApplicationMenu(menu)
|
||||
|
||||
finderWindow = require('./finder-window')
|
||||
})
|
||||
|
||||
module.exports = app
|
||||
84
lib/finder-menu.js
Normal file
84
lib/finder-menu.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const electron = require('electron')
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
|
||||
const OSX = process.platform === 'darwin'
|
||||
const WIN = process.platform === 'win32'
|
||||
|
||||
var edit = {
|
||||
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:'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var view = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Focus Search',
|
||||
accelerator: 'Control + Alt + F',
|
||||
click: function () {
|
||||
console.log('focus find')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Toggle Markdown Preview',
|
||||
accelerator: OSX ? 'Command + P' : 'Ctrl + P',
|
||||
click: function () {
|
||||
console.log('markdown')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: (function () {
|
||||
if (process.platform === 'darwin') return 'Command+R'
|
||||
else return 'Ctrl+R'
|
||||
})(),
|
||||
click: function () {
|
||||
BrowserWindow.getFocusedWindow().reload()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = process.platform === 'darwin'
|
||||
? [edit, view]
|
||||
: [view]
|
||||
171
lib/finder-window.js
Normal file
171
lib/finder-window.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const electron = require('electron')
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const Menu = electron.Menu
|
||||
const MenuItem = electron.MenuItem
|
||||
const app = electron.app
|
||||
const ipcMain = electron.ipcMain
|
||||
const Tray = electron.Tray
|
||||
const path = require('path')
|
||||
const nodeIpc = require('@rokt33r/node-ipc')
|
||||
|
||||
var appQuit = false
|
||||
var isFinderLoaded = false
|
||||
|
||||
nodeIpc.config.id = 'finder'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
nodeIpc.connectTo(
|
||||
'main',
|
||||
path.join(app.getPath('userData'), 'boost.service'),
|
||||
function () {
|
||||
nodeIpc.of.main.on(
|
||||
'error',
|
||||
function (err) {
|
||||
nodeIpc.log('<< ## err ##'.rainbow, nodeIpc.config.delay)
|
||||
nodeIpc.log(err)
|
||||
}
|
||||
)
|
||||
nodeIpc.of.main.on(
|
||||
'connect',
|
||||
function () {
|
||||
nodeIpc.log('<< ## connected to world ##'.rainbow, nodeIpc.config.delay)
|
||||
}
|
||||
)
|
||||
nodeIpc.of.main.on(
|
||||
'disconnect',
|
||||
function () {
|
||||
nodeIpc.log('<< disconnected from main'.notice)
|
||||
if (process.platform === 'darwin') {
|
||||
appQuit = true
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
)
|
||||
nodeIpc.of.main.on(
|
||||
'message',
|
||||
function (payload) {
|
||||
if (isFinderLoaded) {
|
||||
switch (payload.type) {
|
||||
case 'open-finder':
|
||||
if (finderWindow.isFocused()) {
|
||||
hideFinder()
|
||||
} else {
|
||||
openFinder()
|
||||
}
|
||||
break
|
||||
case 'config-apply': {
|
||||
finderWindow.webContents.send('config-apply', payload.data)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
function emit (type, data) {
|
||||
var payload = {
|
||||
type: type,
|
||||
data: data
|
||||
}
|
||||
nodeIpc.of.main.emit('message', payload)
|
||||
}
|
||||
|
||||
var config = {
|
||||
width: 640,
|
||||
height: 400,
|
||||
show: false,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
'zoom-factor': 1.0,
|
||||
'web-preferences': {
|
||||
'overlay-scrollbars': true,
|
||||
'skip-taskbar': true
|
||||
},
|
||||
'standard-window': false
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
config['always-on-top'] = true
|
||||
}
|
||||
|
||||
var finderWindow = new BrowserWindow(config)
|
||||
|
||||
var url = path.resolve(__dirname, './finder.html')
|
||||
|
||||
finderWindow.loadURL('file://' + url)
|
||||
finderWindow.setSkipTaskbar(true)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
finderWindow.setVisibleOnAllWorkspaces(true)
|
||||
}
|
||||
|
||||
finderWindow.on('blur', function () {
|
||||
hideFinder()
|
||||
})
|
||||
|
||||
finderWindow.on('close', function (e) {
|
||||
if (process.platform === 'darwin') {
|
||||
if (appQuit) return true
|
||||
e.preventDefault()
|
||||
finderWindow.hide()
|
||||
}
|
||||
})
|
||||
|
||||
var appIcon = new Tray(path.join(__dirname, '../resources/tray-icon.png'))
|
||||
appIcon.setToolTip('Boost')
|
||||
|
||||
var trayMenu = new Menu()
|
||||
trayMenu.append(new MenuItem({
|
||||
label: 'Open Main window',
|
||||
click: function () {
|
||||
emit('show-main-window')
|
||||
}
|
||||
}))
|
||||
trayMenu.append(new MenuItem({
|
||||
label: 'Open Finder window',
|
||||
click: function () {
|
||||
openFinder()
|
||||
}
|
||||
}))
|
||||
trayMenu.append(new MenuItem({
|
||||
label: 'Quit',
|
||||
click: function () {
|
||||
emit('quit-app')
|
||||
}
|
||||
}))
|
||||
|
||||
appIcon.setContextMenu(trayMenu)
|
||||
appIcon.on('click', function (e) {
|
||||
e.preventDefault()
|
||||
appIcon.popUpContextMenu(trayMenu)
|
||||
})
|
||||
|
||||
ipcMain.on('copy-finder', function () {
|
||||
emit('copy-finder')
|
||||
})
|
||||
|
||||
ipcMain.on('hide-finder', function () {
|
||||
hideFinder()
|
||||
})
|
||||
|
||||
finderWindow.webContents.on('did-finish-load', function () {
|
||||
isFinderLoaded = true
|
||||
})
|
||||
|
||||
function openFinder () {
|
||||
if (isFinderLoaded) finderWindow.show()
|
||||
}
|
||||
function hideFinder () {
|
||||
if (process.platform === 'win32') {
|
||||
finderWindow.minimize()
|
||||
return
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
finderWindow.hide()
|
||||
}
|
||||
module.exports = finderWindow
|
||||
45
lib/finder.html
Normal file
45
lib/finder.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>Boostnote Finder</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="stylesheet" href="../node_modules/highlight.js/styles/xcode.css">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="../resources/katex.min.css">
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../resources/fonts/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 src="../resources/katex.min.js"></script>
|
||||
<script>
|
||||
const electron = require('electron')
|
||||
electron.webFrame.setZoomLevelLimits(1, 1)
|
||||
const _ = require('lodash')
|
||||
var scriptUrl = _.find(electron.remote.process.argv, a => a === '--hot')
|
||||
? '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>
|
||||
108
lib/hotkey.js
Normal file
108
lib/hotkey.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const ipc = electron.ipcMain
|
||||
const Menu = electron.Menu
|
||||
const globalShortcut = electron.globalShortcut
|
||||
const jetpack = require('fs-jetpack')
|
||||
const mainWindow = require('./main-window')
|
||||
const nodeIpc = require('@rokt33r/node-ipc')
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultKeymap = {
|
||||
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
|
||||
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
|
||||
}
|
||||
const keymapFilename = 'keymap.json'
|
||||
|
||||
var userDataPath = app.getPath('userData')
|
||||
|
||||
function getKeymap () {
|
||||
var userDataPath = app.getPath('userData')
|
||||
if (jetpack.cwd(userDataPath).exists(keymapFilename)) {
|
||||
try {
|
||||
return JSON.parse(jetpack.cwd(userDataPath).read(keymapFilename, 'utf-8'))
|
||||
} catch (err) {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function saveKeymap () {
|
||||
var content
|
||||
try {
|
||||
content = JSON.stringify(global.keymap)
|
||||
} catch (e) {
|
||||
global.keymap = {}
|
||||
content = JSON.stringify(global.keymap)
|
||||
}
|
||||
jetpack.cwd(userDataPath).file(keymapFilename, { content })
|
||||
}
|
||||
|
||||
function emitToFinder (type, data) {
|
||||
var payload = {
|
||||
type: type,
|
||||
data: data
|
||||
}
|
||||
|
||||
nodeIpc.server.broadcast('message', payload)
|
||||
}
|
||||
|
||||
function toggleFinder () {
|
||||
emitToFinder('open-finder')
|
||||
mainWindow.webContents.send('open-finder', {})
|
||||
}
|
||||
|
||||
function toggleMain () {
|
||||
if (mainWindow.isFocused()) {
|
||||
if (process.platform === 'darwin') {
|
||||
Menu.sendActionToFirstResponder('hide:')
|
||||
} else {
|
||||
mainWindow.minimize()
|
||||
}
|
||||
} else {
|
||||
if (process.platform === 'darwin') {
|
||||
mainWindow.show()
|
||||
} else {
|
||||
mainWindow.minimize()
|
||||
mainWindow.restore()
|
||||
}
|
||||
mainWindow.webContents.send('list-focus')
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
global.keymap = Object.assign({}, defaultKeymap, getKeymap())
|
||||
|
||||
function registerKey (name, callback, broadcast) {
|
||||
if (broadcast == null) broadcast = true
|
||||
|
||||
try {
|
||||
globalShortcut.register(global.keymap[name], callback)
|
||||
if (broadcast) {
|
||||
mainWindow.webContents.send('APP_SETTING_DONE', {})
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
if (broadcast) {
|
||||
mainWindow.webContents.send('APP_SETTING_ERROR', {
|
||||
message: 'Failed to apply hotkey: Invalid format'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerAllKeys (broadcast) {
|
||||
if (broadcast == null) broadcast = true
|
||||
registerKey('toggleFinder', toggleFinder, broadcast)
|
||||
registerKey('toggleMain', toggleMain, broadcast)
|
||||
}
|
||||
|
||||
registerAllKeys(false)
|
||||
|
||||
ipc.on('hotkeyUpdated', function (event, newKeymap) {
|
||||
global.keymap = Object.assign({}, defaultKeymap, global.keymap, newKeymap)
|
||||
saveKeymap()
|
||||
globalShortcut.unregisterAll()
|
||||
registerAllKeys()
|
||||
})
|
||||
|
||||
301
lib/main-app.js
Normal file
301
lib/main-app.js
Normal file
@@ -0,0 +1,301 @@
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const Menu = electron.Menu
|
||||
const ipc = electron.ipcMain
|
||||
const autoUpdater = electron.autoUpdater
|
||||
const path = require('path')
|
||||
const ChildProcess = require('child_process')
|
||||
const _ = require('lodash')
|
||||
const nodeIpc = require('@rokt33r/node-ipc')
|
||||
const GhReleases = require('electron-gh-releases')
|
||||
// electron.crashReporter.start()
|
||||
|
||||
require('./config')
|
||||
var mainWindow = null
|
||||
var finderProcess = null
|
||||
var finderWindow = null
|
||||
var update = null
|
||||
|
||||
// app.on('window-all-closed', function () {
|
||||
// if (process.platform !== 'darwin') app.quit()
|
||||
// })
|
||||
|
||||
const appRootPath = path.join(process.execPath, '../..')
|
||||
const updateDotExePath = path.join(appRootPath, 'Update.exe')
|
||||
const exeName = path.basename(process.execPath)
|
||||
|
||||
function spawnUpdate (args, cb) {
|
||||
var stdout = ''
|
||||
var updateProcess = null
|
||||
try {
|
||||
updateProcess = ChildProcess.spawn(updateDotExePath, args)
|
||||
} catch (e) {
|
||||
process.nextTick(function () {
|
||||
cb(e)
|
||||
})
|
||||
}
|
||||
|
||||
updateProcess.stdout.on('data', function (data) {
|
||||
stdout += data
|
||||
})
|
||||
|
||||
error = null
|
||||
updateProcess.on('error', function (_error) {
|
||||
error = _error
|
||||
})
|
||||
updateProcess.on('close', function (code, signal) {
|
||||
if (code !== 0) {
|
||||
error = new Error("Command failed: #{signal ? code}")
|
||||
error.code = code
|
||||
error.stdout = stdout
|
||||
}
|
||||
|
||||
cb(error, stdout)
|
||||
})
|
||||
}
|
||||
|
||||
var handleStartupEvent = function () {
|
||||
if (process.platform !== 'win32') {
|
||||
return false
|
||||
}
|
||||
|
||||
var squirrelCommand = process.argv[1]
|
||||
switch (squirrelCommand) {
|
||||
case '--squirrel-install':
|
||||
spawnUpdate(['--createShortcut', exeName], function (err) {
|
||||
quitApp()
|
||||
})
|
||||
return true
|
||||
case '--squirrel-updated':
|
||||
quitApp()
|
||||
return true
|
||||
case '--squirrel-uninstall':
|
||||
spawnUpdate(['--removeShortcut', exeName], function (err) {
|
||||
quitApp()
|
||||
})
|
||||
quitApp()
|
||||
return true
|
||||
case '--squirrel-obsolete':
|
||||
quitApp()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (handleStartupEvent()) {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) {
|
||||
if (mainWindow) {
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.minimize()
|
||||
mainWindow.restore()
|
||||
}
|
||||
mainWindow.focus()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (shouldQuit) {
|
||||
quitApp()
|
||||
return
|
||||
}
|
||||
|
||||
var appQuit = false
|
||||
|
||||
var version = app.getVersion()
|
||||
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
|
||||
var versionNotified = false
|
||||
|
||||
function notify (title, body) {
|
||||
if (mainWindow != null) {
|
||||
mainWindow.webContents.send('notify', {
|
||||
title: title,
|
||||
body: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var isUpdateReady = false
|
||||
|
||||
var ghReleasesOpts = {
|
||||
repo: 'BoostIO/boost-releases',
|
||||
currentVersion: app.getVersion()
|
||||
}
|
||||
|
||||
const updater = new GhReleases(ghReleasesOpts)
|
||||
|
||||
// Check for updates
|
||||
// `status` returns true if there is a new update available
|
||||
function checkUpdate () {
|
||||
updater.check((err, status) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
if (!versionNotified) notify('Updater error!', message)
|
||||
}
|
||||
if (!err) {
|
||||
if (status) {
|
||||
notify('Update is available!', 'Download started.. wait for the update ready.')
|
||||
updater.download()
|
||||
} else {
|
||||
if (!versionNotified) {
|
||||
versionNotified = true
|
||||
notify('Latest Build!! ' + versionText, 'Hope you to enjoy our app :D')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updater.on('update-downloaded', (info) => {
|
||||
if (mainWindow != null) {
|
||||
notify('Ready to Update!', 'Click update button on Main window.')
|
||||
mainWindow.webContents.send('update-available', 'Update available!')
|
||||
isUpdateReady = true
|
||||
}
|
||||
})
|
||||
|
||||
nodeIpc.config.id = 'node'
|
||||
nodeIpc.config.retry = 1500
|
||||
nodeIpc.config.silent = true
|
||||
|
||||
function spawnFinder() {
|
||||
if (process.platform === 'darwin') {
|
||||
var finderArgv = [path.join(__dirname, 'finder-app.js'), '--finder']
|
||||
if (_.find(process.argv, a => a === '--hot')) finderArgv.push('--hot')
|
||||
finderProcess = ChildProcess
|
||||
.execFile(process.execPath, finderArgv)
|
||||
}
|
||||
}
|
||||
|
||||
nodeIpc.serve(
|
||||
path.join(app.getPath('userData'), 'boost.service'),
|
||||
function () {
|
||||
nodeIpc.server.on(
|
||||
'connect',
|
||||
function (socket) {
|
||||
socket.on('close', function () {
|
||||
console.log('socket dead')
|
||||
if (!appQuit) spawnFinder()
|
||||
})
|
||||
}
|
||||
)
|
||||
nodeIpc.server.on(
|
||||
'message',
|
||||
function (data, socket) {
|
||||
console.log('>>', data)
|
||||
format(data)
|
||||
}
|
||||
)
|
||||
nodeIpc.server.on(
|
||||
'error',
|
||||
function (err) {
|
||||
console.log('>>', err)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
function format (payload) {
|
||||
switch (payload.type) {
|
||||
case 'show-main-window':
|
||||
if (process.platform === 'darwin') {
|
||||
mainWindow.show()
|
||||
} else {
|
||||
mainWindow.minimize()
|
||||
mainWindow.restore()
|
||||
}
|
||||
break
|
||||
case 'copy-finder':
|
||||
mainWindow.webContents.send('copy-finder')
|
||||
break
|
||||
case 'quit-app':
|
||||
quitApp()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function quitApp () {
|
||||
appQuit = true
|
||||
if (finderProcess) finderProcess.kill()
|
||||
app.quit()
|
||||
}
|
||||
|
||||
app.on('ready', function () {
|
||||
app.on('before-quit', function () {
|
||||
console.log('before quite')
|
||||
appQuit = true
|
||||
if (finderProcess) finderProcess.kill()
|
||||
})
|
||||
|
||||
var template = require('./main-menu')
|
||||
if (process.platform === 'win32') {
|
||||
template.unshift({
|
||||
label: 'Boostnote',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Control+Q',
|
||||
click: function (e) {
|
||||
quitApp()
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
var menu = Menu.buildFromTemplate(template)
|
||||
if (process.platform === 'darwin') {
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
setInterval(function () {
|
||||
checkUpdate()
|
||||
}, 1000 * 60 * 60 * 24)
|
||||
|
||||
ipc.on('check-update', function (event, msg) {
|
||||
if (update == null) checkUpdate()
|
||||
})
|
||||
|
||||
ipc.on('update-app', function (event, msg) {
|
||||
if (isUpdateReady) {
|
||||
appQuit = true
|
||||
updater.install()
|
||||
}
|
||||
})
|
||||
|
||||
checkUpdate()
|
||||
|
||||
mainWindow = require('./main-window')
|
||||
if (process.platform === 'win32') {
|
||||
mainWindow.setMenu(menu)
|
||||
}
|
||||
mainWindow.on('close', function (e) {
|
||||
if (appQuit) return true
|
||||
e.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
|
||||
if (finderProcess == null && process.platform === 'darwin') {
|
||||
spawnFinder()
|
||||
} else {
|
||||
finderWindow = require('./finder-window')
|
||||
|
||||
finderWindow.on('close', function (e) {
|
||||
if (appQuit) return true
|
||||
e.preventDefault()
|
||||
finderWindow.hide()
|
||||
})
|
||||
}
|
||||
|
||||
nodeIpc.server.start(function (err) {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
notify('Error occurs!', 'You have to kill other Boostnote processes.')
|
||||
quitApp()
|
||||
}
|
||||
})
|
||||
|
||||
require('./hotkey')
|
||||
})
|
||||
|
||||
module.exports = app
|
||||
|
||||
204
lib/main-menu.js
Normal file
204
lib/main-menu.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const electron = require('electron')
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const shell = electron.shell
|
||||
const mainWindow = require('./main-window')
|
||||
|
||||
const OSX = process.platform === 'darwin'
|
||||
const WIN = process.platform === 'win32'
|
||||
|
||||
var boost = {
|
||||
label: 'Boostnote',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About Boostnote',
|
||||
selector: 'orderFrontStandardAboutPanel:'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Hide Boostnote',
|
||||
accelerator: 'Command+H',
|
||||
selector: 'hide:'
|
||||
},
|
||||
{
|
||||
label: 'Hide Others',
|
||||
accelerator: 'Command+Shift+H',
|
||||
selector: 'hideOtherApplications:'
|
||||
},
|
||||
{
|
||||
label: 'Show All',
|
||||
selector: 'unhideAllApplications:'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
selector: 'terminate:'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var file = {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'New Post',
|
||||
accelerator: OSX ? 'Command + N' : 'Control + N',
|
||||
click: function () {
|
||||
mainWindow.webContents.send('top-new-post')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'New Folder',
|
||||
accelerator: OSX ? 'Command + Shift + N' : 'Control + Shift + N',
|
||||
click: function () {
|
||||
mainWindow.webContents.send('nav-new-folder')
|
||||
}
|
||||
},
|
||||
// {
|
||||
// type: 'separator'
|
||||
// },
|
||||
// {
|
||||
// label: 'Save Post',
|
||||
// accelerator: OSX ? 'Command + S' : 'Control + S',
|
||||
// click: function () {
|
||||
// mainWindow.webContents.send('detail-save')
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// label: 'Save All Posts',
|
||||
// accelerator: OSX ? 'Command + Shift + S' : 'Control + Shift + S',
|
||||
// click: function () {
|
||||
// mainWindow.webContents.send('top-save-all')
|
||||
// }
|
||||
// },
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Delete Post',
|
||||
accelerator: OSX ? 'Control + Backspace' : 'Control + Delete',
|
||||
click: function () {
|
||||
mainWindow.webContents.send('detail-delete')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var edit = {
|
||||
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:'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var view = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Focus Search',
|
||||
accelerator: 'Control + Alt + F',
|
||||
click: function () {
|
||||
mainWindow.webContents.send('top-focus-search')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: (function () {
|
||||
if (process.platform === 'darwin') return 'Command+R'
|
||||
else return 'Ctrl+R'
|
||||
})(),
|
||||
click: function () {
|
||||
BrowserWindow.getFocusedWindow().reload()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var window = {
|
||||
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:'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var help = {
|
||||
label: 'Help',
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Boostnote official site',
|
||||
click: function () { shell.openExternal('https://b00st.io/') }
|
||||
},
|
||||
{
|
||||
label: 'Tutorial page',
|
||||
click: function () { shell.openExternal('https://b00st.io/tutorial.html') }
|
||||
},
|
||||
{
|
||||
label: 'Discussions',
|
||||
click: function () { shell.openExternal('https://github.com/BoostIO/boost-app-discussions/issues') }
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
click: function () { shell.openExternal('https://github.com/BoostIO/boost-releases/blob/master/changelog.md') }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = process.platform === 'darwin'
|
||||
? [boost, file, edit, view, window, help]
|
||||
: [file, view, help]
|
||||
39
lib/main-window.js
Normal file
39
lib/main-window.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const electron = require('electron')
|
||||
const app = electron.app
|
||||
const BrowserWindow = electron.BrowserWindow
|
||||
const path = require('path')
|
||||
|
||||
var mainWindow = new BrowserWindow({
|
||||
width: 1080,
|
||||
height: 720,
|
||||
'zoom-factor': 1.0,
|
||||
'web-preferences': {
|
||||
'overlay-scrollbars': true
|
||||
},
|
||||
'standard-window': false
|
||||
})
|
||||
|
||||
const url = path.resolve(__dirname, './main.html')
|
||||
|
||||
mainWindow.loadURL('file://' + url)
|
||||
|
||||
mainWindow.webContents.on('new-window', function (e) {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
mainWindow.webContents.sendInputEvent({
|
||||
type: 'keyDown',
|
||||
keyCode: '\u0008'
|
||||
})
|
||||
|
||||
mainWindow.webContents.sendInputEvent({
|
||||
type: 'keyUp',
|
||||
keyCode: '\u0008'
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
if (mainWindow == null) return null
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
module.exports = mainWindow
|
||||
74
lib/main.html
Normal file
74
lib/main.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!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="stylesheet" href="../node_modules/highlight.js/styles/xcode.css">
|
||||
<link rel="shortcut icon" href="../resources/favicon.ico">
|
||||
<title>Boostnote</title>
|
||||
<link rel="stylesheet" href="../resources/katex.min.css">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../resources/fonts/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/app.png">
|
||||
<div class='message'>Loading...</div>
|
||||
</div>
|
||||
|
||||
<div id="content"></div>
|
||||
|
||||
<script src="../submodules/ace/src-min/ace.js"></script>
|
||||
<script src="../resources/katex.min.js"></script>
|
||||
<script type='text/javascript'>
|
||||
const electron = require('electron')
|
||||
electron.webFrame.setZoomLevelLimits(1, 1)
|
||||
var version = electron.remote.app.getVersion()
|
||||
const _ = require('lodash')
|
||||
var scriptUrl = _.find(electron.remote.process.argv, a => a === '--hot')
|
||||
? '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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user