1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 02:06:29 +00:00

Compare commits

...

513 Commits

Author SHA1 Message Date
Junyoung Choi
7ab81608e8 v0.11.3 2018-03-22 23:07:28 +09:00
Junyoung Choi (Sai)
017e40fcb9 Merge pull request #1722 from BoostIO/disable-i18n-dev-mode
Disable devMode by default
2018-03-22 23:01:49 +09:00
Junyoung Choi
29309fbaa3 Disable devMode by default 2018-03-22 23:01:20 +09:00
Junyoung Choi (Sai)
b766b08bf5 Merge pull request #1709 from rayou/add-editor-rulers
Added editor rulers
2018-03-22 22:54:55 +09:00
Junyoung Choi
e796e00963 Merge branch 'master' into add-editor-rulers 2018-03-22 22:30:03 +09:00
Junyoung Choi (Sai)
10136df977 Merge pull request #1713 from frankkanis/master
Update text for internationalization
2018-03-22 22:09:15 +09:00
Junyoung Choi (Sai)
44ec107ce8 Merge pull request #1715 from BoostIO/sanitize-options
Make sanitization configurable
2018-03-22 21:57:37 +09:00
Junyoung Choi (Sai)
c7fb5b0475 Merge pull request #1718 from rayou/fix-1705-wrap-long-latex
Wrap long LaTeX formula in Preview
2018-03-22 21:57:21 +09:00
Junyoung Choi (Sai)
785735ccf5 Merge pull request #1721 from BoostIO/fix-quit
Always quit app when all window closed
2018-03-22 21:54:13 +09:00
Junyoung Choi (Sai)
f0736ccf8d Merge pull request #1720 from bimlas/i18n-hu-hungarian
i18n: Add Hungarian (HU) translations
2018-03-22 21:50:23 +09:00
Junyoung Choi
a24a7e03be Quit app when all window closed 2018-03-22 20:59:19 +09:00
bimlas
be235e5204 i18n: Add HU to locales 2018-03-22 12:35:42 +01:00
bimlas
57ec598672 i18n: Add Hungarian (HU) translations 2018-03-22 12:35:42 +01:00
Yu-Hung Ou
2627c09cda added new local strings to all locals 2018-03-22 21:44:26 +11:00
Yu-Hung Ou
36e63bb8a9 wrap long LaTeX formula in Preview 2018-03-22 21:33:32 +11:00
Junyoung Choi
5ea24f650b Make sanitization configurable 2018-03-22 12:01:16 +09:00
Junyoung Choi (Sai)
8c792ce7a1 Merge pull request #1714 from BoostIO/fix-lint-errors
Fix lint errors
2018-03-22 11:10:52 +09:00
Frank Kanis
055969f5c6 Update text for internationalization 2018-03-21 22:17:34 +01:00
Yu-Hung Ou
bdb9349b52 updated en local 2018-03-21 22:32:05 +11:00
Yu-Hung Ou
8b11b57ec5 allow users to enable/disable editor rulers. default: disable 2018-03-21 22:31:46 +11:00
Yu-Hung Ou
29888c89ad added CSS style for rulers to make it fit most of the themes 2018-03-21 21:11:26 +11:00
Yu-Hung Ou
281fb2afd3 added CodeMirror ruler support 2018-03-21 21:11:26 +11:00
Yu-Hung Ou
fbb7839f83 added Editor Rulers field in Preferences 2018-03-21 21:11:26 +11:00
Junyoung Choi (Sai)
d3f9c170ac Merge pull request #1703 from Joel-Costamagna/patch-1
typo
2018-03-21 15:28:38 +09:00
Joël COSTAMAGNA
2a784deb4b typo 2018-03-20 11:46:33 -04:00
Junyoung Choi (Sai)
ab8b6d806d Merge pull request #1700 from BoostIO/add-korean
Add korean
2018-03-20 16:36:54 +09:00
Junyoung Choi
6c4206a2dd Fix lint errors 2018-03-20 16:18:18 +09:00
Junyoung Choi
0d61d9cee4 Add missing locale 2018-03-20 15:10:55 +09:00
Junyoung Choi
48bdb9e818 Do not use devMode on production 2018-03-20 15:10:35 +09:00
Junyoung Choi
cfe447a403 Add Korean 2018-03-20 15:04:56 +09:00
Junyoung Choi (Sai)
2c30f0e487 Merge pull request #1684 from kawmra/prevent-fetching-title-in-link-tag
Prevent generating a link tag when pasting URL to inside of a link tag.
2018-03-20 10:32:59 +09:00
Junyoung Choi (Sai)
147211478c Merge pull request #1698 from nlopin/fix_export_folder
Fix filenames with forbidden symbols during export #1613
2018-03-19 23:23:47 +09:00
Junyoung Choi (Sai)
35b7674e69 Merge pull request #1696 from frankkanis/master
Fix Copy Note Link in Context Menu
2018-03-19 22:58:38 +09:00
Junyoung Choi (Sai)
55075b2a96 Merge pull request #1566 from nlopin/new-snippet-tabbar
New snippet tabbar
2018-03-19 21:27:37 +09:00
Nikolay Lopin
6577599959 Replace forbidden filename symbols to _ for image export 2018-03-19 00:33:23 +03:00
Nikolay Lopin
0c4e72e507 Replace forbidden filename symbols to _ 2018-03-19 00:28:36 +03:00
Nikolay Lopin
9bf96e943d Add filenamify dependency 2018-03-19 00:22:07 +03:00
Nikolay Lopin
2811843e70 Merge branch 'master' into new-snippet-tabbar
# Conflicts:
#	browser/main/Detail/SnippetNoteDetail.js
2018-03-18 21:26:12 +03:00
Frank Kanis
842ece2a8a Fix Copy Note Link in Context Menu 2018-03-18 11:14:22 +01:00
Junyoung Choi (Sai)
222de03ea3 Merge pull request #1678 from rayou/add-unit-tests-for-lib-markdown
Added unit tests for lib/markdown
2018-03-18 15:24:48 +09:00
Junyoung Choi (Sai)
d31ae9f3fc Merge pull request #1692 from romainwn/master
add missing i18n preview word + fr translation
2018-03-18 15:23:55 +09:00
Yu-Hung Ou
1dd7644e12 added smart quotes unit test for lib/markdown 2018-03-18 15:36:57 +11:00
Romain Le Quellec
2cee54f70a fix missing import 2018-03-17 10:23:44 +01:00
Romain Le Quellec
a8431fae96 add missing i18n preview word + fr translation 2018-03-17 10:16:51 +01:00
Yu-Hung Ou
03d11b7b58 Merge remote-tracking branch 'upstream/master' into add-unit-tests-for-lib-markdown 2018-03-17 18:54:18 +11:00
Junyoung Choi (Sai)
316e2eeefb Merge pull request #1640 from SiSchae/localization
Localization
2018-03-17 11:13:52 +09:00
Junyoung Choi (Sai)
c21e19337a Merge pull request #1691 from rayou/fix-smart-quotes-post-to-blog
Fixed smart quotes when posting a note to blog
2018-03-17 01:04:50 +09:00
Yu-Hung Ou
b9cab0dae8 Init markdown lib only when it's needed when posting a note to blog 2018-03-16 23:33:40 +11:00
Yu-Hung Ou
33b3299ca2 changed the default options of markdown lib to follow config 2018-03-16 23:32:18 +11:00
Yu-Hung Ou
00ba38beba reload config every time when markdown lib initiated 2018-03-16 23:30:57 +11:00
Yu-Hung Ou
433fce286e added electron as global variable to pass eslint check 2018-03-16 23:11:43 +11:00
kawmra
b36322bba4 Prevent generating a link tag when pasting URL to inside of a link tag. 2018-03-16 00:11:47 +09:00
Yu-Hung Ou
c147e0a789 mocked electron in ava unit tests 2018-03-15 22:14:21 +11:00
Yu-Hung Ou
8bf5d02624 Revert "updated lib/consts to access electron remote.app only when in production mode"
This reverts commit bbcd674516.
2018-03-15 22:12:22 +11:00
Simon
35616c1ccd Merge pull request #1 from nlopin/patch-1
Add russian translation
2018-03-15 09:47:09 +01:00
Nikolai Lopin
7c8939ecb8 Add russian translation 2018-03-15 02:23:15 +03:00
Simon
ccb0302d3f Resolved Merge issue; Added Russian; 2018-03-14 13:13:47 +01:00
Simon
0cfd048013 Merge branch 'master' into localization 2018-03-14 12:49:40 +01:00
Yu-Hung Ou
f72b4f0249 added unit test for checkbox markdown syntax 2018-03-14 21:47:07 +11:00
Yu-Hung Ou
b5cb209f14 added browser testing support to ava 2018-03-14 21:46:20 +11:00
Yu-Hung Ou
1af374439d added unit tests for lib/markdown 2018-03-14 20:37:25 +11:00
Yu-Hung Ou
bbcd674516 updated lib/consts to access electron remote.app only when in production mode 2018-03-14 20:37:25 +11:00
Yu-Hung Ou
3cba71b7a8 upgraded ava to 0.25.0 2018-03-14 20:37:25 +11:00
Yu-Hung Ou
6a1e9c5818 removed katex script tag from main.html, use import instead 2018-03-14 20:37:25 +11:00
Junyoung Choi (Sai)
ede41d01b4 Merge pull request #1676 from lijinglue/hotfix/markdown-render-in-blog-post
hotfix: update markdown render in blog post feature
2018-03-14 13:56:18 +09:00
Junyoung Choi (Sai)
826a67b550 Merge pull request #1677 from BoostIO/allow-more
Allow b tag and style attribute
2018-03-14 13:09:38 +09:00
Junyoung Choi
f3d59a9b61 Allow b tag and style attribute 2018-03-14 13:03:57 +09:00
lijinglue
65434453b8 hotfix: update markdown render in blog post feature 2018-03-14 00:30:29 +08:00
Junyoung Choi
b8658abbad v0.11.2 2018-03-13 23:12:32 +09:00
Junyoung Choi (Sai)
0387b33f6c Merge pull request #1673 from BoostIO/update-whitelist
Use same settings of github
2018-03-13 23:02:52 +09:00
Junyoung Choi (Sai)
41042fe64e Merge pull request #1664 from mirsch/fix-copy-note-link-from-list
fix "copy note link from note list" link format
2018-03-13 23:02:44 +09:00
Junyoung Choi (Sai)
1efc3bb15d Merge pull request #1663 from BoostIO/note-list-ui
Fix note list UI
2018-03-13 22:45:51 +09:00
Junyoung Choi
0816a9410b Use same settings of github 2018-03-13 22:41:17 +09:00
Junyoung Choi (Sai)
cb823eaaa6 Merge pull request #1670 from rayou/fix-1661-move-folder-cursor-style
Added draggable folder handle #1661
2018-03-13 22:04:53 +09:00
Junyoung Choi (Sai)
44f027bf18 Merge pull request #1671 from BoostIO/more-whitelist
Add table and details stuff to whitelist
2018-03-13 22:02:06 +09:00
Junyoung Choi
571d159a1f Add table and details stuff to whitelist 2018-03-13 22:00:07 +09:00
Yu-Hung Ou
a30d977727 added draggable folder handle 2018-03-13 23:42:14 +11:00
Simon
847ad2d781 Merge branch 'master' into localization 2018-03-13 09:24:53 +01:00
mirsch
da9067a672 fix "copy note link from note list" introduced in #1583 to reflect changes from #1636 2018-03-12 15:23:37 +01:00
Kazu Yokomizo
5ed5950ed5 Fix note list UI 2018-03-12 22:14:06 +09:00
Junyoung Choi
f53c182cfb Fix version 2018-03-12 18:50:22 +09:00
Junyoung Choi
bb2978b370 v0.11.1 2018-03-12 17:53:16 +09:00
Junyoung Choi (Sai)
66ae4a5163 Merge pull request #1660 from JKetelaar/bugfix/grammar-change
Fixed English typo & grammar
2018-03-12 17:44:40 +09:00
Jeroen Ketelaar
f435595ad8 Fixed English typo & grammar 2018-03-12 09:42:44 +01:00
Junyoung Choi (Sai)
4bec4596fe Merge pull request #1659 from BoostIO/fix-checkbox
Allow checkbox
2018-03-12 17:41:29 +09:00
Junyoung Choi
10b86aec49 Allow checkbox 2018-03-12 17:40:27 +09:00
Simon
ecabd37cbb Setting default language in DEFAULT_CONFIG 2018-03-12 07:26:19 +01:00
Junyoung Choi (Sai)
f3cabfcb4d Merge pull request #1656 from BoostIO/feature-v0-11-0
v0.11.0
2018-03-12 11:45:12 +09:00
Junyoung Choi
46fc010cf9 v0.11.0 2018-03-12 11:34:17 +09:00
Junyoung Choi (Sai)
e370c76521 Merge pull request #1655 from rayou/fix-1609-missing-classname
fixed missing classname in lib/markdown highlight render function
2018-03-12 11:32:18 +09:00
Junyoung Choi (Sai)
1c283f88d0 Merge pull request #1636 from mirsch/remove-volatile-hash-from-note-links
remove volatile hash from note links - #1623
2018-03-12 11:26:41 +09:00
Yu-Hung Ou
cfffa1b10a fixed missing classname in lib/markdown highlight render function 2018-03-11 16:22:04 +11:00
Junyoung Choi (Sai)
8aaae4de49 Merge pull request #1650 from Paalon/katex_update
Update KaTeX Library from 0.8.3 to 0.9.0.
2018-03-10 18:43:18 +09:00
Simon
4f6c35713e Added languages; better German translation 2018-03-09 17:30:11 +01:00
paalon
8fb0b5b572 Update KaTeX Library from 0.8.3 to 0.9.0. 2018-03-10 00:36:00 +09:00
Junyoung Choi (Sai)
12262671ee Merge pull request #1649 from rayou/fix-1562-change-dummy-data-minimum-count
Changed the minimum count of dummy data (folders and notes)
2018-03-09 23:51:39 +09:00
Junyoung Choi (Sai)
3165c62985 Merge pull request #1621 from rayou/feature/add-smartquotes-toggle
Added support for toggling smart quotes in preview
2018-03-09 23:37:42 +09:00
Junyoung Choi
1ca1166f74 Merge branch 'master' into feature/add-smartquotes-toggle 2018-03-09 23:30:54 +09:00
Junyoung Choi (Sai)
7b4d2f3b97 Merge pull request #1617 from pfftdammitchris/folder-dragging
Folders in the side nav are now draggable
2018-03-09 23:17:41 +09:00
Junyoung Choi (Sai)
ccce75a047 Merge pull request #1616 from pfftdammitchris/master
Sort tags alphabetically
2018-03-09 21:33:38 +09:00
Yu-Hung Ou
410e82e375 increased the minimum count of dummy notes to 2 2018-03-09 23:28:01 +11:00
Yu-Hung Ou
bd385ec062 increased the minimum count of dummy folders to 2 2018-03-09 23:27:49 +11:00
Junyoung Choi (Sai)
638227ef25 Merge pull request #1614 from RomanKlopsch/master
Fix missing progress bar in note list
2018-03-09 20:51:34 +09:00
Junyoung Choi (Sai)
e70b35126b Merge pull request #1648 from rayou/fix-1609-wrong-codeblock-style
fixed code block style in exported HTML file
2018-03-09 20:50:22 +09:00
Simon
4d41c7f37f Changed default language to en 2018-03-09 07:39:08 +01:00
Yu-Hung Ou
d01a7b16a1 fixed codeblock style in exported HTML file 2018-03-08 23:40:13 +11:00
Kazz Yokomizo
326d873279 Merge pull request #1646 from BoostIO/update-readme
Update-readme
2018-03-08 11:38:19 +09:00
Kazz Yokomizo
74c32bed29 Update-readme 2018-03-08 11:35:41 +09:00
Nikolay Lopin
a222fd0786 Add animation for scrolling with buttons 2018-03-08 01:02:49 +03:00
Yu-Hung Ou
c633ba9497 Merge branch 'master' into feature/add-smartquotes-toggle 2018-03-07 22:25:39 +11:00
Simon
40b5472866 Adjustments for Travis CI 2018-03-07 08:37:55 +01:00
Simon
a413e273ca Added strings in notifications 2018-03-07 08:20:05 +01:00
Simon
0f82085cae Adjustments for Travis CI 2018-03-07 07:10:49 +01:00
Junyoung Choi (Sai)
fa1ab04409 Merge pull request #1643 from mirsch/export-responsive-html
Export responsive html, fixes #1442
2018-03-07 10:16:14 +09:00
mirsch
f370508c93 fixes #1442, Export responsive html 2018-03-07 01:08:10 +01:00
Nikolay Lopin
7472019422 Change property responsible for scrolling from "offsetLeft" to "scrollLeft" 2018-03-07 01:55:04 +03:00
Simon
aa3597881a added more strings 2018-03-06 15:56:48 +01:00
Simon
a36841e501 Implemented language switch in Interface Config Tab 2018-03-06 15:04:04 +01:00
Simon
fe9afc8952 extracted all strings from html lines 2018-03-06 14:07:04 +01:00
Simon
63d39a81aa Merge branch 'localization' of github.com:SiSchae/Boostnote into localization
hoffe es läuft
2018-03-06 10:02:29 +01:00
Simon
b11dc2ca20 first testing of translation 2018-03-06 09:54:37 +01:00
Junyoung Choi (Sai)
10f5ad6127 Merge pull request #1639 from thedavidgay/add-charset-to-html-export
add UTF-8 tag to HTML export (fixes #1624)
2018-03-06 17:30:04 +09:00
David Gay
4800a88cf6 add UTF-8 tag to HTML export (fixes #1624) 2018-03-05 21:22:42 -05:00
Junyoung Choi (Sai)
9d8bc40594 Merge pull request #1634 from Redsandro/xssSecurity
SECURITY HOTFIX - Remove XSS attack
2018-03-06 03:08:19 +09:00
Junyoung Choi (Sai)
7d3d96a6e0 Merge pull request #1612 from kawmra/fix-title-decoding
Properly decoding for the title of pasted website
2018-03-05 11:36:53 +09:00
Junyoung Choi
4c99429d76 Merge branch 'master' into fix-title-decoding 2018-03-05 11:36:31 +09:00
Junyoung Choi (Sai)
96fbd7572c Merge pull request #1611 from romainwn/feat/addFullScreen
add Toggle FullScreen feat + shortcut
2018-03-05 11:28:28 +09:00
Junyoung Choi (Sai)
b012bba6d6 Merge pull request #1601 from rayou/fix-missing-zoom-button
fixed missing zoom button - #1598
2018-03-05 11:27:27 +09:00
Junyoung Choi (Sai)
45d90b2530 Merge pull request #1594 from clone1612/list_fix
Fix insertion in Markdown lists - #1588 & 1605
2018-03-05 11:25:46 +09:00
Junyoung Choi (Sai)
a8b946a07a Merge pull request #1593 from Redsandro/searchFixes
Add search tweaks; closes 1525 closes 1167
2018-03-05 11:23:48 +09:00
Junyoung Choi (Sai)
18392c4f23 Merge pull request #1591 from Redsandro/scrollSyncFix
Fixed bad merge; closes #1586; related to #1521
2018-03-05 11:16:42 +09:00
Junyoung Choi (Sai)
88e7869284 Merge pull request #1584 from pfftdammitchris/codeeditor-snippet-options-overlapping
Fixes UI overlapping in the snippet editor (bottom options)
2018-03-05 11:14:26 +09:00
Junyoung Choi (Sai)
3c8332c448 Merge pull request #1583 from pfftdammitchris/copylink-notelist
Adds the ability to copy the note link from the note list
2018-03-05 11:13:00 +09:00
Junyoung Choi
207f95693a Merge branch 'master' into copylink-notelist 2018-03-05 11:12:42 +09:00
Junyoung Choi (Sai)
de15120bf8 Merge pull request #1579 from nlopin/move-pictures-between-storages
Move images between storages together with note
2018-03-05 11:06:32 +09:00
Junyoung Choi (Sai)
1dcf11e1c4 Merge pull request #1575 from lurong-hkg/feature/publish-to-wordpress
allow publishing markdown to wordpress
2018-03-05 10:55:05 +09:00
mirsch
b74ba22c44 adjust keygen to use uuid only for notes (uuid on storage/folders woud need more refactoring) 2018-03-05 00:02:30 +01:00
mirsch
fa2d34dcfc fix delete and empty trash 2018-03-04 23:28:18 +01:00
mirsch
0280a5f09e use uuid in keygen, remove storage.key from note hash 2018-03-04 22:21:14 +01:00
Nikolay Lopin
a35f876f4f Merge branch 'master' into new-snippet-tabbar 2018-03-05 00:06:57 +03:00
Sander Steenhuis
c22b7f81c1 Allow iframe size, allow fullscreen 2018-03-04 17:49:17 +01:00
Sander Steenhuis
9344fd78d8 Remove xss attack; closes #1443 at least partially 2018-03-04 17:28:41 +01:00
Yu-Hung Ou
e89c0a3e61 added support for toggling smart quotes in preview 2018-03-02 00:06:58 +11:00
pfftdammitchris
5aa7ef5738 Folders in the side nav are now draggable 2018-02-28 18:33:14 -08:00
pfftdammitchris
88ac6c6fc6 Sort tags alphabetically 2018-02-28 16:34:42 -08:00
Roman Klopsch
a537f9762b Fix missing progress bar in note list 2018-02-28 16:00:42 +01:00
Romain Le Quellec
aab192223a add Toggle FullScreen feat + shortcut 2018-02-28 11:39:45 +01:00
kawmra
6439810d03 Decode the fetched body correctly if content-type was provided 2018-02-28 02:09:35 +09:00
Yu-Hung Ou
c5c78763d1 fixed missing zoom button 2018-02-27 23:17:15 +11:00
Jannick Hemelhof
012908e32d Linting fixes 2018-02-26 16:43:13 +01:00
Jannick Hemelhof
1f1e545538 Fix for list insertion 2018-02-26 16:32:47 +01:00
Sander Steenhuis
5b8519b2b8 Bind up/down to prev/next event; closes #1167 2018-02-26 16:30:14 +01:00
Sander Steenhuis
6502158716 Add search clear options; closes #1525 2018-02-26 16:30:08 +01:00
lurong
7c525ab7a6 fix lint 2018-02-26 21:27:42 +08:00
Lu Rong
778e3d9799 Merge pull request #1 from lijinglue/feature/publish-to-wordpress
checking if return is valid before creating the blog attr in note
2018-02-26 21:21:15 +08:00
lijinglue
e5f986a959 checking if return is valid before creating the blog attr in note 2018-02-26 21:19:55 +08:00
Sander Steenhuis
d76ecd5423 Fixed bad merge; closes #1586; related to #1521 2018-02-26 13:20:24 +01:00
Markus Deuerlein
6bcb6398f8 German translation: minor corrections (#1585)
* minor corrections

some german improvements and corrections of the translation

* update link to docs/de/debug.md

* minor corrections

* minor corrections

* minor corrections

* minor corrections

* minor corrections
2018-02-26 21:08:03 +09:00
pfftdammitchris
6c341d8fa5 Fixes UI overlapping in the snippet editor (bottom options) 2018-02-25 07:41:50 -08:00
pfftdammitchris
0d5e57eeab Adding the ability to copy the note link from the note list 2018-02-25 07:17:01 -08:00
lurong
aef603ed8c rebase and prefer const in node list 2018-02-25 22:50:51 +08:00
lurong
97600e526b remove the first occurrence of title in content when publishing to WP 2018-02-25 22:44:57 +08:00
lurong
f3f6095d81 allow publishing markdown to wordpress 2018-02-25 22:44:57 +08:00
Junyoung Choi (Sai)
fb7280127c Merge pull request #1581 from BoostIO/v0.10.0
v0.10.0
2018-02-25 15:51:59 +09:00
Junyoung Choi
60df509fc6 v0.10.0 2018-02-25 14:30:57 +09:00
Junyoung Choi (Sai)
ced237fbda Merge pull request #1556 from pfftdammitchris/all-notes-storage-labels
Add storage labels to 'All Notes' and figure a better solution to the dull storage notes list view
2018-02-25 14:27:35 +09:00
Nikolay Lopin
9473a26892 Move images between storages together with note
1. Added new step of moving note's images to `moveNote` function
2. Changed behaviour of sidebar navigation after move finished

Issue #1578
2018-02-25 02:47:28 +03:00
Nikolay Lopin
e5825e898d allow to copy images without generating new name 2018-02-25 02:45:59 +03:00
Nikolay Lopin
8d59bd9f71 Remove redundant symbol from regexp 2018-02-25 02:42:15 +03:00
pfftdammitchris
8f90bb83e7 Added title attributes to support long folder names and foldernames are no longer overflowing nearby elements 2018-02-24 08:42:02 -08:00
pfftdammitchris
ba888a7165 Moved viewType to outer scope so it invokes once (to save resources) 2018-02-24 07:10:46 -08:00
Christopher Tran
9591a7cac2 Merge branch 'master' into all-notes-storage-labels 2018-02-24 06:40:25 -08:00
Junyoung Choi (Sai)
e8fd53d8b7 Merge pull request #1567 from pfftdammitchris/fix-toggle-view-css-positioning
Fixed ToggleMode button overlapping CSS issue
2018-02-24 16:44:41 +09:00
Junyoung Choi (Sai)
3b7c36b1c9 Merge pull request #1559 from pfftdammitchris/tags-note-count
Added note counts to tags view in side nav
2018-02-24 16:19:53 +09:00
Junyoung Choi (Sai)
2750a3ef30 Merge pull request #1558 from pfftdammitchris/empty-trash
Added option Empty Trash
2018-02-24 16:18:43 +09:00
Junyoung Choi (Sai)
7d59f51244 Merge pull request #1549 from nlopin/restore-note-enhancing
Restore note UX enhancements
2018-02-24 16:05:00 +09:00
Nikolay Lopin
a3ec6f470a Add horizontal scroll for touch devices 2018-02-24 01:03:25 +03:00
Nikolay Lopin
8e85a33dac Show Arrows at snippets tab bar only when needed 2018-02-24 00:49:21 +03:00
Nikolay Lopin
77542597f5 onClick listener fix 2018-02-23 10:26:48 +03:00
pfftdammitchris
2a26e8a010 Added folder labels to note items when viewing a specific storage 2018-02-22 17:31:31 -08:00
pfftdammitchris
8b4c1a6c38 Merge branch 'master' of https://github.com/BoostIO/Boostnote into all-notes-storage-labels 2018-02-22 16:16:43 -08:00
Junyoung Choi (Sai)
7d7e277cc6 Merge pull request #1533 from forestail/display-filename-in-codeblock
Display filename in codeblock (resend)
2018-02-22 11:06:20 +09:00
Junyoung Choi (Sai)
cb1da609a4 Merge pull request #1524 from Redsandro/highlightSearch
Highlight global search matches on code editor
2018-02-22 11:06:06 +09:00
Junyoung Choi
e4fbdb35a3 Rename a variable name again 2018-02-22 10:54:12 +09:00
Junyoung Choi
c3586372e0 Rename variables 2018-02-22 10:49:59 +09:00
Junyoung Choi
808d193543 Merge remote-tracking branch 'origin/master' into highlightSearch 2018-02-22 09:56:09 +09:00
Junyoung Choi (Sai)
0b65b70af2 Merge pull request #1570 from BoostIO/fix-memory-lick
Fix memory lickage
2018-02-22 09:43:43 +09:00
Junyoung Choi
deab34abd6 Fix memory lick 2018-02-22 09:39:56 +09:00
Junyoung Choi (Sai)
646cded650 Merge pull request #1521 from Redsandro/scrollsync
Sync Split Editor scroll position
2018-02-22 07:36:41 +09:00
Sander Steenhuis
9047320301 Update CodeEditor.js - Forgot curly brace
Forgot curly brace when merging master
2018-02-21 19:17:38 +01:00
Sander Steenhuis
f16b054f01 Merge branch 'master' into scrollsync 2018-02-21 19:04:24 +01:00
Junyoung Choi (Sai)
fea856202d Merge pull request #1509 from Antogin/feat/display-url-title
Feat/display url title
2018-02-21 16:33:34 +09:00
pfftdammitchris
4f15cc3f08 Fixed ToggleMode button overlapping CSS issue
The absolute positioning of the toggle mode button was creating a static overlapping position issue with the top bar. This fix solves that problem by removing the static positioning and coupling the button component with the buttons to the right
2018-02-20 21:09:43 -08:00
Nikolay Lopin
1769dd959e Revert spaces 2018-02-21 03:21:40 +03:00
Nikolay Lopin
1253b81a01 Introduce tabs with minimal width 2018-02-21 03:08:40 +03:00
Sander Steenhuis
3b524f6aba Highlight global search matches on code editor 2018-02-20 22:46:13 +01:00
Sander Steenhuis
051ccad29a Rename variables 2018-02-20 22:36:38 +01:00
Sander Steenhuis
c82eba05d1 Sync Split Editor scroll position 2018-02-20 22:31:53 +01:00
pfftdammitchris
74af199afc Added note counts to tags view in side nav 2018-02-19 13:08:09 -08:00
pfftdammitchris
c2afdba659 Removed useless url route check 2018-02-19 12:41:36 -08:00
pfftdammitchris
e6ae45f133 Added option Empty Trash 2018-02-19 12:27:04 -08:00
pfftdammitchris
129ef6766b Add storage labels to 'All Notes'
Added storage labelings to 'All Notes' in Default and Compressed views
2018-02-19 10:17:36 -08:00
Masahide Morio
3194a7b1a2 followed eslintrc 2018-02-19 23:02:25 +09:00
Kazz Yokomizo
46a733ba5b Merge pull request #1543 from nlopin/edit-on-dblckick
Switch from preview to edit by double click
2018-02-19 15:56:22 +09:00
Kazz Yokomizo
466844fc55 Merge pull request #1528 from Redsandro/selectionOnSearch
UX: Keep selection synced with note visibility
2018-02-19 15:07:25 +09:00
Kazz Yokomizo
0cc793e3fe Merge pull request #1554 from BoostIO/change-blog-url
Change the blog link to Boostlog from Medium
2018-02-19 15:05:00 +09:00
Kazu Yokomizo
0fdfb385a4 Change blog to Boostlog from Medium 2018-02-19 15:01:08 +09:00
Nikolay Lopin
419a4c6b2d Add "Restore note" item to menu in Trash view. 2018-02-17 00:38:19 +03:00
Nikolay Lopin
55e9441547 Introduce RestoreButton component instead of plain JSX 2018-02-17 00:37:36 +03:00
Kazz Yokomizo
7abff6ded4 Merge pull request #1527 from Redsandro/dont_count_trashed_notes
Trashed notes should not be counted
2018-02-15 11:49:20 +09:00
Kazz Yokomizo
a648d310a5 Merge pull request #1539 from pfftdammitchris/master
Added title attributes to elements to show information on hover --- more like a native app feel to it
2018-02-15 11:40:26 +09:00
Kazz Yokomizo
4b56fa56b5 Merge pull request #1544 from nlopin/global-pin-to-top
Show pinned notes in All notes view and allow to pin them there
2018-02-15 11:33:44 +09:00
Nikolay Lopin
18bf700936 Show pinned notes in All notes view and allow to pin them there
All restrictions of pin functionality were removed. Now user can see pinned notes and also pin notes right from 'home'. Technically, a note still pins to the storage it belong.

#1506
2018-02-14 23:35:11 +03:00
Nikolay Lopin
67ddff736c Switch from preview to edit by double click
User want to click markdown to work with text (for example copy).
If they have "Switch to Preview" setting in "When Editor Blurred",
they enter edit mode after click. To fix that issue I introduced new
value to the "Switch to Preview" setting that do that thing. User can
enter edit mode by double click on the editor and leave it on blur.

Unfortunately, it's impossible to switch both ways by double click
because the editor mode is listening for double click event.

#1523
2018-02-13 23:43:11 +03:00
Kazz Yokomizo
1bba125a2a Merge pull request #1542 from BoostIO/change-company-name
Change the company name
2018-02-13 13:44:18 +09:00
Kazu Yokomizo
187acad592 Change the company name 2018-02-13 13:37:04 +09:00
Sander Steenhuis
5e07c7b3e1 Update selection on new note; closes #1507 2018-02-12 18:15:05 +01:00
pfftdammitchris
cd942cf8b6 Added title attributes to elements to show quick information on each element---like a native app 2018-02-11 19:12:28 -08:00
Masahide Morio
c43589fe6a Add file name and any first line number in code block (fixed) 2018-02-11 02:28:03 +09:00
Junyoung Choi (Sai)
d4594eff3b Merge pull request #1502 from Matts966/feature/new_line_completion
I fixed the behavior of the editor when the enter key is pushed.
2018-02-10 22:53:29 +09:00
Junyoung Choi (Sai)
809a0e846b Merge pull request #1531 from nlopin/export-note-with-images
Remove direct css styling todo lists from MarkdownPreview component
2018-02-10 22:39:10 +09:00
Nikolay Lopin
7f00ce2598 Merge remote-tracking branch 'origin/export-note-with-images' into export-note-with-images 2018-02-10 14:05:34 +03:00
Nikolay Lopin
d7d77dbfe9 Fix wrong styling of todo items in exported HTML
The issue happened because styles connected with todo list were applied directly to HTML in Preview component. I added class to `li` tag of each todo item and style it from css.
2018-02-10 14:03:02 +03:00
Junyoung Choi (Sai)
7a124c74cc Merge pull request #1483 from andyklimczak/delete-note
Fix permanently deleting note
2018-02-10 18:47:41 +09:00
Junyoung Choi (Sai)
f8549f4643 Merge pull request #1501 from Matts966/typo/fix_a_typo_of_log
very easy fix.
2018-02-10 18:47:22 +09:00
Junyoung Choi (Sai)
0476ff70da Merge pull request #1306 from nlopin/export-note-with-images
Export note with local images
2018-02-10 18:45:53 +09:00
Junyoung Choi
5ec541c3c1 Manipulate left margin of task item to hide circle 2018-02-10 18:29:57 +09:00
Sander Steenhuis
7b920348f3 UX: Keep selection synced with note visibility 2018-02-10 01:30:14 +01:00
Sander Steenhuis
51b1ef41a1 Trashed notes should not be counted 2018-02-09 23:10:40 +01:00
Nikolay Lopin
12447effc9 Reject promise if write to file failed 2018-02-06 22:19:06 +03:00
Georges Indrianjafy
dc1b059a9d clean up 2018-02-06 19:12:25 +02:00
Georges Indrianjafy
a676ebf46c fix(editor): replace [] with <> 2018-02-06 18:35:11 +02:00
Nikolay Lopin
338f9fb5a9 Semicolon fix 2018-02-05 13:04:01 +03:00
Nikolay Lopin
98c9132d4a Merge branch 'master' into export-note-with-images
# Conflicts:
#	browser/components/MarkdownPreview.js
2018-02-05 12:59:45 +03:00
Georges Indrianjafy
a054f12492 Merge branch 'master' into feat/display-url-title
# Conflicts:
#	browser/components/CodeEditor.js
#	browser/finder/NoteDetail.js
2018-02-05 11:50:14 +02:00
Georges Indrianjafy
c42ac1df1b chore(editor): resolve lint issues 2018-02-05 11:00:56 +02:00
Georges Indrianjafy
d9d46cda1c feat(editor): extract pasting url method 2018-02-05 10:59:36 +02:00
Nikolay Lopin
f678a17505 Export images together with document 2018-02-05 02:08:33 +03:00
Nikolay Lopin
3da4bb69ce Export images together with document 2018-02-05 02:08:09 +03:00
Georges Indrianjafy
3e2b876c59 fix(config): refrase 2018-02-04 17:49:23 +02:00
Georges Indrianjafy
10daf2599d chore: fix lint isues 2018-02-04 17:39:32 +02:00
Georges Indrianjafy
56d6eb7077 feat(config): add ablility to opt out of fetch url 2018-02-04 13:58:02 +02:00
Georges Indrianjafy
9b54272f8e feat(code-editor): fetch title when pasting url 2018-02-04 13:57:00 +02:00
松井誠泰
685e003db8 delete log. 2018-02-04 10:12:55 +09:00
松井誠泰
701fc0bc80 I fixed the behavior of the editor.
On enter key pushed, if already the line contains a token in markdown list, no completion is needed, I think.

By this change, node_modules/codemirror/addon/edit/continueList.js could be trashed, but I can't because of the gitignore.
2018-02-04 10:03:50 +09:00
松井誠泰
e5518ac0fa very easy fix. 2018-02-04 09:14:31 +09:00
Junyoung Choi
8e1bf48cd1 Close immediately on windows 2018-02-04 00:59:54 +09:00
Junyoung Choi
8dd82e1a3b Disable uglify 2018-02-04 00:12:38 +09:00
Junyoung Choi
4418bfe965 v0.9.0 2018-02-03 23:50:49 +09:00
Junyoung Choi
39c4d710bc Remove unnecessary logging 2018-02-03 23:45:46 +09:00
Junyoung Choi (Sai)
51a8c47afd Discard finder (#1497)
* Discard finder

* Upgrade electron

* Discard anything related with finder

* Fix lint errors

* Run test serial

* Test on v6

* Test on v6 only
2018-02-03 23:39:53 +09:00
Kazz Yokomizo
922570bb5c Merge pull request #1495 from BoostIO/fix-todo-style
Fix percentage bar in markdown preview
2018-02-03 16:32:37 +09:00
Kazu Yokomizo
ca282d5635 Fix percentage bar in markdown preview 2018-02-03 16:32:02 +09:00
Junyoung Choi
ff67043210 Bump version 2018-02-03 15:52:51 +09:00
Junyoung Choi
31da231c1c Rollback .travis.yml 2018-02-03 15:28:19 +09:00
Junyoung Choi
eb698a7430 Fix lint error 2018-02-03 15:10:16 +09:00
Junyoung Choi
03be809ba9 Text stable version only
The current version of boostnote doesn't support the latest stable version, v9.x
2018-02-03 15:03:33 +09:00
Junyoung Choi
69601bf15a Fix but: line numbers of editor isnt applied properly 2018-02-03 15:02:26 +09:00
Junyoung Choi
7cad3d403b Discard unnecessary logging 2018-02-03 15:00:06 +09:00
Junyoung Choi (Sai)
cc84af3346 Merge pull request #1458 from cyalins/cyalins-patch-1
Improved overall wording and grammar of preferences
2018-02-03 14:27:55 +09:00
Junyoung Choi
33ef54a162 Fix lint errors 2018-02-03 14:16:26 +09:00
Junyoung Choi (Sai)
ac43ff886a Merge pull request #1494 from Paalon/katex_update
Update KaTeX library from 0.7.1 to 0.8.3
2018-02-03 13:57:30 +09:00
Junyoung Choi (Sai)
a64f73ca0c Merge pull request #879 from xxdavid/feature-editor-line-lines
Make line numbers in the editor optional
2018-02-03 13:57:00 +09:00
Paalon
314477d2fc Update yarn.lock 2018-02-02 17:01:15 +09:00
Paalon
c63fc93daa Update katex from 0.7.1 to 0.8.3. 2018-02-02 16:45:33 +09:00
Andy Klimczak
f04ad1e702 list:moved -> list:next 2018-01-31 22:17:36 -05:00
Kazz Yokomizo
2183c4bda6 Merge pull request #1480 from allejo/feature-preview-scroll
Allow preview area to scroll past end of file
2018-01-31 18:01:32 +09:00
Kazz Yokomizo
6ba91c1515 Merge pull request #1472 from Antogin/feat/add-ability-to-clone-note
Feat/add ability to clone note
2018-01-31 17:55:46 +09:00
Andy Klimczak
dc03bb76e6 Fix 2018-01-30 23:15:16 -05:00
Vladimir Jimenez
d0559c16b5 Pass correct preview prop to MarkdownEditor 2018-01-30 07:42:16 -08:00
Kazz Yokomizo
eb693f7b48 Merge pull request #1481 from andyklimczak/1476-tag-check
Remove leading # when creating tag
2018-01-30 21:50:12 +09:00
Georges Indrianjafy
ef5639ff4b chore: removing space 2018-01-30 11:56:59 +02:00
Georges Indrianjafy
34a335797c chore: remove trailing space 2018-01-30 11:49:39 +02:00
Georges Indrianjafy
edda3a4d23 chore: cleanup 2018-01-30 11:45:47 +02:00
Georges Indrianjafy
184839423f fix(NoteList): remove router 2018-01-30 10:37:58 +02:00
Kazz Yokomizo
c0f3600a52 Merge pull request #1484 from BoostIO/fix-#1477
Fix #1477
2018-01-30 01:00:13 -05:00
Kazu Yokomizo
2b1302aa07 Fix #1477 2018-01-30 14:38:53 +09:00
Andy Klimczak
d894bb4aab Fix permanently deleting note
- Permanently deleting note would not remove note from list until after
refresh
- Fix so permanently deleted note is removed list
2018-01-29 21:51:57 -05:00
Andy Klimczak
470c071344 Remove leading # when creating tag
- Remove leading # from tags that are created with leading #
- Convenience method for users who tend to type them, but did not want a
tag with double #
- If a user wants a tag with a leading #, then can double it (ie: ##OfPeople)
2018-01-29 20:42:59 -05:00
Vladimir Jimenez
4bd639c6c4 Allow preview area to scroll past end of file 2018-01-29 17:11:42 -08:00
Georges Indrianjafy
4cb7e63421 chore: clean up 2018-01-29 09:25:01 +02:00
Georges Indrianjafy
9f14a503d8 fix(history): use hashHistory 2018-01-29 09:24:12 +02:00
Georges Indrianjafy
d5da6de86c fix(NoteList): fix router issue 2018-01-29 09:13:09 +02:00
Georges Indrianjafy
4c2b233722 fix: resolve ci issues 2018-01-27 17:45:50 +02:00
Georges Indrianjafy
ca5b1eea13 chore: removing console.log 2018-01-27 17:33:22 +02:00
Georges Indrianjafy
614e9b6d55 feat(NoteList): add ability to clone note 2018-01-27 14:16:29 +02:00
cyalins
27b2530b8d Undoing an accidental edit 2018-01-26 21:12:10 +11:00
Kazz Yokomizo
2259167200 Merge pull request #1464 from BoostIO/fix-editor-lock-btn
Fix the editor lock button layout
2018-01-25 13:38:33 -05:00
Kazu Yokomizo
2e05214828 Fix the editor lock button layout 2018-01-25 13:30:36 -05:00
Kazz Yokomizo
cfb996039b Merge pull request #1453 from BoostIO/fix-info-right-layout
WIP - Fix info right buttons layout
2018-01-25 09:37:24 -05:00
Kazz Yokomizo
44c4d56214 Merge pull request #1457 from stevequinn/1238
Allows keyboard text selection in Finder search box
2018-01-25 09:34:59 -05:00
cyalins
8e6be91f7c Improved consistency of wording in Storage tab 2018-01-25 13:57:15 +11:00
cyalins
00816fb2c8 Improved clarity of wording in UI Tab 2018-01-25 13:56:11 +11:00
cyalins
535356b77f Improved clarity of tab titles 2018-01-25 13:53:50 +11:00
cyalins
5e558746ce Reworded Hotkey preferences 2018-01-25 13:52:04 +11:00
cyalins
f7bd52ac0c Improved clarity of crowdfunding message 2018-01-25 13:46:08 +11:00
cyalins
9165f518a9 Improved wording of data collection 2018-01-25 13:41:24 +11:00
Steve Quinn
01605aa221 Allows keyboard text selection in Finder search box 2018-01-25 13:27:43 +11:00
cyalins
8b0b29c424 Improved clarity of wording 2018-01-25 13:27:02 +11:00
Kazu Yokomizo
7a116966fa Add tooltip to full-screen-btn on the snippet note detail 2018-01-24 16:16:14 -05:00
Kazu Yokomizo
e7e8f11a74 Fix tooltips position 2018-01-24 16:08:44 -05:00
Kazu Yokomizo
f235d832d5 Fix note detail layout 2018-01-24 16:03:50 -05:00
Kazu Yokomizo
7730b5e20b Change size of icons on note detail 2018-01-24 15:39:27 -05:00
Kazu Yokomizo
8c3ba4ce48 Fix infopanel 2018-01-24 15:26:45 -05:00
Kazu Yokomizo
e9a126f586 Fix multiple colors 2018-01-24 15:19:13 -05:00
Kazu Yokomizo
097e7d2ff2 Fix tooltip 2018-01-24 15:13:45 -05:00
Kazu Yokomizo
81265f1238 Reorder of buttons 2018-01-23 21:02:19 -05:00
Kazu Yokomizo
2b507e6e20 Zoom button to display none 2018-01-23 20:57:57 -05:00
Kazu Yokomizo
747d3a8f13 Fix note detail width 2018-01-23 20:55:29 -05:00
Kazu Yokomizo
30f6f07434 Fix info right buttons layout 2018-01-23 20:50:48 -05:00
Kazz Yokomizo
6de5488a15 Merge pull request #1451 from BoostIO/update-readme
Update readme
2018-01-23 11:50:32 -05:00
Kazz Yokomizo
5413647166 Update readme 2018-01-23 11:46:22 -05:00
Kazz Yokomizo
e83fe73b18 Merge pull request #1271 from mslourens/utf8-uml
convert uml to utf8 before converting to base64
2018-01-17 09:47:56 -05:00
Kazz Yokomizo
87a289ec65 Merge pull request #1429 from yamash723/fix-markdown-list-link-style
Remove inline-style from markdown link list
2018-01-17 09:33:51 -05:00
yamash723
8a0a118dba Remove inline-style from markdown link list 2018-01-17 16:32:43 +09:00
Kazz Yokomizo
687126ce87 Merge pull request #1419 from mslourens/html-export-encoding
added encoding meta tag
2018-01-16 08:06:30 -05:00
Kazz Yokomizo
8a05d577da Merge pull request #1425 from BoostIO/fixed-save-button
make save button position:fixed
2018-01-16 08:05:08 -05:00
Kazz Yokomizo
4c3ebfc0f8 Merge pull request #1418 from mslourens/import-note-name
name note to imported file
2018-01-16 08:04:25 -05:00
Sosuke Suzuki
6093f25f9a make save button position:fixed 2018-01-16 19:37:01 +09:00
Maurits Lourens
ecab68d676 removed unused imports 2018-01-16 10:09:02 +01:00
Maurits Lourens
1cb4f37c95 moved the import outside the use config section 2018-01-15 22:27:38 +01:00
Maurits Lourens
14318528b9 added encoding meta tag 2018-01-15 22:18:23 +01:00
Maurits Lourens
9c0e1f8f1a name note to imported file 2018-01-15 21:33:17 +01:00
Kazz Yokomizo
2034ce9e4d Merge pull request #1416 from mslourens/copy-note-link
added button for copy note link
2018-01-15 15:17:17 -05:00
Maurits Lourens
657489caf6 added button for copy note link 2018-01-15 21:01:18 +01:00
Kazz Yokomizo
94be3d1fe5 Merge pull request #1413 from BoostIO/fix-folded-layout
Fix the folded layout on side bar
2018-01-15 08:39:32 -05:00
Kazz Yokomizo
f6eae41cee Merge pull request #1412 from BoostIO/fix-1409
Fix the Solarized Dark Styling
2018-01-15 08:34:04 -05:00
Kazu Yokomizo
69c64434e3 Fix the folded layout on side bar 2018-01-15 08:33:23 -05:00
Kazz Yokomizo
256cabfce1 Merge pull request #1411 from BoostIO/change-ctrl-icon-windows
Change the control icon on Windows
2018-01-15 08:26:41 -05:00
Kazu Yokomizo
e8b8272cf9 Fix #1409 - Fix the Solarized Dark Styling 2018-01-15 08:22:02 -05:00
Kazu Yokomizo
bd5ab4881c Change the control icon on Windows 2018-01-15 07:57:36 -05:00
Junyoung Choi (Sai)
9630744bdb Merge pull request #1395 from BoostIO/feature-v0-8-20
v0.8.20
2018-01-13 19:43:49 +09:00
Junyoung Choi
ab3ad0eb97 v0.8.20 2018-01-13 19:01:53 +09:00
Junyoung Choi (Sai)
2393889028 Merge pull request #1394 from BoostIO/fix-export-folder-test
Fix exportFolder error
2018-01-13 18:58:45 +09:00
Junyoung Choi
c36ecb1ed1 Fix exportFolder error
writeFileSync doesn't require any errors
2018-01-13 18:48:34 +09:00
Junyoung Choi (Sai)
3e9b28ff0c Merge pull request #1384 from BoostIO/fix-background-color-bug
fix bug happen on solarized-dark CreateFolderModal.
2018-01-13 18:12:59 +09:00
Junyoung Choi (Sai)
d4eec461a9 Merge pull request #1393 from BoostIO/fix-update
Fix windows update
2018-01-13 18:00:44 +09:00
Junyoung Choi (Sai)
b584f37087 Merge pull request #1392 from BoostIO/cm-close-brackets
Cm close brackets
2018-01-13 18:00:12 +09:00
Junyoung Choi
5c75644db2 Check upate every day rather than every hour 2018-01-13 17:36:33 +09:00
Junyoung Choi
72d9e3e00b Fix update error
This error was caused by using super old version of
electron-winstaller.
2018-01-13 17:35:23 +09:00
Sosuke Suzuki
b3d3362f34 set autoCloseBrackets to cm defaultConfigurations 2018-01-13 15:37:14 +09:00
Sosuke Suzuki
1cbaf55cee import the addon for autoCloseBrackets 2018-01-13 15:10:18 +09:00
Sosuke Suzuki
7771875d57 Change CreateFolderInput style from raw color code to variable 2018-01-13 11:37:47 +09:00
Kazz Yokomizo
85937d8e23 Merge pull request #1324 from mslourens/confirmation-dialog
show delete confirmation dialog
2018-01-12 09:37:05 +09:00
Sosuke Suzuki
1480986a3a change submit button style 2018-01-11 18:34:00 +09:00
Sosuke Suzuki
bc968736df add colors for SolarizedDark 2018-01-11 18:30:57 +09:00
Kazz Yokomizo
ad80b8e8b6 Merge pull request #1379 from BoostIO/destroy-initmodal
remove initmodal
2018-01-10 23:36:35 +09:00
Sosuke Suzuki
d44d220c55 change default storage name from debugging to production 2018-01-10 19:40:04 +09:00
Sosuke Suzuki
f6bcef0789 remove unnecessary variables 2018-01-10 19:36:45 +09:00
Sosuke Suzuki
a28ec752e8 remove InitModalComponent 2018-01-10 19:36:13 +09:00
Sosuke Suzuki
48ea5746d9 move init function to Main.js 2018-01-10 19:33:58 +09:00
Kazz Yokomizo
f473d31970 Merge pull request #1368 from marcusstenbeck/patch-1
fix(code-editor): use correct prop for font family
2018-01-05 19:06:57 +09:00
Marcus Stenbeck
9cd1d4b466 fix(code-editor): use correct prop for font family 2018-01-05 10:54:48 +01:00
Kazz Yokomizo
4f02065eaf Merge pull request #1354 from trinode/feature-elixir-support
Elixir syntax highlighting
2018-01-04 20:31:45 +09:00
Kazz Yokomizo
4b85e3e8fb Merge pull request #1363 from BoostIO/fix-note-disppaer-bug
fix bug when add tag, disappear note.
2018-01-04 19:57:40 +09:00
Sosuke Suzuki
e4e8c2205e splid update functions 2018-01-04 18:12:27 +09:00
Maurits Lourens
18649dd074 fixed lint errors 2018-01-04 08:32:42 +01:00
Maurits Lourens
9f9463f0e8 fixed review comments 2018-01-03 23:08:42 +01:00
Kazz Yokomizo
6cf9bc5de2 Merge pull request #1361 from BoostIO/fix-ci-errror
Fix CI error at #1147
2018-01-04 04:07:49 +09:00
Kazu Yokomizo
297b4346c5 Fix CI error at #1147 2018-01-04 03:51:49 +09:00
Kazz Yokomizo
767a203439 Merge pull request #1147 from mslourens/export-folder
export folder as md or text
2018-01-04 03:50:24 +09:00
Kazz Yokomizo
c564c253b1 Merge pull request #1302 from mslourens/open-finder
fixed opening finder on Windows and Linux
2018-01-04 03:39:14 +09:00
Anthony Graham
b4e138e21b Elixir Syntax Highlighting 2018-01-01 21:17:29 +00:00
Kazz Yokomizo
8ca01921c4 Merge pull request #1349 from BoostIO/fix-live-preview-indent-bug
fix bug that config of editorIndentSize is not working.
2017-12-30 16:38:58 +09:00
Sosuke Suzuki
c8b97ffde3 pass fontSize and indentSize to CodeEditor as props 2017-12-30 11:04:13 +09:00
Kazz Yokomizo
abc84e5710 Merge pull request #1347 from BoostIO/fix-solarized-dark-infopanel
fix style
2017-12-29 21:56:55 +09:00
Sosuke Suzuki
d732d195f3 fix style 2017-12-29 21:38:40 +09:00
Kazz Yokomizo
789975abb0 Merge pull request #1343 from BoostIO/fix-table-backgroud-color
add table style in solarized-dark-theme
2017-12-28 22:57:39 +09:00
Sosuke Suzuki
ed1eab7fcc add table style in solarized-dark-theme 2017-12-28 22:48:59 +09:00
Kazz Yokomizo
29b31c114a Merge pull request #1337 from BoostIO/update-readme
Update readme
2017-12-28 11:13:48 +09:00
Kazz Yokomizo
c8cf353c21 Merge pull request #1220 from PaulRosset/add-notification-onChangeUi
Add notification on change ui
2017-12-28 10:45:21 +09:00
Kazz Yokomizo
e82e2c71f1 Update readme 2017-12-28 10:44:07 +09:00
Kazz Yokomizo
dd729c406f Merge pull request #1256 from mslourens/export-html
first attempt to export html
2017-12-28 10:38:55 +09:00
Kazz Yokomizo
3127e85900 Merge pull request #1329 from robbawebba/fix/jump-notes-by-link
Fix linking to other notes
2017-12-26 11:51:51 +09:00
Kazz Yokomizo
304eeb3158 Merge pull request #1331 from ncaron/master
Fix starred item count
2017-12-26 11:41:28 +09:00
Niko Caron
bf781c6b50 Fix starred item count
* Deleting a starred item will now update the starred count in the side nav. (delete from starred set)
* Restoring a starred item will add it the the starred set again.

Fix: #1326
2017-12-23 18:17:48 -04:00
David Pavlík
da1098e441 Merge branch 'master' into feature-editor-line-lines 2017-12-23 23:06:26 +01:00
David Pavlík
85065357e2 Make the option to disable line numbers in editor affect snippet notes too 2017-12-23 22:52:50 +01:00
David Pavlík
1f5f6c3b0e Rename 'lineNumber' to 'displayLineNumbers' 2017-12-23 22:51:38 +01:00
Rob Weber
4f7026969f Fix linking to other notes 2017-12-23 01:33:03 -08:00
Kazz Yokomizo
16d264ebfa Merge pull request #1328 from BoostIO/fix-style
Fix style
2017-12-23 15:32:02 +09:00
Kazu Yokomizo
04ffbe945b Delete unnecessary style 2017-12-23 15:25:45 +09:00
Kazu Yokomizo
974a1a1e7d Fix style 2017-12-23 15:18:16 +09:00
Kazz Yokomizo
ca2c07244f Merge pull request #1321 from mslourens/tooltips
added tooltips like the new note button
2017-12-23 15:12:17 +09:00
Kohei TAKATA
f90786d1c0 Merge pull request #1327 from BoostIO/feature-v0-8-19
v0.8.19
2017-12-23 12:51:13 +09:00
Kohei TAKATA
bdf55568c7 v0.8.19 2017-12-23 12:03:28 +09:00
Maurits Lourens
e262d2f19b fixed lint error 2017-12-21 19:15:34 +01:00
Maurits Lourens
aabfe820ac refactored to prevent duplicate code 2017-12-21 17:41:08 +01:00
Maurits Lourens
3bba5442bd show delete confirmation dialog 2017-12-21 15:57:27 +01:00
Maurits Lourens
6ce1922fb3 added tooltips like the new note button (and consequently extracted some buttons and styles) 2017-12-21 15:23:13 +01:00
Maurits Lourens
9367a404ef converted line-endings back to lf 2017-12-21 09:37:17 +01:00
Kazz Yokomizo
7c8e19c681 Merge pull request #1312 from BoostIO/fix-layout
Fix switch button layout
2017-12-20 18:56:31 +09:00
Kazu Yokomizo
7ccc5eb9b8 Fix switch button layout 2017-12-20 18:50:53 +09:00
Kazz Yokomizo
b4b6d3d07c Merge pull request #1243 from BoostIO/detect-AMA-errors
Detect ama errors
2017-12-20 18:27:24 +09:00
Kazz Yokomizo
9007bac7b2 Merge pull request #1307 from BoostIO/split-markdown
Split markdown
2017-12-20 18:20:49 +09:00
Sosuke Suzuki
df13ca3c92 make splitMode Default 2017-12-20 18:11:01 +09:00
Kazz Yokomizo
d86935acaa Merge pull request #1308 from ytk141/split-icon
Split icon
2017-12-19 13:57:49 +09:00
Yutaka Ishii
72b450d526 icon updated 2017-12-19 10:21:01 +09:00
Sosuke Suzuki
4b7afeeb4f remove unnecesarry state 2017-12-19 03:46:20 +09:00
Sosuke Suzuki
0e0e779cbe remove unnecessary arg 2017-12-18 21:38:58 +09:00
Sosuke Suzuki
89b2f48a06 remove an unnecessary valiable 2017-12-18 19:31:43 +09:00
Sosuke Suzuki
c80bdb8d0c apply ignorePreviewPointerEvents 2017-12-18 19:28:42 +09:00
Sosuke Suzuki
50d89a8ec9 fix to apply fontSize and fontFamily 2017-12-18 19:21:52 +09:00
Sosuke Suzuki
f5ccaa7b48 fix word-wrap 2017-12-18 18:52:59 +09:00
Sosuke Suzuki
e682ee8541 avoid handling events for not split editor 2017-12-18 18:52:33 +09:00
Sosuke Suzuki
caaa7a9e74 implement checkbox 2017-12-18 18:42:35 +09:00
Sosuke Suzuki
6ef0e325e2 fix style 2017-12-18 18:39:40 +09:00
Sosuke Suzuki
ea064deeb8 implement splitEditor 2017-12-18 18:26:25 +09:00
Sosuke Suzuki
8e81609a39 implement minimun splitEditor 2017-12-18 17:40:06 +09:00
Nikolay Lopin
83da07a941 Export note with local images
Looks through the note and searches for local images. Copies them to ‘images’ folder in the export path and replaces affected links.

#1261
2017-12-17 21:39:34 +03:00
Sosuke Suzuki
977e80c829 apply switching style 2017-12-17 02:07:07 +09:00
Sosuke Suzuki
8ba0d10f40 set editorType into Config and state 2017-12-17 02:02:19 +09:00
Sosuke Suzuki
2581091652 add editorType into ConfigManager 2017-12-16 23:17:45 +09:00
Kazz Yokomizo
e72a7ceaea Merge pull request #1207 from ytk141/add-note-mode-tab
add note mode tab
2017-12-16 17:33:49 +09:00
Sosuke Suzuki
a17ddf6d54 use getSendEventCond func 2017-12-16 15:30:21 +09:00
Sosuke Suzuki
b5e2d21f33 split to function send event conditions 2017-12-16 15:30:21 +09:00
Sosuke Suzuki
d09f8dff18 detect internet connection when send events 2017-12-16 15:30:21 +09:00
Sosuke Suzuki
bdb3406dcb modify: change the file where detect Internet disconnection 2017-12-16 15:30:21 +09:00
Sosuke Suzuki
f54b49db1a detect internet disconnection 2017-12-16 15:30:21 +09:00
Kazz Yokomizo
0cc9f006c5 Merge pull request #1217 from SCdF/use-file-metadata-on-import
Use existing file metadata for created and modified dates
2017-12-16 14:29:19 +09:00
Kazz Yokomizo
db1398b65f Merge pull request #1305 from BoostIO/fix-ui-bug
Fix several UI bugs
2017-12-16 13:58:00 +09:00
Kazu Yokomizo
e8192e6c3f Fix several UI bugs 2017-12-16 13:49:53 +09:00
Maurits Lourens
775200bdd6 fixed opening finder on Windows and Linux
fixes #1291 - tested on Ubuntu Linux Mate and Windows 10
2017-12-15 12:57:18 +01:00
Kazz Yokomizo
820a2a093c Merge pull request #1275 from BoostIO/add-pin-note-item-simple
Add Pin to NoteItemSimple
2017-12-15 15:32:26 +09:00
Kazz Yokomizo
d3995b9b10 Merge pull request #1293 from ytk141/Solarized-Dark-Theme
Add Solarized Dark theme
2017-12-15 15:27:43 +09:00
Yutaka Ishii
f6867f9338 Finder style updated 2017-12-15 14:52:58 +09:00
Yutaka Ishii
3e2548fcd5 add active notelist color 2017-12-15 11:04:09 +09:00
Stefan du Fresne
745d250787 Reorder Object.assign
- Respects the dates that may be in input over default dates
 - Respects key and storage over what might be in input
2017-12-14 13:16:37 +00:00
Stefan du Fresne
b1063eb38f Not used to this no semi colon thing 2017-12-14 13:13:29 +00:00
Stefan du Fresne
9032a1debb Use existing file metadata for created and modified dates
NB: this definintely works on OSX. Other operating systems may have
slightly different interpretations of birthtime.

See: https://nodejs.org/api/fs.html#fs_class_fs_stats
2017-12-14 13:13:29 +00:00
Paul Rosset
795fe8ae1d Add isEqual and changing haveToSaveNotif method 2017-12-14 12:26:05 +00:00
Kazz Yokomizo
e5a908af68 Merge pull request #1295 from BoostIO/update-readme
Update readme
2017-12-14 18:26:38 +09:00
Kohei TAKATA
6ce16c1cc0 Merge pull request #1296 from BoostIO/add-lint-plugin
Add lint plugin
2017-12-14 18:22:35 +09:00
Kazu Yokomizo
6ff2a5ac94 Add lint plugin 2017-12-14 18:09:49 +09:00
Kazz Yokomizo
fcea16e43a Update readme 2017-12-14 18:07:27 +09:00
Kazz Yokomizo
7b8e42382e Merge pull request #1294 from BoostIO/update-readme
Update readme
2017-12-14 18:03:38 +09:00
Kazz Yokomizo
a372b5ea39 Update readme 2017-12-14 17:51:48 +09:00
Yutaka Ishii
1aafee2a7c Add Solarized Dark theme 2017-12-14 15:46:29 +09:00
Kazz Yokomizo
7afe3d5181 Merge pull request #1287 from nebbers1111/master
Fixed regex to stop [anytingx] being counted as a checkmark
2017-12-14 12:25:34 +09:00
Maurits Lourens
6fba62d062 fixed review comments 2017-12-13 17:20:22 +01:00
Maurits Lourens
6906c0ab0d changed var into const 2017-12-13 17:16:28 +01:00
Maurits Lourens
5d46adf8fd fixed review comments 2017-12-13 17:11:43 +01:00
Kazz Yokomizo
eda1f11d42 Merge pull request #1267 from hooskers/add-scroll-past-end
Add scroll past end
2017-12-14 00:54:38 +09:00
Ben Coleman
6431a8255d Fixed trailing spaces 2017-12-12 13:59:38 +00:00
Ben Coleman
48fd1d11e2 Fixed regex to stop [anytingx] being counted as a checkmark 2017-12-12 13:34:37 +00:00
Kazz Yokomizo
4c3e62efad Merge pull request #1284 from BoostIO/fix-ci-error
Fix Ci error
2017-12-12 17:29:14 +09:00
Kazu Yokomizo
52a15a5d92 Fix Ci error 2017-12-12 17:19:58 +09:00
Kazz Yokomizo
57f4aa5995 Merge pull request #1268 from ytk141/color-update
Color update
2017-12-12 17:17:44 +09:00
Yutaka Ishii
ab9ab004b7 delete non-use icon 2017-12-12 17:02:09 +09:00
Yutaka Ishii
ac624eb98f Change Edit Lock Icon 2017-12-12 17:01:33 +09:00
Yutaka Ishii
1a4c37820d little bit darker color for new dark theme 2017-12-12 10:26:55 +09:00
Yutaka Ishii
685206950b restore StatusBar 2017-12-12 10:22:31 +09:00
Kazu Yokomizo
eececf8a93 Add Pin to NoteItemSimple 2017-12-12 02:27:44 +09:00
Kazz Yokomizo
9bc3d65554 Merge pull request #1274 from BoostIO/add-german-documents
Add german documents
2017-12-12 02:09:24 +09:00
Kazz Yokomizo
f9b854ce39 Update debug.md 2017-12-12 01:36:37 +09:00
Kazz Yokomizo
1416968fe5 Update debug.md in German 2017-12-12 01:33:46 +09:00
Kazz Yokomizo
efc183c709 Update build.md in German 2017-12-12 01:28:28 +09:00
Kazz Yokomizo
07a2442718 Add debug.md in German 2017-12-12 01:27:23 +09:00
Kazz Yokomizo
f549c50a58 Create German build.md 2017-12-12 01:26:24 +09:00
Kazz Yokomizo
8d6ce1a2f7 Merge pull request #1272 from oisu/cloud-sync-comment
Fix link to Cloud-Syncing
2017-12-12 01:09:25 +09:00
oisu
c5b33e025e Fix link to Cloud-Syncing 2017-12-11 23:45:34 +09:00
Maurits Lourens
4cdfc738c0 forgot to run eslint (again) 2017-12-11 15:14:02 +01:00
Maurits Lourens
46d46f21e4 convert uml to utf8 before converting to base64 2017-12-11 15:02:42 +01:00
Yutaka Ishii
4983605b23 fixed sideNav icons 2017-12-11 22:03:29 +09:00
Yutaka Ishii
9e8d04d806 progressBar padding修正 2017-12-11 21:52:08 +09:00
Kazz Yokomizo
042f935133 Merge pull request #1269 from BoostIO/add-solarized-dark-theme
Add the Solarized Dark Theme
2017-12-11 20:28:22 +09:00
Kazu Yokomizo
ed9d3639e2 Add the Solarized Dark Theme 2017-12-11 19:01:24 +09:00
Yutaka Ishii
728f105830 dark theme update 2017-12-11 15:42:31 +09:00
Yutaka Ishii
6f359fa6a8 color setting 2017-12-11 15:42:31 +09:00
Yutaka Ishii
57a88743bc icon style update. Delete Zoom 2017-12-11 15:42:30 +09:00
Wade Johnson
667f022086 Remove semicolon 2017-12-10 21:13:21 -08:00
Wade Johnson
b742a3a4cd Put this back?? 2017-12-10 17:58:06 -08:00
Wade Johnson
8d5c9742f8 Add ability to scroll pasted last line of editor 2017-12-09 21:14:10 -08:00
Kohei TAKATA
c2a49efe73 Merge pull request #1120 from mslourens/drag-drop-tabs
implemented drag/drop for tabs
2017-12-09 08:05:57 +09:00
Maurits Lourens
8c8a0ab46d forgot to run lint 2017-12-08 16:21:31 +01:00
Maurits Lourens
959b75bddd export folder as md or text 2017-12-08 16:21:31 +01:00
Kazz Yokomizo
d29d5105f1 Merge pull request #1257 from mslourens/preferences-shortcut
added ctrl+, shortcut to preference modal
2017-12-08 22:15:20 +09:00
Kazz Yokomizo
38e82872a5 Merge pull request #1258 from mslourens/import-in-new-folder
fixed import of new notes
2017-12-08 22:07:38 +09:00
Maurits Lourens
15d9ce1d36 fixed import of new notes 2017-12-08 13:27:16 +01:00
Maurits Lourens
67f7cdb36c added ctrl+, shortcut to preference modal 2017-12-08 12:02:07 +01:00
Maurits Lourens
6a9d4ae0fd first attempt to export html 2017-12-08 11:43:50 +01:00
Kazz Yokomizo
6a761c3fb5 Merge pull request #1254 from BoostIO/fix-folded-sidebar
Fix the folded side bar layout
2017-12-08 14:27:29 +09:00
Kazu Yokomizo
3baf97e69f Fix the folded side bar layout 2017-12-08 14:18:45 +09:00
Kazz Yokomizo
694dc60481 Merge pull request #1244 from mslourens/font-family
fixed incorrect fontFamily for multiple fonts
2017-12-07 17:39:10 +09:00
Kazz Yokomizo
e3c6f0452c Merge pull request #1241 from mslourens/global.styl
fixed global styles
2017-12-06 15:56:47 +09:00
Kazz Yokomizo
ed2401a87b Merge branch 'master' into global.styl 2017-12-06 15:39:46 +09:00
Paul Rosset
cb59458c79 refactor 2017-12-05 18:28:59 +00:00
Paul Rosset
125a493400 Change name for state 2017-12-05 18:24:30 +00:00
Paul Rosset
83910b55d2 Correction eslint code format 2017-12-05 18:18:12 +00:00
Paul Rosset
f4fd131100 Requested Review 2017-12-05 18:16:42 +00:00
Maurits Lourens
cfdc880d8c fixed incorrect fontFamily for multiple fonts 2017-12-05 12:51:16 +01:00
Kazz Yokomizo
7303e8bdd2 Merge pull request #1242 from mslourens/ide-related-gitignore
added IDE related ignore rules
2017-12-05 19:32:05 +09:00
Maurits Lourens
ecde465d9f undo ide related gitignore changes 2017-12-05 11:03:30 +01:00
Maurits Lourens
5c5e70a805 added IDE related ignore rules 2017-12-05 11:02:15 +01:00
Maurits Lourens
4e41d9be55 fixed global styles 2017-12-05 09:06:00 +01:00
Maurits Lourens
d06d94449c forgot to reload tab at oldindex 2017-12-04 19:24:50 +01:00
Maurits Lourens
1af2c83c63 removed the onDragEnd handler 2017-12-04 19:24:50 +01:00
Maurits Lourens
6c75136777 implemented drag/drop for tabs 2017-12-04 19:24:50 +01:00
Kazz Yokomizo
31351e34e1 Merge pull request #1236 from Alaev/patch-1
Added border radius & background color to NavToggleButton
2017-12-04 22:32:08 +09:00
Michael Alaev
a058a774e9 Added border radius with background color
At the moment when you hover the left menu icon its hard to see that you fully hovering it, yes there is a white color and a cursor icon I was thinking it would be useful to have a more visual effect
2017-12-04 15:10:36 +02:00
Kazz Yokomizo
e6db28485c Merge pull request #1194 from fabien0102/checked-style
Add a specific style for checked inputs
2017-12-04 13:48:18 +09:00
Kazz Yokomizo
391bb096d6 Merge pull request #1221 from Paalon/master
Add custom LaTeX delimiters to preferences
2017-12-04 13:45:49 +09:00
Paalon
2b496dc2e5 Get back into original yarn.lock 2017-12-02 17:42:11 +09:00
Paalon
a6e0b30576 Bug fix: fix mistaken name. 2017-12-02 17:15:50 +09:00
paalon
1ac31264b7 Change 'var' to 'const'. 2017-12-02 12:55:10 +09:00
paalon
ca4b8224fd Add LaTeX delimiters preferences 2017-12-02 07:23:12 +09:00
Paul Rosset
d1e5781c24 Correction UiTab 2017-12-01 19:03:04 +00:00
Paul Rosset
c86e451198 Merge branch 'master' of github.com:BoostIO/Boostnote into add-notification-onChangeUi 2017-12-01 17:34:16 +00:00
Paul Rosset
b4a7b547f0 Add notification when not saved 2017-12-01 15:23:23 +00:00
Yutaka Ishii
07b395c24a add note mode tab 2017-11-30 18:36:41 +09:00
fabien0102
53b9630fa5 Remove italic style 2017-11-28 15:18:22 +01:00
fabien0102
f820c3089e Add a specific style for checked inputs 2017-11-27 17:30:16 +01:00
David Pavlík
7a4258bb20 Make line numbers in the editor optional 2017-09-20 22:34:18 +02:00
207 changed files with 9213 additions and 3819 deletions

View File

@@ -10,7 +10,6 @@
"theme": "monokai"
},
"hotkey": {
"toggleFinder": "Cmd + Alt + S",
"toggleMain": "Cmd + Alt + L"
},
"isSideNavFolded": false,

View File

@@ -1,3 +1,4 @@
node_modules/
compiled/
dist/
extra_scripts/

View File

@@ -3,7 +3,9 @@
"plugins": ["react"],
"rules": {
"no-useless-escape": 0,
"prefer-const": "warn",
"prefer-const": ["warn", {
"destructuring": "all"
}],
"no-unused-vars": "warn",
"no-undef": "warn",
"no-lone-blocks": "warn",

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ node_modules/*
/compiled
/secret
*.log
.idea
.vscode

View File

@@ -1,7 +1,6 @@
language: node_js
node_js:
- stable
- lts/*
- 6
script:
- npm run lint && npm run test
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'

View File

@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you.
Copyright (C) 2017 Maisin&Co., Inc.
Copyright (C) 2017 - 2018 BoostIO
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@@ -2,14 +2,20 @@ import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import path from 'path'
import copyImage from 'browser/main/lib/dataApi/copyImage'
import { findStorage } from 'browser/lib/findStorage'
import fs from 'fs'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
const { ipcRenderer } = require('electron')
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
function pass (name) {
switch (name) {
@@ -30,8 +36,13 @@ export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.changeHandler = (e) => this.handleChange(e)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null
let el = e.relatedTarget
while (el != null) {
@@ -46,21 +57,59 @@ export default class CodeEditor extends React.Component {
this.loadStyleHandler = (e) => {
this.editor.refresh()
}
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
}
handleSearch (msg) {
const cm = this.editor
const component = this
if (component.searchState) cm.removeOverlay(component.searchState)
if (msg.length < 3) return
cm.operation(function () {
component.searchState = makeOverlay(msg, 'searching')
cm.addOverlay(component.searchState)
function makeOverlay (query, style) {
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
return {
token: function (stream) {
query.lastIndex = stream.pos
var match = query.exec(stream.string)
if (match && match.index === stream.pos) {
stream.pos += match[0].length || 1
return style
} else if (match) {
stream.pos = match.index
} else {
stream.skipToEnd()
}
}
}
}
})
}
componentDidMount () {
const { rulers, enableRulers } = this.props
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value,
lineNumbers: true,
lineNumbers: this.props.displayLineNumbers,
lineWrapping: true,
theme: this.props.theme,
indentUnit: this.props.indentSize,
tabSize: this.props.indentSize,
indentWithTabs: this.props.indentType !== 'space',
keyMap: this.props.keyMap,
scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea',
dragDrop: false,
autoCloseBrackets: true,
extraKeys: {
Tab: function (cm) {
const cursor = cm.getCursor()
@@ -68,7 +117,7 @@ export default class CodeEditor extends React.Component {
if (cm.somethingSelected()) cm.indentSelection('add')
else {
const tabs = cm.getOption('indentWithTabs')
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
cm.execCommand('goLineStart')
if (tabs) {
cm.execCommand('insertTab')
@@ -88,7 +137,7 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) {
// Do nothing
},
Enter: 'newlineAndIndentContinueMarkdownList',
Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': (cm) => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy')
@@ -100,9 +149,14 @@ export default class CodeEditor extends React.Component {
this.setMode(this.props.mode)
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
eventEmitter.on('top:search', this.searchHandler)
eventEmitter.emit('code:init')
this.editor.on('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -119,15 +173,19 @@ export default class CodeEditor extends React.Component {
}
componentWillUnmount () {
this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler)
}
componentDidUpdate (prevProps, prevState) {
let needRefresh = false
const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode)
}
@@ -145,6 +203,10 @@ export default class CodeEditor extends React.Component {
needRefresh = true
}
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
}
if (prevProps.indentSize !== this.props.indentSize) {
this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize)
@@ -153,6 +215,14 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
}
if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
}
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
}
if (needRefresh) {
this.editor.refresh()
}
@@ -219,32 +289,110 @@ export default class CodeEditor extends React.Component {
}
handlePaste (editor, e) {
const dataTransferItem = e.clipboardData.items[0]
if (!dataTransferItem.type.match('image')) return
const blob = dataTransferItem.getAsFile()
const reader = new FileReader()
let base64data
reader.readAsDataURL(blob)
reader.onloadend = () => {
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary')
const imageName = Math.random().toString(36).slice(-16)
const storagePath = findStorage(this.props.storageKey).path
const imageDir = path.join(storagePath, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
const imagePath = path.join(imageDir, `${imageName}.png`)
fs.writeFile(imagePath, binaryData, 'binary')
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})`
this.insertImageMd(imageMd)
const clipboardData = e.clipboardData
const dataTransferItem = clipboardData.items[0]
const pastedTxt = clipboardData.getData('text')
const isURL = (str) => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str)
}
const isInLinkTag = (editor) => {
const startCursor = editor.getCursor('start')
const prevChar = editor.getRange(
{ line: startCursor.line, ch: startCursor.ch - 2 },
{ line: startCursor.line, ch: startCursor.ch }
)
const endCursor = editor.getCursor('end')
const nextChar = editor.getRange(
{ line: endCursor.line, ch: endCursor.ch },
{ line: endCursor.line, ch: endCursor.ch + 1 }
)
return prevChar === '](' && nextChar === ')'
}
if (dataTransferItem.type.match('image')) {
const blob = dataTransferItem.getAsFile()
const reader = new FileReader()
let base64data
reader.readAsDataURL(blob)
reader.onloadend = () => {
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary')
const imageName = Math.random().toString(36).slice(-16)
const storagePath = findStorage(this.props.storageKey).path
const imageDir = path.join(storagePath, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
const imagePath = path.join(imageDir, `${imageName}.png`)
fs.writeFile(imagePath, binaryData, 'binary')
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})`
this.insertImageMd(imageMd)
}
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(e, editor, pastedTxt)
}
}
handleScroll (e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handlePasteUrl (e, editor, pastedTxt) {
e.preventDefault()
const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl)
fetch(pastedTxt, {
method: 'get'
}).then((response) => {
return this.decodeResponse(response)
}).then((response) => {
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
const value = editor.getValue()
const cursor = editor.getCursor()
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
const newValue = value.replace(taggedUrl, LinkWithTitle)
editor.setValue(newValue)
editor.setCursor(cursor)
}).catch((e) => {
const value = editor.getValue()
const newValue = value.replace(taggedUrl, pastedTxt)
const cursor = editor.getCursor()
editor.setValue(newValue)
editor.setCursor(cursor)
})
}
decodeResponse (response) {
const headers = response.headers
const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type'))
: undefined
return response.arrayBuffer().then((buff) => {
return new Promise((resolve, reject) => {
try {
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString())
} catch (e) {
reject(e)
}
})
})
}
extractContentTypeCharset (contentType) {
return contentType.split(';').filter((str) => {
return str.trim().toLowerCase().startsWith('charset')
}).map((str) => {
return str.replace(/['"]/g, '').split('=')[1]
})[0]
}
render () {
const { className, fontSize } = this.props
let fontFamily = this.props.className
let fontFamily = this.props.fontFamily
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
@@ -268,6 +416,8 @@ export default class CodeEditor extends React.Component {
CodeEditor.propTypes = {
value: PropTypes.string,
enableRulers: PropTypes.bool,
rulers: PropTypes.arrayOf(Number),
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,

View File

@@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
import {findStorage} from 'browser/lib/findStorage'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
if (this.state.isLocked) return
this.setState({ keyPressed: new Set() })
const { config } = this.props
if (config.editor.switchPreview === 'BLUR') {
if (config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
) {
const cursorPosition = this.refs.code.editor.getCursor()
this.setState({
status: 'PREVIEW'
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
}
}
handleDoubleClick (e) {
if (this.state.isLocked) return
this.setState({keyPressed: new Set()})
const { config } = this.props
if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
})
}
}
handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date()
}
@@ -242,7 +258,12 @@ class MarkdownEditor extends React.Component {
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
@@ -259,8 +280,12 @@ class MarkdownEditor extends React.Component {
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
sanitize={config.preview.sanitize}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}
tabIndex='0'
value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}

179
browser/components/MarkdownPreview.js Normal file → Executable file
View File

@@ -1,17 +1,18 @@
import PropTypes from 'prop-types'
import React from 'react'
import markdown from 'browser/lib/markdown'
import Markdown from 'browser/lib/markdown'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import consts from 'browser/lib/consts'
import Raphael from 'raphael'
import flowchart from 'flowchart'
import SequenceDiagram from 'js-sequence-diagrams'
import eventEmitter from 'browser/main/lib/eventEmitter'
import fs from 'fs'
import htmlTextHelper from 'browser/lib/htmlTextHelper'
import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
const { remote } = require('electron')
const { app } = remote
@@ -22,8 +23,12 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
? app.getAppPath()
: path.resolve())
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
return `
@font-face {
font-family: 'Lato';
@@ -47,14 +52,15 @@ ${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
@@ -114,13 +120,26 @@ export default class MarkdownPreview extends React.Component {
this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e)
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
}
initMarkdown () {
const { smartQuotes, sanitize } = this.props
this.markdown = new Markdown({
typographer: smartQuotes,
sanitize
})
}
handlePreviewAnchorClick (e) {
@@ -143,10 +162,20 @@ export default class MarkdownPreview extends React.Component {
this.props.onCheckboxClick(e)
}
handleScroll (e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handleContextMenu (e) {
this.props.onContextMenu(e)
}
handleDoubleClick (e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
}
handleMouseDown (e) {
if (e.target != null) {
switch (e.target.tagName) {
@@ -159,6 +188,7 @@ export default class MarkdownPreview extends React.Component {
}
handleMouseUp (e) {
if (!this.props.onMouseUp) return
if (e.target != null && e.target.tagName === 'A') {
return null
}
@@ -173,25 +203,66 @@ export default class MarkdownPreview extends React.Component {
this.exportAsDocument('md')
}
handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
const body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach((file) => {
file = file.replace('file://', '')
exportTasks.push({
src: file,
dst: 'css'
})
})
let styles = ''
files.forEach((file) => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
})
}
handlePrint () {
this.refs.root.contentWindow.print()
}
exportAsDocument (fileType) {
exportAsDocument (fileType, contentFormatter) {
const options = {
filters: [
{ name: 'Documents', extensions: [fileType] }
{name: 'Documents', extensions: [fileType]}
],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options,
(filename) => {
if (filename) {
fs.writeFile(filename, this.props.value, (err) => {
if (err) throw err
(filename) => {
if (filename) {
const content = this.props.value
const storage = this.props.storagePath
exportNote(storage, content, filename, contentFormatter)
.then((res) => {
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
}).catch((err) => {
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
throw err
})
}
})
}
})
}
fixDecodedURI (node) {
@@ -208,21 +279,29 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.head.innerHTML = `
let styles = `
<style id='style'></style>
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" id="codeTheme">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
`
CSS_FILES.forEach((file) => {
styles += `<link rel="stylesheet" href="${file}">`
})
this.refs.root.contentWindow.document.head.innerHTML = styles
this.rewriteIframe()
this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('print', this.printHandler)
}
@@ -230,46 +309,60 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
eventEmitter.off('print', this.printHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize) {
this.initMarkdown()
this.rewriteIframe()
}
if (prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
prevProps.theme !== this.props.theme) {
prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.applyStyle()
this.rewriteIframe()
}
}
applyStyle () {
const { fontSize, lineNumber, codeBlockTheme } = this.props
getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? [fontFamily].concat(defaultFontFamily)
: defaultFontFamily
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
this.setCodeTheme(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
}
setCodeTheme (theme) {
applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams()
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
}
GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
? theme
: 'elegant'
this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
}
@@ -297,11 +390,7 @@ export default class MarkdownPreview extends React.Component {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
})
}
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
el.parentNode.parentNode.style.listStyleType = 'none'
})
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
@@ -317,9 +406,9 @@ export default class MarkdownPreview extends React.Component {
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
el.src = markdown.normalizeLinkText(el.src)
el.src = this.markdown.normalizeLinkText(el.src)
if (!/\/:storage/.test(el.src)) return
el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
})
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
@@ -346,9 +435,9 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = ''
if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ')
el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
} else {
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
el.parentNode.className += ` cm-s-${codeBlockTheme}`
}
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
@@ -431,9 +520,20 @@ export default class MarkdownPreview extends React.Component {
handlelinkClick (e) {
const noteHash = e.target.href.split('/').pop()
const regexIsNoteLink = /^(.{20})-(.{20})$/
// this will match the new uuid v4 hash and the old hash
// e.g.
// :note:1c211eb7dcb463de6490 and
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
if (regexIsNoteLink.test(noteHash)) {
eventEmitter.emit('list:jump', noteHash)
eventEmitter.emit('list:jump', noteHash.replace(':note:', ''))
}
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
if (regexIsLegacyNoteLink.test(noteHash)) {
eventEmitter.emit('list:jump', noteHash.split('-')[1])
}
}
@@ -460,5 +560,6 @@ MarkdownPreview.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string
storagePath: PropTypes.string,
smartQuotes: PropTypes.bool
}

View File

@@ -0,0 +1,147 @@
import React from 'react'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import { findStorage } from 'browser/lib/findStorage'
import _ from 'lodash'
import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules'
class MarkdownSplitEditor extends React.Component {
constructor (props) {
super(props)
this.value = props.value
this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload()
this.userScroll = true
}
handleOnChange () {
this.value = this.refs.code.value
this.props.onChange()
}
handleScroll (e) {
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
if (this.userScroll) {
if (e.doc) {
srcTop = _.get(e, 'doc.scrollTop')
srcHeight = _.get(e, 'doc.height')
targetTop = _.get(previewDoc, 'body.scrollTop')
targetHeight = _.get(previewDoc, 'body.scrollHeight')
} else {
srcTop = _.get(previewDoc, 'body.scrollTop')
srcHeight = _.get(previewDoc, 'body.scrollHeight')
targetTop = _.get(codeDoc, 'scrollTop')
targetHeight = _.get(codeDoc, 'height')
}
const distance = (targetHeight * srcTop / srcHeight) - targetTop
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate
this.userScroll = false
let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos = time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => { this.userScroll = true }, refractory)
}
frame++
}, framerate)
}
}
handleCheckboxClick (e) {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /\[x\]/i
const uncheckedMatch = /\[ \]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
.split('\n')
const targetLine = lines[lineIndex]
if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
}
}
render () {
const { config, value, storageKey } = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
return (
<div styleName='root'>
<CodeEditor
styleName='codeEditor'
ref='code'
mode='GitHub Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
displayLineNumbers={config.editor.displayLineNumbers}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
storageKey={storageKey}
onChange={this.handleOnChange.bind(this)}
onScroll={this.handleScroll.bind(this)}
/>
<MarkdownPreview
style={previewStyle}
styleName='preview'
theme={config.ui.theme}
keyMap={config.editor.keyMap}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
sanitize={config.preview.sanitize}
ref='preview'
tabInde='0'
value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
/>
</div>
)
}
}
export default CSSModules(MarkdownSplitEditor, styles)

View File

@@ -0,0 +1,9 @@
.root
width 100%
height 100%
font-size 30px
display flex
.codeEditor
width 50%
.preview
width 50%

View File

@@ -9,6 +9,10 @@
width 34px
line-height 32px
padding 0
&:hover
border: 1px solid #1EC38B;
background-color: alpha(#1EC38B, 30%)
border-radius: 50%;
body[data-theme="white"]
navWhiteButtonColor()

View File

@@ -46,14 +46,25 @@ const TagElementList = (tags) => {
* @param {Function} handleDragStart
* @param {string} dateDisplay
*/
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => (
const NoteItem = ({
isActive,
note,
dateDisplay,
handleNoteClick,
handleNoteContextMenu,
handleDragStart,
pathname,
storageName,
folderName,
viewType
}) => (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
? 'item--active'
: 'item'
}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)}
draggable='true'
>
@@ -68,23 +79,33 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
: <span styleName='item-title-empty'>Empty</span>
}
</div>
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div>
<div styleName='item-middle-app-meta'>
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
{viewType === 'ALL' && storageName}
{viewType === 'STORAGE' && folderName}
</div>
</div>
</div>}
<div styleName='item-bottom-time'>{dateDisplay}</div>
{note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
}
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''
}
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags)
: <span styleName='item-bottom-tagList-empty' />
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>No tags</span>
}
</div>
<div>
{note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
}
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
}
{note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
: ''
}
</div>
</div>
@@ -102,7 +123,11 @@ NoteItem.propTypes = {
title: PropTypes.string.isrequired,
tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired
isTrashed: PropTypes.bool.isRequired,
blog: {
blogLink: PropTypes.string,
blogId: PropTypes.number
}
}),
handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired,

