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

Compare commits

...

303 Commits

Author SHA1 Message Date
Junyoung Choi
4b9cf775ff v0.11.4 2018-04-11 08:16:20 +09:00
Junyoung Choi (Sai)
56879924bd Merge pull request #1701 from bimlas/fix-filter-by-tag-in-search-entry
Fix tag filtering in search entry
2018-04-10 19:59:20 +09:00
Junyoung Choi (Sai)
6142f2d641 Merge pull request #1724 from ehhc/Issue_1723
Fixes #1723
2018-04-10 19:16:05 +09:00
Sosuke Suzuki
0d53f799b7 to short an arrow function 2018-04-10 18:40:57 +09:00
Junyoung Choi (Sai)
65e77e9669 Merge pull request #1742 from bimlas/order-of-tags
Add selector to sort tags by counter or alphabetically
2018-04-10 17:40:31 +09:00
Sosuke Suzuki
2c8f3b56ae use arrow functions 2018-04-10 17:14:25 +09:00
Junyoung Choi (Sai)
8ab190affc Merge pull request #1752 from nlopin/add-jest
Add jest and simple test
2018-04-10 16:50:49 +09:00
Sosuke Suzuki
4fcc9af933 add jest-localstorage-mock 2018-04-10 15:47:58 +09:00
Kazz Yokomizo
e2b4ac6ee8 Merge pull request #1778 from BoostIO/fix-folded-stragelist-bug
fix folded storagelist width
2018-04-04 22:28:37 +09:00
Sosuke Suzuki
c151049cc2 fix folded storagelist width 2018-04-04 21:17:07 +09:00
Junyoung Choi (Sai)
ac778d3f65 Merge pull request #1764 from frankkanis/languages-production
Fixed change language in production mode
2018-04-03 17:44:14 +09:00
Junyoung Choi (Sai)
1aed0cb4b9 Merge pull request #1774 from li1xu1bin/master
Translate into Chinese
2018-04-03 02:06:12 +09:00
Junyoung Choi (Sai)
5836b65aad Merge pull request #1744 from azu/search-routing
fix reload crash on /searched
2018-04-01 07:51:38 +09:00
Junyoung Choi (Sai)
46f750efba Merge pull request #1745 from frankkanis/trash-context-menu
Clean up trash context menu
2018-04-01 07:51:07 +09:00
Junyoung Choi (Sai)
b33c9e23ce Merge pull request #1737 from yosmoc/noteList_error_handling
remove broken note from note list
2018-04-01 07:48:38 +09:00
Junyoung Choi (Sai)
14694f1cb0 Merge pull request #1751 from gediminasml/folding
Add folding to CodeEditor
2018-04-01 07:48:25 +09:00
Junyoung Choi (Sai)
75575348cd Merge pull request #1757 from hidaiy/fix-vim-mode
Fix #1003 Add CodeMirror addon
2018-04-01 07:48:06 +09:00
Junyoung Choi (Sai)
f6ad0a235c Merge pull request #1759 from bimlas/fix-highlighting-of-active-tag
Fix highlighting of active tag
2018-04-01 07:47:49 +09:00
“li1xu1bin”
bbf6c60888 Chinesization 2018-03-31 18:58:21 +08:00
Junyoung Choi (Sai)
f5915f3e10 Merge pull request #1753 from nlopin/encode_content_in_html_export
Escape html characters before convert to HTML
2018-03-31 17:57:24 +09:00
ehhc
c90a878c9e Merge branch 'master' into Issue_1723 2018-03-30 12:25:35 +02:00
Frank Kanis
6943b06a88 Added missing space 2018-03-28 22:06:50 +02:00
Frank Kanis
27a9def88c Fixed change language in production 2018-03-28 21:55:22 +02:00
bimlas
254c8816f1 Fix highlighting of active tag
The currently selected tag was not highlighted in the list.

Before:

![screencast](https://i.imgur.com/6JsjYk7.gif)

After:

![screencast](https://i.imgur.com/xD6fc0c.gif)
2018-03-27 22:14:47 +02:00
hidai
0dfb14962a Add CodeMirror addon 2018-03-27 18:37:20 +09:00
bimlas
d493df4295 Move selector next to title 2018-03-27 07:34:11 +02:00
Nikolay Lopin
90f21f4ed1 Escape html characters before convert to HTML 2018-03-25 23:47:17 +03:00
Nikolay Lopin
646ebe592e Add setup files and localstorage mock 2018-03-25 21:02:35 +03:00
Nikolay Lopin
b098a15e9c Add jest and simple test 2018-03-25 20:02:06 +03:00
Junyoung Choi (Sai)
9590559b81 Merge pull request #1741 from wAuner/patch-1
fix incorrect app closing for linux mint cinnamon
2018-03-26 00:51:30 +09:00
Junyoung Choi (Sai)
24bd2eddf1 Merge pull request #1748 from davidmigloz/master
Add Spanish (es-ES) locale
2018-03-26 00:50:51 +09:00
Junyoung Choi (Sai)
f4ba06c401 Merge pull request #1747 from lfromanini/master
Brazilian Portuguese localization
2018-03-26 00:49:31 +09:00
Gediminas Petrikas
cd405d1df9 Add cursor pointer when hovering fold gutter 2018-03-25 14:55:33 +03:00
Gediminas Petrikas
9d6dbc1a6f Remove folding keyboard shortcut 2018-03-25 14:55:06 +03:00
Gediminas Petrikas
6d57712fca Add fold gutters 2018-03-25 14:13:19 +03:00
David Miguel
080448af3a Add Spanish (es-ES) locale 2018-03-25 02:09:04 +01:00
Luiz
6e2272d043 Brazilian Portuguese localization 2018-03-24 18:04:45 -03:00
Luiz
71078dea4f Brazilian Portuguese localization 2018-03-24 17:41:48 -03:00
Frank Kanis
333f0be879 Clean up trash context menu 2018-03-24 20:49:04 +01:00
azu
3b0f664a3b fix: fix crash on /searched
Add routing for search word:

- `/searched/:searchword`

Restore the state from the `:searchword` params.
2018-03-25 00:33:57 +09:00
bimlas
2a23d19321 Add selector to sort tags by counter or alphabetically
![screencast](https://i.imgur.com/XUkTyRe.gif)
2018-03-24 15:46:49 +01:00
wAuner
5ee4237510 fix incorrect app closing for linux mint cinnamon
excluded the cinnamon desktop for the macOS-like behavior which closes the window, but does not shutdown the app processes
2018-03-24 14:48:34 +01:00
yosmoc
bdb906c26d remove broken note from note list
When .cson is broken and catch error in processing this file,  undefined is collected in notes.

remved broken note(s) from notes list.
2018-03-23 22:24:53 +01:00
Junyoung Choi (Sai)
02095ac155 Merge pull request #1736 from rayou/fix-typo-in-locales
Fixed typo in locales
2018-03-24 02:47:26 +09:00
Junyoung Choi (Sai)
80d1ca81ac Merge pull request #1725 from frankkanis/master
Updated German translation
2018-03-24 02:33:02 +09:00
Yu-Hung Ou
3bbabbc80b fixed typo in locales 2018-03-23 23:22:49 +11:00
Frank Kanis
f8ff3d4bf5 Update German translation 2018-03-22 21:21:15 +01:00
Frank Kanis
3a40f9ebd6 Prepared text for translation 2018-03-22 21:20:13 +01:00
ceh
cf776088e6 Fixes #1723 2018-03-22 17:26:59 +01:00
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
bimlas
145ae10a79 Add test: find tags without hash symbol
Searching has to find tags without hash symbol too (`tag` instead of
`#tag`).
2018-03-21 20:07:21 +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
bimlas
4f9a0b0040 Merge findByTag() and findByWord() into one
Both looked for word in tags and content too, the only difference is
when searched for `#tag`, the prefix (`#`) was truncated before compared
with list of tags.
2018-03-20 19:30:57 +01:00
bimlas
aae584106a Look for tags in context too
The previous commit broke this behaviour.

Looking for a tag means the union of **tags** AND **tag in content**, so
it has to search in the in currently found notes separetely, thus it has
to clone the list first (`.slice(0)`).
2018-03-20 17:11:08 +01:00
Joël COSTAMAGNA
2a784deb4b typo 2018-03-20 11:46:33 -04:00
bimlas
5f5a7880a6 Fix tag filtering in search entry
Some issues introduced in #954
(https://github.com/BoostIO/Boostnote/pull/954#issuecomment-336344915):

- search by `#tag1 #tag2` returns the results of only `#tag2`
- search by `#tag1 content` is as same as AND search by `#tag1 content` (and
  `#tag1` is regarded a word, not a tag)
- search by `content #tag1` returns the results of only `#tag1`

This commit fixing these:

- search by `#tag1 #tag2` returns the results of only `#tag2`

  ![screencast](https://i.imgur.com/SjhQIhl.gif)

- search by `#tag1 content` is as same as AND search by `#tag1 content` (and
  `#tag1` is regarded a word, not a tag)

  ![screencast](https://i.imgur.com/G0Tmd8c.gif)

- search by `content #tag1` returns the results of only `#tag1`

  ![screencast](https://i.imgur.com/5MrMbE6.gif)

NOTE: the examples works without `#` character too, because
`findByWord()` checks the tags too.
2018-03-20 09:01:11 +01: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
Andy Klimczak
f04ad1e702 list:moved -> list:next 2018-01-31 22:17:36 -05:00
Andy Klimczak
dc03bb76e6 Fix 2018-01-30 23:15:16 -05: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
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
134 changed files with 8317 additions and 1272 deletions

View File

@@ -5,7 +5,7 @@
"presets": ["react-hmre"] "presets": ["react-hmre"]
}, },
"test": { "test": {
"presets": ["react", "es2015"], "presets": ["env" ,"react", "es2015"],
"plugins": [ "plugins": [
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ] [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
] ]

View File

@@ -23,6 +23,7 @@
"lineNumber": true "lineNumber": true
}, },
"sortBy": "UPDATED_AT", "sortBy": "UPDATED_AT",
"sortTagsBy": "ALPHABETICAL",
"ui": { "ui": {
"defaultNote": "ALWAYS_ASK", "defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false, "disableDirectWrite": false,

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you. 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 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 it under the terms of the GNU General Public License as published by

7
__mocks__/electron.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
require: jest.genMockFunction(),
match: jest.genMockFunction(),
app: jest.genMockFunction(),
remote: jest.genMockFunction(),
dialog: jest.genMockFunction()
}

View File

@@ -7,10 +7,15 @@ import path from 'path'
import copyImage from 'browser/main/lib/dataApi/copyImage' import copyImage from 'browser/main/lib/dataApi/copyImage'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import fs from 'fs' 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' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
function pass (name) { function pass (name) {
switch (name) { switch (name) {
@@ -31,8 +36,13 @@ export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.changeHandler = (e) => this.handleChange(e) this.changeHandler = (e) => this.handleChange(e)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
this.blurHandler = (editor, e) => { this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null if (e == null) return null
let el = e.relatedTarget let el = e.relatedTarget
while (el != null) { while (el != null) {
@@ -47,12 +57,47 @@ export default class CodeEditor extends React.Component {
this.loadStyleHandler = (e) => { this.loadStyleHandler = (e) => {
this.editor.refresh() 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 () { componentDidMount () {
const { rulers, enableRulers } = this.props
this.value = this.props.value this.value = this.props.value
this.editor = CodeMirror(this.refs.root, { this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value, value: this.props.value,
lineNumbers: this.props.displayLineNumbers, lineNumbers: this.props.displayLineNumbers,
lineWrapping: true, lineWrapping: true,
@@ -64,6 +109,8 @@ export default class CodeEditor extends React.Component {
scrollPastEnd: this.props.scrollPastEnd, scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea', inputStyle: 'textarea',
dragDrop: false, dragDrop: false,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: true, autoCloseBrackets: true,
extraKeys: { extraKeys: {
Tab: function (cm) { Tab: function (cm) {
@@ -92,7 +139,7 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) { 'Cmd-T': function (cm) {
// Do nothing // Do nothing
}, },
Enter: 'newlineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': (cm) => { 'Ctrl-C': (cm) => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') { if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy') document.execCommand('copy')
@@ -104,9 +151,14 @@ export default class CodeEditor extends React.Component {
this.setMode(this.props.mode) this.setMode(this.props.mode)
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler) 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') const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler) editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -123,15 +175,19 @@ export default class CodeEditor extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler) this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler) this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler) this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
} }
componentDidUpdate (prevProps, prevState) { componentDidUpdate (prevProps, prevState) {
let needRefresh = false let needRefresh = false
const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) { if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode) this.setMode(this.props.mode)
} }
@@ -149,6 +205,10 @@ export default class CodeEditor extends React.Component {
needRefresh = true needRefresh = true
} }
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
}
if (prevProps.indentSize !== this.props.indentSize) { if (prevProps.indentSize !== this.props.indentSize) {
this.editor.setOption('indentUnit', this.props.indentSize) this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize) this.editor.setOption('tabSize', this.props.indentSize)
@@ -217,11 +277,16 @@ export default class CodeEditor extends React.Component {
handleDropImage (e) { handleDropImage (e) {
e.preventDefault() e.preventDefault()
const imagePath = e.dataTransfer.files[0].path const ValidImageTypes = ['image/gif', 'image/jpeg', 'image/png']
const filename = path.basename(imagePath)
copyImage(imagePath, this.props.storageKey).then((imagePath) => { const file = e.dataTransfer.files[0]
const imageMd = `![${filename}](${path.join('/:storage', imagePath)})` const filePath = file.path
const filename = path.basename(filePath)
const fileType = file['type']
copyImage(filePath, this.props.storageKey).then((imagePath) => {
var showPreview = ValidImageTypes.indexOf(fileType) > 0
const imageMd = `${showPreview ? '!' : ''}[${filename}](${path.join('/:storage', imagePath)})`
this.insertImageMd(imageMd) this.insertImageMd(imageMd)
}) })
} }
@@ -231,11 +296,29 @@ export default class CodeEditor extends React.Component {
} }
handlePaste (editor, e) { handlePaste (editor, e) {
const dataTransferItem = e.clipboardData.items[0] const clipboardData = e.clipboardData
if (!dataTransferItem.type.match('image')) return 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 blob = dataTransferItem.getAsFile()
const reader = new window.FileReader() const reader = new FileReader()
let base64data let base64data
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
@@ -252,6 +335,66 @@ export default class CodeEditor extends React.Component {
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})`
this.insertImageMd(imageMd) 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 () { render () {
@@ -280,6 +423,8 @@ export default class CodeEditor extends React.Component {
CodeEditor.propTypes = { CodeEditor.propTypes = {
value: PropTypes.string, value: PropTypes.string,
enableRulers: PropTypes.bool,
rulers: PropTypes.arrayOf(Number),
mode: PropTypes.string, mode: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
onBlur: PropTypes.func, onBlur: PropTypes.func,

View File

@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
if (this.state.isLocked) return if (this.state.isLocked) return
this.setState({ keyPressed: new Set() }) this.setState({ keyPressed: new Set() })
const { config } = this.props 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() const cursorPosition = this.refs.code.editor.getCursor()
this.setState({ this.setState({
status: 'PREVIEW' 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) { handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date() this.previewMouseDownedAt = new Date()
} }
@@ -242,9 +258,12 @@ class MarkdownEditor extends React.Component {
fontSize={editorFontSize} fontSize={editorFontSize}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey} storageKey={storageKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={(e) => this.handleBlur(e)}
/> />
@@ -262,8 +281,11 @@ class MarkdownEditor extends React.Component {
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize} indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
sanitize={config.preview.sanitize}
ref='preview' ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}
tabIndex='0' tabIndex='0'
value={this.state.renderValue} value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseUp={(e) => this.handlePreviewMouseUp(e)}

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

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import markdown from 'browser/lib/markdown' import Markdown from 'browser/lib/markdown'
import _ from 'lodash' import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
@@ -9,10 +9,11 @@ import Raphael from 'raphael'
import flowchart from 'flowchart' import flowchart from 'flowchart'
import SequenceDiagram from 'js-sequence-diagrams' import SequenceDiagram from 'js-sequence-diagrams'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import fs from 'fs'
import htmlTextHelper from 'browser/lib/htmlTextHelper' import htmlTextHelper from 'browser/lib/htmlTextHelper'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import {escapeHtmlCharacters} from 'browser/lib/utils'
const { remote } = require('electron') const { remote } = require('electron')
const { app } = remote const { app } = remote
@@ -23,6 +24,10 @@ const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production' const appPath = 'file://' + (process.env.NODE_ENV === 'production'
? app.getAppPath() ? app.getAppPath()
: path.resolve()) : 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, scrollPastEnd) { function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) {
return ` return `
@@ -116,6 +121,8 @@ export default class MarkdownPreview extends React.Component {
this.contextMenuHandler = (e) => this.handleContextMenu(e) this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(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.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsTextHandler = () => this.handleSaveAsText()
@@ -124,6 +131,16 @@ export default class MarkdownPreview extends React.Component {
this.printHandler = () => this.handlePrint() this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this) 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) { handlePreviewAnchorClick (e) {
@@ -146,13 +163,21 @@ export default class MarkdownPreview extends React.Component {
this.props.onCheckboxClick(e) this.props.onCheckboxClick(e)
} }
handleScroll (e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handleContextMenu (e) { handleContextMenu (e) {
if (!this.props.onContextMenu) return
this.props.onContextMenu(e) this.props.onContextMenu(e)
} }
handleDoubleClick (e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
}
handleMouseDown (e) { handleMouseDown (e) {
if (!this.props.onMouseDown) return
if (e.target != null) { if (e.target != null) {
switch (e.target.tagName) { switch (e.target.tagName) {
case 'A': case 'A':
@@ -180,8 +205,35 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (value) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
return this.refs.root.contentWindow.document.documentElement.outerHTML const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
const body = this.markdown.render(escapeHtmlCharacters(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>`
}) })
} }
@@ -189,20 +241,26 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.print() this.refs.root.contentWindow.print()
} }
exportAsDocument (fileType, formatter) { exportAsDocument (fileType, contentFormatter) {
const options = { const options = {
filters: [ filters: [
{name: 'Documents', extensions: [fileType]} {name: 'Documents', extensions: [fileType]}
], ],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
const value = formatter ? formatter.call(this, this.props.value) : this.props.value
dialog.showSaveDialog(remote.getCurrentWindow(), options, dialog.showSaveDialog(remote.getCurrentWindow(), options,
(filename) => { (filename) => {
if (filename) { if (filename) {
fs.writeFile(filename, value, (err) => { const content = this.props.value
if (err) throw err 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
}) })
} }
}) })
@@ -222,20 +280,26 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.head.innerHTML = ` let styles = `
<style id='style'></style> <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"> <link rel="stylesheet" id="codeTheme">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <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.rewriteIframe()
this.applyStyle() this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler) this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) 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('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.addEventListener('dragover', 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-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -246,8 +310,10 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) 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('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) 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('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.removeEventListener('dragover', 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-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -256,6 +322,10 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() 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 || if (prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize || prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
@@ -269,7 +339,7 @@ export default class MarkdownPreview extends React.Component {
} }
} }
applyStyle () { getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
@@ -279,15 +349,21 @@ export default class MarkdownPreview extends React.Component {
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily : defaultCodeBlockFontFamily
this.setCodeTheme(codeBlockTheme) return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd}
}
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) this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd)
} }
setCodeTheme (theme) { GetCodeThemeLink (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default' theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
? theme ? theme
: 'elegant' : '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/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css` : `${appPath}/node_modules/codemirror/theme/${theme}.css`
} }
@@ -315,14 +391,13 @@ export default class MarkdownPreview extends React.Component {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
}) })
} }
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
el.parentNode.parentNode.style.listStyleType = 'none'
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el) this.fixDecodedURI(el)
el.href = this.markdown.normalizeLinkText(el.href)
if (!/\/:storage/.test(el.href)) return
el.href = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.href)))}`
el.addEventListener('click', this.anchorClickHandler) el.addEventListener('click', this.anchorClickHandler)
}) })
@@ -335,9 +410,9 @@ export default class MarkdownPreview extends React.Component {
}) })
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { _.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 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) codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
@@ -364,9 +439,9 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = '' el.innerHTML = ''
if (codeBlockTheme.indexOf('solarized') === 0) { if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ') 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 { } else {
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` el.parentNode.className += ` cm-s-${codeBlockTheme}`
} }
CodeMirror.runMode(content, syntax.mime, el, { CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize tabSize: indentSize
@@ -449,9 +524,20 @@ export default class MarkdownPreview extends React.Component {
handlelinkClick (e) { handlelinkClick (e) {
const noteHash = e.target.href.split('/').pop() 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)) { 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])
} }
} }
@@ -478,5 +564,6 @@ MarkdownPreview.propTypes = {
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
showCopyNotification: PropTypes.bool, showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string storagePath: PropTypes.string,
smartQuotes: PropTypes.bool
} }