View File

@@ -90,6 +90,26 @@ $control-height = 30px
font-weight normal
color $ui-inactive-text-color
.item-middle
font-size 13px
padding-left 2px
padding-bottom 2px
.item-middle-time
color $ui-inactive-text-color
display inline-block
.item-middle-app-meta
float right
.item-middle-app-meta-label
opacity 0.4
color $ui-inactive-text-color
padding 0 3px
white-space nowrap
text-overflow ellipsis
overflow hidden
max-width 200px
.item-bottom
position relative
bottom 0px
@@ -97,7 +117,7 @@ $control-height = 30px
font-size 12px
line-height 20px
overflow ellipsis
display flex
display block
.item-bottom-tagList
flex 1
@@ -125,10 +145,8 @@ $control-height = 30px
.item-star
position absolute
right -6px
bottom 23px
width 16px
height 16px
right 2px
top 5px
color alpha($ui-favorite-star-button-color, 60%)
font-size 12px
padding 0
@@ -136,10 +154,8 @@ $control-height = 30px
.item-pin
position absolute
right 0px
bottom 2px
width 34px
height 34px
right 25px
top 7px
color #E54D42
font-size 14px
padding 0
@@ -192,7 +208,7 @@ body[data-theme="dark"]
.item-bottom-tagList-item
transition 0.15s
background-color alpha(white, 10%)
color $ui-dark-text-color
color $ui-dark-text-color
.item-wrapper
border-color alpha($ui-dark-button--active-backgroundColor, 60%)
@@ -231,3 +247,77 @@ body[data-theme="dark"]
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
body[data-theme="solarized-dark"]
.root
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
.item
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
&:hover
transition 0.15s
// background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
color $ui-solarized-dark-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 20%)
color $ui-solarized-dark-text-color
&:active
transition 0.15s
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-solarized-dark-noteList-backgroundColor, 10%)
color $ui-solarized-dark-text-color
.item-wrapper
border-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
.item--active
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-button-backgroundColor
.item-wrapper
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color $ui-solarized-dark-text-color
.item-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-solarized-dark-text-color
&:hover
// background-color alpha($ui-solarized-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-bottom-tagList-item
background-color alpha(#fff, 20%)
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -14,14 +14,23 @@ import styles from './NoteItemSimple.styl'
* @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart
*/
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart }) => (
const NoteItemSimple = ({
isActive,
isAllNotesView,
note,
handleNoteClick,
handleNoteContextMenu,
handleDragStart,
pathname,
storage
}) => (
<div styleName={isActive
? 'item-simple--active'
: 'item-simple'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
? 'item-simple--active'
: 'item-simple'
}
key={note.key}
onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)}
draggable='true'
>
@@ -30,10 +39,19 @@ const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
}
{note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' />
: ''
}
{note.title.trim().length > 0
? note.title
: <span styleName='item-simple-title-empty'>Empty</span>
}
{isAllNotesView && <div styleName='item-simple-right'>
<span styleName='item-simple-right-storageName'>
{storage.name}
</span>
</div>}
</div>
</div>
)