View File

@@ -2,6 +2,7 @@ import React from 'react'
import CodeEditor from 'browser/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import _ from 'lodash'
import styles from './MarkdownSplitEditor.styl' import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -12,6 +13,7 @@ class MarkdownSplitEditor extends React.Component {
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = true
} }
handleOnChange () { handleOnChange () {
@@ -19,6 +21,49 @@ class MarkdownSplitEditor extends React.Component {
this.props.onChange() 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) { handleCheckboxClick (e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@@ -65,9 +110,13 @@ class MarkdownSplitEditor extends React.Component {
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
storageKey={storageKey} storageKey={storageKey}
onChange={this.handleOnChange.bind(this)} onChange={this.handleOnChange.bind(this)}
onScroll={this.handleScroll.bind(this)}
/> />
<MarkdownPreview <MarkdownPreview
style={previewStyle} style={previewStyle}
@@ -80,10 +129,13 @@ class MarkdownSplitEditor extends React.Component {
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
sanitize={config.preview.sanitize}
ref='preview' ref='preview'
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
/> />

View File

@@ -8,6 +8,7 @@ import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus' import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl' import styles from './NoteItem.styl'
import TodoProcess from './TodoProcess' import TodoProcess from './TodoProcess'
import i18n from 'browser/lib/i18n'
/** /**
* @description Tag element component. * @description Tag element component.
@@ -46,14 +47,25 @@ const TagElementList = (tags) => {
* @param {Function} handleDragStart * @param {Function} handleDragStart
* @param {string} dateDisplay * @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 <div styleName={isActive
? 'item--active' ? 'item--active'
: 'item' : 'item'
} }
key={`${note.storage}-${note.key}`} key={note.key}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)} onDragStart={e => handleDragStart(e, note)}
draggable='true' draggable='true'
> >
@@ -65,27 +77,37 @@ const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteCont
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 {note.title.trim().length > 0
? note.title ? note.title
: <span styleName='item-title-empty'>Empty</span> : <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
} }
</div> </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> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags)
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
}
</div>
<div>
{note.isStarred {note.isStarred
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : '' ? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
} }
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/) {note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : '' ? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
} }
{note.type === 'MARKDOWN_NOTE' {note.type === 'MARKDOWN_NOTE'
? <TodoProcess todoStatus={getTodoStatus(note.content)} /> ? <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' />
}
</div> </div>
</div> </div>
</div> </div>
@@ -102,7 +124,11 @@ NoteItem.propTypes = {
title: PropTypes.string.isrequired, title: PropTypes.string.isrequired,
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired, isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired isTrashed: PropTypes.bool.isRequired,
blog: {
blogLink: PropTypes.string,
blogId: PropTypes.number
}
}), }),
handleNoteClick: PropTypes.func.isRequired, handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired,

View File

@@ -90,6 +90,26 @@ $control-height = 30px
font-weight normal font-weight normal
color $ui-inactive-text-color 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 .item-bottom
position relative position relative
bottom 0px bottom 0px
@@ -97,7 +117,7 @@ $control-height = 30px
font-size 12px font-size 12px
line-height 20px line-height 20px
overflow ellipsis overflow ellipsis
display flex display block
.item-bottom-tagList .item-bottom-tagList
flex 1 flex 1
@@ -125,10 +145,8 @@ $control-height = 30px
.item-star .item-star
position absolute position absolute
right -6px right 2px
bottom 23px top 5px
width 16px
height 16px
color alpha($ui-favorite-star-button-color, 60%) color alpha($ui-favorite-star-button-color, 60%)
font-size 12px font-size 12px
padding 0 padding 0
@@ -136,10 +154,8 @@ $control-height = 30px
.item-pin .item-pin
position absolute position absolute
right 0px right 25px
bottom 2px top 7px
width 34px
height 34px
color #E54D42 color #E54D42
font-size 14px font-size 14px
padding 0 padding 0

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteItemSimple.styl' import styles from './NoteItemSimple.styl'
import i18n from 'browser/lib/i18n'
/** /**
* @description Note item component when using simple display mode. * @description Note item component when using simple display mode.
@@ -14,14 +15,23 @@ import styles from './NoteItemSimple.styl'
* @param {Function} handleNoteContextMenu * @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart * @param {Function} handleDragStart
*/ */
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu, handleDragStart, pathname }) => ( const NoteItemSimple = ({
isActive,
isAllNotesView,
note,
handleNoteClick,
handleNoteContextMenu,
handleDragStart,
pathname,
storage
}) => (
<div styleName={isActive <div styleName={isActive
? 'item-simple--active' ? 'item-simple--active'
: 'item-simple' : 'item-simple'
} }
key={`${note.storage}-${note.key}`} key={note.key}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
onDragStart={e => handleDragStart(e, note)} onDragStart={e => handleDragStart(e, note)}
draggable='true' draggable='true'
> >
@@ -30,14 +40,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-code' />
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' /> : <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
} }
{note.isPinned && !pathname.match(/\/home|\/starred|\/trash/) {note.isPinned && !pathname.match(/\/starred|\/trash/)
? <i styleName='item-pin' className='fa fa-thumb-tack' /> ? <i styleName='item-pin' className='fa fa-thumb-tack' />
: '' : ''
} }
{note.title.trim().length > 0 {note.title.trim().length > 0
? note.title ? note.title
: <span styleName='item-simple-title-empty'>Empty</span> : <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
} }
{isAllNotesView && <div styleName='item-simple-right'>
<span styleName='item-simple-right-storageName'>
{storage.name}
</span>
</div>}
</div> </div>
</div> </div>
) )

View File

@@ -207,3 +207,8 @@ body[data-theme="solarized-dark"]
color #c0392b color #c0392b
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha(#fff, 20%)
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNavFilter.styl' import styles from './SideNavFilter.styl'
import i18n from 'browser/lib/i18n'
/** /**
* @param {boolean} isFolded * @param {boolean} isFolded
@@ -17,7 +18,7 @@ import styles from './SideNavFilter.styl'
const SideNavFilter = ({ const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick, isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote, isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote,
counterTotalNote, counterStarredNote counterTotalNote, counterStarredNote, handleFilterButtonContextMenu
}) => ( }) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}> <div styleName={isFolded ? 'menu--folded' : 'menu'}>
@@ -31,7 +32,7 @@ const SideNavFilter = ({
} }
/> />
</div> </div>
<span styleName='menu-button-label'>All Notes</span> <span styleName='menu-button-label'>{i18n.__('All Notes')}</span>
<span styleName='counters'>{counterTotalNote}</span> <span styleName='counters'>{counterTotalNote}</span>
</button> </button>
@@ -45,7 +46,7 @@ const SideNavFilter = ({
} }
/> />
</div> </div>
<span styleName='menu-button-label'>Starred</span> <span styleName='menu-button-label'>{i18n.__('Starred')}</span>
<span styleName='counters'>{counterStarredNote}</span> <span styleName='counters'>{counterStarredNote}</span>
</button> </button>
@@ -59,7 +60,7 @@ const SideNavFilter = ({
} }
/> />
</div> </div>
<span styleName='menu-button-label'>Trash</span> <span onContextMenu={handleFilterButtonContextMenu} styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span> <span styleName='counters'>{counterDelNote}</span>
</button> </button>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl' import styles from './SnippetTab.styl'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
class SnippetTab extends React.Component { class SnippetTab extends React.Component {
constructor (props) { constructor (props) {
@@ -28,7 +29,7 @@ class SnippetTab extends React.Component {
handleContextMenu (e) { handleContextMenu (e) {
context.popup([ context.popup([
{ {
label: 'Rename', label: i18n.__('Rename'),
click: (e) => this.handleRenameClick(e) click: (e) => this.handleRenameClick(e)
} }
]) ])
@@ -114,7 +115,7 @@ class SnippetTab extends React.Component {
{snippet.name.trim().length > 0 {snippet.name.trim().length > 0
? snippet.name ? snippet.name
: <span styleName='button-unnamed'> : <span styleName='button-unnamed'>
Unnamed {i18n.__('Unnamed')}
</span> </span>
} }
</button> </button>

View File

@@ -1,6 +1,7 @@
.root .root
position relative position relative
flex 1 flex 1
min-width 70px
overflow hidden overflow hidden
&:hover &:hover
.deleteButton .deleteButton
@@ -21,7 +22,7 @@
height 29px height 29px
overflow ellipsis overflow ellipsis
text-align left text-align left
padding-right 30px padding-right 23px
border none border none
background-color transparent background-color transparent
transition 0.15s transition 0.15s

View File

@@ -6,6 +6,18 @@ import React from 'react'
import styles from './StorageItem.styl' import styles from './StorageItem.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import _ from 'lodash' 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 * @param {boolean} isActive
@@ -21,34 +33,54 @@ import _ from 'lodash'
* @return {React.Component} * @return {React.Component}
*/ */
const StorageItem = ({ const StorageItem = ({
isActive, handleButtonClick, handleContextMenu, folderName, styles,
folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave isActive,
}) => ( handleButtonClick,
<button styleName={isActive handleContextMenu,
? 'folderList-item--active' folderName,
: 'folderList-item' folderColor,
} isFolded,
noteCount,
handleDrop,
handleDragEnter,
handleDragLeave
}) => {
return (
<button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick} onClick={handleButtonClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onDrop={handleDrop} onDrop={handleDrop}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
<span styleName={isFolded {!isFolded && (
? 'folderList-item-name--folded' : 'folderList-item-name' <DraggableIcon className={styles['folderList-item-reorder']} />
}> )}
<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
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> </span>
{(!isFolded && _.isNumber(noteCount)) && {!isFolded &&
_.isNumber(noteCount) && (
<span styleName='folderList-item-noteCount'>{noteCount}</span> <span styleName='folderList-item-noteCount'>{noteCount}</span>
} )}
{isFolded && {isFolded && (
<span styleName='folderList-item-tooltip'> <span styleName='folderList-item-tooltip'>{folderName}</span>
{folderName} )}
</span>
}
</button> </button>
) )
}
StorageItem.propTypes = { StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,

View File

@@ -13,6 +13,7 @@
border none border none
overflow ellipsis overflow ellipsis
font-size 14px font-size 14px
align-items: center
&:first-child &:first-child
margin-top 0 margin-top 0
&:hover &:hover
@@ -34,9 +35,7 @@
.folderList-item-name .folderList-item-name
display block display block
flex 1 flex 1
padding 0 12px padding-right: 10px
height 26px
line-height 26px
border-width 0 0 0 2px border-width 0 0 0 2px
border-style solid border-style solid
border-color transparent border-color transparent
@@ -69,9 +68,20 @@
.folderList-item-name--folded .folderList-item-name--folded
@extend .folderList-item-name @extend .folderList-item-name
padding-left 7px padding-left 7px
text .folderList-item-icon
font-size 9px 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"] body[data-theme="white"]
.folderList-item .folderList-item
color $ui-inactive-text-color color $ui-inactive-text-color

View File

@@ -10,8 +10,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {Array} storgaeList * @param {Array} storgaeList
*/ */
const StorageList = ({storageList}) => ( const StorageList = ({storageList, isFolded}) => (
<div styleName='storageList'> <div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : ( {storageList.length > 0 ? storageList : (
<div styleName='storgaeList-empty'>No storage mount.</div> <div styleName='storgaeList-empty'>No storage mount.</div>
)} )}

View File

@@ -4,6 +4,10 @@
top 180px top 180px
overflow-y auto overflow-y auto
.storageList-folded
@extend .storageList
width 44px
.storageList-empty .storageList-empty
padding 0 10px padding 0 10px
margin-top 15px margin-top 15px

View File

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

View File

@@ -48,6 +48,9 @@
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
.tagList-item-count
padding 0 3px
body[data-theme="white"] body[data-theme="white"]
.tagList-item .tagList-item
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -63,6 +66,8 @@ body[data-theme="white"]
color $ui-text-color color $ui-text-color
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 60%) background-color alpha($ui-button--active-backgroundColor, 60%)
.tagList-item-count
color $ui-text-color
body[data-theme="dark"] body[data-theme="dark"]
.tagList-item .tagList-item
@@ -82,3 +87,5 @@ body[data-theme="dark"]
&:hover &:hover
color $ui-dark-text-color 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

@@ -58,7 +58,7 @@ body
.katex .katex
font 400 1.2em 'KaTeX_Main' font 400 1.2em 'KaTeX_Main'
line-height 1.2em line-height 1.2em
white-space nowrap white-space initial
text-indent 0 text-indent 0
.katex .mfrac>.vlist>span:nth-child(2) .katex .mfrac>.vlist>span:nth-child(2)
top 0 !important top 0 !important
@@ -76,7 +76,7 @@ body
justify-content left justify-content left
li li
label.taskListItem label.taskListItem
margin-left -2em margin-left -1.8em
&.checked &.checked
text-decoration line-through text-decoration line-through
opacity 0.5 opacity 0.5
@@ -178,6 +178,8 @@ ul
margin-bottom 1em margin-bottom 1em
li li
display list-item display list-item
&.taskListItem
list-style none
p p
margin 0 margin 0
&>li>ul, &>li>ol &>li>ul, &>li>ol
@@ -218,6 +220,7 @@ pre
background-color white background-color white
&.CodeMirror &.CodeMirror
height initial height initial
flex-wrap wrap
&>code &>code
flex 1 flex 1
overflow-x auto overflow-x auto
@@ -227,6 +230,13 @@ pre
padding 0 padding 0
border none border none
border-radius 0 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 &>span.lineNumber
display none display none
font-size 1em font-size 1em

View File

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

@@ -0,0 +1,16 @@
const path = require('path')
const { remote } = require('electron')
const { app } = remote
// 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-ES'],
extension: '.json',
directory: process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'),
devMode: false
})
export default i18n

View File

@@ -1,7 +1,11 @@
const crypto = require('crypto') const crypto = require('crypto')
const _ = require('lodash') const _ = require('lodash')
const uuidv4 = require('uuid/v4')
module.exports = function (length) { module.exports = function (uuid) {
if (!_.isFinite(length)) length = 10 if (typeof uuid === typeof true && uuid) {
return uuidv4()
}
const length = 10
return crypto.randomBytes(length).toString('hex') 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,46 +1,111 @@
import markdownit from 'markdown-it' import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji' import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math' import math from '@rokt33r/markdown-it-math'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import {lastFindInArray} from './utils'
// FIXME We should not depend on global variable. function createGutter (str, firstLineNumber) {
const katex = window.katex if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const config = ConfigManager.get() const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
function createGutter (str) {
const lc = (str.match(/\n/g) || []).length
const lines = [] const lines = []
for (let i = 1; i <= lc; i++) { for (let i = firstLineNumber; i <= lastLineNumber; i++) {
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>') lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
} }
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>' return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
} }
var md = markdownit({ class Markdown {
typographer: true, constructor (options = {}) {
const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
linkify: true, linkify: true,
html: true, html: true,
xhtmlOut: true, xhtmlOut: true,
breaks: true, breaks: true,
highlight: function (str, lang) { highlight: function (str, lang) {
if (lang === 'flowchart') { const delimiter = ':'
const langInfo = lang.split(delimiter)
const langType = langInfo[0]
const fileName = langInfo[1] || ''
const firstLineNumber = parseInt(langInfo[2], 10)
if (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>` return `<pre class="flowchart">${str}</pre>`
} }
if (lang === 'sequence') { if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>` return `<pre class="sequence">${str}</pre>`
} }
return '<pre class="code">' + return '<pre class="code CodeMirror">' +
createGutter(str) + '<span class="filename">' + fileName + '</span>' +
'<code class="' + lang + '">' + createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str + str +
'</code></pre>' '</code></pre>'
},
sanitize: 'STRICT'
} }
const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions)
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'
]
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']
}) })
md.use(emoji, { }
this.md.use(emoji, {
shortcuts: {} shortcuts: {}
}) })
md.use(math, { this.md.use(math, {
inlineOpen: config.preview.latexInlineOpen, inlineOpen: config.preview.latexInlineOpen,
inlineClose: config.preview.latexInlineClose, inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen, blockOpen: config.preview.latexBlockOpen,
@@ -64,10 +129,10 @@ md.use(math, {
return output return output
} }
}) })
md.use(require('markdown-it-imsize')) this.md.use(require('markdown-it-imsize'))
md.use(require('markdown-it-footnote')) this.md.use(require('markdown-it-footnote'))
md.use(require('markdown-it-multimd-table')) this.md.use(require('markdown-it-multimd-table'))
md.use(require('markdown-it-named-headers'), { this.md.use(require('markdown-it-named-headers'), {
slugify: (header) => { slugify: (header) => {
return encodeURI(header.trim() return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
@@ -75,10 +140,10 @@ md.use(require('markdown-it-named-headers'), {
.replace(/\-+$/, '') .replace(/\-+$/, '')
} }
}) })
md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-kbd'))
const deflate = require('markdown-it-plantuml/lib/deflate') const deflate = require('markdown-it-plantuml/lib/deflate')
md.use(require('markdown-it-plantuml'), '', { this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) { generateSource: function (umlCode) {
const s = unescape(encodeURIComponent(umlCode)) const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64( const zippedCode = deflate.encode64(
@@ -89,7 +154,7 @@ md.use(require('markdown-it-plantuml'), '', {
}) })
// Override task item // Override task item
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token let content, terminate, i, l, token
let nextLine = startLine + 1 let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph') const terminatorRules = state.md.block.ruler.getRules('paragraph')
@@ -125,6 +190,13 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
if (state.parentType === 'list') { if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i) const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) { 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>` 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>`
} }
} }
@@ -140,8 +212,8 @@ md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
}) })
// Add line number attribute for scrolling // Add line number attribute for scrolling
const originalRender = md.renderer.render const originalRender = this.md.renderer.render
md.renderer.render = function render (tokens, options, env) { this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => { tokens.forEach((token) => {
switch (token.type) { switch (token.type) {
case 'heading_open': case 'heading_open':
@@ -151,23 +223,22 @@ md.renderer.render = function render (tokens, options, env) {
token.attrPush(['data-line', token.map[0]]) token.attrPush(['data-line', token.map[0]])
} }
}) })
const result = originalRender.call(md.renderer, tokens, options, env) const result = originalRender.call(this.md.renderer, tokens, options, env)
return result return result
} }
// FIXME We should not depend on global variable. // FIXME We should not depend on global variable.
window.md = md window.md = this.md
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
} }
const markdown = { render (content) {
render: function markdown (content) {
if (!_.isString(content)) content = '' if (!_.isString(content)) content = ''
const renderedContent = md.render(content) return this.md.render(content)
return renderedContent
},
normalizeLinkText
} }
export default markdown normalizeLinkText (linkText) {
return this.md.normalizeLinkText(linkText)
}
}
export default Markdown

View File

@@ -4,39 +4,28 @@ export default function searchFromNotes (notes, search) {
if (search.trim().length === 0) return [] if (search.trim().length === 0) return []
const searchBlocks = search.split(' ').filter(block => { return block !== '' }) const searchBlocks = search.split(' ').filter(block => { return block !== '' })
let foundNotes = findByWord(notes, searchBlocks[0]) let foundNotes = notes
searchBlocks.forEach((block) => { searchBlocks.forEach((block) => {
foundNotes = findByWord(foundNotes, block) foundNotes = findByWordOrTag(foundNotes, block)
if (block.match(/^#.+/)) {
foundNotes = foundNotes.concat(findByTag(notes, block))
}
}) })
return foundNotes return foundNotes
} }
function findByTag (notes, block) { function findByWordOrTag (notes, block) {
const tag = block.match(/#(.+)/)[1] let tag = block
const regExp = new RegExp(_.escapeRegExp(tag), 'i') if (tag.match(/^#.+/)) {
return notes.filter((note) => { tag = tag.match(/#(.+)/)[1]
if (!_.isArray(note.tags)) return false
return note.tags.some((_tag) => {
return _tag.match(regExp)
})
})
} }
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
function findByWord (notes, block) { const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
const regExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => { return notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => { if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) {
return _tag.match(regExp)
})) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(regExp) return note.description.match(wordRegExp)
} else if (note.type === 'MARKDOWN_NOTE') { } else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(regExp) return note.content.match(wordRegExp)
} }
return false return false
}) })

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

@@ -0,0 +1,60 @@
export function lastFindInArray (array, callback) {
for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) {
return array[i]
}
}
}
export function escapeHtmlCharacters (text) {
const matchHtmlRegExp = /["'&<>]/
const str = '' + text
const match = matchHtmlRegExp.exec(str)
if (!match) {
return str
}
let escape
let html = ''
let index = 0
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '&quot;'
break
case 38: // &
escape = '&amp;'
break
case 39: // '
escape = '&#39;'
break
case 60: // <
escape = '&lt;'
break
case 62: // >
escape = '&gt;'
break
default:
continue
}
if (lastIndex !== index) {
html += str.substring(lastIndex, index)
}
lastIndex = index + 1
html += escape
}
return lastIndex !== index
? html + str.substring(lastIndex, index)
: html
}
export default {
lastFindInArray,
escapeHtmlCharacters
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component { class InfoPanel extends React.Component {
copyNoteLink () { copyNoteLink () {
@@ -19,7 +20,7 @@ class InfoPanel extends React.Component {
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}> <div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p> <p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div> </div>
<hr /> <hr />
@@ -29,11 +30,11 @@ class InfoPanel extends React.Component {
: <div styleName='count-wrap'> : <div styleName='count-wrap'>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p> <p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>Words</p> <p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
</div> </div>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{letterCount}</p> <p styleName='infoPanel-defaul-count'>{letterCount}</p>
<p styleName='infoPanel-sub-count'>Letters</p> <p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div> </div>
</div> </div>
} }
@@ -45,17 +46,17 @@ class InfoPanel extends React.Component {
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p> <p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{folderName}</p> <p styleName='infoPanel-default'>{folderName}</p>
<p styleName='infoPanel-sub'>FOLDER</p> <p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p> <p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div> </div>
<div> <div>
@@ -63,7 +64,7 @@ class InfoPanel extends React.Component {
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'> <button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' /> <i className='fa fa-clipboard' />
</button> </button>
<p styleName='infoPanel-sub'>NOTE LINK</p> <p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
</div> </div>
<hr /> <hr />
@@ -71,22 +72,22 @@ class InfoPanel extends React.Component {
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}> <button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>.md</p> <p>{i18n.__('.md')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}> <button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>.txt</p> <p>{i18n.__('.txt')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}> <button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>.html</p> <p>{i18n.__('.html')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => print(e)}> <button styleName='export--enable' onClick={(e) => print(e)}>
<i className='fa fa-print' /> <i className='fa fa-print' />
<p>Print</p> <p>{i18n.__('Print')}</p>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoPanel.styl' import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({ const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
@@ -9,24 +10,24 @@ const InfoPanelTrashed = ({
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}> <div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>MODIFICATION DATE</p> <p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
</div> </div>
<hr /> <hr />
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
<p styleName='infoPanel-sub'>STORAGE</p> <p styleName='infoPanel-sub'>{i18n.__('STORAGE')}</p>
</div> </div>
<div> <div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p> <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>
<div> <div>
<p styleName='infoPanel-default'>{createdAt}</p> <p styleName='infoPanel-default'>{createdAt}</p>
<p styleName='infoPanel-sub'>CREATION DATE</p> <p styleName='infoPanel-sub'>{i18n.__('CREATION DATE')}</p>
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>

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

@@ -19,6 +19,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import FullscreenButton from './FullscreenButton' import FullscreenButton from './FullscreenButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton' import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import ToggleModeButton from './ToggleModeButton' import ToggleModeButton from './ToggleModeButton'
@@ -68,11 +69,8 @@ class MarkdownNoteDetail extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
componentDidUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton) ee.off('topbar:togglelockbutton', this.toggleLockButton)
if (this.saveQueue != null) this.saveNow()
} }
handleUpdateTag () { handleUpdateTag () {
@@ -141,7 +139,7 @@ class MarkdownNoteDetail extends React.Component {
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.storage + '-' + newNote.key key: newNote.key
} }
}) })
this.setState({ this.setState({
@@ -198,8 +196,9 @@ class MarkdownNoteDetail extends React.Component {
noteKey: data.noteKey noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:next', dispatchHandler)
}) })
.then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeletion()) {
@@ -323,10 +322,7 @@ class MarkdownNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'> const trashTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<i styleName='undo-button' <RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} /> <PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -361,12 +357,10 @@ class MarkdownNoteDetail extends React.Component {
value={this.state.note.tags} value={this.state.note.tags}
onChange={this.handleUpdateTag.bind(this)} onChange={this.handleUpdateTag.bind(this)}
/> />
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} /> <TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton <StarButton
onClick={(e) => this.handleStarButtonClick(e)} onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
@@ -399,7 +393,7 @@ class MarkdownNoteDetail extends React.Component {
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`} noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd} exportAsMd={this.exportAsMd}

View File

@@ -7,6 +7,7 @@
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
box-shadow none box-shadow none
padding 20px 40px padding 20px 40px
overflow hidden
.lock-button .lock-button
padding-bottom 3px padding-bottom 3px

View File

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

@@ -20,11 +20,13 @@ import _ from 'lodash'
import {findNoteTitle} from 'browser/lib/findNoteTitle' import {findNoteTitle} from 'browser/lib/findNoteTitle'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import TrashButton from './TrashButton' import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton' import PermanentDeleteButton from './PermanentDeleteButton'
import InfoButton from './InfoButton' import InfoButton from './InfoButton'
import InfoPanel from './InfoPanel' import InfoPanel from './InfoPanel'
import InfoPanelTrashed from './InfoPanelTrashed' import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter' import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
function pass (name) { function pass (name) {
switch (name) { switch (name) {
@@ -52,12 +54,30 @@ class SnippetNoteDetail extends React.Component {
this.state = { this.state = {
isMovingNote: false, isMovingNote: false,
snippetIndex: 0, snippetIndex: 0,
showArrows: false,
enableLeftArrow: false,
enableRightArrow: false,
note: Object.assign({ note: Object.assign({
description: '' description: ''
}, props.note, { }, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet)) 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) { componentWillReceiveProps (nextProps) {
@@ -77,6 +97,7 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + index].reload() this.refs['code-' + index].reload()
}) })
if (this.refs.tags) this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
this.setState(this.getArrowsState())
}) })
} }
} }
@@ -146,7 +167,7 @@ class SnippetNoteDetail extends React.Component {
hashHistory.replace({ hashHistory.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: newNote.storage + '-' + newNote.key key: newNote.key
} }
}) })
this.setState({ this.setState({
@@ -191,8 +212,9 @@ class SnippetNoteDetail extends React.Component {
noteKey: data.noteKey noteKey: data.noteKey
}) })
} }
ee.once('list:moved', dispatchHandler) ee.once('list:next', dispatchHandler)
}) })
.then(() => ee.emit('list:next'))
} }
} else { } else {
if (confirmDeletion()) { if (confirmDeletion()) {
@@ -226,6 +248,51 @@ class SnippetNoteDetail extends React.Component {
ee.emit('editor:fullscreen') 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) { handleTabPlusButtonClick (e) {
this.addSnippet() this.addSnippet()
} }
@@ -262,9 +329,9 @@ class SnippetNoteDetail extends React.Component {
if (this.state.note.snippets[index].content.trim().length > 0) { if (this.state.note.snippets[index].content.trim().length > 0) {
const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), { const dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete a snippet', message: i18n.__('Delete a snippet'),
detail: 'This work cannot be undone.', detail: i18n.__('This work cannot be undone.'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (dialogIndex === 0) { if (dialogIndex === 0) {
this.deleteSnippetByIndex(index) this.deleteSnippetByIndex(index)
@@ -285,6 +352,21 @@ class SnippetNoteDetail extends React.Component {
this.setState({ note, snippetIndex }, () => { this.setState({ note, snippetIndex }, () => {
this.save() this.save()
this.refs['code-' + this.state.snippetIndex].reload() 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())
}
}
}) })
} }
@@ -341,6 +423,7 @@ class SnippetNoteDetail extends React.Component {
handleKeyDown (e) { handleKeyDown (e) {
switch (e.keyCode) { switch (e.keyCode) {
// tab key
case 9: case 9:
if (e.ctrlKey && !e.shiftKey) { if (e.ctrlKey && !e.shiftKey) {
e.preventDefault() e.preventDefault()
@@ -353,6 +436,7 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor() this.focusEditor()
} }
break break
// L key
case 76: case 76:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper = global.process.platform === 'darwin'
@@ -364,6 +448,7 @@ class SnippetNoteDetail extends React.Component {
} }
} }
break break
// T key
case 84: case 84:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper = global.process.platform === 'darwin'
@@ -455,6 +540,51 @@ class SnippetNoteDetail extends React.Component {
this.refs.description.focus() 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 () { addSnippet () {
const { note } = this.state const { note } = this.state
@@ -465,10 +595,16 @@ class SnippetNoteDetail extends React.Component {
}]) }])
const snippetIndex = note.snippets.length - 1 const snippetIndex = note.snippets.length - 1
this.setState({ this.setState(Object.assign({
note, note,
snippetIndex snippetIndex
}, () => { }, this.getArrowsState()), () => {
if (this.state.showArrows) {
const tabs = this.allTabs.querySelectorAll('div')
if (tabs) {
this.moveToTab(tabs[snippetIndex])
}
}
this.refs['tab-' + snippetIndex].startRenaming() this.refs['tab-' + snippetIndex].startRenaming()
}) })
} }
@@ -502,9 +638,9 @@ class SnippetNoteDetail extends React.Component {
showWarning () { showWarning () {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Sorry!', message: i18n.__('Sorry!'),
detail: 'md/text import is available only a markdown note.', detail: i18n.__('md/text import is available only a markdown note.'),
buttons: ['OK'] buttons: [i18n.__('OK')]
}) })
} }
@@ -567,6 +703,7 @@ class SnippetNoteDetail extends React.Component {
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleCodeChange(index)(e)} onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index} ref={'code-' + index}
/> />
@@ -587,10 +724,7 @@ class SnippetNoteDetail extends React.Component {
const trashTopBar = <div styleName='info'> const trashTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<i styleName='undo-button' <RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} />
className='fa fa-undo fa-fw'
onClick={(e) => this.handleUndoButtonClick(e)}
/>
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} /> <PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -632,10 +766,10 @@ class SnippetNoteDetail extends React.Component {
isActive={note.isStarred} isActive={note.isStarred}
/> />
<button styleName='control-fullScreenButton' title='Fullscreen' <button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
onMouseDown={(e) => this.handleFullScreenButton(e)}> onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' /> <img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>Fullscreen</span> <span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button> </button>
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} /> <TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -647,7 +781,7 @@ class SnippetNoteDetail extends React.Component {
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={currentOption.storage.name}
folderName={currentOption.folder.name} folderName={currentOption.folder.name}
noteLink={`[${note.title}](${location.query.key})`} noteLink={`[${note.title}](:note:${location.query.key})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
@@ -673,16 +807,32 @@ class SnippetNoteDetail extends React.Component {
fontSize: parseInt(config.preview.fontSize, 10) fontSize: parseInt(config.preview.fontSize, 10)
}} }}
ref='description' ref='description'
placeholder='Description...' placeholder={i18n.__('Description...')}
value={this.state.note.description} value={this.state.note.description}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
/> />
</div> </div>
<div styleName='tabList'> <div styleName='tabList'>
<div styleName='list'> <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} {tabList}
</div> </div>
<button styleName='plusButton' </div>
<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)} onClick={(e) => this.handleTabPlusButtonClick(e)}
> >
<i className='fa fa-plus' /> <i className='fa fa-plus' />
@@ -696,7 +846,7 @@ class SnippetNoteDetail extends React.Component {
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)} onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
> >
{this.state.note.snippets[this.state.snippetIndex].mode == null {this.state.note.snippets[this.state.snippetIndex].mode == null
? 'Select Syntax...' ? i18n.__('Select Syntax...')
: this.state.note.snippets[this.state.snippetIndex].mode : this.state.note.snippets[this.state.snippetIndex].mode
}&nbsp; }&nbsp;
<i className='fa fa-caret-down' /> <i className='fa fa-caret-down' />

View File

@@ -35,13 +35,26 @@
height 30px height 30px
display flex display flex
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
overflow hidden
.tabList .list .tabList .list
flex 1 flex 1
display flex
overflow hidden 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() navWhiteButtonColor()
width 30px width 30px

View File

@@ -3,6 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl' import styles from './StarButton.styl'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class StarButton extends React.Component { class StarButton extends React.Component {
constructor (props) { constructor (props) {
@@ -53,7 +54,7 @@ class StarButton extends React.Component {
: '../resources/icon/icon-star.svg' : '../resources/icon/icon-star.svg'
} }
/> />
<span styleName='tooltip'>Star</span> <span styleName='tooltip'>{i18n.__('Star')}</span>
</button> </button>
) )
} }

View File