View File

@@ -50,6 +50,7 @@ $control-height = 30px
.item-simple-title
font-size 13px
height 40px
padding-right 20px
box-sizing border-box
line-height 24px
padding-top 8px
@@ -67,6 +68,15 @@ $control-height = 30px
font-weight normal
color $ui-inactive-text-color
.item-pin
position absolute
right 0px
top 12px
color #E54D42
font-size 14px
padding 0
border-radius 17px
body[data-theme="white"]
.item-simple
background-color $ui-white-noteList-backgroundColor
@@ -114,7 +124,7 @@ body[data-theme="dark"]
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(white, 10%)
color $ui-dark-text-color
color $ui-dark-text-color
.item-simple--active
border-color $ui-dark-borderColor
@@ -143,3 +153,62 @@ body[data-theme="dark"]
.item-simple-title-empty
color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"]
.root
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
.item-simple
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
&:hover
transition 0.15s
// background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-solarized-dark-text-color
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#fff, 20%)
color $ui-solarized-dark-text-color
&:active
transition 0.15s
background-color $ui-solarized-dark-button--active-backgroundColor
color $ui-solarized-dark-text-color
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(white, 10%)
color $ui-solarized-dark-text-color
.item-simple--active
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-button--active-backgroundColor
.item-simple-wrapper
border-color transparent
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
color $ui-solarized-dark-text-color
.item-simple-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-solarized-dark-text-color
&:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-simple-bottom-tagList-item
background-color alpha(#fff, 20%)
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4

View File

@@ -29,3 +29,15 @@ body[data-theme="dark"]
transition 0.2s
&:hover
color #5CB85C
body[data-theme="solarized-dark"]
.notification-area
background-color none
.notification-link
color $ui-solarized-dark-text-color
border none
background-color $ui-solarized-dark-button-backgroundColor
&:hover
color #5CB85C

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNavFilter.styl'
import i18n from 'browser/lib/i18n'
/**
* @param {boolean} isFolded
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
counterTotalNote, counterStarredNote
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
}) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
@@ -26,12 +27,12 @@ const SideNavFilter = ({
>
<div styleName='iconWrap'>
<img src={isHomeActive
? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg'
}
? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg'
}
/>
</div>
<span styleName='menu-button-label'>All Notes</span>
<span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
<span styleName='counters'>{counterTotalNote}</span>
</button>
@@ -40,12 +41,12 @@ const SideNavFilter = ({
>
<div styleName='iconWrap'>
<img src={isStarredActive
? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star.svg'
}
? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg'
}
/>
</div>
<span styleName='menu-button-label'>Starred</span>
<span styleName='menu-button-label'>{i18n.__('Starred')}</span>
<span styleName='counters'>{counterStarredNote}</span>
</button>
@@ -54,12 +55,12 @@ const SideNavFilter = ({
>
<div styleName='iconWrap'>
<img src={isTrashedActive
? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash.svg'
}
? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg'
}
/>
</div>
<span styleName='menu-button-label'>Trash</span>
<span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span>
</button>

View File

@@ -55,7 +55,7 @@
.menu--folded
@extend .menu
.menu-button, .menu-button--active
.menu-button, .menu-button--active, .menu-button-star--active, .menu-button-trash--active
text-align center
padding 0 12px
&:hover .menu-button-label
@@ -92,7 +92,6 @@ body[data-theme="white"]
color $ui-inactive-text-color
.menu-button--active
@extend .menu-button
color #e74c3c
background-color $ui-button--active-backgroundColor
.menu-button-label
@@ -109,7 +108,6 @@ body[data-theme="white"]
color $ui-text-color
.menu-button-star--active
@extend .menu-button
color #F9BF3B
background-color $ui-button--active-backgroundColor
.menu-button-label
@@ -126,7 +124,6 @@ body[data-theme="white"]
color $ui-text-color
.menu-button-trash--active
@extend .menu-button
color #5D9E36
background-color $ui-button--active-backgroundColor
.menu-button-label
@@ -182,4 +179,47 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
color #5D9E36
.menu-button-label
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.menu-button
&:active
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button--active
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label
color $ui-solarized-dark-text-color
.menu-button-star--active
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label
color $ui-solarized-dark-text-color
.menu-button-trash--active
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
.menu-button-label
color $ui-solarized-dark-text-color
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
.menu-button-label
color $ui-solarized-dark-text-color

View File

@@ -85,6 +85,15 @@ class SnippetTab extends React.Component {
})
}
handleDragStart (e) {
e.dataTransfer.dropEffect = 'move'
this.props.onDragStart(e)
}
handleDrop (e) {
this.props.onDrop(e)
}
render () {
const { isActive, snippet, isDeletable } = this.props
return (
@@ -98,6 +107,9 @@ class SnippetTab extends React.Component {
onClick={(e) => this.handleClick(e)}
onDoubleClick={(e) => this.handleRenameClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
onDragStart={(e) => this.handleDragStart(e)}
onDrop={(e) => this.handleDrop(e)}
draggable='true'
>
{snippet.name.trim().length > 0
? snippet.name
@@ -127,6 +139,7 @@ class SnippetTab extends React.Component {
}
SnippetTab.propTypes = {
}
export default CSSModules(SnippetTab, styles)

View File

@@ -1,6 +1,7 @@
.root
position relative
flex 1
min-width 70px
overflow hidden
&:hover
.deleteButton
@@ -21,7 +22,7 @@
height 29px
overflow ellipsis
text-align left
padding-right 30px
padding-right 23px
border none
background-color transparent
transition 0.15s
@@ -38,7 +39,7 @@
text-align center
border none
padding 0
color transparent
color $ui-inactive-text-color
background-color transparent
border-radius 2px
@@ -89,3 +90,50 @@ body[data-theme="dark"]
.input
background-color $ui-dark-button--hover-backgroundColor
color $ui-dark-text-color
.deleteButton
color alpha($ui-dark-text-color, 30%)
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
.deleteButton
color $ui-solarized-dark-text-color
&:hover
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
.deleteButton
color $ui-solarized-dark-text-color
&:hover
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.button
border none
color $ui-solarized-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
.input
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
.deleteButton
color alpha($ui-solarized-dark-text-color, 30%)

View File

@@ -6,6 +6,18 @@ import React from 'react'
import styles from './StorageItem.styl'
import CSSModules from 'browser/lib/CSSModules'
import _ from 'lodash'
import { SortableHandle } from 'react-sortable-hoc'
const DraggableIcon = SortableHandle(({ className }) => (
<i className={`fa ${className}`} />
))
const FolderIcon = ({ className, color, isActive }) => {
const iconStyle = isActive ? 'fa-folder-open-o' : 'fa-folder-o'
return (
<i className={`fa ${iconStyle} ${className}`} style={{ color: color }} />
)
}
/**
* @param {boolean} isActive
@@ -21,34 +33,54 @@ import _ from 'lodash'
* @return {React.Component}
*/
const StorageItem = ({
isActive, handleButtonClick, handleContextMenu, folderName,
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave
}) => (
<button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
onClick={handleButtonClick}
onContextMenu={handleContextMenu}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
<span styleName={isFolded
? 'folderList-item-name--folded' : 'folderList-item-name'
}>
<text style={{color: folderColor, paddingRight: '10px'}}>{isActive ? <i className='fa fa-folder-open-o' /> : <i className='fa fa-folder-o' />}</text>{isFolded ? _.truncate(folderName, {length: 1, omission: ''}) : folderName}
</span>
{(!isFolded && _.isNumber(noteCount)) &&
<span styleName='folderList-item-noteCount'>{noteCount}</span>
}
{isFolded &&
<span styleName='folderList-item-tooltip'>
{folderName}
styles,
isActive,
handleButtonClick,
handleContextMenu,
folderName,
folderColor,
isFolded,
noteCount,
handleDrop,
handleDragEnter,
handleDragLeave
}) => {
return (
<button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick}
onContextMenu={handleContextMenu}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
{!isFolded && (
<DraggableIcon className={styles['folderList-item-reorder']} />
)}
<span
styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
}
>
<FolderIcon
styleName='folderList-item-icon'
color={folderColor}
isActive={isActive}
/>
{isFolded
? _.truncate(folderName, { length: 1, omission: '' })
: folderName}
</span>
}
</button>
)
{!isFolded &&
_.isNumber(noteCount) && (
<span styleName='folderList-item-noteCount'>{noteCount}</span>
)}
{isFolded && (
<span styleName='folderList-item-tooltip'>{folderName}</span>
)}
</button>
)
}
StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired,