@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl' import styles from './TagSelect.styl'
import _ from 'lodash' import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor (props) {
@@ -137,7 +138,7 @@ class TagSelect extends React.Component {
<input styleName='newTag' <input styleName='newTag'
ref='newTag' ref='newTag'
value={this.state.newTag} value={this.state.newTag}
placeholder='Add tag...' placeholder={i18n.__('Add tag...')}
onChange={(e) => this.handleNewTagInputChange(e)} onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)} onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
onBlur={(e) => this.handleNewTagBlur(e)} onBlur={(e) => this.handleNewTagBlur(e)}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl' import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({ const ToggleModeButton = ({
onClick, editorType onClick, editorType
@@ -13,7 +14,7 @@ const ToggleModeButton = ({
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}> <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'} /> <img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div> </div>
<span styleName='tooltip'>Toggle Mode</span> <span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div> </div>
) )

View File

@@ -5,8 +5,8 @@
width 52px width 52px
display flex display flex
align-items center align-items center
position absolute position: relative
right 165px top 2px
.active .active
background-color #1EC38B background-color #1EC38B
width 33px width 33px

View File

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

View File

@@ -7,6 +7,7 @@ import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail' import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar' import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -40,9 +41,9 @@ class Detail extends React.Component {
const alertConfig = { const alertConfig = {
type: 'warning', type: 'warning',
message: 'Confirm note deletion', message: i18n.__('Confirm note deletion'),
detail: 'This will permanently remove this note.', detail: i18n.__('This will permanently remove this note.'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
} }
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig) const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
@@ -56,11 +57,8 @@ class Detail extends React.Component {
const { location, data, config } = this.props const { location, data, config } = this.props
let note = null let note = null
if (location.query.key != null) { if (location.query.key != null) {
const splitted = location.query.key.split('-') const noteKey = location.query.key
const storageKey = splitted.shift() note = data.noteMap.get(noteKey)
const noteKey = splitted.shift()
note = data.noteMap.get(storageKey + '-' + noteKey)
} }
if (note == null) { if (note == null) {
@@ -70,7 +68,7 @@ class Detail extends React.Component {
tabIndex='0' tabIndex='0'
> >
<div styleName='empty'> <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> </div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

@@ -14,6 +14,7 @@ import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import store from 'browser/main/store' import store from 'browser/main/store'
import i18n from 'browser/lib/i18n'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
@@ -148,6 +149,37 @@ class Main extends React.Component {
} else { } else {
document.body.setAttribute('data-theme', 'default') 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-ES') {
i18n.setLocale('es-ES')
} else {
i18n.setLocale('en')
}
// Reload all data // Reload all data
dataApi.init() dataApi.init()

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import modal from 'browser/main/lib/modal' import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal' import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron') const { remote } = require('electron')
const { dialog } = remote 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] 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 { return {
storage, storage,
@@ -86,7 +87,7 @@ class NewNoteButton extends React.Component {
onClick={(e) => this.handleNewNoteButtonClick(e)}> onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' /> <img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'> <span styleName='control-newNoteButton-tooltip'>
Make a note {OSX ? '⌘' : 'Ctrl'} + N {i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span> </span>
</button> </button>
</div> </div>

View File

@@ -1,3 +1,4 @@
/* global electron */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -13,10 +14,14 @@ import searchFromNotes from 'browser/lib/search'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { hashHistory } from 'react-router' import { hashHistory } from 'react-router'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { Menu, MenuItem, dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) { function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt) return new Date(b.createdAt) - new Date(a.createdAt)
@@ -31,7 +36,7 @@ function sortByUpdatedAt (a, b) {
} }
function findNoteByKey (notes, noteKey) { function findNoteByKey (notes, noteKey) {
return notes.find((note) => `${note.storage}-${note.key}` === noteKey) return notes.find((note) => note.key === noteKey)
} }
function findNotesByKeys (notes, noteKeys) { function findNotesByKeys (notes, noteKeys) {
@@ -39,7 +44,7 @@ function findNotesByKeys (notes, noteKeys) {
} }
function getNoteKey (note) { function getNoteKey (note) {
return `${note.storage}-${note.key}` return note.key
} }
class NoteList extends React.Component { class NoteList extends React.Component {
@@ -66,6 +71,11 @@ class NoteList extends React.Component {
this.deleteNote = this.deleteNote.bind(this) this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this) this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.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) // TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = { this.state = {
@@ -109,14 +119,27 @@ class NoteList extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { location } = this.props 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 const { router } = this.context
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes() 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({ router.replace({
pathname: location.pathname, pathname: location.pathname,
query: { query: {
key: this.notes[0].storage + '-' + this.notes[0].key key: noteKey
} }
}) })
return return
@@ -240,27 +263,38 @@ class NoteList extends React.Component {
handleNoteListKeyDown (e) { handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true if (e.metaKey || e.ctrlKey) return true
// A key
if (e.keyCode === 65 && !e.shiftKey) { if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault() e.preventDefault()
ee.emit('top:new-note') ee.emit('top:new-note')
} }
// D key
if (e.keyCode === 68) { if (e.keyCode === 68) {
e.preventDefault() e.preventDefault()
this.deleteNote() this.deleteNote()
} }
// E key
if (e.keyCode === 69) { if (e.keyCode === 69) {
e.preventDefault() e.preventDefault()
ee.emit('detail:focus') 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() e.preventDefault()
this.selectPriorNote() this.selectPriorNote()
} }
if (e.keyCode === 40) { // DOWN or J key
if (e.keyCode === 40 || e.keyCode === 74) {
e.preventDefault() e.preventDefault()
this.selectNextNote() this.selectNextNote()
} }
@@ -292,8 +326,10 @@ class NoteList extends React.Component {
} }
if (location.pathname.match(/\/searched/)) { if (location.pathname.match(/\/searched/)) {
const searchInputText = document.getElementsByClassName('searchInput')[0].value const searchInputText = params.searchword
if (searchInputText === '') { const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
if (searchInputText === undefined || searchInputText === '') {
return this.sortByPin(this.contextNotes) return this.sortByPin(this.contextNotes)
} }
return searchFromNotes(this.contextNotes, searchInputText) return searchFromNotes(this.contextNotes, searchInputText)
@@ -411,9 +447,9 @@ class NoteList extends React.Component {
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') { if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Sorry!', message: i18n.__('Sorry!'),
detail: 'md/text import is available only a markdown note.', detail: i18n.__('md/text import is available only a markdown note.'),
buttons: ['OK', 'Cancel'] buttons: [i18n.__('OK'), i18n.__('Cancel')]
}) })
} }
} }
@@ -437,12 +473,28 @@ class NoteList extends React.Component {
this.handleNoteClick(e, uniqueKey) this.handleNoteClick(e, uniqueKey)
} }
const pinLabel = note.isPinned ? 'Remove pin' : 'Pin to Top' const pinLabel = note.isPinned ? i18n.__('Remove pin') : i18n.__('Pin to Top')
const deleteLabel = 'Delete Note' const deleteLabel = i18n.__('Delete Note')
const cloneNote = 'Clone 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() const menu = new Menu()
if (!location.pathname.match(/\/home|\/starred|\/trash/)) {
if (location.pathname.match(/\/trash/)) {
menu.append(new MenuItem({
label: restoreNote,
click: this.restoreNote
}))
menu.append(new MenuItem({
label: deleteLabel,
click: this.deleteNote
}))
} else {
if (!location.pathname.match(/\/starred/)) {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: pinLabel, label: pinLabel,
click: this.pinToTop click: this.pinToTop
@@ -456,18 +508,45 @@ class NoteList extends React.Component {
label: cloneNote, label: cloneNote,
click: this.cloneNote.bind(this) 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() menu.popup()
} }
pinToTop () { updateSelectedNotes (updateFunc, cleanSelection = true) {
const { selectedNoteKeys } = this.state const { selectedNoteKeys } = this.state
const { dispatch } = this.props const { dispatch } = this.props
const notes = this.notes.map((note) => Object.assign({}, note)) const notes = this.notes.map((note) => Object.assign({}, note))
const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) 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( Promise.all(
selectedNotes.map((note) => { selectedNotes.map((note) => {
note.isPinned = !note.isPinned note = updateFunc(note)
return dataApi return dataApi
.updateNote(note.storage, note.key, note) .updateNote(note.storage, note.key, note)
}) })
@@ -480,7 +559,24 @@ class NoteList extends React.Component {
}) })
}) })
}) })
this.setState({ selectedNoteKeys: [] })
if (cleanSelection) {
this.selectNextNote()
}
}
pinToTop () {
this.updateSelectedNotes((note) => {
note.isPinned = !note.isPinned
return note
})
}
restoreNote () {
this.updateSelectedNotes((note) => {
note.isTrashed = false
return note
})
} }
deleteNote () { deleteNote () {
@@ -494,17 +590,15 @@ class NoteList extends React.Component {
const noteExp = selectedNotes.length > 1 ? 'notes' : 'note' const noteExp = selectedNotes.length > 1 ? 'notes' : 'note'
const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), { const dialogueButtonIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Confirm note deletion', message: i18n.__('Confirm note deletion'),
detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`, detail: `This will permanently remove ${selectedNotes.length} ${noteExp}.`,
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (dialogueButtonIndex === 1) return if (dialogueButtonIndex === 1) return
Promise.all( Promise.all(
selectedNoteKeys.map((uniqueKey) => { selectedNotes.map((note) => {
const storageKey = uniqueKey.split('-')[0]
const noteKey = uniqueKey.split('-')[1]
return dataApi return dataApi
.deleteNote(storageKey, noteKey) .deleteNote(note.storage, note.key)
}) })
) )
.then((data) => { .then((data) => {
@@ -561,27 +655,138 @@ class NoteList extends React.Component {
.createNote(storage.key, { .createNote(storage.key, {
type: firstNote.type, type: firstNote.type,
folder: folder.key, folder: folder.key,
title: firstNote.title + ' copy', title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content content: firstNote.content
}) })
.then((note) => { .then((note) => {
const uniqueKey = note.storage + '-' + note.key
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
this.setState({ this.setState({
selectedNoteKeys: [uniqueKey] selectedNoteKeys: [note.key]
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: uniqueKey} 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 () { importFromFile () {
const options = { const options = {
filters: [ filters: [
@@ -674,10 +879,28 @@ class NoteList extends React.Component {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: message, 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 () { render () {
const { location, config } = this.props const { location, config } = this.props
let { notes } = this.props let { notes } = this.props
@@ -687,7 +910,7 @@ class NoteList extends React.Component {
: config.sortBy === 'ALPHABETICAL' : config.sortBy === 'ALPHABETICAL'
? sortByAlphabetical ? sortByAlphabetical
: sortByUpdatedAt : sortByUpdatedAt
const sortedNotes = location.pathname.match(/\/home|\/starred|\/trash/) const sortedNotes = location.pathname.match(/\/starred|\/trash/)
? this.getNotes().sort(sortFunc) ? this.getNotes().sort(sortFunc)
: this.sortByPin(this.getNotes().sort(sortFunc)) : this.sortByPin(this.getNotes().sort(sortFunc))
this.notes = notes = sortedNotes.filter((note) => { this.notes = notes = sortedNotes.filter((note) => {
@@ -714,6 +937,8 @@ class NoteList extends React.Component {
} }
}) })
const viewType = this.getViewType()
const noteList = notes const noteList = notes
.map(note => { .map(note => {
if (note == null) { if (note == null) {
@@ -739,6 +964,9 @@ class NoteList extends React.Component {
handleNoteClick={this.handleNoteClick.bind(this)} handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/> />
) )
} }
@@ -752,6 +980,9 @@ class NoteList extends React.Component {
handleNoteClick={this.handleNoteClick.bind(this)} handleNoteClick={this.handleNoteClick.bind(this)}
handleDragStart={this.handleDragStart.bind(this)} handleDragStart={this.handleDragStart.bind(this)}
pathname={location.pathname} pathname={location.pathname}
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
/> />
) )
}) })
@@ -766,16 +997,17 @@ class NoteList extends React.Component {
<div styleName='control-sortBy'> <div styleName='control-sortBy'>
<i className='fa fa-angle-down' /> <i className='fa fa-angle-down' />
<select styleName='control-sortBy-select' <select styleName='control-sortBy-select'
title={i18n.__('Select filter mode')}
value={config.sortBy} value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)} onChange={(e) => this.handleSortByChange(e)}
> >
<option value='UPDATED_AT'>Updated</option> <option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
<option value='CREATED_AT'>Created</option> <option title='Sort by create time' value='CREATED_AT'>{i18n.__('Created')}</option>
<option value='ALPHABETICAL'>Alphabetically</option> <option title='Sort alphabetically' value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
</select> </select>
</div> </div>
<div styleName='control-button-area'> <div styleName='control-button-area'>
<button styleName={config.listStyle === 'DEFAULT' <button title={i18n.__('Default View')} styleName={config.listStyle === 'DEFAULT'
? 'control-button--active' ? 'control-button--active'
: 'control-button' : 'control-button'
} }
@@ -783,7 +1015,7 @@ class NoteList extends React.Component {
> >
<img styleName='iconTag' src='../resources/icon/icon-column.svg' /> <img styleName='iconTag' src='../resources/icon/icon-column.svg' />
</button> </button>
<button styleName={config.listStyle === 'SMALL' <button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active' ? 'control-button--active'
: 'control-button' : 'control-button'
} }

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl' import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const ListButton = ({ const ListButton = ({
onClick, isTagActive onClick, isTagActive
@@ -12,7 +13,7 @@ const ListButton = ({
: '../resources/icon/icon-list-active.svg' : '../resources/icon/icon-list-active.svg'
} }
/> />
<span styleName='tooltip'>Notes</span> <span styleName='tooltip'>{i18n.__('Notes')}</span>
</button> </button>
) )

View File

@@ -2,13 +2,14 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferenceButton.styl' import styles from './PreferenceButton.styl'
import i18n from 'browser/lib/i18n'
const PreferenceButton = ({ const PreferenceButton = ({
onClick onClick
}) => ( }) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}> <button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' /> <img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>Preferences</span> <span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button> </button>
) )

View File

@@ -30,11 +30,33 @@
display flex display flex
flex-direction column flex-direction column
.tag-title .tag-control
display flex
height 30px
line-height 25px
overflow hidden
.tag-control-title
padding-left 15px padding-left 15px
padding-bottom 13px padding-bottom 13px
flex 1
p p
color $ui-button-default-color color $ui-button-default-color
.tag-control-sortTagsBy
user-select none
font-size 12px
color $ui-inactive-text-color
margin-left 12px
margin-right 12px
.tag-control-sortTagsBy-select
appearance: none;
margin-left 5px
color $ui-inactive-text-color
padding 0
border none
background-color transparent
outline none
cursor pointer
font-size 12px
.tagList .tagList
overflow-y auto overflow-y auto

View File

@@ -9,6 +9,8 @@ import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem' import StorageItemChild from 'browser/components/StorageItem'
import _ from 'lodash' import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, dialog } = remote const { Menu, dialog } = remote
@@ -25,14 +27,14 @@ class StorageItem extends React.Component {
handleHeaderContextMenu (e) { handleHeaderContextMenu (e) {
const menu = Menu.buildFromTemplate([ const menu = Menu.buildFromTemplate([
{ {
label: 'Add Folder', label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e) click: (e) => this.handleAddFolderButtonClick(e)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Unlink Storage', label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e) click: (e) => this.handleUnlinkStorageClick(e)
} }
]) ])
@@ -43,9 +45,9 @@ class StorageItem extends React.Component {
handleUnlinkStorageClick (e) { handleUnlinkStorageClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Unlink Storage', message: i18n.__('Unlink Storage'),
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)', detail: i18n.__('This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (index === 0) { if (index === 0) {
@@ -92,21 +94,21 @@ class StorageItem extends React.Component {
handleFolderButtonContextMenu (e, folder) { handleFolderButtonContextMenu (e, folder) {
const menu = Menu.buildFromTemplate([ const menu = Menu.buildFromTemplate([
{ {
label: 'Rename Folder', label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder) click: (e) => this.handleRenameFolderClick(e, folder)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Export Folder', label: i18n.__('Export Folder'),
submenu: [ submenu: [
{ {
label: 'Export as txt', label: i18n.__('Export as txt'),
click: (e) => this.handleExportFolderClick(e, folder, 'txt') click: (e) => this.handleExportFolderClick(e, folder, 'txt')
}, },
{ {
label: 'Export as md', label: i18n.__('Export as md'),
click: (e) => this.handleExportFolderClick(e, folder, 'md') click: (e) => this.handleExportFolderClick(e, folder, 'md')
} }
] ]
@@ -115,7 +117,7 @@ class StorageItem extends React.Component {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Delete Folder', label: i18n.__('Delete Folder'),
click: (e) => this.handleFolderDeleteClick(e, folder) click: (e) => this.handleFolderDeleteClick(e, folder)
} }
]) ])
@@ -134,8 +136,8 @@ class StorageItem extends React.Component {
handleExportFolderClick (e, folder, fileType) { handleExportFolderClick (e, folder, fileType) {
const options = { const options = {
properties: ['openDirectory', 'createDirectory'], properties: ['openDirectory', 'createDirectory'],
buttonLabel: 'Select directory', buttonLabel: i18n.__('Select directory'),
title: 'Select a folder to export the files to', title: i18n.__('Select a folder to export the files to'),
multiSelections: false multiSelections: false
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, dialog.showOpenDialog(remote.getCurrentWindow(), options,
@@ -159,9 +161,9 @@ class StorageItem extends React.Component {
handleFolderDeleteClick (e, folder) { handleFolderDeleteClick (e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Delete Folder', message: i18n.__('Delete Folder'),
detail: 'This will delete all notes in the folder and can not be undone.', detail: i18n.__('This will delete all notes in the folder and can not be undone.'),
buttons: ['Confirm', 'Cancel'] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}) })
if (index === 0) { if (index === 0) {
@@ -191,33 +193,16 @@ class StorageItem extends React.Component {
dropNote (storage, folder, dispatch, location, noteData) { dropNote (storage, folder, dispatch, location, noteData) {
noteData = noteData.filter((note) => folder.key !== note.folder) noteData = noteData.filter((note) => folder.key !== note.folder)
if (noteData.length === 0) return if (noteData.length === 0) return
const newNoteData = noteData.map((note) => Object.assign({}, note, {storage: storage, folder: folder.key}))
Promise.all( 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) => { .then((createdNoteData) => {
createdNoteData.forEach((note) => { createdNoteData.forEach((newNote) => {
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'MOVE_NOTE',
note: note originNote: noteData.find((note) => note.content === newNote.content),
}) note: newNote
})
})
.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
}) })
}) })
}) })
@@ -236,7 +221,8 @@ class StorageItem extends React.Component {
render () { render () {
const { storage, location, isFolded, data, dispatch } = this.props const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data 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 isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
@@ -250,8 +236,9 @@ class StorageItem extends React.Component {
noteCount = noteSet.size - trashedNoteCount noteCount = noteSet.size - trashedNoteCount
} }
return ( return (
<StorageItemChild <SortableStorageItemChild
key={folder.key} key={folder.key}
index={index}
isActive={isActive} isActive={isActive}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)} handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './SwitchButton.styl' import styles from './SwitchButton.styl'
import i18n from 'browser/lib/i18n'
const TagButton = ({ const TagButton = ({
onClick, isTagActive onClick, isTagActive
@@ -12,7 +13,7 @@ const TagButton = ({
: '../resources/icon/icon-tag.svg' : '../resources/icon/icon-tag.svg'
} }
/> />
<span styleName='tooltip'>Tags</span> <span styleName='tooltip'>{i18n.__('Tags')}</span>
</button> </button>
) )

View File

@@ -1,6 +1,9 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' 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 styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal' import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal' import PreferencesModal from '../modals/PreferencesModal'
@@ -14,6 +17,8 @@ import EventEmitter from 'browser/main/lib/eventEmitter'
import PreferenceButton from './PreferenceButton' import PreferenceButton from './PreferenceButton'
import ListButton from './ListButton' import ListButton from './ListButton'
import TagButton from './TagButton' import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
class SideNav extends React.Component { class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7 // TODO: should not use electron stuff v0.7
@@ -65,8 +70,19 @@ class SideNav extends React.Component {
router.push('/alltags') 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) { SideNavComponent (isFolded, storageList) {
const { location, data } = this.props const { location, data, config } = this.props
const isHomeActive = !!location.pathname.match(/^\/home$/) const isHomeActive = !!location.pathname.match(/^\/home$/)
const isStarredActive = !!location.pathname.match(/^\/starred$/) const isStarredActive = !!location.pathname.match(/^\/starred$/)
@@ -86,20 +102,36 @@ class SideNav extends React.Component {
isTrashedActive={isTrashedActive} isTrashedActive={isTrashedActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)} handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(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} counterStarredNote={data.starredSet._set.size}
counterDelNote={data.trashedSet._set.size} counterDelNote={data.trashedSet._set.size}
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
/> />
<StorageList storageList={storageList} /> <StorageList storageList={storageList} isFolded={isFolded} />
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} /> <NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div> </div>
) )
} else { } else {
component = ( component = (
<div styleName='tabBody'> <div styleName='tabBody'>
<div styleName='tag-title'> <div styleName='tag-control'>
<p>Tags</p> <div styleName='tag-control-title'>
<p>{i18n.__('Tags')}</p>
</div>
<div styleName='tag-control-sortTagsBy'>
<i className='fa fa-angle-down' />
<select styleName='tag-control-sortTagsBy-select'
title={i18n.__('Select filter mode')}
value={config.sortTagsBy}
onChange={(e) => this.handleSortTagsByChange(e)}
>
<option title='Sort alphabetically'
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
<option title='Sort by update time'
value='COUNTER'>{i18n.__('Counter')}</option>
</select>
</div>
</div> </div>
<div styleName='tagList'> <div styleName='tagList'>
{this.tagListComponent(data)} {this.tagListComponent(data)}
@@ -112,19 +144,26 @@ class SideNav extends React.Component {
} }
tagListComponent () { tagListComponent () {
const { data, location } = this.props const { data, location, config } = this.props
const tagList = data.tagNoteMap.map((tag, key) => { let tagList = _.sortBy(data.tagNoteMap.map(
return key (tag, name) => ({name, size: tag.size})),
}) ['name']
)
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
}
return (
tagList.map(tag => {
return ( return (
tagList.map(tag => (
<TagListItem <TagListItem
name={tag} name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)} handleClickTagListItem={this.handleClickTagListItem.bind(this)}
isActive={this.getTagActive(location.pathname, tag)} isActive={this.getTagActive(location.pathname, tag.name)}
key={tag} key={tag.name}
count={tag.size}
/> />
)) )
})
) )
} }
@@ -139,19 +178,62 @@ class SideNav extends React.Component {
router.push(`/tags/${name}`) router.push(`/tags/${name}`)
} }
handleSortTagsByChange (e) {
const { dispatch } = this.props
const config = {
sortTagsBy: e.target.value
}
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
}
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 () { render () {
const { data, location, config, dispatch } = this.props const { data, location, config, dispatch } = this.props
const isFolded = config.isSideNavFolded const isFolded = config.isSideNavFolded
const storageList = data.storageMap.map((storage, key) => { const storageList = data.storageMap.map((storage, key) => {
return <StorageItem const SortableStorageItem = SortableContainer(StorageItem)
return <SortableStorageItem
key={storage.key} key={storage.key}
storage={storage} storage={storage}
data={data} data={data}
location={location} location={location}
isFolded={isFolded} isFolded={isFolded}
dispatch={dispatch} dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle
/> />
}) })
const style = {} const style = {}

View File

@@ -21,20 +21,19 @@
color white color white
.zoom .zoom
display none navButtonColor()
// navButtonColor() color rgba(0,0,0,.54)
// color rgba(0,0,0,.54) height 20px
// height 20px display flex
// display flex padding 0
// padding 0 align-items center
// align-items center background-color transparent
// background-color transparent &:hover
// &:hover color $ui-active-color
// color $ui-active-color &:active
// &:active color $ui-active-color
// color $ui-active-color span
// span margin-left 5px
// margin-left 5px
.update .update
navButtonColor() navButtonColor()

View File

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

View File

@@ -40,6 +40,32 @@ $control-height = 34px
padding-bottom 2px padding-bottom 2px
background-color $ui-noteList-backgroundColor 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 .control-search-optionList
position fixed position fixed
z-index 200 z-index 200

View File

@@ -5,6 +5,7 @@ import styles from './TopBar.styl'
import _ from 'lodash' import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton' import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n'
class TopBar extends React.Component { class TopBar extends React.Component {
constructor (props) { constructor (props) {
@@ -22,14 +23,37 @@ class TopBar extends React.Component {
this.focusSearchHandler = () => { this.focusSearchHandler = () => {
this.handleOnSearchFocus() this.handleOnSearchFocus()
} }
this.codeInitHandler = this.handleCodeInit.bind(this)
} }
componentDidMount () { componentDidMount () {
const { params } = this.props
const searchWord = params.searchword
if (searchWord !== undefined) {
this.setState({
search: searchWord,
isSearching: true
})
}
ee.on('top:focus-search', this.focusSearchHandler) ee.on('top:focus-search', this.focusSearchHandler)
ee.on('code:init', this.codeInitHandler)
} }
componentWillUnmount () { componentWillUnmount () {
ee.off('top:focus-search', this.focusSearchHandler) 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) { handleKeyDown (e) {
@@ -39,6 +63,23 @@ class TopBar extends React.Component {
isIME: false 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 // When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) { if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({ this.setState({
@@ -64,23 +105,26 @@ class TopBar extends React.Component {
this.setState({ this.setState({
isConfirmTranslation: true isConfirmTranslation: true
}) })
router.push('/searched') const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({ this.setState({
search: this.refs.searchInput.value search: keyword
}) })
} }
} }
handleSearchChange (e) { handleSearchChange (e) {
const { router } = this.context const { router } = this.context
const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) { if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push('/searched') router.push(`/searched/${encodeURIComponent(keyword)}`)
} else { } else {
e.preventDefault() e.preventDefault()
} }
this.setState({ this.setState({
search: this.refs.searchInput.value search: keyword
}) })
ee.emit('top:search', keyword)
} }
handleSearchFocus (e) { handleSearchFocus (e) {
@@ -108,13 +152,19 @@ class TopBar extends React.Component {
} }
handleOnSearchFocus () { handleOnSearchFocus () {
const el = this.refs.search.childNodes[0]
if (this.state.isSearching) { if (this.state.isSearching) {
this.refs.search.childNodes[0].blur() el.blur()
} else { } 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 () { render () {
const { config, style, location } = this.props const { config, style, location } = this.props
return ( return (
@@ -136,19 +186,19 @@ class TopBar extends React.Component {
onChange={(e) => this.handleSearchChange(e)} onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)} onKeyUp={(e) => this.handleKeyUp(e)}
placeholder='Search' placeholder={i18n.__('Search')}
type='text' type='text'
className='searchInput' className='searchInput'
/> />
</div> {this.state.search !== '' &&
{this.state.search > 0 && <button styleName='control-search-input-clear'
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)} onClick={(e) => this.handleSearchClearButton(e)}
> >
<i className='fa fa-times' /> <i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
</button> </button>
} }
</div>
</div> </div>
</div> </div>
{location.pathname === '/trashed' ? '' {location.pathname === '/trashed' ? ''

View File

@@ -108,6 +108,21 @@ body[data-theme="dark"]
background #B1D7FE background #B1D7FE
::selection ::selection
background #B1D7FE background #B1D7FE
.CodeMirror-foldmarker
font-family: arial
.CodeMirror-foldgutter
width: .7em
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded
cursor: pointer
.CodeMirror-foldgutter-open:after
content: "\25BE"
.CodeMirror-foldgutter-folded:after
content: "\25B8"
.sortableItemHelper .sortableItemHelper
z-index modalZIndex + 5 z-index modalZIndex + 5

View File

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

View File

@@ -1,5 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import RcParser from 'browser/lib/RcParser' import RcParser from 'browser/lib/RcParser'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32' const win = global.process.platform === 'win32'
@@ -15,12 +16,14 @@ export const DEFAULT_CONFIG = {
listWidth: 280, listWidth: 280,
navWidth: 200, navWidth: 200,
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL' sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true, amaEnabled: true,
hotkey: { hotkey: {
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E' toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
}, },
ui: { ui: {
language: 'en',
theme: 'default', theme: 'default',
showCopyNotification: true, showCopyNotification: true,
disableDirectWrite: false, disableDirectWrite: false,
@@ -33,10 +36,13 @@ export const DEFAULT_CONFIG = {
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas', fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
indentType: 'space', indentType: 'space',
indentSize: '2', indentSize: '2',
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true, displayLineNumbers: true,
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
scrollPastEnd: false, scrollPastEnd: false,
type: 'SPLIT' type: 'SPLIT',
fetchUrlTitle: true
}, },
preview: { preview: {
fontSize: '14', fontSize: '14',
@@ -47,7 +53,17 @@ export const DEFAULT_CONFIG = {
latexInlineClose: '$', latexInlineClose: '$',
latexBlockOpen: '$$', latexBlockOpen: '$$',
latexBlockClose: '$$', latexBlockClose: '$$',
scrollPastEnd: false 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: ''
} }
} }
@@ -123,6 +139,8 @@ function set (updates) {
document.body.setAttribute('data-theme', 'default') document.body.setAttribute('data-theme', 'default')
} }
i18n.setLocale(newConfig.ui.language)
let editorTheme = document.getElementById('editorTheme') let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) { if (editorTheme == null) {
editorTheme = document.createElement('link') editorTheme = document.createElement('link')
@@ -150,6 +168,7 @@ function set (updates) {
function assignConfigValues (originalConfig, rcConfig) { function assignConfigValues (originalConfig, rcConfig) {
const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig) const config = Object.assign({}, DEFAULT_CONFIG, originalConfig, rcConfig)
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, originalConfig.hotkey, rcConfig.hotkey) 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.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor) config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview) 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') 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} filePath
* @param {String} storageKey * @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) => { return new Promise((resolve, reject) => {
try { try {
const targetStorage = findStorage(storageKey) const targetStorage = findStorage(storageKey)
const inputImage = fs.createReadStream(filePath) const inputImage = fs.createReadStream(filePath)
const imageExt = path.extname(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 basename = `${imageName}${imageExt}`
const imageDir = path.join(targetStorage.path, 'images') const imageDir = path.join(targetStorage.path, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)

View File

@@ -52,12 +52,12 @@ function createNote (storageKey, input) {
return storage return storage
}) })
.then(function saveNote (storage) { .then(function saveNote (storage) {
let key = keygen() let key = keygen(true)
let isUnique = false let isUnique = false
while (!isUnique) { while (!isUnique) {
try { try {
sander.statSync(path.join(storage.path, 'notes', key + '.cson')) sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
key = keygen() key = keygen(true)
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
isUnique = true isUnique = true

View File

@@ -1,6 +1,7 @@
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
@@ -45,7 +46,7 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
notes notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => { .forEach(snippet => {
const notePath = path.join(exportDir, `${snippet.title}.${fileType}`) const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content) fs.writeFileSync(notePath, snippet.content)
}) })

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

@@ -1,10 +1,12 @@
const resolveStorageData = require('./resolveStorageData') const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash') const _ = require('lodash')
const path = require('path') const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season') const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen') const keygen = require('browser/lib/keygen')
const sander = require('sander') const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
const copyImage = require('./copyImage')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage let oldStorage, newStorage
@@ -37,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return resolveStorageData(newStorage) return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) { .then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage newStorage = _newStorage
newNoteKey = keygen() newNoteKey = keygen(true)
let isUnique = false let isUnique = false
while (!isUnique) { while (!isUnique) {
try { try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson')) sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen() newNoteKey = keygen(true)
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
isUnique = true isUnique = true
@@ -65,6 +67,27 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
return noteData 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) { .then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage'])) CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData return noteData

View File

@@ -27,9 +27,12 @@ function resolveStorageNotes (storage) {
data.storage = storage.key data.storage = storage.key
return data return data
} catch (err) { } catch (err) {
console.error(notePath) console.error(`error on note path: ${notePath}, error: ${err}`)
} }
}) })
.filter(function filterOnlyNoteObject (noteObj) {
return typeof noteObj === 'object'
})
return Promise.resolve(notes) return Promise.resolve(notes)
} }

View File

@@ -30,6 +30,9 @@ function validateInput (input) {
validatedInput.isPinned = !!input.isPinned validatedInput.isPinned = !!input.isPinned
} }
if (!_.isNil(input.blog)) {
validatedInput.blog = input.blog
}
validatedInput.type = input.type validatedInput.type = input.type
switch (input.type) { switch (input.type) {
case 'MARKDOWN_NOTE': case 'MARKDOWN_NOTE':

View File

@@ -1,5 +1,4 @@
import ConfigManager from './ConfigManager' import ConfigManager from './ConfigManager'
import store from 'browser/main/store'
const nodeIpc = require('node-ipc') const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron') const { remote, ipcRenderer } = require('electron')
@@ -18,7 +17,7 @@ nodeIpc.connectTo(
console.log(err) console.log(err)
}) })
nodeIpc.of.node.on('connect', function () { nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully') console.log('Connected successfully')
ipcRenderer.send('config-renew', {config: ConfigManager.get()}) ipcRenderer.send('config-renew', {config: ConfigManager.get()})
}) })
nodeIpc.of.node.on('disconnect', function () { nodeIpc.of.node.on('disconnect', function () {

View File

@@ -7,6 +7,7 @@ import store from 'browser/main/store'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
class CreateFolderModal extends React.Component { class CreateFolderModal extends React.Component {
constructor (props) { constructor (props) {
@@ -79,12 +80,12 @@ class CreateFolderModal extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
<div styleName='header'> <div styleName='header'>
<div styleName='title'>Create new folder</div> <div styleName='title'>{i18n.__('Create new folder')}</div>
</div> </div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} /> <ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='control'> <div styleName='control'>
<div styleName='control-folder'> <div styleName='control-folder'>
<div styleName='control-folder-label'>Folder name</div> <div styleName='control-folder-label'>{i18n.__('Folder name')}</div>
<input styleName='control-folder-input' <input styleName='control-folder-input'
ref='name' ref='name'
value={this.state.name} value={this.state.name}
@@ -95,7 +96,7 @@ class CreateFolderModal extends React.Component {
<button styleName='control-confirmButton' <button styleName='control-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)} onClick={(e) => this.handleConfirmButtonClick(e)}
> >
Create {i18n.__('Create')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
class NewNoteModal extends React.Component { class NewNoteModal extends React.Component {
constructor (props) { constructor (props) {
@@ -35,14 +36,16 @@ class NewNoteModal extends React.Component {
content: '' content: ''
}) })
.then((note) => { .then((note) => {
const noteHash = note.key
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: note.storage + '-' + note.key} query: {key: noteHash}
}) })
ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() this.props.close()
}) })
@@ -73,14 +76,16 @@ class NewNoteModal extends React.Component {
}] }]
}) })
.then((note) => { .then((note) => {
const noteHash = note.key
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
hashHistory.push({ hashHistory.push({
pathname: location.pathname, pathname: location.pathname,
query: {key: note.storage + '-' + note.key} query: {key: noteHash}
}) })
ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
this.props.close() this.props.close()
}) })
@@ -106,7 +111,7 @@ class NewNoteModal extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
<div styleName='header'> <div styleName='header'>
<div styleName='title'>Make a note</div> <div styleName='title'>{i18n.__('Make a note')}</div>
</div> </div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} /> <ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='control'> <div styleName='control'>
@@ -118,8 +123,8 @@ class NewNoteModal extends React.Component {
<i styleName='control-button-icon' <i styleName='control-button-icon'
className='fa fa-file-text-o' className='fa fa-file-text-o'
/><br /> /><br />
<span styleName='control-button-label'>Markdown Note</span><br /> <span styleName='control-button-label'>{i18n.__('Markdown Note')}</span><br />
<span styleName='control-button-description'>This format is for creating text documents. Checklists, code blocks and Latex blocks are available.</span> <span styleName='control-button-description'>{i18n.__('This format is for creating text documents. Checklists, code blocks and Latex blocks are available.')}</span>
</button> </button>
<button styleName='control-button' <button styleName='control-button'
@@ -130,13 +135,13 @@ class NewNoteModal extends React.Component {
<i styleName='control-button-icon' <i styleName='control-button-icon'
className='fa fa-code' className='fa fa-code'
/><br /> /><br />
<span styleName='control-button-label'>Snippet Note</span><br /> <span styleName='control-button-label'>{i18n.__('Snippet Note')}</span><br />
<span styleName='control-button-description'>This format is for creating code snippets. Multiple snippets can be grouped into a single note. <span styleName='control-button-description'>{i18n.__('This format is for creating code snippets. Multiple snippets can be grouped into a single note.')}
</span> </span>
</button> </button>
</div> </div>
<div styleName='description'><i className='fa fa-arrows-h' /> Tab to switch format</div> <div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div>
</div> </div>
) )

View File

@@ -0,0 +1,199 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import PropTypes from 'prop-types'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron')
const { shell } = electron
const ipc = electron.ipcRenderer
class Blog extends React.Component {
constructor (props) {
super(props)
this.state = {
config: props.config,
BlogAlert: null
}
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
clearMessage () {
_.debounce(() => {
this.setState({
BlogAlert: null
})
}, 2000)()
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({BlogAlert: {
type: 'success',
message: i18n.__('Successfully applied!')
}})
}
this.handleSettingError = (err) => {
this.setState({BlogAlert: {
type: 'error',
message: err.message != null ? err.message : i18n.__('Error occurs!')
}})
}
this.oldBlog = this.state.config.blog
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
handleBlogChange (e) {
const { config } = this.state
config.blog = {
password: !_.isNil(this.refs.passwordInput) ? this.refs.passwordInput.value : config.blog.password,
username: !_.isNil(this.refs.usernameInput) ? this.refs.usernameInput.value : config.blog.username,
token: !_.isNil(this.refs.tokenInput) ? this.refs.tokenInput.value : config.blog.token,
authMethod: this.refs.authMethodDropdown.value,
address: this.refs.addressInput.value,
type: this.refs.typeDropdown.value
}
this.setState({
config
})
if (_.isEqual(this.oldBlog, config.blog)) {
this.props.haveToSave()
} else {
this.props.haveToSave({
tab: 'Blog',
type: 'warning',
message: i18n.__('You have to save!')
})
}
}
handleSaveButtonClick (e) {
const newConfig = {
blog: this.state.config.blog
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
this.clearMessage()
this.props.haveToSave()
}
render () {
const {config, BlogAlert} = this.state
const blogAlertElement = BlogAlert != null
? <p className={`alert ${BlogAlert.type}`}>
{BlogAlert.message}
</p>
: null
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>{i18n.__('Blog')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Blog Type')}
</div>
<div styleName='group-section-control'>
<select
value={config.blog.type}
ref='typeDropdown'
onChange={(e) => this.handleBlogChange(e)}
>
<option value='wordpress' key='wordpress'>{i18n.__('wordpress')}</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Blog Address')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='addressInput'
value={config.blog.address}
type='text'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
</button>
{blogAlertElement}
</div>
</div>
<div styleName='group-header2'>{i18n.__('Auth')}</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Authentication Method')}
</div>
<div styleName='group-section-control'>
<select
value={config.blog.authMethod}
ref='authMethodDropdown'
onChange={(e) => this.handleBlogChange(e)}
>
<option value='JWT' key='JWT'>{i18n.__('JWT')}</option>
<option value='USER' key='USER'>{i18n.__('USER')}</option>
</select>
</div>
</div>
{ config.blog.authMethod === 'JWT' &&
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Token')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='tokenInput'
value={config.blog.token}
type='text' />
</div>
</div>
}
{ config.blog.authMethod === 'USER' &&
<div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('UserName')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='usernameInput'
value={config.blog.username}
type='text' />
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Password')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleBlogChange(e)}
ref='passwordInput'
value={config.blog.password}
type='password' />
</div>
</div>
</div>
}
</div>
)
}
}
Blog.propTypes = {
dispatch: PropTypes.func,
haveToSave: PropTypes.func
}
export default CSSModules(Blog, styles)

View File

@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './Crowdfunding.styl' import styles from './Crowdfunding.styl'
import i18n from 'browser/lib/i18n'
const electron = require('electron') const electron = require('electron')
const { shell } = electron const { shell } = electron
@@ -21,22 +22,22 @@ class Crowdfunding extends React.Component {
render () { render () {
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='header'>Crowdfunding</div> <div styleName='header'>{i18n.__('Crowdfunding')}</div>
<p>Dear everyone,</p> <p>{i18n.__('Dear everyone,')}</p>
<br /> <br />
<p>Thank you for using Boostnote!</p> <p>{i18n.__('Thank you for using Boostnote!')}</p>
<p>Boostnote is used in about 200 different countries and regions by an awesome community of developers.</p> <p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
<br /> <br />
<p>To continue supporting this growth, and to satisfy community expectations,</p> <p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
<p>we would like to invest more time and resources in this project.</p> <p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
<br /> <br />
<p>If you like this project and see its potential, you can help by supporting us on OpenCollective!</p> <p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
<br /> <br />
<p>Thanks,</p> <p>{i18n.__('Thanks,')}</p>
<p>Boostnote maintainers</p> <p>{i18n.__('Boostnote maintainers')}</p>
<br /> <br />
<button styleName='cf-link'> <button styleName='cf-link'>
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>Support via OpenCollective</a> <a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
</button> </button>
</div> </div>
) )

View File

@@ -7,6 +7,7 @@ import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store' import store from 'browser/main/store'
import { SketchPicker } from 'react-color' import { SketchPicker } from 'react-color'
import { SortableElement, SortableHandle } from 'react-sortable-hoc' import { SortableElement, SortableHandle } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
class FolderItem extends React.Component { class FolderItem extends React.Component {
constructor (props) { constructor (props) {
@@ -150,12 +151,12 @@ class FolderItem extends React.Component {
<button styleName='folderItem-right-confirmButton' <button styleName='folderItem-right-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)} onClick={(e) => this.handleConfirmButtonClick(e)}
> >
Confirm {i18n.__('Confirm')}
</button> </button>
<button styleName='folderItem-right-button' <button styleName='folderItem-right-button'
onClick={(e) => this.handleCancelButtonClick(e)} onClick={(e) => this.handleCancelButtonClick(e)}
> >
Cancel {i18n.__('Cancel')}
</button> </button>
</div> </div>
</div> </div>
@@ -179,18 +180,18 @@ class FolderItem extends React.Component {
return ( return (
<div styleName='folderItem'> <div styleName='folderItem'>
<div styleName='folderItem-left'> <div styleName='folderItem-left'>
Are you sure to <span styleName='folderItem-left-danger'>delete</span> this folder? {i18n.__('Are you sure to ')} <span styleName='folderItem-left-danger'>{i18n.__(' delete')}</span> {i18n.__('this folder?')}
</div> </div>
<div styleName='folderItem-right'> <div styleName='folderItem-right'>
<button styleName='folderItem-right-dangerButton' <button styleName='folderItem-right-dangerButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)} onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
> >
Confirm {i18n.__('Confirm')}
</button> </button>
<button styleName='folderItem-right-button' <button styleName='folderItem-right-button'
onClick={(e) => this.handleCancelButtonClick(e)} onClick={(e) => this.handleCancelButtonClick(e)}
> >
Cancel {i18n.__('Cancel')}
</button> </button>
</div> </div>
</div> </div>
@@ -231,12 +232,12 @@ class FolderItem extends React.Component {
<button styleName='folderItem-right-button' <button styleName='folderItem-right-button'
onClick={(e) => this.handleEditButtonClick(e)} onClick={(e) => this.handleEditButtonClick(e)}
> >
Edit {i18n.__('Edit')}
</button> </button>
<button styleName='folderItem-right-button' <button styleName='folderItem-right-button'
onClick={(e) => this.handleDeleteButtonClick(e)} onClick={(e) => this.handleDeleteButtonClick(e)}
> >
Delete {i18n.__('Delete')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import styles from './FolderList.styl'
import store from 'browser/main/store' import store from 'browser/main/store'
import FolderItem from './FolderItem' import FolderItem from './FolderItem'
import { SortableContainer } from 'react-sortable-hoc' import { SortableContainer } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
class FolderList extends React.Component { class FolderList extends React.Component {
render () { render () {
@@ -24,7 +25,7 @@ class FolderList extends React.Component {
<div styleName='folderList'> <div styleName='folderList'>
{folderList.length > 0 {folderList.length > 0
? folderList ? folderList
: <div styleName='folderList-empty'>No Folders</div> : <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>
} }
</div> </div>
) )

View File

@@ -5,6 +5,7 @@ import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store' import store from 'browser/main/store'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron') const electron = require('electron')
const ipc = electron.ipcRenderer const ipc = electron.ipcRenderer
@@ -23,13 +24,13 @@ class HotkeyTab extends React.Component {
this.handleSettingDone = () => { this.handleSettingDone = () => {
this.setState({keymapAlert: { this.setState({keymapAlert: {
type: 'success', type: 'success',
message: 'Successfully applied!' message: i18n.__('Successfully applied!')
}}) }})
} }
this.handleSettingError = (err) => { this.handleSettingError = (err) => {
this.setState({keymapAlert: { this.setState({keymapAlert: {
type: 'error', type: 'error',
message: err.message != null ? err.message : 'Error occurs!' message: err.message != null ? err.message : i18n.__('Error occurs!')
}}) }})
} }
this.oldHotkey = this.state.config.hotkey this.oldHotkey = this.state.config.hotkey
@@ -77,7 +78,7 @@ class HotkeyTab extends React.Component {
this.props.haveToSave({ this.props.haveToSave({
tab: 'Hotkey', tab: 'Hotkey',
type: 'warning', type: 'warning',
message: 'You have to save!' message: i18n.__('You have to save!')
}) })
} }
} }
@@ -102,9 +103,9 @@ class HotkeyTab extends React.Component {
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='group'> <div styleName='group'>
<div styleName='group-header'>Hotkeys</div> <div styleName='group-header'>{i18n.__('Hotkeys')}</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'>Show/Hide Boostnote</div> <div styleName='group-section-label'>{i18n.__('Show/Hide Boostnote')}</div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)} onChange={(e) => this.handleHotkeyChange(e)}
@@ -119,18 +120,18 @@ class HotkeyTab extends React.Component {
onClick={(e) => this.handleHintToggleButtonClick(e)} onClick={(e) => this.handleHintToggleButtonClick(e)}
> >
{this.state.isHotkeyHintOpen {this.state.isHotkeyHintOpen
? 'Hide Help' ? i18n.__('Hide Help')
: 'Help' : i18n.__('Help')
} }
</button> </button>
<button styleName='group-control-rightButton' <button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>Save onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}
</button> </button>
{keymapAlertElement} {keymapAlertElement}
</div> </div>
{this.state.isHotkeyHintOpen && {this.state.isHotkeyHintOpen &&
<div styleName='group-hint'> <div styleName='group-hint'>
<p>Available Keys</p> <p>{i18n.__('Available Keys')}</p>
<ul> <ul>
<li><code>0</code> to <code>9</code></li> <li><code>0</code> to <code>9</code></li>
<li><code>A</code> to <code>Z</code></li> <li><code>A</code> to <code>Z</code></li>

View File

@@ -5,6 +5,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store' import store from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const electron = require('electron') const electron = require('electron')
const { shell, remote } = electron const { shell, remote } = electron
@@ -38,11 +39,11 @@ class InfoTab extends React.Component {
if (!newConfig.amaEnabled) { if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
this.setState({ this.setState({
amaMessage: 'We hope we will gain your trust' amaMessage: i18n.__('We hope we will gain your trust')
}) })
} else { } else {
this.setState({ this.setState({
amaMessage: 'Thank\'s for trust us' amaMessage: i18n.__('Thank\'s for trusting us')
}) })
} }
@@ -69,48 +70,48 @@ class InfoTab extends React.Component {
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='header--sub'>Community</div> <div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='top'> <div styleName='top'>
<ul styleName='list'> <ul styleName='list'>
<li> <li>
<a href='https://boostnote.io/#subscribe' <a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Subscribe to Newsletter</a> >{i18n.__('Subscribe to Newsletter')}</a>
</li> </li>
<li> <li>
<a href='https://github.com/BoostIO/Boostnote/issues' <a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>GitHub</a> >{i18n.__('GitHub')}</a>
</li> </li>
<li> <li>
<a href='https://medium.com/boostnote' <a href='https://boostlog.io/@junp1234'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Blog</a> >{i18n.__('Blog')}</a>
</li> </li>
<li> <li>
<a href='https://www.facebook.com/groups/boostnote' <a href='https://www.facebook.com/groups/boostnote'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Facebook Group</a> >{i18n.__('Facebook Group')}</a>
</li> </li>
<li> <li>
<a href='https://twitter.com/boostnoteapp' <a href='https://twitter.com/boostnoteapp'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Twitter</a> >{i18n.__('Twitter')}</a>
</li> </li>
</ul> </ul>
</div> </div>
<hr /> <hr />
<div styleName='header--sub'>About</div> <div styleName='header--sub'>{i18n.__('About')}</div>
<div styleName='top'> <div styleName='top'>
<div styleName='icon-space'> <div styleName='icon-space'>
<img styleName='icon' src='../resources/app.png' width='92' height='92' /> <img styleName='icon' src='../resources/app.png' width='92' height='92' />
<div styleName='icon-right'> <div styleName='icon-right'>
<div styleName='appId'>Boostnote {appVersion}</div> <div styleName='appId'>{i18n.__('Boostnote')} {appVersion}</div>
<div styleName='description'> <div styleName='description'>
An open source note-taking app made for programmers just like you. {i18n.__('An open source note-taking app made for programmers just like you.')}
</div> </div>
</div> </div>
</div> </div>
@@ -120,35 +121,35 @@ class InfoTab extends React.Component {
<li> <li>
<a href='https://boostnote.io' <a href='https://boostnote.io'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Website</a> >{i18n.__('Website')}</a>
</li> </li>
<li> <li>
<a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md' <a href='https://github.com/BoostIO/Boostnote/blob/master/docs/build.md'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Development</a> : Development configurations for Boostnote. >{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
</li> </li>
<li styleName='cc'> <li styleName='cc'>
Copyright (C) 2017 Maisin&Co. {i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
</li> </li>
<li styleName='cc'> <li styleName='cc'>
License: GPL v3 {i18n.__('License: GPL v3')}
</li> </li>
</ul> </ul>
<hr styleName='separate-line' /> <hr styleName='separate-line' />
<div styleName='policy'>Analytics</div> <div styleName='policy'>{i18n.__('Analytics')}</div>
<div>Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.</div> <div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
<div>You can see how it works on <a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div> <div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<br /> <br />
<div>You can choose to enable or disable this option.</div> <div>{i18n.__('You can choose to enable or disable this option.')}</div>
<input onChange={(e) => this.handleConfigChange(e)} <input onChange={(e) => this.handleConfigChange(e)}
checked={this.state.config.amaEnabled} checked={this.state.config.amaEnabled}
ref='amaEnabled' ref='amaEnabled'
type='checkbox' type='checkbox'
/> />
Enable analytics to help improve Boostnote<br /> {i18n.__('Enable analytics to help improve Boostnote')}<br />
<button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>Save</button> <button styleName='policy-submit' onClick={(e) => this.handleSaveButtonClick(e)}>{i18n.__('Save')}</button>
<br /> <br />
{this.infoMessage()} {this.infoMessage()}
</div> </div>

View File

@@ -6,6 +6,7 @@ import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store' import store from 'browser/main/store'
import FolderList from './FolderList' import FolderList from './FolderList'
import i18n from 'browser/lib/i18n'
const { shell, remote } = require('electron') const { shell, remote } = require('electron')
const { dialog } = remote const { dialog } = remote
@@ -22,7 +23,7 @@ class StorageItem extends React.Component {
handleNewFolderButtonClick (e) { handleNewFolderButtonClick (e) {
const { storage } = this.props const { storage } = this.props
const input = { const input = {
name: 'Untitled', name: i18n.__('New Folder'),
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
} }
@@ -46,9 +47,9 @@ class StorageItem extends React.Component {
handleUnlinkButtonClick (e) { handleUnlinkButtonClick (e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), { const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: 'Unlink Storage', message: i18n.__('Unlink Storage'),
detail: 'Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.', detail: i18n.__('Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.'),
buttons: ['Unlink', 'Cancel'] buttons: [i18n.__('Unlink'), i18n.__('Cancel')]
}) })
if (index === 0) { if (index === 0) {
@@ -127,7 +128,7 @@ class StorageItem extends React.Component {
<i className='fa fa-plus' /> <i className='fa fa-plus' />
<span styleName='header-control-button-tooltip' <span styleName='header-control-button-tooltip'
style={{left: -20}} style={{left: -20}}
>Add Folder</span> >{i18n.__('Add Folder')}</span>
</button> </button>
<button styleName='header-control-button' <button styleName='header-control-button'
onClick={(e) => this.handleExternalButtonClick(e)} onClick={(e) => this.handleExternalButtonClick(e)}
@@ -135,7 +136,7 @@ class StorageItem extends React.Component {
<i className='fa fa-external-link' /> <i className='fa fa-external-link' />
<span styleName='header-control-button-tooltip' <span styleName='header-control-button-tooltip'
style={{left: -50}} style={{left: -50}}
>Open Storage folder</span> >{i18n.__('Open Storage folder')}</span>
</button> </button>
<button styleName='header-control-button' <button styleName='header-control-button'
onClick={(e) => this.handleUnlinkButtonClick(e)} onClick={(e) => this.handleUnlinkButtonClick(e)}
@@ -143,7 +144,7 @@ class StorageItem extends React.Component {
<i className='fa fa-unlink' /> <i className='fa fa-unlink' />
<span styleName='header-control-button-tooltip' <span styleName='header-control-button-tooltip'
style={{left: -10}} style={{left: -10}}
>Unlink</span> >{i18n.__('Unlink')}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl' import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import StorageItem from './StorageItem' import StorageItem from './StorageItem'
import i18n from 'browser/lib/i18n'
const electron = require('electron') const electron = require('electron')
const { shell, remote } = electron const { shell, remote } = electron
@@ -14,7 +15,7 @@ function browseFolder () {
const defaultPath = remote.app.getPath('home') const defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
dialog.showOpenDialog({ dialog.showOpenDialog({
title: 'Select Directory', title: i18n.__('Select Directory'),
defaultPath, defaultPath,
properties: ['openDirectory', 'createDirectory'] properties: ['openDirectory', 'createDirectory']
}, function (targetPaths) { }, function (targetPaths) {
@@ -69,16 +70,16 @@ class StoragesTab extends React.Component {
}) })
return ( return (
<div styleName='list'> <div styleName='list'>
<div styleName='header'>Storages</div> <div styleName='header'>{i18n.__('Storages')}</div>
{storageList.length > 0 {storageList.length > 0
? storageList ? storageList
: <div styleName='list-empty'>No storage found.</div> : <div styleName='list-empty'>{i18n.__('No storage found.')}</div>
} }
<div styleName='list-control'> <div styleName='list-control'>
<button styleName='list-control-addStorageButton' <button styleName='list-control-addStorageButton'
onClick={(e) => this.handleAddStorageButton(e)} onClick={(e) => this.handleAddStorageButton(e)}
> >
<i className='fa fa-plus' /> Add Storage Location <i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
</button> </button>
</div> </div>
</div> </div>
@@ -140,13 +141,13 @@ class StoragesTab extends React.Component {
return ( return (
<div styleName='addStorage'> <div styleName='addStorage'>
<div styleName='addStorage-header'>Add Storage</div> <div styleName='addStorage-header'>{i18n.__('Add Storage')}</div>
<div styleName='addStorage-body'> <div styleName='addStorage-body'>
<div styleName='addStorage-body-section'> <div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'> <div styleName='addStorage-body-section-label'>
Name {i18n.__('Name')}
</div> </div>
<div styleName='addStorage-body-section-name'> <div styleName='addStorage-body-section-name'>
<input styleName='addStorage-body-section-name-input' <input styleName='addStorage-body-section-name-input'
@@ -158,25 +159,25 @@ class StoragesTab extends React.Component {
</div> </div>
<div styleName='addStorage-body-section'> <div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Type</div> <div styleName='addStorage-body-section-label'>{i18n.__('Type')}</div>
<div styleName='addStorage-body-section-type'> <div styleName='addStorage-body-section-type'>
<select styleName='addStorage-body-section-type-select' <select styleName='addStorage-body-section-type-select'
value={this.state.newStorage.type} value={this.state.newStorage.type}
readOnly readOnly
> >
<option value='FILESYSTEM'>File System</option> <option value='FILESYSTEM'>{i18n.__('File System')}</option>
</select> </select>
<div styleName='addStorage-body-section-type-description'> <div styleName='addStorage-body-section-type-description'>
Setting up 3rd-party cloud storage integration:{' '} {i18n.__('Setting up 3rd-party cloud storage integration:')}{' '}
<a href='https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup' <a href='https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}
>Cloud-Syncing-and-Backup</a> >{i18n.__('Cloud-Syncing-and-Backup')}</a>
</div> </div>
</div> </div>
</div> </div>
<div styleName='addStorage-body-section'> <div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Location <div styleName='addStorage-body-section-label'>{i18n.__('Location')}
</div> </div>
<div styleName='addStorage-body-section-path'> <div styleName='addStorage-body-section-path'>
<input styleName='addStorage-body-section-path-input' <input styleName='addStorage-body-section-path-input'
@@ -196,10 +197,10 @@ class StoragesTab extends React.Component {
<div styleName='addStorage-body-control'> <div styleName='addStorage-body-control'>
<button styleName='addStorage-body-control-createButton' <button styleName='addStorage-body-control-createButton'
onClick={(e) => this.handleAddStorageCreateButton(e)} onClick={(e) => this.handleAddStorageCreateButton(e)}
>Add</button> >{i18n.__('Add')}</button>
<button styleName='addStorage-body-control-cancelButton' <button styleName='addStorage-body-control-cancelButton'
onClick={(e) => this.handleAddStorageCancelButton(e)} onClick={(e) => this.handleAddStorageCancelButton(e)}
>Cancel</button> >{i18n.__('Cancel')}</button>
</div> </div>
</div> </div>

View File

@@ -9,6 +9,7 @@ import ReactCodeMirror from 'react-codemirror'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -29,13 +30,13 @@ class UiTab extends React.Component {
this.handleSettingDone = () => { this.handleSettingDone = () => {
this.setState({UiAlert: { this.setState({UiAlert: {
type: 'success', type: 'success',
message: 'Successfully applied!' message: i18n.__('Successfully applied!')
}}) }})
} }
this.handleSettingError = (err) => { this.handleSettingError = (err) => {
this.setState({UiAlert: { this.setState({UiAlert: {
type: 'error', type: 'error',
message: err.message != null ? err.message : 'Error occurs!' message: err.message != null ? err.message : i18n.__('Error occurs!')
}}) }})
} }
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -61,6 +62,7 @@ class UiTab extends React.Component {
const newConfig = { const newConfig = {
ui: { ui: {
theme: this.refs.uiTheme.value, theme: this.refs.uiTheme.value,
language: this.refs.uiLanguage.value,
showCopyNotification: this.refs.showCopyNotification.checked, showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked, confirmDeletion: this.refs.confirmDeletion.checked,
disableDirectWrite: this.refs.uiD2w != null disableDirectWrite: this.refs.uiD2w != null
@@ -73,10 +75,13 @@ class UiTab extends React.Component {
fontFamily: this.refs.editorFontFamily.value, fontFamily: this.refs.editorFontFamily.value,
indentType: this.refs.editorIndentType.value, indentType: this.refs.editorIndentType.value,
indentSize: this.refs.editorIndentSize.value, indentSize: this.refs.editorIndentSize.value,
enableRulers: this.refs.enableEditorRulers.value === 'true',
rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','),
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
switchPreview: this.refs.editorSwitchPreview.value, switchPreview: this.refs.editorSwitchPreview.value,
keyMap: this.refs.editorKeyMap.value, keyMap: this.refs.editorKeyMap.value,
scrollPastEnd: this.refs.scrollPastEnd.checked scrollPastEnd: this.refs.scrollPastEnd.checked,
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
}, },
preview: { preview: {
fontSize: this.refs.previewFontSize.value, fontSize: this.refs.previewFontSize.value,
@@ -87,7 +92,9 @@ class UiTab extends React.Component {
latexInlineClose: this.refs.previewLatexInlineClose.value, latexInlineClose: this.refs.previewLatexInlineClose.value,
latexBlockOpen: this.refs.previewLatexBlockOpen.value, latexBlockOpen: this.refs.previewLatexBlockOpen.value,
latexBlockClose: this.refs.previewLatexBlockClose.value, latexBlockClose: this.refs.previewLatexBlockClose.value,
scrollPastEnd: this.refs.previewScrollPastEnd.checked scrollPastEnd: this.refs.previewScrollPastEnd.checked,
smartQuotes: this.refs.previewSmartQuotes.checked,
sanitize: this.refs.previewSanitize.value
} }
} }
@@ -105,7 +112,7 @@ class UiTab extends React.Component {
this.props.haveToSave({ this.props.haveToSave({
tab: 'UI', tab: 'UI',
type: 'warning', type: 'warning',
message: 'You have to save!' message: i18n.__('You have to save!')
}) })
} }
}) })
@@ -147,25 +154,53 @@ class UiTab extends React.Component {
const themes = consts.THEMES const themes = consts.THEMES
const { config, codemirrorTheme } = this.state const { config, codemirrorTheme } = this.state
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};' const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='group'> <div styleName='group'>
<div styleName='group-header'>Interface</div> <div styleName='group-header'>{i18n.__('Interface')}</div>
<div styleName='group-section'> <div styleName='group-section'>
Interface Theme {i18n.__('Interface Theme')}
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.ui.theme} <select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
ref='uiTheme' ref='uiTheme'
> >
<option value='default'>Default</option> <option value='default'>{i18n.__('Default')}</option>
<option value='white'>White</option> <option value='white'>{i18n.__('White')}</option>
<option value='solarized-dark'>Solarized Dark</option> <option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
<option value='dark'>Dark</option> <option value='dark'>{i18n.__('Dark')}</option>
</select> </select>
</div> </div>
</div> </div>
<div styleName='group-section'>
{i18n.__('Language')}
<div styleName='group-section-control'>
<select value={config.ui.language}
onChange={(e) => this.handleUIChange(e)}
ref='uiLanguage'
>
<option value='sq'>{i18n.__('Albanian')}</option>
<option value='zh-CN'>{i18n.__('Chinese (zh-CN)')}</option>
<option value='zh-TW'>{i18n.__('Chinese (zh-TW)')}</option>
<option value='da'>{i18n.__('Danish')}</option>
<option value='en'>{i18n.__('English')}</option>
<option value='fr'>{i18n.__('French')}</option>
<option value='de'>{i18n.__('German')}</option>
<option value='hu'>{i18n.__('Hungarian')}</option>
<option value='ja'>{i18n.__('Japanese')}</option>
<option value='ko'>{i18n.__('Korean')}</option>
<option value='no'>{i18n.__('Norwegian')}</option>
<option value='pl'>{i18n.__('Polish')}</option>
<option value='pt'>{i18n.__('Portuguese')}</option>
<option value='ru'>{i18n.__('Russian')}</option>
<option value='es'>{i18n.__('Spanish')}</option>
</select>
</div>
</div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
<label> <label>
<input onChange={(e) => this.handleUIChange(e)} <input onChange={(e) => this.handleUIChange(e)}
@@ -173,7 +208,7 @@ class UiTab extends React.Component {
ref='showCopyNotification' ref='showCopyNotification'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Show &quot;Saved to Clipboard&quot; notification when copying {i18n.__('Show "Saved to Clipboard" notification when copying')}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
@@ -183,7 +218,7 @@ class UiTab extends React.Component {
ref='confirmDeletion' ref='confirmDeletion'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Show a confirmation dialog when deleting notes {i18n.__('Show a confirmation dialog when deleting notes')}
</label> </label>
</div> </div>
{ {
@@ -205,7 +240,7 @@ class UiTab extends React.Component {
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Editor Theme {i18n.__('Editor Theme')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.editor.theme} <select value={config.editor.theme}
@@ -225,7 +260,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Editor Font Size {i18n.__('Editor Font Size')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -238,7 +273,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Editor Font Family {i18n.__('Editor Font Family')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -251,7 +286,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Editor Indent Style {i18n.__('Editor Indent Style')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.editor.indentSize} <select value={config.editor.indentSize}
@@ -267,41 +302,70 @@ class UiTab extends React.Component {
ref='editorIndentType' ref='editorIndentType'
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
> >
<option value='space'>Spaces</option> <option value='space'>{i18n.__('Spaces')}</option>
<option value='tab'>Tabs</option> <option value='tab'>{i18n.__('Tabs')}</option>
</select> </select>
</div> </div>
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Switch to Preview {i18n.__('Editor Rulers')}
</div>
<div styleName='group-section-control'>
<div>
<select value={config.editor.enableRulers}
ref='enableEditorRulers'
onChange={(e) => this.handleUIChange(e)}
>
<option value='true'>
{i18n.__('Enable')}
</option>
<option value='false'>
{i18n.__('Disable')}
</option>
</select>
</div>
<input styleName='group-section-control-input'
style={{ display: enableEditRulersStyle }}
ref='editorRulers'
value={config.editor.rulers}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Switch to Preview')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.editor.switchPreview} <select value={config.editor.switchPreview}
ref='editorSwitchPreview' ref='editorSwitchPreview'
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
> >
<option value='BLUR'>When Editor Blurred</option> <option value='BLUR'>{i18n.__('When Editor Blurred')}</option>
<option value='RIGHTCLICK'>On Right Click</option> <option value='DBL_CLICK'>{i18n.__('When Editor Blurred, Edit On Double Click')}</option>
<option value='RIGHTCLICK'>{i18n.__('On Right Click')}</option>
</select> </select>
</div> </div>
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Editor Keymap {i18n.__('Editor Keymap')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.editor.keyMap} <select value={config.editor.keyMap}
ref='editorKeyMap' ref='editorKeyMap'
onChange={(e) => this.handleUIChange(e)} onChange={(e) => this.handleUIChange(e)}
> >
<option value='sublime'>default</option> <option value='sublime'>{i18n.__('default')}</option>
<option value='vim'>vim</option> <option value='vim'>{i18n.__('vim')}</option>
<option value='emacs'>emacs</option> <option value='emacs'>{i18n.__('emacs')}</option>
</select> </select>
<p styleName='note-for-keymap'> Please restart boostnote after you change the keymap</p> <p styleName='note-for-keymap'>{i18n.__('⚠️ Please restart boostnote after you change the keymap')}</p>
</div> </div>
</div> </div>
@@ -312,7 +376,7 @@ class UiTab extends React.Component {
ref='editorDisplayLineNumbers' ref='editorDisplayLineNumbers'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Show line numbers in the editor {i18n.__('Show line numbers in the editor')}
</label> </label>
</div> </div>
@@ -323,14 +387,25 @@ class UiTab extends React.Component {
ref='scrollPastEnd' ref='scrollPastEnd'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Allow editor to scroll past the last line {i18n.__('Allow editor to scroll past the last line')}
</label> </label>
</div> </div>
<div styleName='group-header2'>Preview</div> <div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.fetchUrlTitle}
ref='editorFetchUrlTitle'
type='checkbox'
/>&nbsp;
{i18n.__('Bring in web page title when pasting URL on editor')}
</label>
</div>
<div styleName='group-header2'>{i18n.__('Preview')}</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Preview Font Size {i18n.__('Preview Font Size')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -343,7 +418,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
Preview Font Family {i18n.__('Preview Font Family')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -355,7 +430,7 @@ class UiTab extends React.Component {
</div> </div>
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'>Code block Theme</div> <div styleName='group-section-label'>{i18n.__('Code block Theme')}</div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme} <select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme' ref='previewCodeBlockTheme'
@@ -376,7 +451,7 @@ class UiTab extends React.Component {
ref='previewScrollPastEnd' ref='previewScrollPastEnd'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Allow preview to scroll past the last line {i18n.__('Allow preview to scroll past the last line')}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
@@ -386,12 +461,39 @@ class UiTab extends React.Component {
ref='previewLineNumber' ref='previewLineNumber'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Show line numbers for preview code blocks {i18n.__('Show line numbers for preview code blocks')}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.smartQuotes}
ref='previewSmartQuotes'
type='checkbox'
/>&nbsp;
Enable smart quotes
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Sanitization')}
</div>
<div styleName='group-section-control'>
<select value={config.preview.sanitize}
ref='previewSanitize'
onChange={(e) => this.handleUIChange(e)}
>
<option value='STRICT'> {i18n.__('Only allow secure html tags (recommended)')}
</option>
<option value='ALLOW_STYLES'> {i18n.__('Allow styles')}</option>
<option value='NONE'> {i18n.__('Allow dangerous html tags')}</option>
</select>
</div>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
LaTeX Inline Open Delimiter {i18n.__('LaTeX Inline Open Delimiter')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -404,7 +506,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
LaTeX Inline Close Delimiter {i18n.__('LaTeX Inline Close Delimiter')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -417,7 +519,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
LaTeX Block Open Delimiter {i18n.__('LaTeX Block Open Delimiter')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -430,7 +532,7 @@ class UiTab extends React.Component {
</div> </div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
LaTeX Block Close Delimiter {i18n.__('LaTeX Block Close Delimiter')}
</div> </div>
<div styleName='group-section-control'> <div styleName='group-section-control'>
<input styleName='group-section-control-input' <input styleName='group-section-control-input'
@@ -444,7 +546,7 @@ class UiTab extends React.Component {
<div styleName='group-control'> <div styleName='group-control'>
<button styleName='group-control-rightButton' <button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}>Save onClick={(e) => this.handleSaveUIClick(e)}>{i18n.__('Save')}
</button> </button>
{UiAlertElement} {UiAlertElement}
</div> </div>

View File

@@ -6,11 +6,13 @@ import UiTab from './UiTab'
import InfoTab from './InfoTab' import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding' import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab' import StoragesTab from './StoragesTab'
import Blog from './Blog'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferencesModal.styl' import styles from './PreferencesModal.styl'
import RealtimeNotification from 'browser/components/RealtimeNotification' import RealtimeNotification from 'browser/components/RealtimeNotification'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class Preferences extends React.Component { class Preferences extends React.Component {
constructor (props) { constructor (props) {
@@ -19,7 +21,8 @@ class Preferences extends React.Component {
this.state = { this.state = {
currentTab: 'STORAGES', currentTab: 'STORAGES',
UIAlert: '', UIAlert: '',
HotkeyAlert: '' HotkeyAlert: '',
BlogAlert: ''
} }
} }
@@ -75,6 +78,14 @@ class Preferences extends React.Component {
return ( return (
<Crowdfunding /> <Crowdfunding />
) )
case 'BLOG':
return (
<Blog
dispatch={dispatch}
config={config}
haveToSave={alert => this.setState({BlogAlert: alert})}
/>
)
case 'STORAGES': case 'STORAGES':
default: default:
return ( return (
@@ -107,11 +118,12 @@ class Preferences extends React.Component {
const content = this.renderContent() const content = this.renderContent()
const tabs = [ const tabs = [
{target: 'STORAGES', label: 'Storage'}, {target: 'STORAGES', label: i18n.__('Storage')},
{target: 'HOTKEY', label: 'Hotkeys', Hotkey: this.state.HotkeyAlert}, {target: 'HOTKEY', label: i18n.__('Hotkeys'), Hotkey: this.state.HotkeyAlert},
{target: 'UI', label: 'Interface', UI: this.state.UIAlert}, {target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert},
{target: 'INFO', label: 'About'}, {target: 'INFO', label: i18n.__('About')},
{target: 'CROWDFUNDING', label: 'Crowdfunding'} {target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')},
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert}
] ]
const navButtons = tabs.map((tab) => { const navButtons = tabs.map((tab) => {
@@ -140,7 +152,7 @@ class Preferences extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
<div styleName='top-bar'> <div styleName='top-bar'>
<p>Your preferences for Boostnote</p> <p>{i18n.__('Your preferences for Boostnote')}</p>
</div> </div>
<ModalEscButton handleEscButtonClick={(e) => this.handleEscButtonClick(e)} /> <ModalEscButton handleEscButtonClick={(e) => this.handleEscButtonClick(e)} />
<div styleName='nav'> <div styleName='nav'>

View File

@@ -5,6 +5,7 @@ import styles from './RenameFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store' import store from 'browser/main/store'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
class RenameFolderModal extends React.Component { class RenameFolderModal extends React.Component {
constructor (props) { constructor (props) {
@@ -72,13 +73,13 @@ class RenameFolderModal extends React.Component {
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={(e) => this.handleKeyDown(e)}
> >
<div styleName='header'> <div styleName='header'>
<div styleName='title'>Rename Folder</div> <div styleName='title'>{i18n.__('Rename Folder')}</div>
</div> </div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} /> <ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='control'> <div styleName='control'>
<input styleName='control-input' <input styleName='control-input'
placeholder='Folder Name' placeholder={i18n.__('Folder Name')}
ref='name' ref='name'
value={this.state.name} value={this.state.name}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
@@ -87,7 +88,7 @@ class RenameFolderModal extends React.Component {
<button styleName='control-confirmButton' <button styleName='control-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)} onClick={(e) => this.handleConfirmButtonClick(e)}
> >
Confirm {i18n.__('Confirm')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -27,7 +27,7 @@ function data (state = defaultDataMap(), action) {
action.notes.some((note) => { action.notes.some((note) => {
if (note === undefined) return true if (note === undefined) return true
const uniqueKey = note.storage + '-' + note.key const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note) state.noteMap.set(uniqueKey, note)
@@ -66,7 +66,7 @@ function data (state = defaultDataMap(), action) {
case 'UPDATE_NOTE': case 'UPDATE_NOTE':
{ {
const note = action.note const note = action.note
const uniqueKey = note.storage + '-' + note.key const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey) const oldNote = state.noteMap.get(uniqueKey)
@@ -162,9 +162,9 @@ function data (state = defaultDataMap(), action) {
case 'MOVE_NOTE': case 'MOVE_NOTE':
{ {
const originNote = action.originNote const originNote = action.originNote
const originKey = originNote.storage + '-' + originNote.key const originKey = originNote.key
const note = action.note const note = action.note
const uniqueKey = note.storage + '-' + note.key const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder const folderKey = note.storage + '-' + note.folder
const oldNote = state.noteMap.get(uniqueKey) const oldNote = state.noteMap.get(uniqueKey)
@@ -297,7 +297,7 @@ function data (state = defaultDataMap(), action) {
} }
case 'DELETE_NOTE': case 'DELETE_NOTE':
{ {
const uniqueKey = action.storageKey + '-' + action.noteKey const uniqueKey = action.noteKey
const targetNote = state.noteMap.get(uniqueKey) const targetNote = state.noteMap.get(uniqueKey)
state = Object.assign({}, state) state = Object.assign({}, state)
@@ -423,7 +423,7 @@ function data (state = defaultDataMap(), action) {
state.folderNoteMap = new Map(state.folderNoteMap) state.folderNoteMap = new Map(state.folderNoteMap)
state.tagNoteMap = new Map(state.tagNoteMap) state.tagNoteMap = new Map(state.tagNoteMap)
action.notes.forEach((note) => { action.notes.forEach((note) => {
const uniqueKey = note.storage + '-' + note.key const uniqueKey = note.key
const folderKey = note.storage + '-' + note.folder const folderKey = note.storage + '-' + note.folder
state.noteMap.set(uniqueKey, note) state.noteMap.set(uniqueKey, note)
@@ -483,7 +483,7 @@ function data (state = defaultDataMap(), action) {
state.tagNoteMap = new Map(state.tagNoteMap) state.tagNoteMap = new Map(state.tagNoteMap)
state.starredSet = new Set(state.starredSet) state.starredSet = new Set(state.starredSet)
notes.forEach((note) => { notes.forEach((note) => {
const noteKey = storage.key + '-' + note.key const noteKey = note.key
state.noteMap.delete(noteKey) state.noteMap.delete(noteKey)
state.starredSet.delete(noteKey) state.starredSet.delete(noteKey)
note.tags.forEach((tag) => { note.tags.forEach((tag) => {

View File

@@ -5,7 +5,7 @@ $danger-color = #c9302c
$danger-lighten-color = lighten(#c9302c, 5%) $danger-lighten-color = lighten(#c9302c, 5%)
// Layouts // Layouts
$statusBar-height = 0px $statusBar-height = 22px
$sideNav-width = 200px $sideNav-width = 200px
$sideNav--folded-width = 44px $sideNav--folded-width = 44px
$topBar-height = 60px $topBar-height = 60px

View File

@@ -9,7 +9,7 @@ Thank you for your help in advance.
### About copyright of Pull Request ### About copyright of Pull Request
If you make a pull request, It means you agree to transfer the copyright of the code changes to Maisin&Co. If you make a pull request, It means you agree to transfer the copyright of the code changes to BoostIO.
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features. It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen. Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
@@ -27,7 +27,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
### Об авторских правах Pull Request ### Об авторских правах Pull Request
Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в Maisin&Co. Если вы делаете pull request, значит вы согласны передать авторские права на изменения кода в BoostIO.
Это не означает, что Boostnote станет платным приложением. Если мы захотим заработать немного денег, мы найдем другой способ. Например, использование облачного хранилища, интеграцией мобильных приложений или другими специальными функциями. Это не означает, что Boostnote станет платным приложением. Если мы захотим заработать немного денег, мы найдем другой способ. Например, использование облачного хранилища, интеграцией мобильных приложений или другими специальными функциями.
Так как лицензия GPL v3 слишком строгая, чтобы быть совместимой с любой другой лицензией, мы думаем, что нужно заменить лицензию на более свободную (например, BSD, MIT). Так как лицензия GPL v3 слишком строгая, чтобы быть совместимой с любой другой лицензией, мы думаем, что нужно заменить лицензию на более свободную (например, BSD, MIT).
@@ -45,7 +45,7 @@ Because GPL v3 is too strict to be compatible with any other License, We thought
### Pull Request의 저작권에 관하여 ### Pull Request의 저작권에 관하여
당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 Maisin&Co에 양도한다는 것에 동의한다는 의미입니다. 당신이 pull request를 요청하면, 코드 변경에 대한 저작권을 BoostIO에 양도한다는 것에 동의한다는 의미입니다.
이것은 Boostnote가 유료화가 되는 것을 의미하는 건 아닙니다. 만약 우리가 자금이 필요하다면, 우리는 클라우드 연동, 모바일 앱 통합 혹은 특수한 기능 같은 것을 사용해 수입 창출을 시도할 것입니다. 이것은 Boostnote가 유료화가 되는 것을 의미하는 건 아닙니다. 만약 우리가 자금이 필요하다면, 우리는 클라우드 연동, 모바일 앱 통합 혹은 특수한 기능 같은 것을 사용해 수입 창출을 시도할 것입니다.
GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 엄격하므로, 우리는 BSD, MIT 라이센스와 같은 더 자유로운 라이센스로 교체하는 것을 생각하고 있습니다. GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무 엄격하므로, 우리는 BSD, MIT 라이센스와 같은 더 자유로운 라이센스로 교체하는 것을 생각하고 있습니다.
@@ -63,7 +63,7 @@ GPL v3 라이센스는 다른 라이센스와 혼합해 사용하기엔 너무
### Pull requestの著作権について ### Pull requestの著作権について
Pull requestをすることはその変化分のコードの著作権をMaisin&Co.に譲渡することに同意することになります。 Pull requestをすることはその変化分のコードの著作権をBoostIOに譲渡することに同意することになります。
アプリケーションのLicenseをいつでも変える選択肢を残したいと思うからです。 アプリケーションのLicenseをいつでも変える選択肢を残したいと思うからです。
これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。 これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
@@ -83,7 +83,7 @@ Pull requestをすることはその変化分のコードの著作権をMaisin&C
感谢您对我们的支持。 感谢您对我们的支持。
### 关于您提供的Pull Request的著作权版权问题 ### 关于您提供的Pull Request的著作权版权问题
如果您提供了一个Pull Request这表示您将您所修改的代码的著作权移交给Maisin&Co 如果您提供了一个Pull Request这表示您将您所修改的代码的著作权移交给BoostIO
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益我们会尝试一些其他的方法比如说云存储、绑定手机软件等。 这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益我们会尝试一些其他的方法比如说云存储、绑定手机软件等。
因为GPLv3过于严格不能和其他的一些协议兼容所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议比如说BSD、MIT。 因为GPLv3过于严格不能和其他的一些协议兼容所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议比如说BSD、MIT。

View File

@@ -1,5 +1,5 @@
# Build # Build
Diese Seite ist auch verfügbar in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## Umgebungen ## Umgebungen
* npm: 4.x * npm: 4.x

View File

@@ -1,25 +1,31 @@
# How to debug Boostnote (Electron app) # How to debug Boostnote (Electron app)
Diese Seite ist auch verfügbar in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/add-german-documents/docs/de/debug.md).
Boostnote is eine Electron app, somit basiert sie auf Chromium; Entwickler können die `Developer Tools` verwenden, wie Google Chrome. Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md).
Boostnote ist eine Electron App und basiert auf Chromium.
Zum Entwicklen verwendest du am Besten die `Developer Tools` von Google Chrome verwenden. Diese kannst du ganz einfach im unter dem Menüpunkt `View` mit `Toggle Developer Tools` aktivieren:
Du kannst die `Developer Tools` so einschalten:
![how_to_toggle_devTools](https://cloud.githubusercontent.com/assets/11307908/24343585/162187e2-127c-11e7-9c01-23578db03ecf.png) ![how_to_toggle_devTools](https://cloud.githubusercontent.com/assets/11307908/24343585/162187e2-127c-11e7-9c01-23578db03ecf.png)
Die `Developer Tools` schauen dann ungefähr so aus: Die Anzeige der `Developer Tools` sieht in etwa so aus:
![Developer_Tools](https://cloud.githubusercontent.com/assets/11307908/24343545/eff9f3a6-127b-11e7-94cf-cb67bfda634a.png) ![Developer_Tools](https://cloud.githubusercontent.com/assets/11307908/24343545/eff9f3a6-127b-11e7-94cf-cb67bfda634a.png)
Wenn Fehler vorkommen, werden die Fehlermeldungen in der `console` ausgegeben.
## Debugging ## Debugging
Zum Beispiel kannst du mit dem `debugger` Haltepunkte im Code setzen wie hier veranschaulicht:
Fehlermeldungen werden in der Regel in der `console` ausgegeben, die du über den gleichnamigen Reiter der `Developer Tools` anzeigen lassen kannst. Zum Debuggen kannst du beispielsweise über den `debugger` Haltepunkte im Code setzen.
![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png) ![debugger](https://cloud.githubusercontent.com/assets/11307908/24343879/9459efea-127d-11e7-9943-f60bf7f66d4a.png)
Das ist ledigtlich ein Beispiel, du kannst die Art von Debugging verwenden die für dich am besten ist. Du kannst aber natürlich auch die Art von Debugging verwenden mit der du am besten zurecht kommst.
## Referenz ## Referenz
* [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools) * [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools)
--- ---
Special thanks: Translated by [gino909](https://github.com/gino909) Special thanks: Translated by [gino909](https://github.com/gino909), [mdeuerlein](https://github.com/mdeuerlein)

View File

@@ -1,5 +1,5 @@
# How to debug Boostnote (Electron app) # How to debug Boostnote (Electron app)
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/add-german-documents/docs/de/debug.md). This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md).
Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome. Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome.

View File

@@ -1,5 +1,5 @@
# Build # Build
Cette page est également disponible en [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), et en [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md). Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md)
## Environnements ## Environnements
* npm: 4.x * npm: 4.x

View File

@@ -1,5 +1,5 @@
# Comment débugger Boostnote (Application Electron) # Comment débugger Boostnote (Application Electron)
Cette page est également disponible en [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), et en [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md) Cette page est également disponible en [Angalis](https://github.com/BoostIO/Boostnote/blob/master/docs/debug.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md)
Boostnote est une application Electron donc basée sur Chromium. Il est possible d'utiliser les `Developer Tools` comme dans Google Chrome. Boostnote est une application Electron donc basée sur Chromium. Il est possible d'utiliser les `Developer Tools` comme dans Google Chrome.

86
docs/zh_TW/build.md Normal file
View File

@@ -0,0 +1,86 @@
# 編譯
此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## 環境
* npm: 4.x
* node: 7.x
`$ grunt pre-build``npm v5.x` 有問題,所以只能用 `npm v4.x`
## 開發
我們使用 Webpack HMR 來開發 Boostnote。

在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。
**用 yarn 來安裝必要 packages**
```bash
$ yarn
```
**開始開發**
```
$ yarn run dev-start
```
上述指令同時運行了 `yarn run webpack``yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。
`webpack` 會同時監控修改過的程式碼,並
The `webpack` will watch for code changes and then apply them automatically.
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notice
> There are some cases where you have to refresh the app manually.
> 1. When editing a constructor method of a component
> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.)
## Deploy
We use Grunt to automate deployment.
You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode.
So, we've prepared a separate script which just makes an executable file.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
```
grunt pre-build
```
You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed.
If you find it necessary, you can use codesign or authenticode with this executable.
## Make own distribution packages (deb, rpm)
Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora).
> Note: You can create both `.deb` and `.rpm` in a single environment.
After installing the supported version of `node` and `npm`, install build dependency packages.
Ubuntu/Debian:
```
$ sudo apt-get install -y rpm fakeroot
```
Fedora:
```
$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot
```
Then execute `grunt build`.
```
$ grunt build
```
You will find `.deb` and `.rpm` in the `dist` directory.

View File

@@ -0,0 +1,52 @@
(function (mod) {
if (typeof exports === 'object' && typeof module === 'object') { // Common JS
mod(require('../codemirror/lib/codemirror'))
} else if (typeof define === 'function' && define.amd) { // AMD
define(['../codemirror/lib/codemirror'], mod)
} else { // Plain browser env
mod(CodeMirror)
}
})(function (CodeMirror) {
'use strict'
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/
var emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/
var unorderedListRE = /[*+-]\s/
CodeMirror.commands.boostNewLineAndIndentContinueMarkdownList = function (cm) {
if (cm.getOption('disableInput')) return CodeMirror.Pass
var ranges = cm.listSelections()
var replacements = []
for (var i = 0; i < ranges.length; i++) {
var pos = ranges[i].head
var eolState = cm.getStateAfter(pos.line)
var inList = eolState.list !== false
var inQuote = eolState.quote !== 0
var line = cm.getLine(pos.line)
var match = listRE.exec(line)
if (!ranges[i].empty() || (!inList && !inQuote) || !match || pos.ch < match[2].length - 1) {
cm.execCommand('newlineAndIndent')
return
}
if (emptyListRE.test(line)) {
if (!/>\s*$/.test(line)) {
cm.replaceRange('', {
line: pos.line, ch: 0
}, {
line: pos.line, ch: pos.ch + 1
})
}
replacements[i] = '\n'
} else {
var indent = match[1]
var after = match[5]
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf('>') >= 0
? match[2].replace('x', ' ')
: (parseInt(match[3], 10) + 1) + match[4]
replacements[i] = '\n' + indent + bullet + after
}
}
cm.replaceSelections(replacements)
}
})

View File

@@ -69,6 +69,10 @@ ipc.on('update-app-confirm', function (event, msg) {
} }
}) })
app.on('window-all-closed', function () {
app.quit()
})
app.on('ready', function () { app.on('ready', function () {
mainWindow = require('./main-window') mainWindow = require('./main-window')

View File

@@ -1,6 +1,7 @@
const electron = require('electron') const electron = require('electron')
const BrowserWindow = electron.BrowserWindow const BrowserWindow = electron.BrowserWindow
const shell = electron.shell const shell = electron.shell
const ipc = electron.ipcMain
const mainWindow = require('./main-window') const mainWindow = require('./main-window')
const macOS = process.platform === 'darwin' const macOS = process.platform === 'darwin'
@@ -259,6 +260,23 @@ const view = {
] ]
} }
let editorFocused
// Define extra shortcut keys
mainWindow.webContents.on('before-input-event', (event, input) => {
// Synonyms for Search (Find)
if (input.control && input.key === 'f' && input.type === 'keyDown') {
if (!editorFocused) {
mainWindow.webContents.send('top:focus-search')
event.preventDefault()
}
}
})
ipc.on('editor:focused', (event, isFocused) => {
editorFocused = isFocused
})
const window = { const window = {
label: 'Window', label: 'Window',
submenu: [ submenu: [
@@ -267,6 +285,11 @@ const window = {
accelerator: 'Command+M', accelerator: 'Command+M',
selector: 'performMiniaturize:' selector: 'performMiniaturize:'
}, },
{
label: 'Toggle Full Screen',
accelerator: 'Command+Control+F',
selector: 'toggleFullScreen:'
},
{ {
label: 'Close', label: 'Close',
accelerator: 'Command+W', accelerator: 'Command+W',

View File

@@ -40,7 +40,7 @@ mainWindow.webContents.sendInputEvent({
keyCode: '\u0008' keyCode: '\u0008'
}) })
if (process.platform === 'darwin' || process.env.DESKTOP_SESSION === 'cinnamon') { if (process.platform === 'darwin') {
mainWindow.on('close', function (e) { mainWindow.on('close', function (e) {
e.preventDefault() e.preventDefault()
if (mainWindow.isFullScreen()) { if (mainWindow.isFullScreen()) {
@@ -56,11 +56,8 @@ if (process.platform === 'darwin' || process.env.DESKTOP_SESSION === 'cinnamon')
app.on('before-quit', function (e) { app.on('before-quit', function (e) {
mainWindow.removeAllListeners() mainWindow.removeAllListeners()
}) })
} else {
app.on('window-all-closed', function () {
app.quit()
})
} }
mainWindow.on('resize', _.throttle(storeWindowSize, 500)) mainWindow.on('resize', _.throttle(storeWindowSize, 500))
function storeWindowSize () { function storeWindowSize () {

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