View File

@@ -13,6 +13,7 @@
border none
overflow ellipsis
font-size 14px
align-items: center
&:first-child
margin-top 0
&:hover
@@ -22,7 +23,7 @@
&:active
color $$ui-button-default-color
background-color alpha($ui-button-default--active-backgroundColor, 20%)
.folderList-item--active
@extend .folderList-item
color #1EC38B
@@ -34,9 +35,7 @@
.folderList-item-name
display block
flex 1
padding 0 12px
height 26px
line-height 26px
padding-right: 10px
border-width 0 0 0 2px
border-style solid
border-color transparent
@@ -69,9 +68,20 @@
.folderList-item-name--folded
@extend .folderList-item-name
padding-left 7px
text
.folderList-item-icon
font-size 9px
.folderList-item-icon
padding-right: 10px
.folderList-item-reorder
font-size: 9px
padding: 10px 8px 10px 9px;
color: rgba(147, 147, 149, 0.3)
cursor: ns-resize
&:before
content: "\f142 \f142"
body[data-theme="white"]
.folderList-item
color $ui-inactive-text-color
@@ -108,4 +118,23 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
&:hover
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
body[data-theme="solarized-dark"]
.folderList-item
&:hover
background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color
&:active
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor
&:active
background-color $ui-solarized-dark-button-backgroundColor
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-button-backgroundColor

View File

@@ -12,10 +12,11 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {bool} isActive
*/
const TagListItem = ({name, handleClickTagListItem, isActive}) => (
const TagListItem = ({name, handleClickTagListItem, isActive, count}) => (
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
<span styleName='tagList-item-name'>
{`# ${name}`}
<span styleName='tagList-item-count'> {count}</span>
</span>
</button>
)

View File

@@ -48,6 +48,9 @@
overflow hidden
text-overflow ellipsis
.tagList-item-count
padding 0 3px
body[data-theme="white"]
.tagList-item
color $ui-inactive-text-color
@@ -63,6 +66,8 @@ body[data-theme="white"]
color $ui-text-color
&:hover
background-color alpha($ui-button--active-backgroundColor, 60%)
.tagList-item-count
color $ui-text-color
body[data-theme="dark"]
.tagList-item
@@ -81,4 +86,6 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
&:hover
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
background-color alpha($ui-dark-button--active-backgroundColor, 50%)
.tagList-item-count
color $ui-dark-button--active-color

View File

@@ -16,7 +16,9 @@ const TodoListPercentage = ({
}) => (
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
<p styleName='percentageText'>{percentageOfTodo}%</p>
<div styleName='progressBarInner'>
<p styleName='percentageText'>{percentageOfTodo}%</p>
</div>
</div>
</div>
)

View File

@@ -1,6 +1,6 @@
.percentageBar
position absolute
top 50px
top 72px
right 0px
left 0px
background-color #DADFE1
@@ -16,17 +16,36 @@
border-radius 2px
transition 0.4s cubic-bezier(0.4, 0.4, 0, 1)
.progressBarInner
padding 0 10px
min-width 1px
height 100%
display -webkit-box
display box
justify-content center
align-items center
.percentageText
color #f4f4f4
padding: 2px 43%
font-weight 600
body[data-theme="dark"]
.percentageBar
background-color #363A3D
background-color #444444
.progressBar
background-color: alpha(#939395, 50%)
background-color: #1EC38B
.percentageText
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.percentageBar
background-color #002b36
.progressBar
background-color: #2aa198
.percentageText
color #fdf6e3

View File

@@ -58,7 +58,7 @@ body
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space nowrap
white-space initial
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
@@ -76,7 +76,10 @@ body
justify-content left
li
label.taskListItem
margin-left -2em
margin-left -1.8em
&.checked
text-decoration line-through
opacity 0.5
div.math-rendered
text-align center
.math-failed
@@ -102,7 +105,6 @@ a
border-radius 5px
margin -5px
transition .1s
display inline-block
img
vertical-align sub
&:hover
@@ -117,6 +119,7 @@ hr
margin 15px 0
h1, h2, h3, h4, h5, h6
font-weight bold
word-wrap break-word
h1
font-size 2.55em
padding-bottom 0.3em
@@ -154,6 +157,7 @@ p
line-height 1.6em
margin 0 0 1em
white-space pre-line
word-wrap break-word
img
max-width 100%
strong, b
@@ -174,6 +178,8 @@ ul
margin-bottom 1em
li
display list-item
&.taskListItem
list-style none
p
margin 0
&>li>ul, &>li>ol
@@ -214,6 +220,7 @@ pre
background-color white
&.CodeMirror
height initial
flex-wrap wrap
&>code
flex 1
overflow-x auto
@@ -223,6 +230,13 @@ pre
padding 0
border none
border-radius 0
&>span.filename
width 100%
border-radius: 5px 0px 0px 0px
margin -8px 100% 8px -8px
padding 0px 6px
background-color #777;
color white
&>span.lineNumber
display none
font-size 1em
@@ -329,4 +343,31 @@ body[data-theme="dark"]
border-right solid 1px themeDarkTableBorder
kbd
background-color themeDarkBorder
color themeDarkText
color themeDarkText
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
themeSolarizedDarkTableBorder = themeDarkBorder
body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor
table
thead
tr
background-color themeSolarizedDarkTableHead
th
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeSolarizedDarkTableOdd
tr:nth-child(2n)
background-color themeSolarizedDarkTableEven
td
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder

View File

@@ -1,118 +0,0 @@
$search-height = 50px
$nav-width = 175px
$list-width = 250px
.root
absolute top left right bottom
.search
height $search-height
padding 10px
box-sizing border-box
border-bottom $ui-border
text-align center
.search-input
height 30px
width 100%
margin 0 auto
font-size 18px
border none
outline none
text-align center
background-color transparent
.result
absolute left right bottom
top $search-height
background-color $ui-noteDetail-backgroundColor
.result-nav
user-select none
absolute left top bottom
width $nav-width
background-color $ui-backgroundColor
.result-nav-filter
margin-bottom 10px
.result-nav-filter-option
height 25px
line-height 25px
padding 0 10px
label
cursor pointer
.result-nav-menu
navButtonColor()
height 32px
padding 0 10px
font-size 14px
width 100%
outline none
text-align left
line-height 32px
box-sizing border-box
cursor pointer
.result-nav-menu--active
@extend .result-nav-menu
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.result-nav-storageList
absolute bottom left right
top 110px + 32px + 10px + 10px + 20px
overflow-y auto
.result-list
user-select none
absolute top bottom
left $nav-width
width $list-width
box-sizing border-box
overflow-y auto
box-shadow 2px 0 15px -8px #b1b1b1
z-index 1
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-noteDetail-backgroundColor
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
.search
border-color $ui-dark-borderColor
.search-input
color $ui-dark-text-color
.result
background-color $ui-dark-noteList-backgroundColor
.result-nav
background-color $ui-dark-backgroundColor
label
color $ui-dark-text-color
.result-nav-menu
navDarkButtonColor()
.result-nav-menu--active
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
.result-list
border-color $ui-dark-borderColor
box-shadow none
top 0
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-dark-noteDetail-backgroundColor

View File

@@ -1,209 +0,0 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import CodeEditor from 'browser/components/CodeEditor'
import CodeMirror from 'codemirror'
import { findStorage } from 'browser/lib/findStorage'
const electron = require('electron')
const { clipboard } = electron
const path = require('path')
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
function notify (title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
}
return new window.Notification(title, options)
}
class NoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.note !== this.props.note) {
this.setState({
snippetIndex: 0
}, () => {
if (nextProps.note.type === 'SNIPPET_NOTE') {
nextProps.note.snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
}
})
}
}
selectPriorSnippet () {
const { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
})
}
}
selectNextSnippet () {
const { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
})
}
}
saveToClipboard () {
const { note } = this.props
if (note.type === 'MARKDOWN_NOTE') {
clipboard.writeText(note.content)
} else {
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
}
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
handleTabButtonClick (e, index) {
this.setState({
snippetIndex: index
})
}
render () {
const { note, config } = this.props
if (note == null) {
return (
<div styleName='root' />
)
}
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const storage = findStorage(note.storage)
if (note.type === 'SNIPPET_NOTE') {
const tabList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(e, index)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
</div>
})
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
{snippet.mode === 'markdown'
? <MarkdownEditor styleName='tabView-content'
config={config}
value={snippet.content}
ref={'code-' + index}
storageKey={note.storage}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
keyMap={config.editor.keyMap}
readOnly
ref={'code-' + index}
/>
}
</div>
})
return (
<div styleName='root'>
<div styleName='description'>
<textarea styleName='description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={note.description}
readOnly
/>
</div>
<div styleName='tabList'>
{tabList}
</div>
{viewList}
</div>
)
}
return (
<MarkdownPreview styleName='root'
theme={config.ui.theme}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
value={note.content}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
/>
)
}
}
NoteDetail.propTypes = {
}
export default CSSModules(NoteDetail, styles)

View File

@@ -1,97 +0,0 @@
@import('../main/Detail/DetailVars.styl')
.root
absolute top bottom left right
bottom 30px
margin 0 25px
height 100%
width 365px
background-color $ui-noteDetail-backgroundColor
.description
absolute top left right
height 80px
box-sizing border-box
.description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
box-sizing border-box
background-color $ui-noteDetail-backgroundColor
.tabList
absolute left right
top 80px
box-sizing border-box
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
.tabList-item
position relative
flex 1
overflow hidden
&:hover
background-color $ui-button--hover-backgroundColorg
.tabList-item--active
@extend .tabList-item
border-bottom $ui-border
.tabList-item-button
width 100%
height 29px
overflow ellipsis
text-align left
padding-right 30px
padding-left 10px
border none
background-color transparent
transition 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
.tabView
absolute left right bottom
top 130px
.tabView-content
absolute top left right bottom
box-sizing border-box
height 100%
width 100%
body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
.description
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
.description-textarea
background-color $ui-dark-noteDetail-backgroundColor
color white
.tabList
background-color $ui-dark-noteDetail-backgroundColor
.tabList-item
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.tabList-item-button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color white
background-color $ui-dark-button--hover-backgroundColor

View File

@@ -1,90 +0,0 @@
import React from 'react'
import NoteItem from 'browser/components/NoteItem'
import moment from 'moment'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.state = {
range: 0
}
}
componentWillReceiveProps (nextProps) {
if (this.props.search !== nextProps.search) {
this.resetScroll()
}
}
componentDidUpdate () {
const { index } = this.props
if (index > -1) {
const list = this.refs.root
const item = list.childNodes[index]
if (item == null) return null
const overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
}
const overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
resetScroll () {
this.refs.root.scrollTop = 0
this.setState({
range: 0
})
}
handleScroll (e) {
const { notes } = this.props
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
this.setState({
range: this.state.range + 1
})
}
}
render () {
const { notes, index } = this.props
const notesList = notes
.slice(0, 10 + 10 * this.state.range)
.map((note, _index) => {
const isActive = (index === _index)
const key = `${note.storage}-${note.key}`
const dateDisplay = moment(note.updatedAt).fromNow()
return (
<NoteItem
isActive={isActive}
note={note}
dateDisplay={dateDisplay}
key={key}
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
/>
)
})
return (
<div className={this.props.className}
onScroll={(e) => this.handleScroll(e)}
ref='root'
>
{notesList}
</div>
)
}
}
NoteList.propTypes = {
}
export default NoteList

View File

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

View File

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

View File

@@ -1,357 +0,0 @@
import PropTypes from 'prop-types'
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import _ from 'lodash'
import store from './store'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FinderMain.styl'
import StorageSection from './StorageSection'
import NoteList from './NoteList'
import NoteDetail from './NoteDetail'
import SideNavFilter from 'browser/components/SideNavFilter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
require('!!style!css!stylus?sourceMap!../main/global.styl')
require('../lib/customMeta')
require('./ipcClient.js')
const electron = require('electron')
const { remote } = electron
const { Menu } = remote
function hideFinder () {
const finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'win32') {
finderWindow.blur()
finderWindow.hide()
}
if (global.process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
}
remote.getCurrentWindow().hide()
}
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
class FinderMain extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
index: 0,
filter: {
includeSnippet: true,
includeMarkdown: false,
type: 'ALL',
storage: null,
folder: null
}
}
this.focusHandler = (e) => this.handleWindowFocus(e)
this.blurHandler = (e) => this.handleWindowBlur(e)
}
componentDidMount () {
this.refs.search.focus()
window.addEventListener('focus', this.focusHandler)
window.addEventListener('blur', this.blurHandler)
}
componentWillUnmount () {
window.removeEventListener('focus', this.focusHandler)
window.removeEventListener('blur', this.blurHandler)
}
handleWindowFocus (e) {
this.refs.search.focus()
}
handleWindowBlur (e) {
this.setState({
search: ''
})
}
handleKeyDown (e) {
this.refs.search.focus()
if (e.keyCode === 9) {
if (e.shiftKey) {
this.refs.detail.selectPriorSnippet()
} else {
this.refs.detail.selectNextSnippet()
}
e.preventDefault()
}
if (e.keyCode === 38) {
this.selectPrevious()
e.preventDefault()
}
if (e.keyCode === 40) {
this.selectNext()
e.preventDefault()
}
if (e.keyCode === 13) {
this.refs.detail.saveToClipboard()
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('COPY_FINDER')
hideFinder()
e.preventDefault()
}
if (e.keyCode === 27) {
hideFinder()
e.preventDefault()
}
if (e.keyCode === 91 || e.metaKey) {
return
}
}
handleSearchChange (e) {
this.setState({
search: e.target.value,
index: 0
})
}
selectArticle (article) {
this.setState({currentArticle: article})
}
selectPrevious () {
if (this.state.index > 0) {
this.setState({
index: this.state.index - 1
})
}
}
selectNext () {
if (this.state.index < this.noteCount - 1) {
this.setState({
index: this.state.index + 1
})
}
}
handleOnlySnippetCheckboxChange (e) {
const { filter } = this.state
filter.includeSnippet = e.target.checked
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleOnlyMarkdownCheckboxChange (e) {
const { filter } = this.state
filter.includeMarkdown = e.target.checked
this.refs.list.resetScroll()
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleAllNotesButtonClick (e) {
const { filter } = this.state
filter.type = 'ALL'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStarredButtonClick (e) {
const { filter } = this.state
filter.type = 'STARRED'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStorageButtonClick (e, storage) {
const { filter } = this.state
filter.type = 'STORAGE'
filter.storage = storage
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleFolderButtonClick (e, storage, folder) {
const { filter } = this.state
filter.type = 'FOLDER'
filter.storage = storage
filter.folder = folder
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleNoteClick (e, index) {
this.setState({
index
}, () => {
this.refs.search.focus()
})
}
render () {
const { data, config } = this.props
const { filter, search } = this.state
const storageList = []
for (const key in data.storageMap) {
const storage = data.storageMap[key]
const item = (
<StorageSection
filter={filter}
storage={storage}
key={storage.key}
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
/>
)
storageList.push(item)
}
let notes = []
let noteIds
switch (filter.type) {
case 'STORAGE':
noteIds = data.storageNoteMap[filter.storage]
break
case 'FOLDER':
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
break
case 'STARRED':
noteIds = data.starredSet
}
if (noteIds != null) {
noteIds.forEach((id) => {
notes.push(data.noteMap[id])
})
} else {
for (const key in data.noteMap) {
notes.push(data.noteMap[key])
}
}
if (!filter.includeSnippet && filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
} else if (filter.includeSnippet && !filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
}
if (search.trim().length > 0) {
const needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
notes = notes.filter((note) => note.title.match(needle))
}
notes = notes
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
const activeNote = notes[this.state.index]
this.noteCount = notes.length
return (
<div className='Finder'
styleName='root'
ref='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={search}
placeholder='Search...'
onChange={(e) => this.handleSearchChange(e)}
/>
</div>
<div styleName='result'>
<div styleName='result-nav'>
<div styleName='result-nav-filter'>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeSnippet}
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
/> Only Snippets</label>
</div>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeMarkdown}
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
/> Only Markdown</label>
</div>
</div>
<SideNavFilter
isHomeActive={filter.type === 'ALL'}
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
isStarredActive={filter.type === 'STARRED'}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
/>
<div styleName='result-nav-storageList'>
{storageList}
</div>
</div>
<NoteList styleName='result-list'
storageMap={data.storageMap}
notes={notes}
ref='list'
search={search}
index={this.state.index}
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
/>
<div styleName='result-detail'>
<NoteDetail
note={activeNote}
config={config}
ref='detail'
/>
</div>
</div>
</div>
)
}
}
FinderMain.propTypes = {
dispatch: PropTypes.func
}
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
function refreshData () {
// let data = dataStore.getData(true)
}
ReactDOM.render((
<Provider store={store}>
<Finder />
</Provider>
), document.getElementById('content'), function () {
refreshData()
})

View File

@@ -1,124 +0,0 @@
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app, Menu } = remote
const path = require('path')
const store = require('./store')
const consts = require('browser/lib/consts')
nodeIpc.config.id = 'finder'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
function killFinder () {
const finderWindow = remote.getCurrentWindow()
finderWindow.removeAllListeners()
if (global.process.platform === 'darwin') {
// Only OSX has another app process.
nodeIpc.of.node.emit('quit-from-finder')
} else {
finderWindow.close()
}
}
function toggleFinder () {
const finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'darwin') {
if (finderWindow.isVisible()) {
finderWindow.hide()
Menu.sendActionToFirstResponder('hide:')
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
}
} else {
if (finderWindow.isVisible()) {
finderWindow.blur()
finderWindow.hide()
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
finderWindow.focus()
}
}
}
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
console.log(err)
})
nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully')
})
nodeIpc.of.node.on('disconnect', function () {
console.log('disconnected')
})
nodeIpc.of.node.on('open-finder', function () {
toggleFinder()
})
ipcRenderer.on('open-finder-from-tray', function () {
toggleFinder()
})
ipcRenderer.on('open-main-from-tray', function () {
nodeIpc.of.node.emit('open-main-from-finder')
})
ipcRenderer.on('quit-from-tray', function () {
nodeIpc.of.node.emit('quit-from-finder')
killFinder()
})
nodeIpc.of.node.on('throttle-data', function (payload) {
console.log('Received data from Main renderer')
store.default.dispatch({
type: 'THROTTLE_DATA',
data: payload
})
})
nodeIpc.of.node.on('config-renew', function (payload) {
const { config } = payload
if (config.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else if (config.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else {
document.body.setAttribute('data-theme', 'default')
}
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
editorTheme.setAttribute('id', 'editorTheme')
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
if (config.editor.theme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
store.default.dispatch({
type: 'SET_CONFIG',
config: config
})
})
nodeIpc.of.node.on('quit-finder-app', function () {
nodeIpc.of.node.emit('quit-finder-app-confirm')
killFinder()
})
}
)
const ipc = {}
module.exports = ipc

View File

@@ -1,51 +0,0 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
const defaultData = {
storageMap: {},
noteMap: {},
starredSet: [],
storageNoteMap: {},
folderNoteMap: {},
tagNoteMap: {}
}
function data (state = defaultData, action) {
switch (action.type) {
case 'THROTTLE_DATA':
console.log(action)
state = action.data
}
return state
}
function config (state = DEFAULT_CONFIG, action) {
switch (action.type) {
case 'INIT_CONFIG':
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
const reducer = combineReducers({
data,
config,
routing: routerReducer
})
const store = createStore(reducer)
export default store

View File

@@ -2,7 +2,7 @@ const { remote } = require('electron')
const { Menu, MenuItem } = remote
function popup (templates) {
let menu = new Menu()
const menu = new Menu()
templates.forEach((item) => {
menu.append(new MenuItem(item))
})

View File

@@ -1,3 +1,5 @@
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})

View File

@@ -5,7 +5,7 @@ export function getTodoStatus (content) {
splitted.forEach((line) => {
const trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[\s|x\] ./)) {
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
numberOfTodo++
}
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {

9
browser/lib/i18n.js Normal file
View File

@@ -0,0 +1,9 @@
// load package for localization
const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale
locales: ['en', 'sq', 'zh-CN', 'zh-TW', 'da', 'fr', 'de', 'hu', 'ja', 'ko', 'no', 'pl', 'pt', 'es'],
extension: '.json',
devMode: false
})
export default i18n

View File

@@ -1,7 +1,11 @@
const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4')
module.exports = function (length) {
if (!_.isFinite(length)) length = 10
module.exports = function (uuid) {
if (typeof uuid === typeof true && uuid) {
return uuidv4()
}
const length = 10
return crypto.randomBytes(length).toString('hex')
}

View File

@@ -0,0 +1,23 @@
'use strict'
import sanitizeHtml from 'sanitize-html'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => {
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
if (state.tokens[tokenIdx].type === 'html_block') {
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
}
if (state.tokens[tokenIdx].type === 'inline') {
const inlineTokens = state.tokens[tokenIdx].children
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
if (inlineTokens[childIdx].type === 'html_inline') {
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
}
}
}
}
})
}

View File

@@ -1,157 +1,244 @@
import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import {lastFindInArray} from './utils'
// FIXME We should not depend on global variable.
const katex = window.katex
function createGutter (str) {
const lc = (str.match(/\n/g) || []).length
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
const lines = []
for (let i = 1; i <= lc; i++) {
for (let i = firstLineNumber; i <= lastLineNumber; i++) {
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
}
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
}
var md = markdownit({
typographer: true,
linkify: true,
html: true,
xhtmlOut: true,
breaks: true,
highlight: function (str, lang) {
if (lang === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
if (lang === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code">' +
createGutter(str) +
'<code class="' + lang + '">' +
str +
'</code></pre>'
}
})
md.use(emoji, {
shortcuts: {}
})
md.use(math, {
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), {displayMode: true})
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-imsize'))
md.use(require('markdown-it-footnote'))
md.use(require('markdown-it-multimd-table'))
md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
md.use(require('markdown-it-kbd'))
md.use(require('markdown-it-plantuml'))
class Markdown {
constructor (options = {}) {
const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
linkify: true,
html: true,
xhtmlOut: true,
breaks: true,
highlight: function (str, lang) {
const delimiter = ':'
const langInfo = lang.split(delimiter)
const langType = langInfo[0]
const fileName = langInfo[1] || ''
const firstLineNumber = parseInt(langInfo[2], 10)
// Override task item
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token
let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
const endLine = state.lineMax
if (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code CodeMirror">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
},
sanitize: 'STRICT'
}
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions)
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = ['iframe', 'input', 'b',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
]
const allowedAttributes = [
'abbr', 'accept', 'accept-charset',
'accesskey', 'action', 'align', 'alt', 'axis',
'border', 'cellpadding', 'cellspacing', 'char',
'charoff', 'charset', 'checked',
'clear', 'cols', 'colspan', 'color',
'compact', 'coords', 'datetime', 'dir',
'disabled', 'enctype', 'for', 'frame',
'headers', 'height', 'hreflang',
'hspace', 'ismap', 'label', 'lang',
'maxlength', 'media', 'method',
'multiple', 'name', 'nohref', 'noshade',
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
'rows', 'rowspan', 'rules', 'scope',
'selected', 'shape', 'size', 'span',
'start', 'summary', 'tabindex', 'target',
'title', 'type', 'usemap', 'valign', 'value',
'vspace', 'width', 'itemprop'
]
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
if (updatedOptions.sanitize === 'ALLOW_STYLES') {
allowedTags.push('style')
allowedAttributes.push('style')
}
// Sanitize use rinput before other plugins
this.md.use(sanitize, {
allowedTags,
allowedAttributes: {
'*': allowedAttributes,
'a': ['href'],
'div': ['itemscope', 'itemtype'],
'blockquote': ['cite'],
'del': ['cite'],
'ins': ['cite'],
'q': ['cite'],
'img': ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
},
allowedIframeHostnames: ['www.youtube.com']
})
}
if (terminate) { break }
this.md.use(emoji, {
shortcuts: {}
})
this.md.use(math, {
inlineOpen: config.preview.latexInlineOpen,
inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
this.md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
})
this.md.use(require('markdown-it-kbd'))
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
)
return `http://www.plantuml.com/plantuml/svg/${zippedCode}`
}
})
// Override task item
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token
let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph')
const endLine = state.lineMax
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
}
}
if (terminate) { break }
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [startLine, state.line]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open')
if (liToken) {
if (!liToken.attrs) {
liToken.attrs = []
}
liToken.attrs.push(['class', 'taskListItem'])
}
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [startLine, state.line]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
const result = originalRender.call(this.md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = this.md
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [ startLine, state.line ]
if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [ startLine, state.line ]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
const originalRender = md.renderer.render
md.renderer.render = function render (tokens, options, env) {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
const result = originalRender.call(md.renderer, tokens, options, env)
return result
}
// FIXME We should not depend on global variable.
window.md = md
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
}
const markdown = {
render: function markdown (content) {
render (content) {
if (!_.isString(content)) content = ''
const renderedContent = md.render(content)
return renderedContent
},
normalizeLinkText
return this.md.render(content)
}
normalizeLinkText (linkText) {
return this.md.normalizeLinkText(linkText)
}
}
export default markdown
export default Markdown

11
browser/lib/utils.js Normal file
View File

@@ -0,0 +1,11 @@
export function lastFindInArray (array, callback) {
for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) {
return array[i]
}
}
}
export default {
lastFindInArray
}

View File

@@ -20,5 +20,13 @@
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
border-left 1px solid $ui-dark-borderColor
.empty-message
color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
border-left 1px solid $ui-solarized-dark-borderColor
.empty-message
color $ui-solarized-dark-text-color

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component {
constructor (props) {
@@ -249,7 +250,7 @@ class FolderSelect extends React.Component {
<input styleName='search-input'
ref='search'
value={this.state.search}
placeholder='Folder...'
placeholder={i18n.__('Folder...')}
onChange={(e) => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}

View File

@@ -3,20 +3,14 @@
border solid 1px transparent
vertical-align middle
border-radius 2px
height 30px
transition 0.15s
user-select none
margin-right 10px
&:hover
background-color $ui-button--hover-backgroundColor
.root--search, .root--focus
@extend .root
background-color $ui-noteDetail-backgroundColor = #fff
border-color $ui-input--focus-borderColor
width 154px
height 30px
&:hover
border-color $ui-input--focus-borderColor = #fff
.idle
position relative

View File

@@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const FullscreenButton = ({
onClick
}) => (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button>
)
FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(FullscreenButton, styles)

View File

@@ -0,0 +1,22 @@
.control-fullScreenButton
top 80px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 70px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n'
const InfoButton = ({
onClick
@@ -10,6 +11,7 @@ const InfoButton = ({
onClick={(e) => onClick(e)}
>
<img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>{i18n.__('Info')}</span>
</button>
)

View File

@@ -1,7 +1,21 @@
.control-infoButton
top 10px
margin-bottom 10px
topBarButtonLight()
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 20px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.infoButton
padding 0px

View File

@@ -2,77 +2,98 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
const InfoPanel = ({
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, wordCount, letterCount, type, print
}) => (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
</div>
class InfoPanel extends React.Component {
copyNoteLink () {
const {noteLink} = this.props
this.refs.noteLink.select()
copy(noteLink)
}
<hr />
{type === 'SNIPPET_NOTE'
? ''
: <div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>Words</p>
render () {
const {
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
} = this.props
return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>Letters</p>
<hr />
{type === 'SNIPPET_NOTE'
? ''
: <div styleName='count-wrap'>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
</div>
<div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div>
</div>
}
{type === 'SNIPPET_NOTE'
? ''
: <hr />
}
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div>
<div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' />
</button>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
</div>
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<i className='fa fa-html5' />
<p>{i18n.__('.html')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print' />
<p>{i18n.__('Print')}</p>
</button>
</div>
</div>
}
{type === 'SNIPPET_NOTE'
? ''
: <hr />
}
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
</div>
<div>
<p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
</div>
<div>
<input styleName='infoPanel-noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<p styleName='infoPanel-sub'>NOTE LINK</p>
</div>
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o fa-fw' />
<p>.md</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o fa-fw' />
<p>.txt</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print fa-fw' />
<p>Print</p>
</button>
</div>
</div>
)
)
}
}
InfoPanel.propTypes = {
storageName: PropTypes.string.isRequired,
@@ -82,6 +103,7 @@ InfoPanel.propTypes = {
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired,

View File

@@ -11,11 +11,10 @@
.control-infoButton-panel
z-index 200
margin-top 0px
right 0
right 25px
position absolute
padding 20px 25px 0 25px
width 300px
height 350px
overflow auto
background-color $ui-noteList-backgroundColor
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
@@ -41,12 +40,12 @@
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)
border-radius 2px
.count-wrap
.count-wrap
display flex
position relative
width 100%
.count-number
.count-number
position relative
display block
width 50%
@@ -70,26 +69,41 @@
color $ui-text-color
.infoPanel-sub
font-size 14px
font-size 12px
font-weight 600
color $ui-inactive-text-color
padding-bottom 8px
.infoPanel-noteLink
padding-right 5px
width 200px
width 210px
height 25px
margin-bottom 6px
margin 6px 0
.infoPanel-copyButton
outline none
font-size 16px
color #A0A0A0
background-color transparent
border none
margin 0 5px
border-radius 5px
cursor pointer
&:hover
transition 0.2s
background-color alpha($ui-button--hover-backgroundColor, 30%)
color $ui-inactive-text-color
.infoPanel-trash
color #EA4447
font-weight 600
color #EA4447
font-weight 600
font-size 14px
width 70px
background-color rgba(226,33,113,0.1)
border none
border none
outline none
border-radius 2px
margin-right 5px
border-radius 2px
margin-right 5px
padding 2px 5px
[id=export-wrap]
@@ -160,4 +174,44 @@ body[data-theme="dark"]
p
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.control-infoButton-panel
background-color $ui-solarized-dark-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-solarized-ark-noteList-backgroundColor
.modification-date
color $ui-solarized-ark-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-solarized-dark-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-solarized-ark-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-dark-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-ark-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-solarized-ark-text-color

View File

@@ -2,46 +2,52 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
}) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div>
<p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div>
<hr />
<div>
<p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p>
<p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div>
<div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div>
<div>
<p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p>
<p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div>
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o fa-fw' />
<i className='fa fa-file-code-o' />
<p>.md</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o fa-fw' />
<i className='fa fa-file-text-o' />
<p>.txt</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<i className='fa fa-html5' />
<p>.html</p>
</button>
<button styleName='export--unable'>
<i className='fa fa-file-pdf-o fa-fw' />
<i className='fa fa-file-pdf-o' />
<p>.pdf</p>
</button>
</div>
@@ -54,7 +60,8 @@ InfoPanelTrashed.propTypes = {
updatedAt: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired
}
export default CSSModules(InfoPanelTrashed, styles)

175
browser/main/Detail/MarkdownNoteDetail.js Normal file → Executable file
View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownNoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import MarkdownSplitEditor from 'browser/components/MarkdownSplitEditor'
import TodoListPercentage from 'browser/components/TodoListPercentage'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
@@ -15,19 +16,19 @@ import StatusBar from '../StatusBar'
import _ from 'lodash'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import ConfigManager from 'browser/main/lib/ConfigManager'
import TrashButton from './TrashButton'
import FullscreenButton from './FullscreenButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton'
import ToggleModeButton from './ToggleModeButton'
import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
const electron = require('electron')
const { remote } = electron
const { dialog } = remote
class MarkdownNoteDetail extends React.Component {
constructor (props) {
super(props)
@@ -39,7 +40,8 @@ class MarkdownNoteDetail extends React.Component {
content: ''
}, props.note),
isLockButtonShown: false,
isLocked: false
isLocked: false,
editorType: props.config.editor.type
}
this.dispatchTimer = null
@@ -67,24 +69,26 @@ class MarkdownNoteDetail extends React.Component {
}
componentWillUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
if (this.saveQueue != null) this.saveNow()
}
componentDidUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
handleUpdateTag () {
const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value
this.updateNote(note)
}
handleChange (e) {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
if (this.refs.tags) note.tags = this.refs.tags.value
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
note.updatedAt = new Date()
this.updateNote(note)
}
this.setState({
note
}, () => {
updateNote (note) {
note.updatedAt = new Date()
this.setState({note}, () => {
this.save()
})
}
@@ -135,7 +139,7 @@ class MarkdownNoteDetail extends React.Component {
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
key: newNote.key
}
})
this.setState({
@@ -170,41 +174,45 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('export:save-text')
}
exportAsHtml () {
ee.emit('export:save-html')
}
handleTrashButtonClick (e) {
const { note } = this.state
const { isTrashed } = note
const { confirmDeletion } = this.props
if (isTrashed) {
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
})
if (confirmDeletion(true)) {
const {note, dispatch} = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:next', dispatchHandler)
})
.then(() => ee.emit('list:next'))
}
} else {
note.isTrashed = true
if (confirmDeletion()) {
note.isTrashed = true
this.setState({
note
}, () => {
this.save()
})
this.setState({
note
}, () => {
this.save()
})
ee.emit('list:next')
}
}
ee.emit('list:next')
}
handleUndoButtonClick (e) {
@@ -233,7 +241,7 @@ class MarkdownNoteDetail extends React.Component {
}
getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
}
handleDeleteKeyDown (e) {
@@ -262,9 +270,42 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('print')
}
render () {
const { data, config, location } = this.props
handleSwitchMode (type) {
this.setState({ editorType: type }, () => {
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
ConfigManager.set(newConfig)
})
}
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') {
return <MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={note.content}
storageKey={note.storage}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
return <MarkdownSplitEditor
ref='content'
config={config}
value={note.content}
storageKey={note.storage}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
}
}
render () {
const { data, location } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
const folderKey = note.folder
@@ -281,10 +322,7 @@ class MarkdownNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'>
<div styleName='info-left'>
<i styleName='undo-button'
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -296,6 +334,7 @@ class MarkdownNoteDetail extends React.Component {
folderName={currentOption.folder.name}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
/>
@@ -316,17 +355,12 @@ class MarkdownNoteDetail extends React.Component {
<TagSelect
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
<TodoListPercentage
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
onChange={this.handleUpdateTag.bind(this)}
/>
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
@@ -340,6 +374,7 @@ class MarkdownNoteDetail extends React.Component {
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img styleName='iconInfo' src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
return (
@@ -347,22 +382,23 @@ class MarkdownNoteDetail extends React.Component {
)
})()}
<button styleName='control-fullScreenButton'
onMouseDown={(e) => this.handleFullScreenButton(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
</button>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
wordCount={note.content.split(' ').length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
@@ -380,15 +416,7 @@ class MarkdownNoteDetail extends React.Component {
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={this.state.note.content}
storageKey={this.state.note.storage}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
{this.renderEditor()}
</div>
<StatusBar
@@ -409,7 +437,8 @@ MarkdownNoteDetail.propTypes = {
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
ignorePreviewPointerEvents: PropTypes.bool,
confirmDeletion: PropTypes.bool.isRequired
}
export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -7,30 +7,42 @@
background-color $ui-noteDetail-backgroundColor
box-shadow none
padding 20px 40px
overflow hidden
.lock-button
padding-bottom 3px
.control-lockButton
top 160px
margin-bottom 10px
topBarButtonLight()
topBarButtonRight()
position absolute
right 225px
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 35px
right -10px
width 50px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.trashed-infopanel
top 40px
position relative
.control-fullScreenButton
top 80px
topBarButtonLight()
.body
absolute left right
left 0
right 0
top $info-height + $info-margin-under-border
bottom $statusBar-height
margin 0 45px
margin 0 30px
.body-noteEditor
absolute top bottom left right
@@ -43,7 +55,7 @@ body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
border none
border-left 1px solid $ui-dark-borderColor
.control-lockButton
topBarButtonDark()
@@ -53,3 +65,9 @@ body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
body[data-theme="solarized-dark"]
.root
border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor

View File

@@ -1,6 +1,6 @@
@import('DetailVars')
$info-height = 50px
$info-height = 60px
$info-margin-under-border = 30px
.info
@@ -8,11 +8,11 @@ $info-margin-under-border = 30px
left 0
right 0
height $info-height
border-bottom 1px solid #eee
background-color $ui-noteDetail-backgroundColor
width 100%
display flex
align-items center
padding 0 20px
.info-left
padding 0 10px
@@ -20,7 +20,6 @@ $info-margin-under-border = 30px
display flex
align-items center
.info-left-top-folderSelect
display flex
align-items center
@@ -45,12 +44,9 @@ $info-margin-under-border = 30px
color $ui-button--color
.info-right
position absolute
right 40px
top 60px
bottom 1px
padding-left 30px
z-index 101
display inline-flex
margin-top 3px
.undo-button
width 34px
@@ -95,4 +91,10 @@ body[data-theme="dark"]
background-color $ui-dark-noteDetail-backgroundColor
.undo-button
topBarButtonDark()
topBarButtonDark()
body[data-theme="solarized-dark"]
.info
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({
onClick
@@ -10,6 +11,7 @@ const PermanentDeleteButton = ({
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>
)

View File

@@ -0,0 +1,22 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n'
const RestoreButton = ({
onClick
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
<i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span>
</button>
)
RestoreButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(RestoreButton, styles)

View File

@@ -0,0 +1,22 @@
.control-restoreButton
top 115px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
left 25px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-restoreButton
topBarButtonDark()

View File

@@ -8,22 +8,25 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import {hashHistory} from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import SnippetTab from 'browser/components/SnippetTab'
import StatusBar from '../StatusBar'
import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager'
import _ from 'lodash'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import {findNoteTitle} from 'browser/lib/findNoteTitle'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
function pass (name) {
switch (name) {
@@ -51,12 +54,30 @@ class SnippetNoteDetail extends React.Component {
this.state = {
isMovingNote: false,
snippetIndex: 0,
showArrows: false,
enableLeftArrow: false,
enableRightArrow: false,
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
})
}
this.scrollToNextTabThreshold = 0.7
}
componentDidMount () {
const visibleTabs = this.visibleTabs
const allTabs = this.allTabs
if (visibleTabs.offsetWidth < allTabs.scrollWidth) {
this.setState({
showArrows: visibleTabs.offsetWidth < allTabs.scrollWidth,
enableRightArrow: allTabs.offsetLeft !== visibleTabs.offsetWidth - allTabs.scrollWidth,
enableLeftArrow: allTabs.offsetLeft !== 0
})
}
}
componentWillReceiveProps (nextProps) {
@@ -76,6 +97,7 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + index].reload()
})
if (this.refs.tags) this.refs.tags.reset()
this.setState(this.getArrowsState())
})
}
}
@@ -145,7 +167,7 @@ class SnippetNoteDetail extends React.Component {
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
key: newNote.key
}
})
this.setState({
@@ -175,38 +197,38 @@ class SnippetNoteDetail extends React.Component {
handleTrashButtonClick (e) {
const { note } = this.state
const { isTrashed } = note
const { confirmDeletion } = this.props
if (isTrashed) {
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
detail: 'This will permanently remove this note.',
buttons: ['Confirm', 'Cancel']
})
if (dialogueButtonIndex === 1) return
const { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
})
if (confirmDeletion(true)) {
const {note, dispatch} = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
const dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:next', dispatchHandler)
})
.then(() => ee.emit('list:next'))
}
} else {
note.isTrashed = true
if (confirmDeletion()) {
note.isTrashed = true
this.setState({
note
}, () => {
this.save()
})
this.setState({
note
}, () => {
this.save()
})
ee.emit('list:next')
}
}
ee.emit('list:next')
}
handleUndoButtonClick (e) {
@@ -226,6 +248,51 @@ class SnippetNoteDetail extends React.Component {
ee.emit('editor:fullscreen')
}
handleTabMoveLeftButtonClick (e) {
{
const left = this.visibleTabs.scrollLeft
const tabs = this.allTabs.querySelectorAll('div')
const lastVisibleTab = Array.from(tabs).find((tab) => {
return tab.offsetLeft + tab.offsetWidth >= left
})
if (lastVisibleTab) {
const visiblePart = lastVisibleTab.offsetWidth + lastVisibleTab.offsetLeft - left
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
const scrollToTab = (isFullyVisible && lastVisibleTab.previousSibling)
? lastVisibleTab.previousSibling
: lastVisibleTab
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
this.moveToTab(scrollToTab)
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'start', block: 'start'})
}
}
}
handleTabMoveRightButtonClick (e) {
const left = this.visibleTabs.scrollLeft
const width = this.visibleTabs.offsetWidth
const tabs = this.allTabs.querySelectorAll('div')
const lastVisibleTab = Array.from(tabs).find((tab) => {
return tab.offsetLeft + tab.offsetWidth >= width + left
})
if (lastVisibleTab) {
const visiblePart = width + left - lastVisibleTab.offsetLeft
const isFullyVisible = visiblePart > lastVisibleTab.offsetWidth * this.scrollToNextTabThreshold
const scrollToTab = (isFullyVisible && lastVisibleTab.nextSibling)
? lastVisibleTab.nextSibling
: lastVisibleTab
// FIXME use `scrollIntoView()` instead of custom method after update to Electron2.0 (with Chrome 61 its possible animate the scroll)
this.moveToTab(scrollToTab)
// scrollToTab.scrollIntoView({behavior: 'smooth', inline: 'end', block: 'end'})
}
}
handleTabPlusButtonClick (e) {
this.addSnippet()
}
@@ -236,14 +303,35 @@ class SnippetNoteDetail extends React.Component {
})
}
handleTabDragStart (e, index) {
e.dataTransfer.setData('text/plain', index)
}
handleTabDrop (e, index) {
const oldIndex = parseInt(e.dataTransfer.getData('text'))
const snippets = this.state.note.snippets.slice()
const draggedSnippet = snippets[oldIndex]
snippets[oldIndex] = snippets[index]
snippets[index] = draggedSnippet
const snippetIndex = index
const note = Object.assign({}, this.state.note, {snippets})
this.setState({ note, snippetIndex }, () => {
this.save()
this.refs['code-' + index].reload()
this.refs['code-' + oldIndex].reload()
})
}
handleTabDeleteButtonClick (e, index) {
if (this.state.note.snippets.length > 1) {
if (this.state.note.snippets[index].content.trim().length > 0) {
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a snippet',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Delete a snippet'),
detail: i18n.__('This work cannot be undone.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (dialogIndex === 0) {
this.deleteSnippetByIndex(index)
@@ -264,6 +352,21 @@ class SnippetNoteDetail extends React.Component {
this.setState({ note, snippetIndex }, () => {
this.save()
this.refs['code-' + this.state.snippetIndex].reload()
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
console.log('no need for arrows')
this.moveTabBarBy(0)
} else {
const lastTab = this.allTabs.lastChild
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
console.log('need to scroll')
const width = this.visibleTabs.offsetWidth
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
} else {
this.setState(this.getArrowsState())
}
}
})
}
@@ -320,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
handleKeyDown (e) {
switch (e.keyCode) {
// tab key
case 9:
if (e.ctrlKey && !e.shiftKey) {
e.preventDefault()
@@ -332,6 +436,7 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor()
}
break
// L key
case 76:
{
const isSuper = global.process.platform === 'darwin'
@@ -343,6 +448,7 @@ class SnippetNoteDetail extends React.Component {
}
}
break
// T key
case 84:
{
const isSuper = global.process.platform === 'darwin'
@@ -359,7 +465,7 @@ class SnippetNoteDetail extends React.Component {
handleModeButtonClick (e, index) {
const menu = new Menu()
CodeMirror.modeInfo.forEach((mode) => {
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
menu.append(new MenuItem({
label: mode.name,
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
@@ -434,6 +540,51 @@ class SnippetNoteDetail extends React.Component {
this.refs.description.focus()
}
moveToTab (tab) {
const easeOutCubic = t => (--t) * t * t + 1
const startScrollPosition = this.visibleTabs.scrollLeft
const animationTiming = 300
const scrollMoreCoeff = 1.4 // introduce coefficient, because we want to scroll a bit further to see next tab
let scrollBy = (tab.offsetLeft - startScrollPosition)
if (tab.offsetLeft > startScrollPosition) {
// if tab is on the right side and we want to show the whole tab in visible area,
// we need to include width of the tab and visible area in the formula
// ___________________________________________
// |____|_______|________|________|_show_this_|
// ↑_____________________↑
// visible area
scrollBy += (tab.offsetWidth - this.visibleTabs.offsetWidth)
}
let startTime = null
const scrollAnimation = time => {
startTime = startTime || time
const elapsed = (time - startTime) / animationTiming
this.visibleTabs.scrollLeft = startScrollPosition + easeOutCubic(elapsed) * scrollBy * scrollMoreCoeff
if (elapsed < 1) {
window.requestAnimationFrame(scrollAnimation)
} else {
this.setState(this.getArrowsState())
}
}
window.requestAnimationFrame(scrollAnimation)
}
getArrowsState () {
const allTabs = this.allTabs
const visibleTabs = this.visibleTabs
const showArrows = visibleTabs.offsetWidth < allTabs.scrollWidth
const enableRightArrow = visibleTabs.scrollLeft !== allTabs.scrollWidth - visibleTabs.offsetWidth
const enableLeftArrow = visibleTabs.scrollLeft !== 0
return {showArrows, enableRightArrow, enableLeftArrow}
}
addSnippet () {
const { note } = this.state
@@ -444,10 +595,16 @@ class SnippetNoteDetail extends React.Component {
}])
const snippetIndex = note.snippets.length - 1
this.setState({
this.setState(Object.assign({
note,
snippetIndex
}, () => {
}, this.getArrowsState()), () => {
if (this.state.showArrows) {
const tabs = this.allTabs.querySelectorAll('div')
if (tabs) {
this.moveToTab(tabs[snippetIndex])
}
}
this.refs['tab-' + snippetIndex].startRenaming()
})
}
@@ -481,9 +638,9 @@ class SnippetNoteDetail extends React.Component {
showWarning () {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Sorry!',
detail: 'md/text import is available only a markdown note.',
buttons: ['OK']
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
buttons: [i18n.__('OK')]
})
}
@@ -511,6 +668,8 @@ class SnippetNoteDetail extends React.Component {
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
onRename={(name) => this.renameSnippetByIndex(index, name)}
isDeletable={note.snippets.length > 1}
onDragStart={(e) => this.handleTabDragStart(e, index)}
onDrop={(e) => this.handleTabDrop(e, index)}
/>
})
@@ -541,7 +700,10 @@ class SnippetNoteDetail extends React.Component {
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers}
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
@@ -562,10 +724,7 @@ class SnippetNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'>
<div styleName='info-left'>
<i styleName='undo-button'
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
</div>
<div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -579,6 +738,7 @@ class SnippetNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
/>
</div>
</div>
@@ -601,25 +761,27 @@ class SnippetNoteDetail extends React.Component {
/>
</div>
<div styleName='info-right'>
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='control-fullScreenButton'
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-sidebar.svg' />
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
<InfoButton
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`}
noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
@@ -645,16 +807,32 @@ class SnippetNoteDetail extends React.Component {
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
placeholder={i18n.__('Description...')}
value={this.state.note.description}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='tabList'>
<div styleName='list'>
{tabList}
<button styleName='tabButton'
hidden={!this.state.showArrows}
disabled={!this.state.enableLeftArrow}
onClick={(e) => this.handleTabMoveLeftButtonClick(e)}
>
<i className='fa fa-chevron-left' />
</button>
<div styleName='list' onScroll={(e) => { this.setState(this.getArrowsState()) }} ref={(tabs) => { this.visibleTabs = tabs }}>
<div styleName='allTabs' ref={(tabs) => { this.allTabs = tabs }}>
{tabList}
</div>
</div>
<button styleName='plusButton'
<button styleName='tabButton'
hidden={!this.state.showArrows}
disabled={!this.state.enableRightArrow}
onClick={(e) => this.handleTabMoveRightButtonClick(e)}
>
<i className='fa fa-chevron-right' />
</button>
<button styleName='tabButton'
onClick={(e) => this.handleTabPlusButtonClick(e)}
>
<i className='fa fa-plus' />
@@ -668,7 +846,7 @@ class SnippetNoteDetail extends React.Component {
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
>
{this.state.note.snippets[this.state.snippetIndex].mode == null
? 'Select Syntax...'
? i18n.__('Select Syntax...')
: this.state.note.snippets[this.state.snippetIndex].mode
}&nbsp;
<i className='fa fa-caret-down' />
@@ -705,7 +883,8 @@ SnippetNoteDetail.propTypes = {
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
ignorePreviewPointerEvents: PropTypes.bool,
confirmDeletion: PropTypes.bool.isRequired
}
export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -9,8 +9,7 @@
.body
absolute left right
left $snippet-note-detail-left-margin
right $snippet-note-detail-right-margin
margin 0 30px
top $info-height + $info-margin-under-border
bottom $statusBar-height
background-color $ui-noteDetail-backgroundColor
@@ -36,13 +35,26 @@
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
overflow hidden
.tabList .list
flex 1
display flex
overflow hidden
overflow-x scroll
position relative
.tabList .plusButton
&::-webkit-scrollbar {
display: none;
}
.allTabs
display flex
position relative
overflow visible
left 0
transition left 0.1s
.tabList .tabButton
navWhiteButtonColor()
width 30px
@@ -69,7 +81,22 @@
.control-fullScreenButton
top 80px
margin-bottom 10px
topBarButtonLight()
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 70px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="white"]
.root
@@ -78,7 +105,7 @@ body[data-theme="white"]
body[data-theme="dark"]
.root
border none
border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
@@ -109,3 +136,20 @@ body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
body[data-theme="solarized-dark"]
.root
border-left 1px solid $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteDetail-backgroundColor
.body
background-color $ui-solarized-dark-noteDetail-backgroundColor
.body .description textarea
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class StarButton extends React.Component {
constructor (props) {
@@ -46,14 +47,14 @@ class StarButton extends React.Component {
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Star')}</span>
</button>
)
}

View File

@@ -1,9 +1,25 @@
.root
top 45px
topBarButtonLight()
topBarButtonRight()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 115px
width 40px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.root--active
@extend .root

View File

@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
class TagSelect extends React.Component {
constructor (props) {
@@ -64,7 +65,8 @@ class TagSelect extends React.Component {
submitTag () {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
let { value } = this.props
const newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
if (newTag.length <= 0) {
this.setState({
@@ -136,7 +138,7 @@ class TagSelect extends React.Component {
<input styleName='newTag'
ref='newTag'
value={this.state.newTag}
placeholder='Add tag...'
placeholder={i18n.__('Add tag...')}
onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
onBlur={(e) => this.handleNewTagBlur(e)}

View File

@@ -6,6 +6,8 @@
width 100%
overflow-x scroll
white-space nowrap
margin-top 31px
position absolute
.root::-webkit-scrollbar
display none
@@ -63,4 +65,20 @@ body[data-theme="dark"]
.newTag
border-color none
background-color transparent
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.tag
background-color $ui-solarized-dark-tag-backgroundColor
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
.tag-label
color $ui-solarized-dark-text-color
.newTag
border-color none
background-color transparent
color $ui-solarized-dark-text-color

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
}
export default CSSModules(ToggleModeButton, styles)

View File

@@ -0,0 +1,58 @@
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n'
const TrashButton = ({
onClick
@@ -10,6 +11,7 @@ const TrashButton = ({
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Trash')}</span>
</button>
)

View File

@@ -1,11 +1,25 @@
.control-trashButton
top 120px
margin-bottom 10px
topBarButtonLight()
top 115px
topBarButtonRight()
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 50px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-trashButton--in-trash
top 60px
topBarButtonLight()
topBarButtonRight()
.trashButton
padding 0px

View File

@@ -7,6 +7,7 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
@@ -32,15 +33,32 @@ class Detail extends React.Component {
ee.off('detail:delete', this.deleteHandler)
}
confirmDeletion (permanent) {
if (this.props.config.ui.confirmDeletion || permanent) {
const electron = require('electron')
const { remote } = electron
const { dialog } = remote
const alertConfig = {
type: 'warning',
message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
return dialogueButtonIndex === 0
}
return true
}
render () {
const { location, data, config } = this.props
let note = null
if (location.query.key != null) {
const splitted = location.query.key.split('-')
const storageKey = splitted.shift()
const noteKey = splitted.shift()
note = data.noteMap.get(storageKey + '-' + noteKey)
const noteKey = location.query.key
note = data.noteMap.get(noteKey)
}
if (note == null) {
@@ -50,7 +68,7 @@ class Detail extends React.Component {
tabIndex='0'
>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new note</div>
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -64,6 +82,7 @@ class Detail extends React.Component {
<SnippetNoteDetail
note={note}
config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root'
{..._.pick(this.props, [
'dispatch',
@@ -80,6 +99,7 @@ class Detail extends React.Component {
<MarkdownNoteDetail
note={note}
config={config}
confirmDeletion={(permanent) => this.confirmDeletion(permanent)}
ref='root'
{..._.pick(this.props, [
'dispatch',

View File

@@ -10,10 +10,14 @@ import Detail from './Detail'
import dataApi from 'browser/main/lib/dataApi'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router'
import store from 'browser/main/store'
import i18n from 'browser/lib/i18n'
const path = require('path')
const electron = require('electron')
const { remote } = electron
class Main extends React.Component {
@@ -48,6 +52,91 @@ class Main extends React.Component {
}
}
init () {
dataApi
.addStorage({
name: 'My Storage',
path: path.join(remote.app.getPath('home'), 'Boostnote')
})
.then((data) => {
return data
})
.then((data) => {
if (data.storage.folders[0] != null) {
return data
} else {
return dataApi
.createFolder(data.storage.key, {
color: '#1278BD',
name: 'Default'
})
.then((_data) => {
return {
storage: _data.storage,
notes: data.notes
}
})
}
})
.then((data) => {
console.log(data)
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
notes: data.notes
})
const defaultSnippetNote = dataApi
.createNote(data.storage.key, {
type: 'SNIPPET_NOTE',
folder: data.storage.folders[0].key,
title: 'Snippet note example',
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
snippets: [
{
name: 'example.html',
mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
},
{
name: 'example.js',
mode: 'javascript',
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
}
]
})
.then((note) => {
store.dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
const defaultMarkdownNote = dataApi
.createNote(data.storage.key, {
type: 'MARKDOWN_NOTE',
folder: data.storage.folders[0].key,
title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
})
.then((note) => {
store.dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
return Promise.resolve(defaultSnippetNote)
.then(defaultMarkdownNote)
.then(() => data.storage)
})
.then((storage) => {
hashHistory.push('/storages/' + storage.key)
})
.catch((err) => {
throw err
})
}
componentDidMount () {
const { dispatch, config } = this.props
@@ -55,9 +144,42 @@ class Main extends React.Component {
document.body.setAttribute('data-theme', 'dark')
} else if (config.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else if (config.ui.theme === 'solarized-dark') {
document.body.setAttribute('data-theme', 'solarized-dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
if (config.ui.language === 'sq') {
i18n.setLocale('sq')
} else if (config.ui.language === 'zh-CN') {
i18n.setLocale('zh-CN')
} else if (config.ui.language === 'zh-TW') {
i18n.setLocale('zh-TW')
} else if (config.ui.language === 'da') {
i18n.setLocale('da')
} else if (config.ui.language === 'fr') {
i18n.setLocale('fr')
} else if (config.ui.language === 'de') {
i18n.setLocale('de')
} else if (config.ui.language === 'hu') {
i18n.setLocale('hu')
} else if (config.ui.language === 'ja') {
i18n.setLocale('ja')
} else if (config.ui.language === 'ko') {
i18n.setLocale('ko')
} else if (config.ui.language === 'no') {
i18n.setLocale('no')
} else if (config.ui.language === 'pl') {
i18n.setLocale('pl')
} else if (config.ui.language === 'pt') {
i18n.setLocale('pt')
} else if (config.ui.language === 'ru') {
i18n.setLocale('ru')
} else if (config.ui.language === 'es') {
i18n.setLocale('es')
} else {
i18n.setLocale('en')
}
// Reload all data
dataApi.init()
@@ -69,7 +191,7 @@ class Main extends React.Component {
})
if (data.storages.length < 1) {
modal.open(InitModal)
this.init()
}
})

View File

@@ -71,3 +71,7 @@ body[data-theme="dark"]
.control-newNoteButton-tooltip
darkTooltip()
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { dialog } = remote
@@ -56,9 +57,9 @@ class NewNoteButton extends React.Component {
}
}
if (storage == null) this.showMessageBox('No storage to create a note')
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note'))
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0]
if (folder == null) this.showMessageBox('No folder to create a note')
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note'))
return {
storage,
@@ -86,7 +87,7 @@ class NewNoteButton extends React.Component {
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
Make a Note {OSX ? '⌘' : '^'} + n
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span>
</button>
</div>

View File

@@ -88,4 +88,29 @@ body[data-theme="dark"]
.control-button--active
color $ui-dark-text-color
&:active
color $ui-dark-text-color
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
.control
background-color $ui-solarized-dark-noteList-backgroundColor
border-color $ui-solarized-dark-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-solarized-dark-text-color
.control-button
color $ui-solarized-dark-inactive-text-color
&:hover
color $ui-solarized-dark-text-color
.control-button--active
color $ui-solarized-dark-text-color
&:active
color $ui-solarized-dark-text-color

View File

@@ -1,3 +1,4 @@
/* global electron */
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
@@ -11,14 +12,16 @@ import NoteItem from 'browser/components/NoteItem'
import NoteItemSimple from 'browser/components/NoteItemSimple'
import searchFromNotes from 'browser/lib/search'
import fs from 'fs'
import path from 'path'
import { hashHistory } from 'react-router'
import markdown from 'browser/lib/markdown'
import { findNoteTitle } from 'browser/lib/findNoteTitle'
import store from 'browser/main/store'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt)
@@ -33,7 +36,7 @@ function sortByUpdatedAt (a, b) {
}
function findNoteByKey (notes, noteKey) {
return notes.find((note) => `${note.storage}-${note.key}` === noteKey)
return notes.find((note) => note.key === noteKey)
}
function findNotesByKeys (notes, noteKeys) {
@@ -41,7 +44,7 @@ function findNotesByKeys (notes, noteKeys) {
}
function getNoteKey (note) {
return `${note.storage}-${note.key}`
return note.key
}
class NoteList extends React.Component {
@@ -68,6 +71,11 @@ class NoteList extends React.Component {
this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this)
this.getNoteStorage = this.getNoteStorage.bind(this)
this.getNoteFolder = this.getNoteFolder.bind(this)
this.getViewType = this.getViewType.bind(this)
this.restoreNote = this.restoreNote.bind(this)
this.copyNoteLink = this.copyNoteLink.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
@@ -111,14 +119,27 @@ class NoteList extends React.Component {
componentDidUpdate (prevProps) {
const { location } = this.props
const { selectedNoteKeys } = this.state
const visibleNoteKeys = this.notes.map(note => note.key)
const note = this.notes[0]
const prevKey = prevProps.location.query.key
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
if (this.notes.length > 0 && location.query.key == null) {
if (note && location.query.key == null) {
const { router } = this.context
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
// A visible note is an active note
if (!selectedNoteKeys.includes(noteKey)) {
if (selectedNoteKeys.length === 1) selectedNoteKeys.pop()
selectedNoteKeys.push(noteKey)
ee.emit('list:moved')
}
router.replace({
pathname: location.pathname,
query: {
key: this.notes[0].storage + '-' + this.notes[0].key
key: noteKey
}
})
return
@@ -171,9 +192,8 @@ class NoteList extends React.Component {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let { selectedNoteKeys, shiftKeyDown } = this.state
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
let targetIndex = this.getTargetIndex()
@@ -199,9 +219,8 @@ class NoteList extends React.Component {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let { selectedNoteKeys, shiftKeyDown } = this.state
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -235,47 +254,47 @@ class NoteList extends React.Component {
return
}
const { router } = this.context
const { location } = this.props
let targetIndex = this.getTargetIndex()
if (targetIndex < 0) targetIndex = 0
const selectedNoteKeys = []
const nextNoteKey = this.getNoteKeyFromTargetIndex(targetIndex)
selectedNoteKeys.push(nextNoteKey)
this.focusNote(selectedNoteKeys, nextNoteKey)
const selectedNoteKeys = [noteHash]
this.focusNote(selectedNoteKeys, noteHash)
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
const { shiftKeyDown } = this.state
if (e.metaKey || e.ctrlKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
ee.emit('top:new-note')
}
// D key
if (e.keyCode === 68) {
e.preventDefault()
this.deleteNote()
}
// E key
if (e.keyCode === 69) {
e.preventDefault()
ee.emit('detail:focus')
}
if (e.keyCode === 38) {
// F or S key
if (e.keyCode === 70 || e.keyCode === 83) {
e.preventDefault()
ee.emit('top:focus-search')
}
// UP or K key
if (e.keyCode === 38 || e.keyCode === 75) {
e.preventDefault()
this.selectPriorNote()
}
if (e.keyCode === 40) {
// DOWN or J key
if (e.keyCode === 40 || e.keyCode === 74) {
e.preventDefault()
this.selectNextNote()
}
@@ -294,7 +313,7 @@ class NoteList extends React.Component {
getNotes () {
const { data, params, location } = this.props
if (location.pathname.match(/\/home/) || location.pathname.match(/\alltags/)) {
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
return allNotes
@@ -365,9 +384,10 @@ class NoteList extends React.Component {
}
handleNoteClick (e, uniqueKey) {
let { router } = this.context
let { location } = this.props
let { shiftKeyDown, selectedNoteKeys } = this.state
const { router } = this.context
const { location } = this.props
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
@@ -425,9 +445,9 @@ class NoteList extends React.Component {
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Sorry!',
detail: 'md/text import is available only a markdown note.',
buttons: ['OK', 'Cancel']
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
buttons: [i18n.__('OK'), i18n.__('Cancel')]
})
}
}
@@ -451,45 +471,107 @@ class NoteList extends React.Component {
this.handleNoteClick(e, uniqueKey)
}
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top'
const deleteLabel = 'Delete Note'
const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
const deleteLabel = i18n.__('Delete Note')
const cloneNote = i18n.__('Clone Note')
const restoreNote = i18n.__('Restore Note')
const copyNoteLink = i18n.__('Copy Note Link')
const publishLabel = i18n.__('Publish Blog')
const updateLabel = i18n.__('Update Blog')
const openBlogLabel = i18n.__('Open Blog')
const menu = new Menu()
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
if (!location.pathname.match(/\/starred|\/trash/)) {
menu.append(new MenuItem({
label: pinLabel,
click: this.pinToTop
}))
}
if (location.pathname.match(/\/trash/)) {
menu.append(new MenuItem({
label: restoreNote,
click: this.restoreNote
}))
}
menu.append(new MenuItem({
label: deleteLabel,
click: this.deleteNote
}))
menu.append(new MenuItem({
label: cloneNote,
click: this.cloneNote.bind(this)
}))
menu.append(new MenuItem({
label: copyNoteLink,
click: this.copyNoteLink(note)
}))
if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) {
menu.append(new MenuItem({
label: updateLabel,
click: this.publishMarkdown.bind(this)
}))
menu.append(new MenuItem({
label: openBlogLabel,
click: () => this.openBlog.bind(this)(note)
}))
} else {
menu.append(new MenuItem({
label: publishLabel,
click: this.publishMarkdown.bind(this)
}))
}
}
menu.popup()
}
pinToTop () {
updateSelectedNotes (updateFunc, cleanSelection = true) {
const { selectedNoteKeys } = this.state
const { dispatch } = this.props
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
if (!_.isFunction(updateFunc)) {
console.warn('Update function is not defined. No update will happen')
updateFunc = (note) => { return note }
}
Promise.all(
selectedNotes.map((note) => {
note.isPinned = !note.isPinned
return dataApi
.updateNote(note.storage, note.key, note)
})
)
.then((updatedNotes) => {
updatedNotes.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note
selectedNotes.map((note) => {
note = updateFunc(note)
return dataApi
.updateNote(note.storage, note.key, note)
})
})
)
.then((updatedNotes) => {
updatedNotes.forEach((note) => {
dispatch({
type: 'UPDATE_NOTE',
note
})
})
})
if (cleanSelection) {
this.selectNextNote()
}
}
pinToTop () {
this.updateSelectedNotes((note) => {
note.isPinned = !note.isPinned
return note
})
}
restoreNote () {
this.updateSelectedNotes((note) => {
note.isTrashed = false
return note
})
this.setState({ selectedNoteKeys: [] })
}
deleteNote () {
@@ -503,17 +585,15 @@ class NoteList extends React.Component {
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Confirm note deletion',
message: i18n.__('Confirm note deletion'),
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
buttons: ['Confirm', 'Cancel']
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (dialogueButtonIndex === 1) return
Promise.all(
selectedNoteKeys.map((uniqueKey) => {
const storageKey = uniqueKey.split('-')[0]
const noteKey = uniqueKey.split('-')[1]
selectedNotes.map((note) => {
return dataApi
.deleteNote(storageKey, noteKey)
.deleteNote(note.storage, note.key)
})
)
.then((data) => {
@@ -555,6 +635,153 @@ class NoteList extends React.Component {
this.setState({ selectedNoteKeys: [] })
}
cloneNote () {
const { selectedNoteKeys } = this.state
const { dispatch, location } = this.props
const { storage, folder } = this.resolveTargetFolder()
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
const eventName = firstNote.type === 'MARKDOWN_NOTE' ? 'ADD_MARKDOWN' : 'ADD_SNIPPET'
AwsMobileAnalyticsConfig.recordDynamicCustomEvent(eventName)
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
dataApi
.createNote(storage.key, {
type: firstNote.type,
folder: folder.key,
title: firstNote.title + ' copy',
content: firstNote.content
})
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
this.setState({
selectedNoteKeys: [note.key]
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.key}
})
})
}
copyNoteLink (note) {
const noteLink = `[${note.title}](:note:${note.key})`
return copy(noteLink)
}
save (note) {
const { dispatch } = this.props
dataApi
.updateNote(note.storage, note.key, note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
publishMarkdown () {
if (this.pendingPublish) {
clearTimeout(this.pendingPublish)
}
this.pendingPublish = setTimeout(() => {
this.publishMarkdownNow()
}, 1000)
}
publishMarkdownNow () {
const {selectedNoteKeys} = this.state
const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys)
const firstNote = selectedNotes[0]
const config = ConfigManager.get()
const {address, token, authMethod, username, password} = config.blog
let authToken = ''
if (authMethod === 'USER') {
authToken = `Basic ${window.btoa(`${username}:${password}`)}`
} else {
authToken = `Bearer ${token}`
}
const contentToRender = firstNote.content.replace(`# ${firstNote.title}`, '')
const markdown = new Markdown()
const data = {
title: firstNote.title,
content: markdown.render(contentToRender),
status: 'publish'
}
let url = ''
let method = ''
if (firstNote.blog && firstNote.blog.blogId) {
url = `${address}${WP_POST_PATH}/${firstNote.blog.blogId}`
method = 'PUT'
} else {
url = `${address}${WP_POST_PATH}`
method = 'POST'
}
// eslint-disable-next-line no-undef
fetch(url, {
method: method,
body: JSON.stringify(data),
headers: {
'Authorization': authToken,
'Content-Type': 'application/json'
}
}).then(res => res.json())
.then(response => {
if (_.isNil(response.link) || _.isNil(response.id)) {
return Promise.reject()
}
firstNote.blog = {
blogLink: response.link,
blogId: response.id
}
this.save(firstNote)
this.confirmPublish(firstNote)
})
.catch((error) => {
console.error(error)
this.confirmPublishError()
})
}
confirmPublishError () {
const { remote } = electron
const { dialog } = remote
const alertError = {
type: 'warning',
message: i18n.__('Publish Failed'),
detail: i18n.__('Check and update your blog setting and try again.'),
buttons: [i18n.__('Confirm')]
}
dialog.showMessageBox(remote.getCurrentWindow(), alertError)
}
confirmPublish (note) {
const buttonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Publish Succeeded'),
detail: `${note.title} is published at ${note.blog.blogLink}`,
buttons: [i18n.__('Confirm'), i18n.__('Open Blog')]
})
if (buttonIndex === 1) {
this.openBlog(note)
}
}
openBlog (note) {
const { shell } = electron
shell.openExternal(note.blog.blogLink)
}
importFromFile () {
const options = {
filters: [
@@ -584,22 +811,29 @@ class NoteList extends React.Component {
filepaths.forEach((filepath) => {
fs.readFile(filepath, (err, data) => {
if (err) throw Error('File reading error: ', err)
const content = data.toString()
const newNote = {
content: content,
folder: folder.key,
title: markdown.strip(findNoteTitle(content)),
type: 'MARKDOWN_NOTE'
}
dataApi.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: getNoteKey(note)}
fs.stat(filepath, (err, {mtime, birthtime}) => {
if (err) throw Error('File stat reading error: ', err)
const content = data.toString()
const newNote = {
content: content,
folder: folder.key,
title: path.basename(filepath, path.extname(filepath)),
type: 'MARKDOWN_NOTE',
createdAt: birthtime,
updatedAt: mtime
}
dataApi.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: getNoteKey(note)}
})
})
})
})
@@ -640,19 +874,38 @@ class NoteList extends React.Component {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: message,
buttons: ['OK']
buttons: [i18n.__('OK')]
})
}
getNoteStorage (note) { // note.storage = storage key
return this.props.data.storageMap.toJS()[note.storage]
}
getNoteFolder (note) { // note.folder = folder key
return _.find(this.getNoteStorage(note).folders, ({ key }) => key === note.folder)
}
getViewType () {
const { pathname } = this.props.location
const folder = /\/folders\/[a-zA-Z0-9]+/.test(pathname)
const storage = /\/storages\/[a-zA-Z0-9]+/.test(pathname) && !folder
const allNotes = pathname === '/home'
if (allNotes) return 'ALL'
if (folder) return 'FOLDER'
if (storage) return 'STORAGE'
}
render () {
let { location, notes, config, dispatch } = this.props
let { selectedNoteKeys } = this.state
let sortFunc = config.sortBy === 'CREATED_AT'
const { location, config } = this.props
let { notes } = this.props
const { selectedNoteKeys } = this.state
const sortFunc = config.sortBy === 'CREATED_AT'
? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL'
? sortByAlphabetical
: sortByUpdatedAt
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/)
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
? this.getNotes().sort(sortFunc)
: this.sortByPin(this.getNotes().sort(sortFunc))
this.notes = notes = sortedNotes.filter((note) => {
@@ -679,6 +932,8 @@ class NoteList extends React.Component {
}
})
const viewType = this.getViewType()
const noteList = notes
.map(note => {
if (note == null) {
@@ -692,7 +947,6 @@ class NoteList extends React.Component {
config.sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt
).fromNow('D')
const key = `${note.storage}-${note.key}`
if (isDefault) {
return (
@@ -705,6 +959,9 @@ class NoteList extends React.Component {
handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/>
)
}
@@ -717,6 +974,10 @@ class NoteList extends React.Component {
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/>
)
})
@@ -731,16 +992,17 @@ class NoteList extends React.Component {
<div styleName='control-sortBy'>
<i className='fa fa-angle-down' />
<select styleName='control-sortBy-select'
title={i18n.__('Select filter mode')}
value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)}
>
<option value='UPDATED_AT'>Updated</option>
<option value='CREATED_AT'>Created</option>
<option value='ALPHABETICAL'>Alphabetically</option>
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
<option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
<option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
</select>
</div>
<div styleName='control-button-area'>
<button styleName={config.listStyle === 'DEFAULT'
<button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
? 'control-button--active'
: 'control-button'
}
@@ -748,7 +1010,7 @@ class NoteList extends React.Component {
>
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
</button>
<button styleName={config.listStyle === 'SMALL'
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active'
: 'control-button'
}

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Notes')}</span>
</button>
)
ListButton.propTypes = {
onClick: PropTypes.func.isRequired,
isTagActive: PropTypes.bool.isRequired
}
export default CSSModules(ListButton, styles)

View File

@@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>
)
PreferenceButton.propTypes = {
onClick: PropTypes.func.isRequired
}
export default CSSModules(PreferenceButton, styles)

View File

@@ -0,0 +1,51 @@
.top-menu-preference
navButtonColor()
position absolute
top 22px
right 10px
width 2em
background-color transparent
&:hover
color $ui-button-default--active-backgroundColor
background-color transparent
.tooltip
opacity 1
&:active, &:active:hover
color $ui-button-default--active-backgroundColor
body[data-theme="white"]
.top-menu-preference
navWhiteButtonColor()
background-color transparent
&:hover
color #0B99F1
background-color transparent
&:active, &:active:hover
color #0B99F1
background-color transparent
body[data-theme="dark"]
.top-menu-preference
navDarkButtonColor()
background-color transparent
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
.tooltip
tooltip()
position absolute
pointer-events none
top 26px
left -20px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s

View File

@@ -11,19 +11,6 @@
.top
padding-bottom 15px
.top-menu-preference
navButtonColor()
position absolute
top 22px
right 10px
width 2em
background-color transparent
&:hover
color $ui-button-default--active-backgroundColor
background-color transparent
&:active, &:active:hover
color $ui-button-default--active-backgroundColor
.switch-buttons
background-color transparent
border 0
@@ -31,21 +18,7 @@
display flex
text-align center
.non-active-button
color $ui-inactive-text-color
font-size 16px
border 0
background-color transparent
transition 0.2s
display flex
text-align center
margin-right 4px;
&:hover
color alpha(#239F86, 60%)
.active-button
@extend .non-active-button
color $ui-button-default--active-backgroundColor
.top-menu-label
margin-left 5px
@@ -109,57 +82,16 @@ body[data-theme="white"]
background-color #f9f9f9
color $ui-text-color
.top-menu-preference
navWhiteButtonColor()
background-color transparent
&:hover
color #0B99F1
background-color transparent
&:active, &:active:hover
color #0B99F1
background-color transparent
.non-active-button
color $ui-inactive-text-color
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color $ui-text-color
.non-active-button
&:hover
color alpha(#0B99F1, 60%)
.active-button
@extend .non-active-button
color #0B99F1
body[data-theme="dark"]
.root, .root--folded
border-color $ui-dark-borderColor
border-right 1px solid $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
.top
border-color $ui-dark-borderColor
.top-menu-preference
navDarkButtonColor()
background-color transparent
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
background-color transparent
.non-active-button
color alpha($ui-dark-text-color, 60%)
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color alpha($ui-dark-text-color, 60%)
body[data-theme="solarized-dark"]
.root, .root--folded
background-color $ui-solarized-dark-backgroundColor
border-right 1px solid $ui-solarized-dark-borderColor

View File

@@ -8,11 +8,12 @@ import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem'
import eventEmitter from 'browser/main/lib/eventEmitter'
import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
const { Menu, dialog } = remote
class StorageItem extends React.Component {
constructor (props) {
@@ -24,27 +25,29 @@ class StorageItem extends React.Component {
}
handleHeaderContextMenu (e) {
const menu = new Menu()
menu.append(new MenuItem({
label: 'Add Folder',
click: (e) => this.handleAddFolderButtonClick(e)
}))
menu.append(new MenuItem({
type: 'separator'
}))
menu.append(new MenuItem({
label: 'Unlink Storage',
click: (e) => this.handleUnlinkStorageClick(e)
}))
const menu = Menu.buildFromTemplate([
{
label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e)
},
{
type: 'separator'
},
{
label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e)
}
])
menu.popup()
}
handleUnlinkStorageClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Unlink Storage'),
detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
@@ -89,18 +92,36 @@ class StorageItem extends React.Component {
}
handleFolderButtonContextMenu (e, folder) {
const menu = new Menu()
menu.append(new MenuItem({
label: 'Rename Folder',
click: (e) => this.handleRenameFolderClick(e, folder)
}))
menu.append(new MenuItem({
type: 'separator'
}))
menu.append(new MenuItem({
label: 'Delete Folder',
click: (e) => this.handleFolderDeleteClick(e, folder)
}))
const menu = Menu.buildFromTemplate([
{
label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder)
},
{
type: 'separator'
},
{
label: i18n.__('Export Folder'),
submenu: [
{
label: i18n.__('Export as txt'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt')
},
{
label: i18n.__('Export as md'),
click: (e) => this.handleExportFolderClick(e, folder, 'md')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Delete Folder'),
click: (e) => this.handleFolderDeleteClick(e, folder)
}
])
menu.popup()
}
@@ -112,12 +133,37 @@ class StorageItem extends React.Component {
})
}
handleExportFolderClick (e, folder, fileType) {
const options = {
properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options,
(paths) => {
if (paths && paths.length === 1) {
const { storage, dispatch } = this.props
dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0])
.then((data) => {
dispatch({
type: 'EXPORT_FOLDER',
storage: data.storage,
folderKey: data.folderKey,
fileType: data.fileType
})
})
}
})
}
handleFolderDeleteClick (e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete Folder',
detail: 'This will delete all notes in the folder and can not be undone.',
buttons: ['Confirm', 'Cancel']
message: i18n.__('Delete Folder'),
detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
@@ -147,33 +193,16 @@ class StorageItem extends React.Component {
dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder)
if (noteData.length === 0) return
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
Promise.all(
newNoteData.map((note) => dataApi.createNote(storage.key, note))
noteData.map((note) => dataApi.moveNote(note.storage, note.key, storage.key, folder.key))
)
.then((createdNoteData) => {
createdNoteData.forEach((note) => {
createdNoteData.forEach((newNote) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
})
.catch((err) => {
console.error(`error on create notes: ${err}`)
})
.then(() => {
return Promise.all(
noteData.map((note) => dataApi.deleteNote(note.storage, note.key))
)
})
.then((deletedNoteData) => {
deletedNoteData.forEach((note) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: note.storageKey,
noteKey: note.noteKey
type: 'MOVE_NOTE',
originNote: noteData.find((note) => note.content === newNote.content),
note: newNote
})
})
})
@@ -192,7 +221,8 @@ class StorageItem extends React.Component {
render () {
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const folderList = storage.folders.map((folder) => {
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
@@ -206,8 +236,9 @@ class StorageItem extends React.Component {
noteCount = noteSet.size - trashedNoteCount
}
return (
<StorageItemChild
<SortableStorageItemChild
key={folder.key}
index={index}
isActive={isActive}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
@@ -229,9 +260,9 @@ class StorageItem extends React.Component {
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}
? 'header--active'
: 'header'
}
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
>
<button styleName='header-toggleButton'
@@ -240,7 +271,7 @@ class StorageItem extends React.Component {
<img src={this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
}
/>
</button>

View File

@@ -179,4 +179,8 @@ body[data-theme="dark"]
background-color alpha($ui-dark-button--active-backgroundColor, 60%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
background-color $ui-dark-button--active-backgroundColor

View File

@@ -0,0 +1,59 @@
.non-active-button
color $ui-inactive-text-color
font-size 16px
border 0
background-color transparent
transition 0.2s
display flex
text-align center
margin-right 4px
position relative
&:hover
color alpha(#239F86, 60%)
.tooltip
opacity 1
.active-button
@extend .non-active-button
color $ui-button-default--active-backgroundColor
.tooltip
tooltip()
position absolute
pointer-events none
top 22px
left -2px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="white"]
.non-active-button
color $ui-inactive-text-color
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color $ui-text-color
.non-active-button
&:hover
color alpha(#0B99F1, 60%)
.active-button
@extend .non-active-button
color #0B99F1
body[data-theme="dark"]
.non-active-button
color alpha($ui-dark-text-color, 60%)
&:hover
color alpha(#0B99F1, 60%)
.tag-title
p
color alpha($ui-dark-text-color, 60%)

View File

@@ -0,0 +1,25 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({
onClick, isTagActive
}) => (
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={onClick}>
<img src={isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Tags')}</span>
</button>
)
TagButton.propTypes = {
onClick: PropTypes.func.isRequired,
isTagActive: PropTypes.bool.isRequired
}
export default CSSModules(TagButton, styles)

View File

@@ -1,6 +1,9 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
const { remote } = require('electron')
const { Menu } = remote
import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal'
@@ -11,6 +14,11 @@ import SideNavFilter from 'browser/components/SideNavFilter'
import StorageList from 'browser/components/StorageList'
import NavToggleButton from 'browser/components/NavToggleButton'
import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton'
import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
@@ -62,6 +70,17 @@ class SideNav extends React.Component {
router.push('/alltags')
}
onSortEnd (storage) {
return ({oldIndex, newIndex}) => {
const { dispatch } = this.props
dataApi
.reorderFolder(storage.key, oldIndex, newIndex)
.then((data) => {
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
})
}
}
SideNavComponent (isFolded, storageList) {
const { location, data } = this.props
@@ -83,9 +102,10 @@ class SideNav extends React.Component {
isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
counterTotalNote={data.noteMap._map.size}
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
/>
<StorageList storageList={storageList} />
@@ -96,7 +116,7 @@ class SideNav extends React.Component {
component = (
<div styleName='tabBody'>
<div styleName='tag-title'>
<p>Tags</p>
<p>{i18n.__('Tags')}</p>
</div>
<div styleName='tagList'>
{this.tagListComponent(data)}
@@ -110,18 +130,21 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location } = this.props
const tagList = data.tagNoteMap.map((tag, key) => {
return key
})
const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
return { name, size: tag.size }
}), ['name'])
return (
tagList.map(tag => (
<TagListItem
name={tag}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
isActive={this.getTagActive(location.pathname, tag)}
key={tag}
/>
))
tagList.map(tag => {
return (
<TagListItem
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
isActive={this.getTagActive(location.pathname, tag)}
key={tag.name}
count={tag.size}
/>
)
})
)
}
@@ -136,19 +159,48 @@ class SideNav extends React.Component {
router.push(`/tags/${name}`)
}
emptyTrash (entries) {
const { dispatch } = this.props
const deletionPromises = entries.map((note) => {
return dataApi.deleteNote(note.storage, note.key)
})
Promise.all(deletionPromises)
.then((arrayOfStorageAndNoteKeys) => {
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
})
})
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {
const { data } = this.props
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
const menu = Menu.buildFromTemplate([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
])
menu.popup()
}
render () {
const { data, location, config, dispatch } = this.props
const isFolded = config.isSideNavFolded
const storageList = data.storageMap.map((storage, key) => {
return <StorageItem
const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/>
})
const style = {}
@@ -162,27 +214,11 @@ class SideNav extends React.Component {
>
<div styleName='top'>
<div styleName='switch-buttons'>
<button styleName={isTagActive ? 'non-active-button' : 'active-button'} onClick={this.handleSwitchFoldersButtonClick.bind(this)}>
<img src={isTagActive
? '../resources/icon/icon-list.svg'
: '../resources/icon/icon-list-active.svg'
}
/>
</button>
<button styleName={isTagActive ? 'active-button' : 'non-active-button'} onClick={this.handleSwitchTagsButtonClick.bind(this)}>
<img src={isTagActive
? '../resources/icon/icon-tag-active.svg'
: '../resources/icon/icon-tag.svg'
}
/>
</button>
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
</div>
<div>
<button styleName='top-menu-preference'
onClick={(e) => this.handleMenuButtonClick(e)}
>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
</button>
<PreferenceButton onClick={this.handleMenuButtonClick} />
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { remote, ipcRenderer } = electron
@@ -14,9 +15,9 @@ class StatusBar extends React.Component {
updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be installed.'),
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
})
if (index === 0) {
@@ -62,8 +63,8 @@ class StatusBar extends React.Component {
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
</button>
<i styleName='update-icon' className='fa fa-cloud-download' /> {i18n.__('Ready to Update!')}
</button>
: null
}
</div>

View File

@@ -40,6 +40,32 @@ $control-height = 34px
padding-bottom 2px
background-color $ui-noteList-backgroundColor
.control-search-input-clear
height 16px
width 16px
position absolute
right 40px
top 10px
z-index 300
border none
background-color transparent
color #999
&:hover .control-search-input-clear-tooltip
opacity 1
.control-search-input-clear-tooltip
tooltip()
position fixed
pointer-events none
top 50px
left 433px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.control-search-optionList
position fixed
z-index 200
@@ -185,3 +211,26 @@ body[data-theme="dark"]
.control-newPostButton-tooltip
darkTooltip()
body[data-theme="solarized-dark"]
.root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor
.control
border-color $ui-solarized-dark-borderColor
.control-search
background-color $ui-solarized-dark-noteList-backgroundColor
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-solarized-dark-inactive-text-color
background-color $ui-solarized-dark-noteList-backgroundColor
.control-search-input
background-color $ui-solarized-dark-noteList-backgroundColor
input
background-color $ui-solarized-dark-noteList-backgroundColor
color $ui-solarized-dark-text-color

View File

@@ -5,6 +5,7 @@ import styles from './TopBar.styl'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n'
class TopBar extends React.Component {
constructor (props) {
@@ -22,14 +23,29 @@ class TopBar extends React.Component {
this.focusSearchHandler = () => {
this.handleOnSearchFocus()
}
this.codeInitHandler = this.handleCodeInit.bind(this)
}
componentDidMount () {
ee.on('top:focus-search', this.focusSearchHandler)
ee.on('code:init', this.codeInitHandler)
}
componentWillUnmount () {
ee.off('top:focus-search', this.focusSearchHandler)
ee.off('code:init', this.codeInitHandler)
}
handleSearchClearButton (e) {
const { router } = this.context
this.setState({
search: '',
isSearching: false
})
this.refs.search.childNodes[0].blur
router.push('/searched')
e.preventDefault()
}
handleKeyDown (e) {
@@ -39,6 +55,23 @@ class TopBar extends React.Component {
isIME: false
})
// Clear search on ESC
if (e.keyCode === 27) {
return this.handleSearchClearButton(e)
}
// Next note on DOWN key
if (e.keyCode === 40) {
ee.emit('list:next')
e.preventDefault()
}
// Prev note on UP key
if (e.keyCode === 38) {
ee.emit('list:prior')
e.preventDefault()
}
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
@@ -73,14 +106,16 @@ class TopBar extends React.Component {
handleSearchChange (e) {
const { router } = this.context
const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push('/searched')
} else {
e.preventDefault()
}
this.setState({
search: this.refs.searchInput.value
search: keyword
})
ee.emit('top:search', keyword)
}
handleSearchFocus (e) {
@@ -108,13 +143,19 @@ class TopBar extends React.Component {
}
handleOnSearchFocus () {
const el = this.refs.search.childNodes[0]
if (this.state.isSearching) {
this.refs.search.childNodes[0].blur()
el.blur()
} else {
this.refs.search.childNodes[0].focus()
el.focus()
el.setSelectionRange(0, el.value.length)
}
}
handleCodeInit () {
ee.emit('top:search', this.refs.searchInput.value)
}
render () {
const { config, style, location } = this.props
return (
@@ -136,19 +177,19 @@ class TopBar extends React.Component {
onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
placeholder='Search'
placeholder={i18n.__('Search')}
type='text'
className='searchInput'
/>
{this.state.search !== '' &&
<button styleName='control-search-input-clear'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
</button>
}
</div>
{this.state.search > 0 &&
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-times' />
</button>
}
</div>
</div>
{location.pathname === '/trashed' ? ''

View File

@@ -1,4 +1,5 @@
global-reset()
@import '../styles/vars.styl'
DEFAULT_FONTS = 'OpenSans', helvetica, arial, sans-serif
@@ -84,15 +85,19 @@ modalBackColor = white
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
body[data-theme="dark"]
.ModalBase
.modalBack
background-color $ui-dark-backgroundColor
.sortableItemHelper
color: $ui-dark-text-color
.CodeMirror
font-family inherit !important
line-height 1.4em
height 96%
height 100%
.CodeMirror > div > textarea
margin-bottom -1em
.CodeMirror-focused .CodeMirror-selected
@@ -107,6 +112,11 @@ body[data-theme="dark"]
.sortableItemHelper
z-index modalZIndex + 5
body[data-theme="dark"]
body[data-theme="solarized-dark"]
.ModalBase
.modalBack
background-color $ui-solarized-dark-backgroundColor
.sortableItemHelper
color: $ui-dark-text-color
color: $ui-solarized-dark-text-color

View File

@@ -8,6 +8,7 @@ import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-rou
import { syncHistoryWithStore } from 'react-router-redux'
require('./lib/ipcClient')
require('../lib/customMeta')
import i18n from 'browser/lib/i18n'
const electron = require('electron')
@@ -46,9 +47,9 @@ function notify (...args) {
function updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be installed.'),
buttons: [i18n.__('Restart & Install'), i18n.__('Not Now')]
})
if (index === 0) {

View File

@@ -7,7 +7,7 @@ const os = require('os')
let mobileAnalyticsClient
AWS.config.region = 'us-east-1'
if (process.env.NODE_ENV === 'production' && ConfigManager.default.get().amaEnabled) {
if (!getSendEventCond()) {
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-east-1:xxxxxxxxxxxxxxxxxxxxxxxxx'
})
@@ -34,8 +34,15 @@ function convertPlatformName (platformName) {
}
}
function getSendEventCond () {
const isDev = process.env.NODE_ENV !== 'production'
const isDisable = !ConfigManager.default.get().amaEnabled
const isOffline = !window.navigator.onLine
return isDev || isDisable || isOffline
}
function initAwsMobileAnalytics () {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
if (!err) {
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
@@ -46,7 +53,7 @@ function initAwsMobileAnalytics () {
}
function recordDynamicCustomEvent (type, options = {}) {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent(type, options)
} catch (analyticsError) {
@@ -57,7 +64,7 @@ function recordDynamicCustomEvent (type, options = {}) {
}
function recordStaticCustomEvent () {
if (process.env.NODE_ENV !== 'production' || !ConfigManager.default.get().amaEnabled) return
if (getSendEventCond()) return
try {
mobileAnalyticsClient.recordEvent('UI_COLOR_THEME', {
uiColorTheme: ConfigManager.default.get().ui.theme

View File

@@ -1,5 +1,6 @@
import _ from 'lodash'
import RcParser from 'browser/lib/RcParser'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32'
@@ -18,10 +19,10 @@ export const DEFAULT_CONFIG = {
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true,
hotkey: {
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
},
ui: {
language: 'en',
theme: 'default',
showCopyNotification: true,
disableDirectWrite: false,
@@ -34,13 +35,34 @@ export const DEFAULT_CONFIG = {
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
indentType: 'space',
indentSize: '2',
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
scrollPastEnd: false,
type: 'SPLIT',
fetchUrlTitle: true
},
preview: {
fontSize: '14',
fontFamily: win ? 'Segoe UI' : 'Lato',
codeBlockTheme: 'dracula',
lineNumber: true
lineNumber: true,
latexInlineOpen: '$',
latexInlineClose: '$',
latexBlockOpen: '$$',
latexBlockClose: '$$',
scrollPastEnd: false,
smartQuotes: true,
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
},
blog: {
type: 'wordpress', // Available value: wordpress, add more types in the future plz
address: 'http://wordpress.com/wp-json',
authMethod: 'JWT', // Available value: JWT, USER
token: '',
username: '',
password: ''
}
}
@@ -110,10 +132,44 @@ function set (updates) {
document.body.setAttribute('data-theme', 'dark')
} else if (newConfig.ui.theme === 'white') {
document.body.setAttribute('data-theme', 'white')
} else if (newConfig.ui.theme === 'solarized-dark') {
document.body.setAttribute('data-theme', 'solarized-dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
if (newConfig.ui.language === 'sq') {
i18n.setLocale('sq')
} else if (newConfig.ui.language === 'zh-CN') {
i18n.setLocale('zh-CN')
} else if (newConfig.ui.language === 'zh-TW') {
i18n.setLocale('zh-TW')
} else if (newConfig.ui.language === 'da') {
i18n.setLocale('da')
} else if (newConfig.ui.language === 'fr') {
i18n.setLocale('fr')
} else if (newConfig.ui.language === 'de') {
i18n.setLocale('de')
} else if (newConfig.ui.language === 'hu') {
i18n.setLocale('hu')
} else if (newConfig.ui.language === 'ja') {
i18n.setLocale('ja')
} else if (newConfig.ui.language === 'ko') {
i18n.setLocale('ko')
} else if (newConfig.ui.language === 'no') {
i18n.setLocale('no')
} else if (newConfig.ui.language === 'pl') {
i18n.setLocale('pl')
} else if (newConfig.ui.language === 'pt') {
i18n.setLocale('pt')
} else if (newConfig.ui.language === 'ru') {
i18n.setLocale('ru')
} else if (newConfig.ui.language === 'es') {
i18n.setLocale('es')
} else {
i18n.setLocale('en')
}
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
@@ -141,6 +197,7 @@ function set (updates) {
function assignConfigValues (originalConfig, rcConfig) {
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey)
config.blog = Object.assign({}, DEFAULT_CONFIG.blog, originalConfig.blog, rcConfig.blog)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)

View File

@@ -0,0 +1,31 @@
const fs = require('fs')
const path = require('path')
/**
* @description Copy a file from source to destination
* @param {String} srcPath
* @param {String} dstPath
* @return {Promise} an image path
*/
function copyFile (srcPath, dstPath) {
if (!path.extname(dstPath)) {
dstPath = path.join(dstPath, path.basename(srcPath))
}
return new Promise((resolve, reject) => {
const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
const input = fs.createReadStream(srcPath)
const output = fs.createWriteStream(dstPath)
output.on('error', reject)
input.on('error', reject)
input.on('end', () => {
resolve(dstPath)
})
input.pipe(output)
})
}
module.exports = copyFile

View File

@@ -3,19 +3,20 @@ const path = require('path')
const { findStorage } = require('browser/lib/findStorage')
/**
* @description To copy an image and return the path.
* @description Copy an image and return the path.
* @param {String} filePath
* @param {String} storageKey
* @return {String} an image path
* @param {Boolean} rename create new filename or leave the old one
* @return {Promise<any>} an image path
*/
function copyImage (filePath, storageKey) {
function copyImage (filePath, storageKey, rename = true) {
return new Promise((resolve, reject) => {
try {
const targetStorage = findStorage(storageKey)
const inputImage = fs.createReadStream(filePath)
const imageExt = path.extname(filePath)
const imageName = Math.random().toString(36).slice(-16)
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
const basename = `${imageName}${imageExt}`
const imageDir = path.join(targetStorage.path, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)

View File

@@ -52,12 +52,12 @@ function createNote (storageKey, input) {
return storage
})
.then(function saveNote (storage) {
let key = keygen()
let key = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
key = keygen()
key = keygen(true)
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
@@ -66,12 +66,16 @@ function createNote (storageKey, input) {
}
}
}
const noteData = Object.assign({}, input, {
key,
createdAt: new Date(),
updatedAt: new Date(),
storage: storageKey
})
const noteData = Object.assign({},
{
createdAt: new Date(),
updatedAt: new Date()
},
input, // input may contain more accurate dates
{
key,
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))

View File

@@ -0,0 +1,62 @@
import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
import * as path from 'path'
import * as fs from 'fs'
/**
* @param {String} storageKey
* @param {String} folderKey
* @param {String} fileType
* @param {String} exportDir
*
* @return {Object}
* ```
* {
* storage: Object,
* folderKey: String,
* fileType: String,
* exportDir: String
* }
* ```
*/
function exportFolder (storageKey, folderKey, fileType, exportDir) {
let targetStorage
try {
targetStorage = findStorage(storageKey)
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
})
.then(function exportNotes (data) {
const { storage, notes } = data
notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => {
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content)
})
return {
storage,
folderKey,
fileType,
exportDir
}
})
}
module.exports = exportFolder

View File

@@ -0,0 +1,112 @@
import copyFile from 'browser/main/lib/dataApi/copyFile'
import {findStorage} from 'browser/lib/findStorage'
import filenamify from 'filenamify'
const fs = require('fs')
const path = require('path')
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
const IMAGES_FOLDER_NAME = 'images'
/**
* Export note together with images
*
* If images is stored in the storage, creates 'images' subfolder in target directory
* and copies images to it. Changes links to images in the content of the note
*
* @param {String} storageKey or storage path
* @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []
if (!storagePath) {
throw new Error('Storage path is not found')
}
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
dstFilename = filenamify(dstFilename, {replacement: '_'})
if (!path.extname(dstFilename)) {
dstFilename += path.extname(srcFilename)
}
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
exportTasks.push({
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
dst: dstRelativePath
})
return `![${dstFilename}](${dstRelativePath})`
})
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
}).catch((err) => {
rollbackExport(tasks)
throw err
})
}
function prepareTasks (tasks, storagePath, targetPath) {
return tasks.map((task) => {
if (!path.isAbsolute(task.src)) {
task.src = path.join(storagePath, task.src)
}
if (!path.isAbsolute(task.dst)) {
task.dst = path.join(targetPath, task.dst)
}
return task
})
}
function saveToFile (data, filename) {
return new Promise((resolve, reject) => {
fs.writeFile(filename, data, (err) => {
if (err) return reject(err)
resolve(filename)
})
})
}
/**
* Remove exported files
* @param tasks Array of copy task objects. Object consists of two mandatory fields `src` and `dst`
*/
function rollbackExport (tasks) {
const folders = new Set()
tasks.forEach((task) => {
let fullpath = task.dst
if (!path.extname(task.dst)) {
fullpath = path.join(task.dst, path.basename(task.src))
}
if (fs.existsSync(fullpath)) {
fs.unlink(fullpath)
folders.add(path.dirname(fullpath))
}
})
folders.forEach((folder) => {
if (fs.readdirSync(folder).length === 0) {
fs.rmdir(folder)
}
})
}
export default exportNote

View File

@@ -7,6 +7,7 @@ const dataApi = {
updateFolder: require('./updateFolder'),
deleteFolder: require('./deleteFolder'),
reorderFolder: require('./reorderFolder'),
exportFolder: require('./exportFolder'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),

View File

@@ -1,10 +1,12 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const copyImage = require('./copyImage')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage
@@ -37,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage
newNoteKey = keygen()
newNoteKey = keygen(true)
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen()
newNoteKey = keygen(true)
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return noteData
})
.then(function moveImages (noteData) {
const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
let match = searchImagesRegex.exec(noteData.content)
const moveTasks = []
while (match != null) {
const [, filename] = match
const oldPath = path.join(oldStorage.path, 'images', filename)
moveTasks.push(
copyImage(oldPath, noteData.storage, false)
.then(() => {
fs.unlinkSync(oldPath)
})
)
// find next occurence
match = searchImagesRegex.exec(noteData.content)
}
return Promise.all(moveTasks).then(() => noteData)
})
.then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData

Some files were not shown because too many files have changed in this diff Show More